internal/gitutil/merge.go
117 lines · 3933 bytes
1package gitutil
2
3import (
4 "fmt"
5 "os"
6 "os/exec"
7 "strings"
8)
9
10// FetchInto copies srcRef from srcDir into dstDir as dstRef, forcing the
11// update. Objects are copied, not shared — the destination owns everything
12// afterward, which is what keeps MRs alive when their fork is deleted.
13func FetchInto(dstDir, srcDir, srcRef, dstRef string) error {
14 cmd := exec.Command("git", "-C", dstDir, "fetch", "--quiet", "--no-write-fetch-head",
15 srcDir, "+"+srcRef+":"+dstRef)
16 if out, err := cmd.CombinedOutput(); err != nil {
17 return fmt.Errorf("fetch %s from %s: %v\n%s", srcRef, srcDir, err, out)
18 }
19 return nil
20}
21
22// UpdateRefCAS points ref at newSHA only if it currently points at oldSHA
23// (empty oldSHA = must not exist). This is the compare-and-swap that makes
24// merges safe against concurrent pushes.
25func UpdateRefCAS(dir, ref, newSHA, oldSHA string) error {
26 args := []string{"-C", dir, "update-ref", ref, newSHA}
27 if oldSHA != "" {
28 args = append(args, oldSHA)
29 }
30 cmd := exec.Command("git", args...)
31 if out, err := cmd.CombinedOutput(); err != nil {
32 return fmt.Errorf("update-ref %s: %v\n%s", ref, err, out)
33 }
34 return nil
35}
36
37func DeleteRef(dir, ref string) error {
38 cmd := exec.Command("git", "-C", dir, "update-ref", "-d", ref)
39 if out, err := cmd.CombinedOutput(); err != nil {
40 return fmt.Errorf("delete-ref %s: %v\n%s", ref, err, out)
41 }
42 return nil
43}
44
45// RevListRange returns commits in old..new, newest first.
46func RevListRange(dir, old, new string) ([]string, error) {
47 cmd := exec.Command("git", "-C", dir, "rev-list", new, "^"+old)
48 out, err := cmd.Output()
49 if err != nil {
50 return nil, fmt.Errorf("rev-list %s..%s: %w", old, new, err)
51 }
52 var shas []string
53 for _, l := range strings.Split(strings.TrimSpace(string(out)), "\n") {
54 if l != "" {
55 shas = append(shas, l)
56 }
57 }
58 return shas, nil
59}
60
61// MergeTree performs a real merge of ours and theirs, returning the merged
62// tree id. conflict=true means the merge cannot be done automatically.
63func MergeTree(dir, ours, theirs string) (tree string, conflict bool, err error) {
64 cmd := exec.Command("git", "-C", dir, "merge-tree", "--write-tree", ours, theirs)
65 out, runErr := cmd.Output()
66 tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0])
67 if runErr != nil {
68 if ee, ok := runErr.(*exec.ExitError); ok && ee.ExitCode() == 1 {
69 return "", true, nil // conflicted merge
70 }
71 return "", false, fmt.Errorf("merge-tree: %w", runErr)
72 }
73 return tree, false, nil
74}
75
76// CommitTree creates a merge commit with the given parents, authored and
77// committed by the merging user. There is no server signing key by design.
78func CommitTree(dir, tree string, parents []string, name, email, message string) (string, error) {
79 args := []string{"-C", dir, "commit-tree", tree, "-m", message}
80 for _, p := range parents {
81 args = append(args, "-p", p)
82 }
83 cmd := exec.Command("git", args...)
84 cmd.Env = append(os.Environ(),
85 "GIT_AUTHOR_NAME="+name, "GIT_AUTHOR_EMAIL="+email,
86 "GIT_COMMITTER_NAME="+name, "GIT_COMMITTER_EMAIL="+email,
87 )
88 out, err := cmd.Output()
89 if err != nil {
90 return "", fmt.Errorf("commit-tree: %w", err)
91 }
92 return strings.TrimSpace(string(out)), nil
93}
94
95// Diff returns the patch for old..new (three-dot semantics are the caller's
96// job: pass the merge base as old).
97func Diff(dir, old, new string, limit int64) (string, error) {
98 cmd := exec.Command("git", "-C", dir, "diff", "--stat", "--patch", old, new)
99 out, err := cmd.Output()
100 if err != nil {
101 return "", fmt.Errorf("diff: %w", err)
102 }
103 if int64(len(out)) > limit {
104 out = out[:limit]
105 }
106 return string(out), nil
107}
108
109// MergeBase returns the best common ancestor, or an error if none exists.
110func MergeBase(dir, a, b string) (string, error) {
111 cmd := exec.Command("git", "-C", dir, "merge-base", a, b)
112 out, err := cmd.Output()
113 if err != nil {
114 return "", fmt.Errorf("no common history between %s and %s", a, b)
115 }
116 return strings.TrimSpace(string(out)), nil
117}