forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
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 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.Commit.FromGoGitCommit(c)
81
82 return &nd, nil
83}
84
85func (g *GitRepo) DiffTree(commit1, commit2 *object.Commit) (*types.DiffTree, error) {
86 tree1, err := commit1.Tree()
87 if err != nil {
88 return nil, err
89 }
90
91 tree2, err := commit2.Tree()
92 if err != nil {
93 return nil, err
94 }
95
96 diff, err := object.DiffTree(tree1, tree2)
97 if err != nil {
98 return nil, err
99 }
100
101 patch, err := diff.Patch()
102 if err != nil {
103 return nil, err
104 }
105
106 diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String()))
107 if err != nil {
108 return nil, err
109 }
110
111 return &types.DiffTree{
112 Rev1: commit1.Hash.String(),
113 Rev2: commit2.Hash.String(),
114 Patch: patch.String(),
115 Diff: diffs,
116 }, nil
117}
118
119// FormatPatch generates a git-format-patch output between two commits,
120// and returns the raw format-patch series, a parsed FormatPatch and an error.
121func (g *GitRepo) formatSinglePatch(commit plumbing.Hash, extraArgs ...string) (string, *types.FormatPatch, error) {
122 var stdout bytes.Buffer
123
124 args := []string{
125 "-C",
126 g.path,
127 "format-patch",
128 "-1",
129 commit.String(),
130 "--stdout",
131 }
132 args = append(args, extraArgs...)
133
134 cmd := exec.Command("git", args...)
135 cmd.Stdout = &stdout
136 cmd.Stderr = os.Stderr
137 err := cmd.Run()
138 if err != nil {
139 return "", nil, err
140 }
141
142 formatPatch, err := patchutil.ExtractPatches(stdout.String())
143 if err != nil {
144 return "", nil, err
145 }
146
147 if len(formatPatch) > 1 {
148 return "", nil, fmt.Errorf("running format-patch on single commit produced more than on patch")
149 }
150
151 return stdout.String(), &formatPatch[0], nil
152}
153
154func (g *GitRepo) ResolveRevision(revStr string) (*object.Commit, error) {
155 rev, err := g.r.ResolveRevision(plumbing.Revision(revStr))
156 if err != nil {
157 return nil, fmt.Errorf("resolving revision %s: %w", revStr, err)
158 }
159
160 commit, err := g.r.CommitObject(*rev)
161 if err != nil {
162
163 return nil, fmt.Errorf("getting commit for %s: %w", revStr, err)
164 }
165
166 return commit, nil
167}
168
169func (g *GitRepo) commitsBetween(newCommit, oldCommit *object.Commit) ([]*object.Commit, error) {
170 var commits []*object.Commit
171
172 output, err := g.revList(
173 "--no-merges", // format-patch explicitly prepares only non-merges
174 fmt.Sprintf("%s..%s", oldCommit.Hash.String(), newCommit.Hash.String()),
175 )
176 if err != nil {
177 return nil, fmt.Errorf("revlist: %w", err)
178 }
179
180 lines := strings.Split(strings.TrimSpace(string(output)), "\n")
181 if len(lines) == 1 && lines[0] == "" {
182 return commits, nil
183 }
184
185 for _, item := range lines {
186 obj, err := g.r.CommitObject(plumbing.NewHash(item))
187 if err != nil {
188 continue
189 }
190 commits = append(commits, obj)
191 }
192
193 return commits, nil
194}
195
196func (g *GitRepo) FormatPatch(base, commit2 *object.Commit) (string, []types.FormatPatch, error) {
197 // get list of commits between commit2 and base
198 commits, err := g.commitsBetween(commit2, base)
199 if err != nil {
200 return "", nil, fmt.Errorf("failed to get commits: %w", err)
201 }
202
203 // reverse the list so we start from the oldest one and go up to the most recent one
204 slices.Reverse(commits)
205
206 var allPatchesContent strings.Builder
207 var allPatches []types.FormatPatch
208
209 for _, commit := range commits {
210 changeId := ""
211 if val, ok := commit.ExtraHeaders["change-id"]; ok {
212 changeId = string(val)
213 }
214
215 var additionalArgs []string
216 if changeId != "" {
217 additionalArgs = append(additionalArgs, "--add-header", fmt.Sprintf("Change-Id: %s", changeId))
218 }
219
220 stdout, patch, err := g.formatSinglePatch(commit.Hash, additionalArgs...)
221 if err != nil {
222 return "", nil, fmt.Errorf("failed to format patch for commit %s: %w", commit.Hash.String(), err)
223 }
224
225 allPatchesContent.WriteString(stdout)
226 allPatchesContent.WriteString("\n")
227
228 allPatches = append(allPatches, *patch)
229 }
230
231 return allPatchesContent.String(), allPatches, nil
232}