forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
this repo has no description
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 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}