Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

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 nd.Stat.Insertions += tf.LinesAdded 68 nd.Stat.Deletions += tf.LinesDeleted 69 } 70 71 nd.Diff = append(nd.Diff, ndiff) 72 } 73 74 nd.Stat.FilesChanged += len(diffs) 75 nd.Commit.FromGoGitCommit(c) 76 77 return &nd, nil 78} 79 80func (g *GitRepo) MergeBase(a, b *object.Commit) (*object.Commit, error) { 81 out, err := g.mergeBase(a.Hash.String(), b.Hash.String()) 82 if err != nil { 83 return nil, fmt.Errorf("merge-base %s %s: %w", a.Hash, b.Hash, err) 84 } 85 86 hash := plumbing.NewHash(strings.TrimSpace(string(out))) 87 return g.r.CommitObject(hash) 88} 89 90func (g *GitRepo) DiffTree(commit1, commit2 *object.Commit) (*types.DiffTree, error) { 91 tree1, err := commit1.Tree() 92 if err != nil { 93 return nil, err 94 } 95 96 tree2, err := commit2.Tree() 97 if err != nil { 98 return nil, err 99 } 100 101 diff, err := object.DiffTree(tree1, tree2) 102 if err != nil { 103 return nil, err 104 } 105 106 patch, err := diff.Patch() 107 if err != nil { 108 return nil, err 109 } 110 111 patchStr := patch.String() 112 diffs, _, err := gitdiff.Parse(strings.NewReader(patchStr)) 113 if err != nil { 114 return nil, err 115 } 116 117 return &types.DiffTree{ 118 Rev1: commit1.Hash.String(), 119 Rev2: commit2.Hash.String(), 120 Patch: patchStr, 121 Diff: diffs, 122 }, nil 123} 124 125// FormatPatch generates a git-format-patch output between two commits, 126// and returns the raw format-patch series, a parsed FormatPatch and an error. 127func (g *GitRepo) formatSinglePatch(commit plumbing.Hash, extraArgs ...string) (string, *types.FormatPatch, error) { 128 var stdout bytes.Buffer 129 130 args := []string{ 131 "-C", 132 g.path, 133 "format-patch", 134 "-1", 135 commit.String(), 136 "--stdout", 137 } 138 args = append(args, extraArgs...) 139 140 cmd := exec.Command("git", args...) 141 cmd.Stdout = &stdout 142 cmd.Stderr = os.Stderr 143 err := cmd.Run() 144 if err != nil { 145 return "", nil, err 146 } 147 148 raw := stdout.String() 149 formatPatch, err := patchutil.ExtractPatches(raw) 150 if err != nil { 151 return "", nil, err 152 } 153 154 if len(formatPatch) > 1 { 155 return "", nil, fmt.Errorf("running format-patch on single commit produced more than on patch") 156 } 157 158 return raw, &formatPatch[0], nil 159} 160 161// ChangedFilesBetween returns the list of files changed between oldSha and newSha. 162// If oldSha is the zero hash (initial push), all files in newSha are returned. 163func (g *GitRepo) ChangedFilesBetween(oldSha, newSha string) ([]string, error) { 164 newCommit, err := g.ResolveRevision(newSha) 165 if err != nil { 166 return nil, err 167 } 168 169 if plumbing.NewHash(oldSha) == plumbing.ZeroHash { 170 tree, err := newCommit.Tree() 171 if err != nil { 172 return nil, err 173 } 174 var files []string 175 tree.Files().ForEach(func(f *object.File) error { 176 files = append(files, f.Name) 177 return nil 178 }) 179 return files, nil 180 } 181 182 oldCommit, err := g.ResolveRevision(oldSha) 183 if err != nil { 184 return nil, err 185 } 186 187 dt, err := g.DiffTree(oldCommit, newCommit) 188 if err != nil { 189 return nil, err 190 } 191 192 seen := make(map[string]struct{}) 193 for _, f := range dt.Diff { 194 if f.OldName != "" { 195 seen[f.OldName] = struct{}{} 196 } 197 if f.NewName != "" { 198 seen[f.NewName] = struct{}{} 199 } 200 } 201 202 files := make([]string, 0, len(seen)) 203 for name := range seen { 204 files = append(files, name) 205 } 206 return files, nil 207} 208 209func (g *GitRepo) ResolveRevision(revStr string) (*object.Commit, error) { 210 rev, err := g.r.ResolveRevision(plumbing.Revision(revStr)) 211 if err != nil { 212 return nil, fmt.Errorf("resolving revision %s: %w", revStr, err) 213 } 214 215 commit, err := g.r.CommitObject(*rev) 216 if err != nil { 217 218 return nil, fmt.Errorf("getting commit for %s: %w", revStr, err) 219 } 220 221 return commit, nil 222} 223 224func (g *GitRepo) commitsBetween(newCommit, oldCommit *object.Commit) ([]*object.Commit, error) { 225 var commits []*object.Commit 226 227 output, err := g.revList( 228 "--no-merges", // format-patch explicitly prepares only non-merges 229 fmt.Sprintf("%s..%s", oldCommit.Hash.String(), newCommit.Hash.String()), 230 ) 231 if err != nil { 232 return nil, fmt.Errorf("revlist: %w", err) 233 } 234 235 lines := strings.Split(strings.TrimSpace(string(output)), "\n") 236 if len(lines) == 1 && lines[0] == "" { 237 return commits, nil 238 } 239 240 for _, item := range lines { 241 obj, err := g.r.CommitObject(plumbing.NewHash(item)) 242 if err != nil { 243 continue 244 } 245 commits = append(commits, obj) 246 } 247 248 return commits, nil 249} 250 251func (g *GitRepo) FormatPatch(base, commit2 *object.Commit) (string, []types.FormatPatch, error) { 252 // get list of commits between commit2 and base 253 commits, err := g.commitsBetween(commit2, base) 254 if err != nil { 255 return "", nil, fmt.Errorf("failed to get commits: %w", err) 256 } 257 258 // reverse the list so we start from the oldest one and go up to the most recent one 259 slices.Reverse(commits) 260 261 var allPatchesContent strings.Builder 262 var allPatches []types.FormatPatch 263 264 for _, commit := range commits { 265 changeId := "" 266 if val, ok := commit.ExtraHeaders["change-id"]; ok { 267 changeId = string(val) 268 } 269 270 var additionalArgs []string 271 if changeId != "" { 272 additionalArgs = append(additionalArgs, "--add-header", fmt.Sprintf("Change-Id: %s", changeId)) 273 } 274 275 stdout, patch, err := g.formatSinglePatch(commit.Hash, additionalArgs...) 276 if err != nil { 277 return "", nil, fmt.Errorf("failed to format patch for commit %s: %w", commit.Hash.String(), err) 278 } 279 280 allPatchesContent.WriteString(stdout) 281 allPatchesContent.WriteString("\n") 282 283 allPatches = append(allPatches, *patch) 284 } 285 286 return allPatchesContent.String(), allPatches, nil 287}