add reviewer assigning
This commit is contained in:
183
pr_editor.go
183
pr_editor.go
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +16,8 @@ import (
|
||||
const (
|
||||
prEditTitleField = iota
|
||||
prEditBaseField
|
||||
prEditReviewersField
|
||||
prEditAssigneesField
|
||||
prEditBodyField
|
||||
prEditFieldCount
|
||||
)
|
||||
@@ -27,6 +31,12 @@ func (m *App) startPREdit() tea.Cmd {
|
||||
m.prEditField = prEditBodyField
|
||||
m.prEditEditors[prEditTitleField] = newTextEditor(m.details.Title, false)
|
||||
m.prEditEditors[prEditBaseField] = newTextEditor(m.details.BaseRef, false)
|
||||
m.prEditEditors[prEditReviewersField] = newTextEditor(
|
||||
strings.Join(m.details.RequestedReviewers, ", "), false,
|
||||
)
|
||||
m.prEditEditors[prEditAssigneesField] = newTextEditor(
|
||||
strings.Join(m.details.Assignees, ", "), false,
|
||||
)
|
||||
m.prEditEditors[prEditBodyField] = newTextEditor(
|
||||
normalizeLineEndings(m.details.Body),
|
||||
m.editorMode == "vim",
|
||||
@@ -42,11 +52,15 @@ func (m *App) startPREdit() tea.Cmd {
|
||||
m.prEditBranchesLoading = false
|
||||
m.prEditBranchesError = ""
|
||||
m.prEditBranchIndex = 0
|
||||
m.prEditUsers = nil
|
||||
m.prEditUsersLoading = false
|
||||
m.prEditUsersError = ""
|
||||
m.prEditUserIndex = 0
|
||||
m.scroll = 0
|
||||
m.err = m.prEditEditors[m.prEditField].err
|
||||
m.prEditEditors[m.prEditField].err = nil
|
||||
m.ensurePREditCursorVisible()
|
||||
return m.loadPREditBranches()
|
||||
return tea.Batch(m.loadPREditBranches(), m.loadPREditUsers())
|
||||
}
|
||||
|
||||
func (m *App) loadPREditBranches() tea.Cmd {
|
||||
@@ -65,6 +79,22 @@ func (m *App) loadPREditBranches() tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *App) loadPREditUsers() tea.Cmd {
|
||||
service, ok := m.service.(GitHubRepositoryPeopleService)
|
||||
if !ok {
|
||||
m.prEditUsersError = "configured GitHub service cannot list repository users"
|
||||
return nil
|
||||
}
|
||||
m.prEditUsersLoading = true
|
||||
owner, repo := m.details.Owner, m.details.Repository
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
users, err := service.ListRepositoryUsers(ctx, owner, repo)
|
||||
return repositoryUsersLoadedMsg{owner: owner, repo: repo, users: users, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (m App) pullRequestUpdateUnavailable() string {
|
||||
if m.loading {
|
||||
return "pull request update unavailable while PR data is refreshing"
|
||||
@@ -75,6 +105,9 @@ func (m App) pullRequestUpdateUnavailable() string {
|
||||
if _, ok := m.service.(GitHubPullRequestWriteService); !ok {
|
||||
return "configured GitHub service does not support pull request updates"
|
||||
}
|
||||
if _, ok := m.service.(GitHubPullRequestPeopleWriteService); !ok {
|
||||
return "configured GitHub service does not support reviewer and assignee updates"
|
||||
}
|
||||
if m.details.ID == "" {
|
||||
return "pull request details are not loaded"
|
||||
}
|
||||
@@ -118,6 +151,20 @@ func (m App) updatePREditInput(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case "n", "esc":
|
||||
m.writeMode = writePREdit
|
||||
m.ensurePREditCursorVisible()
|
||||
case "down":
|
||||
m.helpScroll = min(m.helpScroll+1, m.prEditConfirmationMaxScroll())
|
||||
case "up":
|
||||
m.helpScroll = max(0, m.helpScroll-1)
|
||||
case "ctrl+d":
|
||||
m.helpScroll = min(
|
||||
m.helpScroll+max(1, m.height/2), m.prEditConfirmationMaxScroll(),
|
||||
)
|
||||
case "ctrl+u":
|
||||
m.helpScroll = max(0, m.helpScroll-max(1, m.height/2))
|
||||
case "g":
|
||||
m.helpScroll = 0
|
||||
case "G":
|
||||
m.helpScroll = m.prEditConfirmationMaxScroll()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -134,11 +181,16 @@ func (m App) updatePREditInput(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
} else {
|
||||
m.writeMode = writePREditConfirm
|
||||
m.helpScroll = 0
|
||||
m.err = nil
|
||||
return m, nil
|
||||
}
|
||||
case "tab":
|
||||
if m.prEditField != prEditBaseField || !m.completeBranchSuggestion() {
|
||||
completed := m.prEditField == prEditBaseField && m.completeBranchSuggestion()
|
||||
if isPREditPeopleField(m.prEditField) {
|
||||
completed = m.completeUserSuggestion()
|
||||
}
|
||||
if !completed {
|
||||
m.movePREditField(1)
|
||||
}
|
||||
case "shift+tab":
|
||||
@@ -146,10 +198,14 @@ func (m App) updatePREditInput(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case "ctrl+n":
|
||||
if m.prEditField == prEditBaseField {
|
||||
m.moveBranchSuggestion(1)
|
||||
} else if isPREditPeopleField(m.prEditField) {
|
||||
m.moveUserSuggestion(1)
|
||||
}
|
||||
case "ctrl+p":
|
||||
if m.prEditField == prEditBaseField {
|
||||
m.moveBranchSuggestion(-1)
|
||||
} else if isPREditPeopleField(m.prEditField) {
|
||||
m.moveUserSuggestion(-1)
|
||||
}
|
||||
case "ctrl+d", "ctrl+u":
|
||||
if m.prEditField == prEditBodyField {
|
||||
@@ -165,6 +221,9 @@ func (m App) updatePREditInput(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.prEditField == prEditBaseField && m.completeBranchSuggestion() {
|
||||
break
|
||||
}
|
||||
if isPREditPeopleField(m.prEditField) && m.completeUserSuggestion() {
|
||||
break
|
||||
}
|
||||
if m.prEditField != prEditBodyField {
|
||||
m.movePREditField(1)
|
||||
} else {
|
||||
@@ -204,6 +263,9 @@ func (m App) updatePREditInput(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.prEditField == prEditBaseField && editor.Text != before {
|
||||
m.prEditBranchIndex = 0
|
||||
}
|
||||
if isPREditPeopleField(m.prEditField) && editor.Text != before {
|
||||
m.prEditUserIndex = 0
|
||||
}
|
||||
}
|
||||
m.err = nil
|
||||
m.ensurePREditCursorVisible()
|
||||
@@ -235,13 +297,44 @@ func (m App) positionPREditHardwareCursor(scroll, viewportHeight int) {
|
||||
|
||||
func (m App) submitPREdit() tea.Cmd {
|
||||
writer := m.service.(GitHubPullRequestWriteService)
|
||||
peopleWriter := m.service.(GitHubPullRequestPeopleWriteService)
|
||||
id := m.details.ID
|
||||
update := m.prEditMetadata()
|
||||
owner, repo, number := m.details.Owner, m.details.Repository, m.details.Number
|
||||
peopleUpdate := PullRequestPeopleUpdate{
|
||||
CurrentReviewers: slices.Clone(m.details.RequestedReviewers),
|
||||
CurrentAssignees: slices.Clone(m.details.Assignees),
|
||||
Reviewers: slices.Clone(update.Reviewers), Assignees: slices.Clone(update.Assignees),
|
||||
}
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
metadata, err := writer.UpdatePullRequest(ctx, id, update)
|
||||
return pullRequestUpdatedMsg{metadata: metadata, err: err}
|
||||
result := pullRequestUpdatedMsg{}
|
||||
peopleChanged := !slices.Equal(update.Reviewers, m.prEditOriginal.Reviewers) ||
|
||||
!slices.Equal(update.Assignees, m.prEditOriginal.Assignees)
|
||||
if peopleChanged {
|
||||
result.people, result.err = peopleWriter.UpdatePullRequestPeople(
|
||||
ctx, owner, repo, number, peopleUpdate,
|
||||
)
|
||||
if result.err != nil {
|
||||
result.peopleSaved =
|
||||
!equalLoginSets(result.people.Reviewers, peopleUpdate.CurrentReviewers) ||
|
||||
!equalLoginSets(result.people.Assignees, peopleUpdate.CurrentAssignees)
|
||||
return result
|
||||
}
|
||||
result.peopleSaved = true
|
||||
}
|
||||
if !samePRMetadataCore(update, m.prEditOriginal) {
|
||||
result.metadata, result.err = writer.UpdatePullRequest(ctx, id, update)
|
||||
if result.err != nil {
|
||||
return result
|
||||
}
|
||||
} else {
|
||||
result.metadata = m.prEditOriginal
|
||||
}
|
||||
result.metadata.Reviewers = slices.Clone(update.Reviewers)
|
||||
result.metadata.Assignees = slices.Clone(update.Assignees)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,8 +358,11 @@ func (m App) validatePREdit() error {
|
||||
return fmt.Errorf("target branch %q is not an available repository branch", update.BaseRef)
|
||||
}
|
||||
}
|
||||
if err := m.validatePREditUsers(update); err != nil {
|
||||
return err
|
||||
}
|
||||
if samePRMetadata(update, m.prEditOriginal) {
|
||||
return errors.New("title, target branch, and description are unchanged")
|
||||
return errors.New("pull request fields are unchanged")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -278,6 +374,8 @@ func (m App) prEditIsStale() bool {
|
||||
func (m App) currentPRMetadata() PullRequestMetadata {
|
||||
return PullRequestMetadata{
|
||||
Title: m.details.Title, Body: m.details.Body, BaseRef: m.details.BaseRef,
|
||||
Reviewers: normalizedLogins(m.details.RequestedReviewers),
|
||||
Assignees: normalizedLogins(m.details.Assignees),
|
||||
Mergeable: m.details.Mergeable, MergeState: m.details.MergeState,
|
||||
UpdatedAt: m.details.UpdatedAt,
|
||||
}
|
||||
@@ -292,24 +390,68 @@ func (m App) prEditMetadata() PullRequestMetadata {
|
||||
body = m.prEditOriginal.Body
|
||||
}
|
||||
return PullRequestMetadata{
|
||||
Title: strings.TrimSpace(m.prEditEditors[prEditTitleField].Text),
|
||||
Body: body,
|
||||
BaseRef: strings.TrimSpace(m.prEditEditors[prEditBaseField].Text),
|
||||
Title: strings.TrimSpace(m.prEditEditors[prEditTitleField].Text),
|
||||
Body: body,
|
||||
BaseRef: strings.TrimSpace(m.prEditEditors[prEditBaseField].Text),
|
||||
Reviewers: parseLoginList(m.prEditEditors[prEditReviewersField].Text),
|
||||
Assignees: parseLoginList(m.prEditEditors[prEditAssigneesField].Text),
|
||||
}
|
||||
}
|
||||
|
||||
func samePRMetadata(left, right PullRequestMetadata) bool {
|
||||
return samePRMetadataCore(left, right) &&
|
||||
slices.Equal(left.Reviewers, right.Reviewers) &&
|
||||
slices.Equal(left.Assignees, right.Assignees)
|
||||
}
|
||||
|
||||
func samePRMetadataCore(left, right PullRequestMetadata) bool {
|
||||
return left.Title == right.Title && left.Body == right.Body && left.BaseRef == right.BaseRef
|
||||
}
|
||||
|
||||
func (m *App) applyPREditPeople(people PullRequestPeople) {
|
||||
oldRequested := make(map[string]bool, len(m.details.RequestedReviewers))
|
||||
for _, login := range m.details.RequestedReviewers {
|
||||
oldRequested[strings.ToLower(login)] = true
|
||||
}
|
||||
desired := make(map[string]bool, len(people.Reviewers))
|
||||
for _, login := range people.Reviewers {
|
||||
desired[strings.ToLower(login)] = true
|
||||
}
|
||||
filtered := m.details.Reviewers[:0]
|
||||
known := make(map[string]bool)
|
||||
for _, reviewer := range m.details.Reviewers {
|
||||
key := strings.ToLower(reviewer.Login)
|
||||
if oldRequested[key] && reviewer.State == "REVIEW_REQUESTED" && !desired[key] {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, reviewer)
|
||||
known[key] = true
|
||||
}
|
||||
for _, login := range people.Reviewers {
|
||||
if !known[strings.ToLower(login)] {
|
||||
filtered = append(filtered, Reviewer{Login: login, State: "REVIEW_REQUESTED"})
|
||||
}
|
||||
}
|
||||
sort.Slice(filtered, func(i, j int) bool {
|
||||
return strings.ToLower(filtered[i].Login) < strings.ToLower(filtered[j].Login)
|
||||
})
|
||||
m.details.Reviewers = filtered
|
||||
m.details.RequestedReviewers = slices.Clone(people.Reviewers)
|
||||
m.details.Assignees = slices.Clone(people.Assignees)
|
||||
}
|
||||
|
||||
func (m *App) clearPREdit() {
|
||||
m.prEditField = 0
|
||||
m.prEditEditors = [3]textEditor{}
|
||||
m.prEditEditors = [prEditFieldCount]textEditor{}
|
||||
m.prEditOriginal = PullRequestMetadata{}
|
||||
m.prEditBranches = nil
|
||||
m.prEditBranchesLoading = false
|
||||
m.prEditBranchesError = ""
|
||||
m.prEditBranchIndex = 0
|
||||
m.prEditUsers = nil
|
||||
m.prEditUsersLoading = false
|
||||
m.prEditUsersError = ""
|
||||
m.prEditUserIndex = 0
|
||||
}
|
||||
|
||||
func (m *App) movePREditField(delta int) {
|
||||
@@ -387,6 +529,8 @@ func (m App) dashboardEditLayout() ([]string, int) {
|
||||
}
|
||||
appendField("title", prEditTitleField)
|
||||
appendField("target branch", prEditBaseField)
|
||||
appendField("reviewers", prEditReviewersField)
|
||||
appendField("assignees", prEditAssigneesField)
|
||||
appendField("description", prEditBodyField)
|
||||
return lines, cursorLine
|
||||
}
|
||||
@@ -422,6 +566,9 @@ func (m App) prEditFieldLines(label string, field, width int) []string {
|
||||
if active && field == prEditBaseField {
|
||||
lines = append(lines, m.branchCompletionLines(max(1, width-2))...)
|
||||
}
|
||||
if active && isPREditPeopleField(field) {
|
||||
lines = append(lines, m.userCompletionLines(max(1, width-2))...)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
@@ -469,6 +616,20 @@ func (m App) prEditConfirmationLines(width int) []string {
|
||||
len([]rune(m.prEditOriginal.Body)), len([]rune(update.Body)),
|
||||
), "")
|
||||
}
|
||||
if !slices.Equal(update.Reviewers, m.prEditOriginal.Reviewers) {
|
||||
lines = append(lines,
|
||||
dimStyle.Render("reviewers"),
|
||||
loginChangeSummary(m.prEditOriginal.Reviewers, update.Reviewers),
|
||||
"",
|
||||
)
|
||||
}
|
||||
if !slices.Equal(update.Assignees, m.prEditOriginal.Assignees) {
|
||||
lines = append(lines,
|
||||
dimStyle.Render("assignees"),
|
||||
loginChangeSummary(m.prEditOriginal.Assignees, update.Assignees),
|
||||
"",
|
||||
)
|
||||
}
|
||||
lines = append(lines, warnStyle.Render(fmt.Sprintf(
|
||||
"%s submit • %s continue editing",
|
||||
primaryKeyLabel(m.keybindings.General.Confirm),
|
||||
@@ -476,3 +637,7 @@ func (m App) prEditConfirmationLines(width int) []string {
|
||||
)))
|
||||
return lines
|
||||
}
|
||||
|
||||
func (m App) prEditConfirmationMaxScroll() int {
|
||||
return max(0, len(m.prEditConfirmationLines(max(1, min(74, m.width-6))))-max(3, m.height-4))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user