Record merge progress durably and finish pushed merges at startup

This commit is contained in:
Ian Gulliver
2026-10-16 13:01:17 -07:00
parent 51dfbf78a0
commit d9e0498d10
6 changed files with 211 additions and 42 deletions
+129 -32
View File
@@ -136,26 +136,82 @@ func (m *Manager) recoverMerges(ctx context.Context) error {
}
func (m *Manager) recoverFinalizing(ctx context.Context) error {
finalizing, err := m.store.FinalizingTasks(ctx)
finalizing, err := m.store.FinalizingMerges(ctx)
if err != nil {
return err
}
for _, t := range finalizing {
err = m.store.ClearFinalizing(ctx, t.Id)
for _, f := range finalizing {
ferr := m.resumeFinalizing(ctx, f)
if ferr == nil {
slog.Info("finished merge at startup", "task", f.Task.Id, "commit", f.Commit)
continue
}
slog.Error("finish merge at startup", "task", f.Task.Id, "err", ferr)
unfinished := &unfinishedMergeError{}
if !errors.As(ferr, &unfinished) {
err = m.store.ClearFinalizing(ctx, f.Task.Id)
if err != nil {
return err
}
}
sessions, err := m.store.ListSessions(ctx, f.Task.Id)
if err != nil {
return err
}
sessions, err := m.store.ListSessions(ctx, t.Id)
if err != nil {
return err
}
slog.Error("merge was finishing at startup", "task", t.Id)
m.fileTaskError(ctx, t, sessions, "byre restarted while finishing this task's merge, so it may not have been pushed, or the task's worktree may be gone. Check the default branch before merging again.")
m.fileTaskError(ctx, f.Task, sessions, "byre restarted while finishing this task's merge: "+ferr.Error()+".")
}
return nil
}
func (m *Manager) resumeFinalizing(ctx context.Context, f store.FinalizingMerge) error {
project, err := m.store.GetProject(ctx, f.Task.ProjectId)
if err != nil {
return err
}
if f.Unchanged {
return m.closeUnchanged(ctx, f.Task, project, f.By)
}
if f.Commit == "" {
return errors.New("nothing was pushed. Merge again to retry")
}
landed, err := pushLanded(project, f.Commit)
if err != nil {
return err
}
if !landed {
return fmt.Errorf("its squash commit %s is not on %s/%s, so the push didn't land. Merge again to retry", f.Commit, git.Remote, project.DefaultBranch)
}
return m.closePushed(ctx, f.Task, project, f.Commit, f.Notes, f.By)
}
func pushLanded(project api.Project, commit string) (bool, error) {
target := git.Remote + "/" + project.DefaultBranch
err := git.Fetch(project.RepoPath)
if err != nil {
return false, &unfinishedMergeError{fmt.Errorf("checking whether %s reached %s failed: %w", commit, target, err)}
}
landed, err := git.IsAncestor(project.RepoPath, commit, target)
if err != nil {
return false, &unfinishedMergeError{fmt.Errorf("checking whether %s reached %s failed: %w", commit, target, err)}
}
return landed, nil
}
type unfinishedMergeError struct {
err error
}
func (e *unfinishedMergeError) Error() string {
return e.err.Error() + "; byre finishes the merge when it next starts"
}
func (e *unfinishedMergeError) Unwrap() error {
return e.err
}
func (m *Manager) startHeldMerges(ctx context.Context) {
projects, err := m.store.ProjectsWithQueuedMerges(ctx)
if err != nil {
@@ -274,6 +330,11 @@ func (m *Manager) finishMerge(l *live, login string) {
}
slog.Error("finish merge", "task", l.taskID, "err", err)
unfinished := &unfinishedMergeError{}
if errors.As(err, &unfinished) {
m.fileError(l, "Merging the task failed: "+err.Error()+".")
return
}
cerr := m.store.ClearFinalizing(ctx, l.taskID)
if cerr != nil {
slog.Error("clear merge finalizing", "task", l.taskID, "err", cerr)
@@ -315,38 +376,74 @@ func (m *Manager) squashAndClose(ctx context.Context, taskID, login string) erro
return err
}
pushed := ""
if commit != "" {
note, err := m.mergeNote(ctx, task, login)
if commit == "" {
err = m.store.SetFinalizingUnchanged(ctx, taskID)
if err != nil {
return err
}
notes, err := git.NoteCommit(project.RepoPath, commit, note, env)
if err != nil {
return err
}
err = git.Push(project.RepoPath, commit, project.DefaultBranch, notes)
if err != nil {
return err
}
pushed = git.Remote + "/" + project.DefaultBranch
err = git.SetNotes(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)
}
err = git.FastForward(project.RepoPath, commit)
if err != nil {
return fmt.Errorf("pushed %s to %s, but fast-forwarding the project checkout failed: %w", commit, pushed, err)
}
return m.closeUnchanged(ctx, task, project, login)
}
err = m.RemoveTask(ctx, task, project, login)
note, err := m.mergeNote(ctx, task, login)
if err != nil {
return err
}
notes, err := git.NoteCommit(project.RepoPath, commit, note, env)
if err != nil {
return err
}
err = m.store.SetFinalizingCommit(ctx, taskID, commit, notes)
if err != nil {
return err
}
err = git.Push(project.RepoPath, commit, project.DefaultBranch, notes)
if errors.Is(err, git.ErrRemoteMoved) {
return err
}
if err != nil {
landed, lerr := pushLanded(project, commit)
if lerr != nil {
return &unfinishedMergeError{fmt.Errorf("%w, and %w", err, errors.Unwrap(lerr))}
}
if !landed {
return err
}
slog.Warn("push reported failure but landed", "task", taskID, "commit", commit, "err", err)
}
return m.closePushed(ctx, task, project, commit, notes, login)
}
_, err = m.store.MergeTask(ctx, taskID, commit, pushed, login)
return err
func (m *Manager) closeUnchanged(ctx context.Context, task api.Task, project api.Project, login string) error {
err := m.RemoveTask(ctx, task, project, login)
if err != nil {
return &unfinishedMergeError{fmt.Errorf("the merge changed nothing, but removing the task's worktree failed: %w", err)}
}
_, err = m.store.MergeTask(ctx, task.Id, "", "", login)
if err != nil {
return &unfinishedMergeError{fmt.Errorf("the merge changed nothing, but closing the task failed: %w", err)}
}
return nil
}
func (m *Manager) closePushed(ctx context.Context, task api.Task, project api.Project, commit, notes, login string) error {
pushed := git.Remote + "/" + project.DefaultBranch
err := git.AdvanceNotes(project.RepoPath, notes)
if err != nil {
return &unfinishedMergeError{fmt.Errorf("pushed %s to %s, but updating %s in the project checkout failed: %w", commit, pushed, git.NotesRef, err)}
}
err = git.FastForward(project.RepoPath, commit)
if err != nil {
return &unfinishedMergeError{fmt.Errorf("pushed %s to %s, but fast-forwarding the project checkout failed: %w", commit, pushed, err)}
}
err = m.RemoveTask(ctx, task, project, login)
if err != nil {
return &unfinishedMergeError{fmt.Errorf("pushed %s to %s, but removing the task's worktree failed: %w", commit, pushed, err)}
}
_, err = m.store.MergeTask(ctx, task.Id, commit, pushed, login)
if err != nil {
return &unfinishedMergeError{fmt.Errorf("pushed %s to %s, but closing the task failed: %w", commit, pushed, err)}
}
return nil
}
func (m *Manager) CoAuthors(ctx context.Context, taskID, login string) ([]api.Participant, error) {