1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package gitcli
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "strings"
10
11 "bnema.dev/ermite/internal/domain"
12 "bnema.dev/ermite/internal/ports"
13)
14
15var _ ports.PullGit = (*Git)(nil)
16
17// TestMerge runs `git merge-tree --write-tree`, which merges in the object
18// database and touches no ref or index: it prints the tree of the result and
19// exits 0, or exits 1 when the merge conflicts. Commits with no common history
20// do not merge (GitHub makes no test merge for them either).
21//
22//hexcheck:ignore suspicious-business-logic-in-adapter maps git exit codes to the clean/conflict answer of the port
23func (g *Git) TestMerge(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID) (domain.ObjectID, bool, error) {
24 dir, err := g.repoDir(repo)
25 if err != nil {
26 return domain.ObjectID{}, false, err
27 }
28 if _, err := g.MergeBase(ctx, repo, base, head); errors.Is(err, domain.ErrNotFound) {
29 return domain.ObjectID{}, false, nil
30 } else if err != nil {
31 return domain.ObjectID{}, false, err
32 }
33 out, err := g.run(ctx, readArgs(dir, "merge-tree", "--write-tree", "--no-messages", "--end-of-options", base.String(), head.String())...)
34 switch {
35 case exitCode(err) == 1:
36 return domain.ObjectID{}, false, nil
37 case err != nil:
38 return domain.ObjectID{}, false, fmt.Errorf("test merge %s into %s in %q: %w", head, base, repo, err)
39 }
40 line, _, _ := strings.Cut(out, "\n")
41 tree, err := parseID(line)
42 if err != nil {
43 return domain.ObjectID{}, false, err
44 }
45 return tree, true, nil
46}
47
48// SetPullRef runs `git update-ref` on a ref that domain.IsPullRef accepts, or
49// `update-ref -d` for a zero id (deleting a ref that is not there is fine).
50//
51//hexcheck:ignore suspicious-business-logic-in-adapter checks the ref name with the domain rule before handing it to git
52func (g *Git) SetPullRef(ctx context.Context, repo domain.RepoName, ref string, id domain.ObjectID) error {
53 if !domain.IsPullRef(ref) {
54 return fmt.Errorf("pull ref %q of %q: %w", ref, repo, domain.ErrInvalid)
55 }
56 dir, err := g.repoDir(repo)
57 if err != nil {
58 return err
59 }
60 args := []string{"--git-dir=" + dir, "update-ref"}
61 if id.IsZero() {
62 args = append(args, "-d", ref)
63 } else {
64 args = append(args, ref, id.String())
65 }
66 if _, err := g.run(ctx, args...); err != nil {
67 return fmt.Errorf("update pull ref %q of %q: %w", ref, repo, err)
68 }
69 return nil
70}
71
72// PullRef reads a ref that domain.IsPullRef accepts with `git rev-parse`.
73//
74//hexcheck:ignore suspicious-business-logic-in-adapter checks the ref name with the domain rule before handing it to git
75func (g *Git) PullRef(ctx context.Context, repo domain.RepoName, ref string) (domain.ObjectID, error) {
76 if !domain.IsPullRef(ref) {
77 return domain.ObjectID{}, fmt.Errorf("pull ref %q of %q: %w", ref, repo, domain.ErrNotFound)
78 }
79 dir, err := g.repoDir(repo)
80 if err != nil {
81 return domain.ObjectID{}, err
82 }
83 out, err := g.run(ctx, readArgs(dir, "rev-parse", "--verify", "--quiet", ref+"^{commit}")...)
84 if exitCode(err) == 1 {
85 return domain.ObjectID{}, fmt.Errorf("pull ref %q of %q: %w", ref, repo, domain.ErrNotFound)
86 }
87 if err != nil {
88 return domain.ObjectID{}, fmt.Errorf("read pull ref %q of %q: %w", ref, repo, err)
89 }
90 return parseID(out)
91}