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}