Skip pushing merges when the repo has no remote

This commit is contained in:
Ian Gulliver
2026-10-22 18:05:52 -07:00
parent f3b15696b3
commit da7f324bab
6 changed files with 69 additions and 15 deletions
+1
View File
@@ -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.
+1 -2
View File
@@ -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)
+15 -5
View File
@@ -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}
}
+9 -3
View File
@@ -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
}
+39 -5
View File
@@ -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
}
+4
View File
@@ -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 {