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.
| 1 | package 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 | |
| 11 | import ( |
| 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. |
| 20 | var 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. |
| 30 | func (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. |
| 40 | func (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. |
| 62 | func 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. |
| 69 | func 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. |
| 87 | func (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 |