forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 5.4 kB view raw
1package git 2 3import ( 4 "bytes" 5 "fmt" 6 "log" 7 "os" 8 "os/exec" 9 "slices" 10 "strings" 11 12 "github.com/bluekeyes/go-gitdiff/gitdiff" 13 "github.com/go-git/go-git/v5/plumbing" 14 "github.com/go-git/go-git/v5/plumbing/object" 15 "tangled.org/core/patchutil" 16 "tangled.org/core/types" 17) 18 19func (g *GitRepo) Diff() (*types.NiceDiff, error) { 20 c, err := g.r.CommitObject(g.h) 21 if err != nil { 22 return nil, fmt.Errorf("commit object: %w", err) 23 } 24 25 patch := &object.Patch{} 26 commitTree, err := c.Tree() 27 parent := &object.Commit{} 28 if err == nil { 29 parentTree := &object.Tree{} 30 if c.NumParents() != 0 { 31 parent, err = c.Parents().Next() 32 if err == nil { 33 parentTree, err = parent.Tree() 34 if err == nil { 35 patch, err = parentTree.Patch(commitTree) 36 if err != nil { 37 return nil, fmt.Errorf("patch: %w", err) 38 } 39 } 40 } 41 } else { 42 patch, err = parentTree.Patch(commitTree) 43 if err != nil { 44 return nil, fmt.Errorf("patch: %w", err) 45 } 46 } 47 } 48 49 diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String())) 50 if err != nil { 51 log.Println(err) 52 } 53 54 nd := types.NiceDiff{} 55 for _, d := range diffs { 56 ndiff := types.Diff{} 57 ndiff.Name.New = d.NewName 58 ndiff.Name.Old = d.OldName 59 ndiff.IsBinary = d.IsBinary 60 ndiff.IsNew = d.IsNew 61 ndiff.IsDelete = d.IsDelete 62 ndiff.IsCopy = d.IsCopy 63 ndiff.IsRename = d.IsRename 64 65 for _, tf := range d.TextFragments { 66 ndiff.TextFragments = append(ndiff.TextFragments, *tf) 67 for _, l := range tf.Lines { 68 switch l.Op { 69 case gitdiff.OpAdd: 70 nd.Stat.Insertions += 1 71 case gitdiff.OpDelete: 72 nd.Stat.Deletions += 1 73 } 74 } 75 } 76 77 nd.Diff = append(nd.Diff, ndiff) 78 } 79 80 nd.Commit.FromGoGitCommit(c) 81 82 return &nd, nil 83} 84 85func (g *GitRepo) DiffTree(commit1, commit2 *object.Commit) (*types.DiffTree, error) { 86 tree1, err := commit1.Tree() 87 if err != nil { 88 return nil, err 89 } 90 91 tree2, err := commit2.Tree() 92 if err != nil { 93 return nil, err 94 } 95 96 diff, err := object.DiffTree(tree1, tree2) 97 if err != nil { 98 return nil, err 99 } 100 101 patch, err := diff.Patch() 102 if err != nil { 103 return nil, err 104 } 105 106 diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String())) 107 if err != nil { 108 return nil, err 109 } 110 111 return &types.DiffTree{ 112 Rev1: commit1.Hash.String(), 113 Rev2: commit2.Hash.String(), 114 Patch: patch.String(), 115 Diff: diffs, 116 }, nil 117} 118 119// FormatPatch generates a git-format-patch output between two commits, 120// and returns the raw format-patch series, a parsed FormatPatch and an error. 121func (g *GitRepo) formatSinglePatch(commit plumbing.Hash, extraArgs ...string) (string, *types.FormatPatch, error) { 122 var stdout bytes.Buffer 123 124 args := []string{ 125 "-C", 126 g.path, 127 "format-patch", 128 "-1", 129 commit.String(), 130 "--stdout", 131 } 132 args = append(args, extraArgs...) 133 134 cmd := exec.Command("git", args...) 135 cmd.Stdout = &stdout 136 cmd.Stderr = os.Stderr 137 err := cmd.Run() 138 if err != nil { 139 return "", nil, err 140 } 141 142 formatPatch, err := patchutil.ExtractPatches(stdout.String()) 143 if err != nil { 144 return "", nil, err 145 } 146 147 if len(formatPatch) > 1 { 148 return "", nil, fmt.Errorf("running format-patch on single commit produced more than on patch") 149 } 150 151 return stdout.String(), &formatPatch[0], nil 152} 153 154func (g *GitRepo) ResolveRevision(revStr string) (*object.Commit, error) { 155 rev, err := g.r.ResolveRevision(plumbing.Revision(revStr)) 156 if err != nil { 157 return nil, fmt.Errorf("resolving revision %s: %w", revStr, err) 158 } 159 160 commit, err := g.r.CommitObject(*rev) 161 if err != nil { 162 163 return nil, fmt.Errorf("getting commit for %s: %w", revStr, err) 164 } 165 166 return commit, nil 167} 168 169func (g *GitRepo) commitsBetween(newCommit, oldCommit *object.Commit) ([]*object.Commit, error) { 170 var commits []*object.Commit 171 172 output, err := g.revList( 173 "--no-merges", // format-patch explicitly prepares only non-merges 174 fmt.Sprintf("%s..%s", oldCommit.Hash.String(), newCommit.Hash.String()), 175 ) 176 if err != nil { 177 return nil, fmt.Errorf("revlist: %w", err) 178 } 179 180 lines := strings.Split(strings.TrimSpace(string(output)), "\n") 181 if len(lines) == 1 && lines[0] == "" { 182 return commits, nil 183 } 184 185 for _, item := range lines { 186 obj, err := g.r.CommitObject(plumbing.NewHash(item)) 187 if err != nil { 188 continue 189 } 190 commits = append(commits, obj) 191 } 192 193 return commits, nil 194} 195 196func (g *GitRepo) FormatPatch(base, commit2 *object.Commit) (string, []types.FormatPatch, error) { 197 // get list of commits between commit2 and base 198 commits, err := g.commitsBetween(commit2, base) 199 if err != nil { 200 return "", nil, fmt.Errorf("failed to get commits: %w", err) 201 } 202 203 // reverse the list so we start from the oldest one and go up to the most recent one 204 slices.Reverse(commits) 205 206 var allPatchesContent strings.Builder 207 var allPatches []types.FormatPatch 208 209 for _, commit := range commits { 210 changeId := "" 211 if val, ok := commit.ExtraHeaders["change-id"]; ok { 212 changeId = string(val) 213 } 214 215 var additionalArgs []string 216 if changeId != "" { 217 additionalArgs = append(additionalArgs, "--add-header", fmt.Sprintf("Change-Id: %s", changeId)) 218 } 219 220 stdout, patch, err := g.formatSinglePatch(commit.Hash, additionalArgs...) 221 if err != nil { 222 return "", nil, fmt.Errorf("failed to format patch for commit %s: %w", commit.Hash.String(), err) 223 } 224 225 allPatchesContent.WriteString(stdout) 226 allPatchesContent.WriteString("\n") 227 228 allPatches = append(allPatches, *patch) 229 } 230 231 return allPatchesContent.String(), allPatches, nil 232}