diff --git a/docs/operations.md b/docs/operations.md index 868324c..ec50fb9 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -96,6 +96,7 @@ Coordination of merges, discards, reviews, ready-for-merge prompts, workflow sta ## git +- A project's remote is `origin`, checked each time byre would fetch or push. A repo with no `origin` still works: merges land on the local default branch and byre skips the fetch, the push and the notes refspec, so a fresh `git init` is enough to start. Adding `origin` later takes effect on the next merge; the notes refspec is added at the next server start. - Merge notes live under `refs/notes/byre`. byre adds the fetch refspec `refs/notes/byre*:refs/notes/byre*` to each checkout; it's a pattern because git refuses a fetch whose exact refspec names a ref the remote doesn't have yet, as it doesn't before the first note is pushed. Another clone adds it with `git config --add remote.origin.fetch 'refs/notes/byre*:refs/notes/byre*'`; `git log --notes=byre` shows the notes. - `git fetch` and `git push` run non-interactively (`GIT_TERMINAL_PROMPT=0`, ssh `BatchMode=yes`, overriding any `core.sshCommand`), so a remote needing a password or an unknown host key fails at once instead of waiting for input. Every git command runs in its own process group, killed whole on timeout, so an `ssh` it started can't finish a fetch or push after byre has moved on. - A push error other than a non-fast-forward rejection is ambiguous, since the remote may have taken the push before the connection dropped; byre then fetches and checks whether the commit landed before deciding. diff --git a/internal/agents/maincommit.go b/internal/agents/maincommit.go index 8a3907e..79d3648 100644 --- a/internal/agents/maincommit.go +++ b/internal/agents/maincommit.go @@ -27,8 +27,7 @@ func (m *Manager) prepareMainCommit(ctx context.Context, task api.Task, project return commit, notes, err } -func advanceMainCheckout(project api.Project, commit, notes string) error { - pushed := git.Remote + "/" + project.DefaultBranch +func advanceMainCheckout(project api.Project, commit, notes, pushed string) error { err := git.AdvanceNotes(context.Background(), project.RepoPath, notes) if err != nil { return fmt.Errorf("pushed %s to %s, but updating %s in the project checkout failed: %w", commit, pushed, git.NotesRef, err) diff --git a/internal/agents/mergein.go b/internal/agents/mergein.go index ccbf701..ab3655f 100644 --- a/internal/agents/mergein.go +++ b/internal/agents/mergein.go @@ -218,8 +218,11 @@ func (m *Manager) deliverMerge(ctx context.Context, sess api.AgentSession, task } func pushLanded(ctx context.Context, project api.Project, commit string) (bool, error) { - target := git.Remote + "/" + project.DefaultBranch - err := git.Fetch(ctx, project.RepoPath) + target, err := git.Upstream(ctx, project.RepoPath, project.DefaultBranch) + if err != nil { + return false, &unfinishedMergeError{fmt.Errorf("checking whether %s reached %s failed: %w", commit, project.DefaultBranch, err)} + } + err = git.Fetch(ctx, project.RepoPath) if err != nil { return false, &unfinishedMergeError{fmt.Errorf("checking whether %s reached %s failed: %w", commit, target, err)} } @@ -341,7 +344,11 @@ func (m *Manager) landMerge(ctx context.Context, f store.FinalizingMerge) error } return m.closeUnchanged(ctx, task, project, f.By) } - err = m.store.SetFinalizingCommit(ctx, task.Id, commit, notes, git.Remote+"/"+project.DefaultBranch) + pushed, err := git.Upstream(ctx, project.RepoPath, project.DefaultBranch) + if err != nil { + return err + } + err = m.store.SetFinalizingCommit(ctx, task.Id, commit, notes, pushed) if err != nil { return err } @@ -376,8 +383,11 @@ func (m *Manager) closeUnchanged(ctx context.Context, task api.Task, project api } func (m *Manager) closePushed(ctx context.Context, task api.Task, project api.Project, commit, notes, login string) error { - pushed := git.Remote + "/" + project.DefaultBranch - err := advanceMainCheckout(project, commit, notes) + pushed, err := git.Upstream(ctx, project.RepoPath, project.DefaultBranch) + if err != nil { + return &unfinishedMergeError{err} + } + err = advanceMainCheckout(project, commit, notes, pushed) if err != nil { return &unfinishedMergeError{err} } diff --git a/internal/agents/mergemain.go b/internal/agents/mergemain.go index 523e127..b71253a 100644 --- a/internal/agents/mergemain.go +++ b/internal/agents/mergemain.go @@ -102,7 +102,10 @@ func (m *Manager) mergeToMain(ctx context.Context, l *live, raw json.RawMessage) return fmt.Sprintf("Nothing to push: %s already has all of this branch's work, so byre merged %s into this branch. Keep working; the task stays open.", project.DefaultBranch, project.DefaultBranch), nil } - pushed := git.Remote + "/" + project.DefaultBranch + pushed, err := git.Upstream(ctx, project.RepoPath, project.DefaultBranch) + if err != nil { + return "", err + } err = m.emit(ctx, l, agentActor, "task.merged_to_main", map[string]any{"id": task.Id, "commit": commit, "pushed": pushed, "merge_title": title}) if err != nil { return "", err @@ -142,8 +145,11 @@ func (m *Manager) pushToMain(ctx context.Context, l *live, task api.Task, projec return "", err } - pushed := git.Remote + "/" + project.DefaultBranch - err = advanceMainCheckout(project, commit, notes) + pushed, err := git.Upstream(ctx, project.RepoPath, project.DefaultBranch) + if err != nil { + return "", err + } + err = advanceMainCheckout(project, commit, notes, pushed) if err != nil { return "", err } diff --git a/internal/git/git.go b/internal/git/git.go index 59cd9cf..ee4480e 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -53,14 +53,30 @@ func Inspect(ctx context.Context, path string) (RepoInfo, error) { return RepoInfo{}, err } - remote, err := run(ctx, root, "remote", "get-url", "origin") + remote, _, code, err := runCode(ctx, root, nil, "remote", "get-url", Remote) if err != nil { return RepoInfo{}, err } + if code != 0 { + remote = "" + } return RepoInfo{Root: root, DefaultBranch: branch, Remote: remote}, nil } +func HasRemote(ctx context.Context, repo string) (bool, error) { + _, _, code, err := runCode(ctx, repo, nil, "remote", "get-url", Remote) + return err == nil && code == 0, err +} + +func Upstream(ctx context.Context, repo, branch string) (string, error) { + has, err := HasRemote(ctx, repo) + if err != nil || !has { + return branch, err + } + return Remote + "/" + branch, nil +} + func AddWorktree(ctx context.Context, repo, path, branch, base string) (string, error) { sha, err := run(ctx, repo, "rev-parse", "--verify", base+"^{commit}") if err != nil { @@ -92,12 +108,18 @@ func EnsureBranchCommit(ctx context.Context, repo, branch string, env []string) if err != nil { return err } - _, stderr, code, err := runNetwork(ctx, repo, "push", Remote, commit+":refs/heads/"+branch) + has, err := HasRemote(ctx, repo) if err != nil { return err } - if code != 0 { - return fmt.Errorf("pushing %s to %s: exit status %d: %s", branch, Remote, code, stderr) + if has { + _, stderr, code, err := runNetwork(ctx, repo, "push", Remote, commit+":refs/heads/"+branch) + if err != nil { + return err + } + if code != 0 { + return fmt.Errorf("pushing %s to %s: exit status %d: %s", branch, Remote, code, stderr) + } } _, err = run(ctx, repo, "update-ref", "refs/heads/"+branch, commit, "") return err @@ -258,6 +280,10 @@ var networkEnv = []string{ var ErrRemoteMoved = errors.New("the remote's branch has moved") func Push(ctx context.Context, repo, commit, branch, notes string) error { + has, err := HasRemote(ctx, repo) + if err != nil || !has { + return err + } out, stderr, code, err := runNetwork(ctx, repo, "push", "--atomic", "--porcelain", Remote, commit+":refs/heads/"+branch, notes+":"+NotesRef) if err != nil { return err @@ -279,6 +305,10 @@ func FastForward(ctx context.Context, repo, rev string) error { } func Fetch(ctx context.Context, repo string) error { + has, err := HasRemote(ctx, repo) + if err != nil || !has { + return err + } _, stderr, code, err := runNetwork(ctx, repo, "fetch", Remote) if err != nil { return err @@ -290,7 +320,11 @@ func Fetch(ctx context.Context, repo string) error { } func FetchFastForward(ctx context.Context, repo, branch string) error { - err := Fetch(ctx, repo) + has, err := HasRemote(ctx, repo) + if err != nil || !has { + return err + } + err = Fetch(ctx, repo) if err != nil { return err } diff --git a/internal/git/notes.go b/internal/git/notes.go index 8077d94..78c4067 100644 --- a/internal/git/notes.go +++ b/internal/git/notes.go @@ -14,6 +14,10 @@ const NotesRef = "refs/notes/byre" const notesFetch = NotesRef + "*:" + NotesRef + "*" func ConfigureNotesFetch(ctx context.Context, repo string) error { + has, err := HasRemote(ctx, repo) + if err != nil || !has { + return err + } key := "remote." + Remote + ".fetch" out, stderr, code, err := runCode(ctx, repo, nil, "config", "--get-all", key) if err != nil {