Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

395 commit(s) on main

update readme Keenan Nemetz · 56m ago 32290114
history blame raw 3.4 KiB
1package git
2
3// "Would this land?" without a worktree.
4//
5// `git merge-tree --write-tree` performs the merge in memory and names the paths
6// that conflict. A change request page can then say "this needs a rebase" instead
7// of leaving the discovery to whoever runs `git merge` locally an hour later.
8// Nothing is written: the tree it names is thrown away and the repository is
9// unchanged, which is what makes it safe to run on a page view.
10
11import (
12 "errors"
13 "fmt"
14 "os/exec"
15 "regexp"
16 "strings"
17)
18
19// treeIDRE is what a successful merge prints first: the id of the tree it built.
20var treeIDRE = regexp.MustCompile(`^[0-9a-f]{40}([0-9a-f]{24})?$`)
21
22// MergeConflicts reports the paths that would conflict if head were merged into
23// base. An empty slice means the merge is clean; an error means the question could
24// not be asked.
25//
26// merge-tree exits 1 both when the merge conflicts and when it cannot resolve a
27// revision, so the exit status alone is not an answer. What separates them is the
28// output: a real merge prints the resulting tree's id first, and a refusal prints
29// a message instead.
30func (r *Repo) MergeConflicts(base, head string) ([]string, error) {
31 _, conflicts, err := r.MergeTree(base, head)
32 return conflicts, err
33}
34
35// MergeTree performs the same in-memory merge and also returns the id of the tree
36// it built, which is what a real merge commit needs. Asking for it is how a
37// landing operation merges without a worktree: the tree is in the object store,
38// so a commit over it costs nothing even when the merge is thrown away because the
39// ref moved in the meantime.
40func (r *Repo) MergeTree(base, head string) (string, []string, error) {
41 out, errs, err := r.run(nil, "merge-tree", "--write-tree", "--name-only", "--no-messages", base, head)
42 text := string(out)
43 if err != nil {
44 var ee *exec.ExitError
45 if !errors.As(err, &ee) || ee.ExitCode() != 1 {
46 return "", nil, fmt.Errorf("merge-tree %s %s: %w: %s", base, head, err, errs)
47 }
48 if !treeIDAhead(text) {
49 reason := strings.TrimSpace(text)
50 if reason == "" {
51 reason = errs
52 }
53 return "", nil, fmt.Errorf("cannot merge %s into %s: %s", head, base, reason)
54 }
55 }
56 tree, _, _ := strings.Cut(strings.TrimSpace(text), "\n")
57 return strings.TrimSpace(tree), parseMergeTree(text), nil
58}
59
60// treeIDAhead reports whether the output starts with a tree id rather than a
61// complaint.
62func treeIDAhead(out string) bool {
63 first, _, _ := strings.Cut(out, "\n")
64 return treeIDRE.MatchString(strings.TrimSpace(first))
65}
66
67// parseMergeTree reads --write-tree output: the merged tree's id, then one
68// conflicted path per line. A clean merge prints the id and nothing else.
69func parseMergeTree(out string) []string {
70 lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
71 if len(lines) < 2 {
72 return nil
73 }
74 var paths []string
75 for _, line := range lines[1:] {
76 if strings.TrimSpace(line) == "" {
77 break // informational messages follow, and they are not paths
78 }
79 paths = append(paths, line)
80 }
81 return paths
82}
83
84// MergeBase is the commit where two histories diverged, which is the "old" side of
85// a three-dot review: what the branch changed relative to, not the tip it will be
86// merged onto.
87func (r *Repo) MergeBase(a, b string) (string, error) {
88 out, errs, err := r.run(nil, "merge-base", a, b)
89 if err != nil {
90 return "", fmt.Errorf("merge-base %s %s: %w: %s", a, b, err, errs)
91 }
92 return strings.TrimSpace(string(out)), nil
93}
94