1package git
2
3import (
4 "bytes"
5 "fmt"
6 "log"
7 "os"
8 "os/exec"
9 "strings"
10
11 "github.com/bluekeyes/go-gitdiff/gitdiff"
12 "github.com/go-git/go-git/v5/plumbing"
13 "github.com/go-git/go-git/v5/plumbing/object"
14 "tangled.sh/tangled.sh/core/patchutil"
15 "tangled.sh/tangled.sh/core/types"
16)
17
18func (g *GitRepo) Diff() (*types.NiceDiff, error) {
19 c, err := g.r.CommitObject(g.h)
20 if err != nil {
21 return nil, fmt.Errorf("commit object: %w", err)
22 }
23
24 patch := &object.Patch{}
25 commitTree, err := c.Tree()
26 parent := &object.Commit{}
27 if err == nil {
28 parentTree := &object.Tree{}
29 if c.NumParents() != 0 {
30 parent, err = c.Parents().Next()
31 if err == nil {
32 parentTree, err = parent.Tree()
33 if err == nil {
34 patch, err = parentTree.Patch(commitTree)
35 if err != nil {
36 return nil, fmt.Errorf("patch: %w", err)
37 }
38 }
39 }
40 } else {
41 patch, err = parentTree.Patch(commitTree)
42 if err != nil {
43 return nil, fmt.Errorf("patch: %w", err)
44 }
45 }
46 }
47
48 diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String()))
49 if err != nil {
50 log.Println(err)
51 }
52
53 nd := types.NiceDiff{}
54 for _, d := range diffs {
55 ndiff := types.Diff{}
56 ndiff.Name.New = d.NewName
57 ndiff.Name.Old = d.OldName
58 ndiff.IsBinary = d.IsBinary
59 ndiff.IsNew = d.IsNew
60 ndiff.IsDelete = d.IsDelete
61 ndiff.IsCopy = d.IsCopy
62 ndiff.IsRename = d.IsRename
63
64 for _, tf := range d.TextFragments {
65 ndiff.TextFragments = append(ndiff.TextFragments, *tf)
66 for _, l := range tf.Lines {
67 switch l.Op {
68 case gitdiff.OpAdd:
69 nd.Stat.Insertions += 1
70 case gitdiff.OpDelete:
71 nd.Stat.Deletions += 1
72 }
73 }
74 }
75
76 nd.Diff = append(nd.Diff, ndiff)
77 }
78
79 nd.Stat.FilesChanged = len(diffs)
80 nd.Commit.This = c.Hash.String()
81
82 if parent.Hash.IsZero() {
83 nd.Commit.Parent = ""
84 } else {
85 nd.Commit.Parent = parent.Hash.String()
86 }
87 nd.Commit.Author = c.Author
88 nd.Commit.Message = c.Message
89
90 return &nd, nil
91}
92
93func (g *GitRepo) DiffTree(commit1, commit2 *object.Commit) (*types.DiffTree, error) {
94 tree1, err := commit1.Tree()
95 if err != nil {
96 return nil, err
97 }
98
99 tree2, err := commit2.Tree()
100 if err != nil {
101 return nil, err
102 }
103
104 diff, err := object.DiffTree(tree1, tree2)
105 if err != nil {
106 return nil, err
107 }
108
109 patch, err := diff.Patch()
110 if err != nil {
111 return nil, err
112 }
113
114 diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String()))
115 if err != nil {
116 return nil, err
117 }
118
119 return &types.DiffTree{
120 Rev1: commit1.Hash.String(),
121 Rev2: commit2.Hash.String(),
122 Patch: patch.String(),
123 Diff: diffs,
124 }, nil
125}
126
127// FormatPatch generates a git-format-patch output between two commits,
128// and returns the raw format-patch series, a parsed FormatPatch and an error.
129func (g *GitRepo) FormatPatch(base, commit2 *object.Commit) (string, []patchutil.FormatPatch, error) {
130 var stdout bytes.Buffer
131 cmd := exec.Command(
132 "git",
133 "-C",
134 g.path,
135 "format-patch",
136 fmt.Sprintf("%s..%s", base.Hash.String(), commit2.Hash.String()),
137 "--stdout",
138 )
139 cmd.Stdout = &stdout
140 cmd.Stderr = os.Stderr
141 err := cmd.Run()
142 if err != nil {
143 return "", nil, err
144 }
145
146 formatPatch, err := patchutil.ExtractPatches(stdout.String())
147 if err != nil {
148 return "", nil, err
149 }
150
151 return stdout.String(), formatPatch, nil
152}
153
154func (g *GitRepo) MergeBase(commit1, commit2 *object.Commit) (*object.Commit, error) {
155 isAncestor, err := commit1.IsAncestor(commit2)
156 if err != nil {
157 return nil, err
158 }
159
160 if isAncestor {
161 return commit1, nil
162 }
163
164 mergeBase, err := commit1.MergeBase(commit2)
165 if err != nil {
166 return nil, err
167 }
168
169 if len(mergeBase) == 0 {
170 return nil, fmt.Errorf("failed to find a merge-base")
171 }
172
173 return mergeBase[0], nil
174}
175
176func (g *GitRepo) ResolveRevision(revStr string) (*object.Commit, error) {
177 rev, err := g.r.ResolveRevision(plumbing.Revision(revStr))
178 if err != nil {
179 return nil, fmt.Errorf("resolving revision %s: %w", revStr, err)
180 }
181
182 commit, err := g.r.CommitObject(*rev)
183 if err != nil {
184
185 return nil, fmt.Errorf("getting commit for %s: %w", revStr, err)
186 }
187
188 return commit, nil
189}