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