Add Co-authored-by trailers to Merge's squash commit

This commit is contained in:
Ian Gulliver
2026-10-15 07:15:51 -07:00
parent c778741a4a
commit 60a1d221f4
15 changed files with 478 additions and 123 deletions
+106 -2
View File
@@ -407,11 +407,16 @@ func (e *env) openQuestions(sessionID string) []api.InboxItem {
}
func (e *env) respond(m *agents.Manager, item api.InboxItem, typ api.InboxResponseType, option, text string, state api.InboxState) {
e.t.Helper()
e.respondAs(m, login, item, typ, option, text, state)
}
func (e *env) respondAs(m *agents.Manager, user string, item api.InboxItem, typ api.InboxResponseType, option, text string, state api.InboxState) {
e.t.Helper()
resp := api.InboxResponse{
Id: store.NewID(),
ItemId: item.Id,
User: login,
User: user,
Type: typ,
Attachments: []api.Attachment{},
CreatedAt: time.Now().UTC(),
@@ -423,7 +428,7 @@ func (e *env) respond(m *agents.Manager, item api.InboxItem, typ api.InboxRespon
resp.Text = &text
}
err := e.store.Respond(e.ctx, login, []store.Resolution{{Response: resp, State: state}})
err := e.store.Respond(e.ctx, user, []store.Resolution{{Response: resp, State: state}})
if err != nil {
e.t.Fatalf("respond: %v", err)
}
@@ -672,6 +677,105 @@ func TestMergeResolvesConflict(t *testing.T) {
}
}
func TestMergeCoAuthors(t *testing.T) {
t.Parallel()
e := newEnv(t)
m := e.manager()
people := []struct{ login, name, gitName, gitEmail string }{
{"carol@example.com", "Carol", "Carol Test", "carol@byre.invalid"},
{"bob@example.com", "Bob", "Bob Test", "bob@byre.invalid"},
{"dave@example.com", "Dave", "", ""},
}
const dismisser = "erin@example.com"
_, err := e.store.EnsureUser(e.ctx, dismisser, "Erin")
if err != nil {
t.Fatalf("ensure user: %v", err)
}
_, err = e.store.UpdateUserGit(e.ctx, dismisser, "Erin Test", "erin@byre.invalid")
if err != nil {
t.Fatalf("update user: %v", err)
}
for _, p := range people {
_, err := e.store.EnsureUser(e.ctx, p.login, p.name)
if err != nil {
t.Fatalf("ensure user: %v", err)
}
if p.gitName != "" {
_, err = e.store.UpdateUserGit(e.ctx, p.login, p.gitName, p.gitEmail)
if err != nil {
t.Fatalf("update user: %v", err)
}
}
}
sess, err := m.Create(e.ctx, e.task, e.user, colorPrompt, testModel)
if err != nil {
t.Fatalf("create session: %v", err)
}
e.waitIdle()
questions := e.openQuestions(sess.Id)
if len(questions) != 1 {
t.Fatalf("want one open question, got %+v", questions)
}
options, _ := questions[0].Payload["options"].([]any)
e.respondAs(m, people[0].login, questions[0], api.Option, fmt.Sprint(options[0]), "", api.Answered)
e.waitIdle()
for _, p := range people[1:] {
_, err = m.Send(e.ctx, sess.Id, p.login, "Reply with the single word ok.", "")
if err != nil {
t.Fatalf("send as %s: %v", p.login, err)
}
e.waitIdle()
}
reports, err := e.store.ListInbox(e.ctx, store.InboxFilter{TaskID: e.task.Id, State: api.Open})
if err != nil || len(reports) == 0 {
t.Fatalf("want open reports to dismiss: %+v %v", reports, err)
}
e.respondAs(m, dismisser, reports[0], api.Dismiss, "", "", api.Dismissed)
coAuthors, err := m.CoAuthors(e.ctx, e.task.Id, login)
if err != nil {
t.Fatalf("co-authors: %v", err)
}
got := []string{}
for _, p := range coAuthors {
got = append(got, p.Login+"|"+p.GitName+"|"+p.GitEmail)
}
want := []string{}
for _, p := range people {
want = append(want, p.login+"|"+p.gitName+"|"+p.gitEmail)
}
if strings.Join(got, ",") != strings.Join(want, ",") {
t.Fatalf("co-authors %v, want %v", got, want)
}
_, err = m.Merge(e.ctx, e.task, e.project, e.user)
if err != nil {
t.Fatalf("merge: %v", err)
}
e.waitFor("task merged", func(ev api.Event) bool { return ev.Kind == "task.merged" })
message := gitOut(t, e.repo, "log", "-1", "--format=%B")
t.Logf("merge commit message:\n%s", message)
if author := gitOut(t, e.repo, "log", "-1", "--format=%an <%ae>|%cn <%ce>"); author != gitName+" <"+gitEmail+">|"+gitName+" <"+gitEmail+">" {
t.Fatalf("merge commit author|committer %s, want %s", author, gitName)
}
cmd := exec.Command("git", "interpret-trailers", "--parse")
cmd.Stdin = strings.NewReader(message)
out, err := cmd.Output()
if err != nil {
t.Fatalf("interpret-trailers: %v", err)
}
trailers := strings.TrimSpace(string(out))
wantTrailers := "Co-authored-by: Carol Test <carol@byre.invalid>\nCo-authored-by: Bob Test <bob@byre.invalid>"
if trailers != wantTrailers {
t.Fatalf("trailers:\n%s\nwant:\n%s", trailers, wantTrailers)
}
}
func TestMergeFetchesMovedRemote(t *testing.T) {
t.Parallel()
e := newEnv(t)
+35 -1
View File
@@ -100,7 +100,12 @@ func (m *Manager) squashAndClose(ctx context.Context, taskID, login string) erro
return err
}
commit, err := git.SquashCommit(project.RepoPath, task.WorktreePath, project.DefaultBranch, task.MergeTitle, []string{
coAuthors, err := m.CoAuthors(ctx, taskID, login)
if err != nil {
return err
}
commit, err := git.SquashCommit(project.RepoPath, task.WorktreePath, project.DefaultBranch, squashMessage(task.MergeTitle, coAuthors), []string{
"GIT_AUTHOR_NAME=" + user.GitName,
"GIT_AUTHOR_EMAIL=" + user.GitEmail,
"GIT_COMMITTER_NAME=" + user.GitName,
@@ -132,6 +137,35 @@ func (m *Manager) squashAndClose(ctx context.Context, taskID, login string) erro
return err
}
func (m *Manager) CoAuthors(ctx context.Context, taskID, login string) ([]api.Participant, error) {
participants, err := m.store.Participants(ctx, taskID)
if err != nil {
return nil, err
}
coAuthors := []api.Participant{}
for _, p := range participants {
if p.Login != login {
coAuthors = append(coAuthors, p)
}
}
return coAuthors, nil
}
func squashMessage(title string, coAuthors []api.Participant) string {
trailers := []string{}
for _, p := range coAuthors {
if p.GitName == "" || p.GitEmail == "" {
continue
}
trailers = append(trailers, fmt.Sprintf("Co-authored-by: %s <%s>", p.GitName, p.GitEmail))
}
if len(trailers) == 0 {
return title
}
return title + "\n\n" + strings.Join(trailers, "\n")
}
func (m *Manager) RemoveTask(ctx context.Context, task api.Task, project api.Project, actor string) error {
err := m.StopTask(ctx, task.Id, actor)
if err != nil {