forked from tangled.org/core
this repo has no description
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.sh/tangled.sh/core/patchutil" 16 "tangled.sh/tangled.sh/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.Stat.FilesChanged = len(diffs) 81 nd.Commit.This = c.Hash.String() 82 nd.Commit.PGPSignature = c.PGPSignature 83 nd.Commit.Committer = c.Committer 84 nd.Commit.Tree = c.TreeHash.String() 85 86 if parent.Hash.IsZero() { 87 nd.Commit.Parent = "" 88 } else { 89 nd.Commit.Parent = parent.Hash.String() 90 } 91 nd.Commit.Author = c.Author 92 nd.Commit.Message = c.Message 93 94 if v, ok := c.ExtraHeaders["change-id"]; ok { 95 nd.Commit.ChangedId = string(v) 96 } 97 98 return &nd, nil 99} 100 101func (g *GitRepo) DiffTree(commit1, commit2 *object.Commit) (*types.DiffTree, error) { 102 tree1, err := commit1.Tree() 103 if err != nil { 104 return nil, err 105 } 106 107 tree2, err := commit2.Tree() 108 if err != nil { 109 return nil, err 110 } 111 112 diff, err := object.DiffTree(tree1, tree2) 113 if err != nil { 114 return nil, err 115 } 116 117 patch, err := diff.Patch() 118 if err != nil { 119 return nil, err 120 } 121 122 diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String())) 123 if err != nil { 124 return nil, err 125 } 126 127 return &types.DiffTree{ 128 Rev1: commit1.Hash.String(), 129 Rev2: commit2.Hash.String(), 130 Patch: patch.String(), 131 Diff: diffs, 132 }, nil 133} 134 135// FormatPatch generates a git-format-patch output between two commits, 136// and returns the raw format-patch series, a parsed FormatPatch and an error. 137func (g *GitRepo) formatSinglePatch(base, commit2 plumbing.Hash, extraArgs ...string) (string, *types.FormatPatch, error) { 138 var stdout bytes.Buffer 139 140 args := []string{ 141 "-C", 142 g.path, 143 "format-patch", 144 fmt.Sprintf("%s..%s", base.String(), commit2.String()), 145 "--stdout", 146 } 147 args = append(args, extraArgs...) 148 149 cmd := exec.Command("git", args...) 150 cmd.Stdout = &stdout 151 cmd.Stderr = os.Stderr 152 err := cmd.Run() 153 if err != nil { 154 return "", nil, err 155 } 156 157 formatPatch, err := patchutil.ExtractPatches(stdout.String()) 158 if err != nil { 159 return "", nil, err 160 } 161 162 if len(formatPatch) > 1 { 163 return "", nil, fmt.Errorf("running format-patch on single commit produced more than on patch") 164 } 165 166 return stdout.String(), &formatPatch[0], nil 167} 168 169func (g *GitRepo) MergeBase(commit1, commit2 *object.Commit) (*object.Commit, error) { 170 isAncestor, err := commit1.IsAncestor(commit2) 171 if err != nil { 172 return nil, err 173 } 174 175 if isAncestor { 176 return commit1, nil 177 } 178 179 mergeBase, err := commit1.MergeBase(commit2) 180 if err != nil { 181 return nil, err 182 } 183 184 if len(mergeBase) == 0 { 185 return nil, fmt.Errorf("failed to find a merge-base") 186 } 187 188 return mergeBase[0], nil 189} 190 191func (g *GitRepo) ResolveRevision(revStr string) (*object.Commit, error) { 192 rev, err := g.r.ResolveRevision(plumbing.Revision(revStr)) 193 if err != nil { 194 return nil, fmt.Errorf("resolving revision %s: %w", revStr, err) 195 } 196 197 commit, err := g.r.CommitObject(*rev) 198 if err != nil { 199 200 return nil, fmt.Errorf("getting commit for %s: %w", revStr, err) 201 } 202 203 return commit, nil 204} 205 206func (g *GitRepo) commitsBetween(newCommit, oldCommit *object.Commit) ([]*object.Commit, error) { 207 var commits []*object.Commit 208 current := newCommit 209 210 for { 211 if current.Hash == oldCommit.Hash { 212 break 213 } 214 215 commits = append(commits, current) 216 217 if len(current.ParentHashes) == 0 { 218 return nil, fmt.Errorf("old commit %s not found in history of new commit %s", oldCommit.Hash, newCommit.Hash) 219 } 220 221 parent, err := current.Parents().Next() 222 if err != nil { 223 return nil, fmt.Errorf("error getting parent: %w", err) 224 } 225 226 current = parent 227 } 228 229 return commits, nil 230} 231 232func (g *GitRepo) FormatPatch(base, commit2 *object.Commit) (string, []types.FormatPatch, error) { 233 // get list of commits between commir2 and base 234 commits, err := g.commitsBetween(commit2, base) 235 if err != nil { 236 return "", nil, fmt.Errorf("failed to get commits: %w", err) 237 } 238 239 // reverse the list so we start from the oldest one and go up to the most recent one 240 slices.Reverse(commits) 241 242 var allPatchesContent strings.Builder 243 var allPatches []types.FormatPatch 244 245 for _, commit := range commits { 246 changeId := "" 247 if val, ok := commit.ExtraHeaders["change-id"]; ok { 248 changeId = string(val) 249 } 250 251 var parentHash plumbing.Hash 252 if len(commit.ParentHashes) > 0 { 253 parentHash = commit.ParentHashes[0] 254 } else { 255 parentHash = plumbing.NewHash("4b825dc642cb6eb9a060e54bf8d69288fbee4904") // git empty tree hash 256 } 257 258 var additionalArgs []string 259 if changeId != "" { 260 additionalArgs = append(additionalArgs, "--add-header", fmt.Sprintf("Change-Id: %s", changeId)) 261 } 262 263 stdout, patch, err := g.formatSinglePatch(parentHash, commit.Hash, additionalArgs...) 264 if err != nil { 265 return "", nil, fmt.Errorf("failed to format patch for commit %s: %w", commit.Hash.String(), err) 266 } 267 268 allPatchesContent.WriteString(stdout) 269 allPatchesContent.WriteString("\n") 270 271 allPatches = append(allPatches, *patch) 272 } 273 274 return allPatchesContent.String(), allPatches, nil 275}