diff --git a/.changes/unreleased/Added-20260314-155204.yaml b/.changes/unreleased/Added-20260314-155204.yaml new file mode 100644 index 000000000..ffd94f42f --- /dev/null +++ b/.changes/unreleased/Added-20260314-155204.yaml @@ -0,0 +1,3 @@ +kind: Added +body: 'branch comment: Add commands for managing change request review comments from the CLI' +time: 2026-03-14T15:52:04.220487-04:00 diff --git a/.changes/unreleased/Added-20260502-102600.yaml b/.changes/unreleased/Added-20260502-102600.yaml new file mode 100644 index 000000000..af000b4cb --- /dev/null +++ b/.changes/unreleased/Added-20260502-102600.yaml @@ -0,0 +1,3 @@ +kind: Added +body: 'branch comment list: Add --json flag to write comments as a stream of JSON objects' +time: 2026-05-02T10:26:00.668941-04:00 diff --git a/branch.go b/branch.go index 160587832..e359605ba 100644 --- a/branch.go +++ b/branch.go @@ -38,6 +38,9 @@ type branchCmd struct { // Pull request management Merge branchMergeCmd `cmd:"" aliases:"m" experiment:"merge" help:"Merge a branch into trunk"` Submit branchSubmitCmd `cmd:"" aliases:"s" help:"Submit a branch"` + + // Comment management + Comment branchCommentCmd `cmd:"" aliases:"cmt" help:"Manage change request comments"` } // BranchPromptConfig defines configuration for the branch tree prompt diff --git a/branch_comment.go b/branch_comment.go new file mode 100644 index 000000000..a1ec02b5d --- /dev/null +++ b/branch_comment.go @@ -0,0 +1,10 @@ +package main + +type branchCommentCmd struct { + List branchCommentListCmd `cmd:"" aliases:"ls" help:"List comments on a change request"` + Stage branchCommentStageCmd `cmd:"" help:"Stage an inline comment for batch submission"` + Add branchCommentAddCmd `cmd:"" help:"Post an inline comment immediately"` + SubmitStaged branchCommentSubmitStagedCmd `cmd:"" aliases:"ss" help:"Submit all staged comments as a review"` + Resolve branchCommentResolveCmd `cmd:"" help:"Resolve or unresolve a review thread"` + Edit branchCommentEditCmd `cmd:"" help:"Edit a comment"` +} diff --git a/branch_comment_add.go b/branch_comment_add.go new file mode 100644 index 000000000..e53c32bbf --- /dev/null +++ b/branch_comment_add.go @@ -0,0 +1,153 @@ +package main + +import ( + "context" + "errors" + "fmt" + "strings" + + "go.abhg.dev/gs/internal/diffmap" + "go.abhg.dev/gs/internal/forge" + "go.abhg.dev/gs/internal/git" + "go.abhg.dev/gs/internal/silog" + "go.abhg.dev/gs/internal/spice" + "go.abhg.dev/gs/internal/spice/state" + "go.abhg.dev/gs/internal/text" +) + +type branchCommentAddCmd struct { + FileAndLine string `arg:"" optional:"" help:"File and line in the form file.go:42."` + Message string `short:"m" placeholder:"MSG" help:"Comment body. Opens editor if not provided."` + Respond string `placeholder:"THREAD_ID" help:"Thread ID to reply to instead of starting a new thread."` + Branch string `short:"b" placeholder:"BRANCH" predictor:"trackedBranches" help:"Branch to add comment for. Defaults to current branch."` +} + +func (*branchCommentAddCmd) Help() string { + return text.Dedent(` + Posts an inline comment immediately + on the change request for the current branch. + Provide the file and line number as file.go:42. + + If no message is given with -m, an editor is opened. + + Use --respond to reply to an existing thread + instead of starting a new one. + `) +} + +func (cmd *branchCommentAddCmd) Run( + ctx context.Context, + log *silog.Logger, + wt *git.Worktree, + svc *spice.Service, + repo *git.Repository, + forgeRepo forge.Repository, +) error { + branch := cmd.Branch + if branch == "" { + var err error + branch, err = wt.CurrentBranch(ctx) + if err != nil { + return fmt.Errorf("get current branch: %w", err) + } + } + + var file string + var line int + if cmd.Respond == "" { + if cmd.FileAndLine == "" { + return errors.New( + "file:line argument is required " + + "unless --respond is used", + ) + } + var err error + file, line, err = parseFileAndLine(cmd.FileAndLine) + if err != nil { + return err + } + } + + body := cmd.Message + if body == "" { + var err error + body, err = editCommentBody( + ctx, repo, "" /* initial */) + if err != nil { + return err + } + } + if strings.TrimSpace(body) == "" { + return errors.New("empty comment body, aborting") + } + + b, err := svc.LookupBranch(ctx, branch) + if err != nil { + if errors.Is(err, state.ErrNotExist) { + return fmt.Errorf( + "branch not tracked: %s", branch, + ) + } + return fmt.Errorf("get branch: %w", err) + } + + if b.Change == nil { + return fmt.Errorf( + "no change request for %s; "+ + "submit the branch first with "+ + "'gs branch submit'", + branch, + ) + } + + inline, ok := forgeRepo.(forge.WithInlineComments) + if !ok { + return errors.New( + "forge does not support inline comments", + ) + } + + req := forge.InlineCommentRequest{ + Body: body, + ThreadID: cmd.Respond, + } + + // Map file:line to diff coordinates + // if this is a new comment (not a reply). + if cmd.Respond == "" { + diff, err := wt.DiffBranchBytes(ctx, b.Base, branch) + if err != nil { + return fmt.Errorf("get diff: %w", err) + } + + mapper, err := diffmap.New(diff) + if err != nil { + return fmt.Errorf("parse diff: %w", err) + } + + path, diffLine, side, err := mapper.Map(file, line) + if err != nil { + return fmt.Errorf( + "map %s:%d to diff: %w", + file, line, err, + ) + } + + req.Path = path + req.Line = diffLine + req.Side = side + } + + posted, err := inline.PostInlineComment( + ctx, b.Change.ChangeID(), req, + ) + if err != nil { + return fmt.Errorf("post inline comment: %w", err) + } + + log.Infof( + "Posted comment %s on %s.", + posted.ID, b.Change.ChangeID(), + ) + return nil +} diff --git a/branch_comment_edit.go b/branch_comment_edit.go new file mode 100644 index 000000000..be7d9fa7d --- /dev/null +++ b/branch_comment_edit.go @@ -0,0 +1,220 @@ +package main + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + + "go.abhg.dev/gs/internal/forge" + "go.abhg.dev/gs/internal/git" + "go.abhg.dev/gs/internal/silog" + "go.abhg.dev/gs/internal/spice" + "go.abhg.dev/gs/internal/spice/state" + "go.abhg.dev/gs/internal/text" +) + +type branchCommentEditCmd struct { + ID string `arg:"" help:"Comment ID to edit. Use 'sc-N' for staged comments or a forge comment ID."` + Message string `short:"m" placeholder:"MSG" help:"New comment body. Opens editor if not provided."` + Branch string `short:"b" placeholder:"BRANCH" predictor:"trackedBranches" help:"Branch whose comments to edit. Defaults to current branch."` +} + +func (*branchCommentEditCmd) Help() string { + return text.Dedent(` + Edits the body of a comment. + + For staged comments (sc-N prefix), + the comment is updated in the local staging area. + + For forge comments, the comment is updated + on the remote forge. + + If no message is given with -m, an editor is opened + with the current comment body pre-filled. + `) +} + +func (cmd *branchCommentEditCmd) Run( + ctx context.Context, + log *silog.Logger, + wt *git.Worktree, + svc *spice.Service, + store *state.Store, + repo *git.Repository, + forgeRepo forge.Repository, +) error { + branch := cmd.Branch + if branch == "" { + var err error + branch, err = wt.CurrentBranch(ctx) + if err != nil { + return fmt.Errorf("get current branch: %w", err) + } + } + + // Handle staged comment edits. + if scID, ok := parseStagedCommentID(cmd.ID); ok { + return cmd.editStaged( + ctx, log, store, repo, branch, scID, + ) + } + + // Handle forge comment edits. + return cmd.editForge( + ctx, log, wt, svc, repo, forgeRepo, branch, + ) +} + +func (cmd *branchCommentEditCmd) editStaged( + ctx context.Context, + log *silog.Logger, + store *state.Store, + repo *git.Repository, + branch string, + scID int, +) error { + staged, err := store.LoadStagedComments(ctx, branch) + if err != nil { + return fmt.Errorf("load staged comments: %w", err) + } + if staged == nil { + staged = &state.StagedComments{} + } + + idx := -1 + for i, c := range staged.Comments { + if c.ID == scID { + idx = i + break + } + } + if idx < 0 { + return fmt.Errorf( + "staged comment sc-%d not found", scID, + ) + } + + body := cmd.Message + if body == "" { + var err error + body, err = editCommentBody( + ctx, repo, staged.Comments[idx].Body, + ) + if err != nil { + return err + } + } + if strings.TrimSpace(body) == "" { + return errors.New("empty comment body, aborting") + } + + staged.Comments[idx].Body = body + if err := store.SaveStagedComments( + ctx, branch, staged, + ); err != nil { + return fmt.Errorf("save staged comments: %w", err) + } + + log.Infof("Updated staged comment sc-%d.", scID) + return nil +} + +func (cmd *branchCommentEditCmd) editForge( + ctx context.Context, + log *silog.Logger, + _ *git.Worktree, + svc *spice.Service, + repo *git.Repository, + forgeRepo forge.Repository, + branch string, +) error { + b, err := svc.LookupBranch(ctx, branch) + if err != nil { + if errors.Is(err, state.ErrNotExist) { + return fmt.Errorf( + "branch not tracked: %s", branch, + ) + } + return fmt.Errorf("get branch: %w", err) + } + + if b.Change == nil { + return fmt.Errorf( + "no change request for %s", branch, + ) + } + + editor, ok := forgeRepo.(forge.WithCommentEdit) + if !ok { + return errors.New( + "forge does not support comment editing", + ) + } + + // Find the comment by ID to get its current body. + inline, ok := forgeRepo.(forge.WithInlineComments) + if !ok { + return errors.New( + "forge does not support inline comments", + ) + } + + comments, err := inline.ListInlineComments( + ctx, b.Change.ChangeID(), + ) + if err != nil { + return fmt.Errorf("list comments: %w", err) + } + + var target *forge.InlineComment + for _, c := range comments { + if c.ID.String() == cmd.ID { + target = c + break + } + } + if target == nil { + return fmt.Errorf( + "comment %s not found", cmd.ID, + ) + } + + body := cmd.Message + if body == "" { + var err error + body, err = editCommentBody( + ctx, repo, target.Body, + ) + if err != nil { + return err + } + } + if strings.TrimSpace(body) == "" { + return errors.New("empty comment body, aborting") + } + + if err := editor.EditComment( + ctx, target.ID, body, + ); err != nil { + return fmt.Errorf("edit comment: %w", err) + } + + log.Infof("Updated comment %s.", cmd.ID) + return nil +} + +// parseStagedCommentID parses "sc-N" into integer N. +// Returns (N, true) on success, (0, false) otherwise. +func parseStagedCommentID(s string) (int, bool) { + after, found := strings.CutPrefix(s, "sc-") + if !found { + return 0, false + } + id, err := strconv.Atoi(after) + if err != nil { + return 0, false + } + return id, true +} diff --git a/branch_comment_list.go b/branch_comment_list.go new file mode 100644 index 000000000..c0ef9f57b --- /dev/null +++ b/branch_comment_list.go @@ -0,0 +1,361 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "strings" + "time" + + "github.com/alecthomas/kong" + "go.abhg.dev/gs/internal/forge" + "go.abhg.dev/gs/internal/git" + "go.abhg.dev/gs/internal/silog" + "go.abhg.dev/gs/internal/spice" + "go.abhg.dev/gs/internal/spice/state" + "go.abhg.dev/gs/internal/text" +) + +type branchCommentListCmd struct { + Branch string `short:"b" placeholder:"BRANCH" predictor:"trackedBranches" help:"Branch to list comments for. Defaults to current branch."` + Staged bool `help:"Show only staged comments."` + Unresolved bool `help:"Show only unresolved comments."` + JSON bool `name:"json" released:"unreleased" help:"Write to stdout as a stream of JSON objects."` +} + +func (*branchCommentListCmd) Help() string { + return text.Dedent(` + Lists comments on the change request + associated with the current branch. + Use --branch to target a different branch. + + Staged comments that have not yet been submitted + are shown with an 'sc-N' prefix. + + Use --staged to show only staged comments. + Use --unresolved to show only unresolved comments. + + With --json, prints output to stdout + as a stream of JSON objects. + `) +} + +func (cmd *branchCommentListCmd) Run( + ctx context.Context, + kctx *kong.Context, + log *silog.Logger, + wt *git.Worktree, + svc *spice.Service, + store *state.Store, + forgeRepo forge.Repository, +) error { + branch, err := cmd.resolveBranch(ctx, wt) + if err != nil { + return err + } + + staged, forgeComments, err := cmd.loadComments( + ctx, log, svc, store, forgeRepo, branch, + ) + if err != nil { + return err + } + + if cmd.JSON { + return cmd.writeJSON( + kctx.Stdout, staged, forgeComments, + ) + } + return cmd.writeText(log, branch, staged, forgeComments) +} + +func (cmd *branchCommentListCmd) resolveBranch( + ctx context.Context, wt *git.Worktree, +) (string, error) { + if cmd.Branch != "" { + return cmd.Branch, nil + } + branch, err := wt.CurrentBranch(ctx) + if err != nil { + return "", fmt.Errorf("get current branch: %w", err) + } + return branch, nil +} + +func (cmd *branchCommentListCmd) loadComments( + ctx context.Context, + log *silog.Logger, + svc *spice.Service, + store *state.Store, + forgeRepo forge.Repository, + branch string, +) ([]*state.StagedComment, []*forge.InlineComment, error) { + staged, err := loadStagedComments(ctx, store, branch) + if err != nil { + return nil, nil, err + } + + if cmd.Staged { + return staged, nil, nil + } + + forgeComments, err := loadForgeComments( + ctx, log, svc, forgeRepo, branch, + ) + if err != nil { + return nil, nil, err + } + + return staged, cmd.filterForge(forgeComments), nil +} + +func loadStagedComments( + ctx context.Context, + store *state.Store, + branch string, +) ([]*state.StagedComment, error) { + staged, err := store.LoadStagedComments(ctx, branch) + if err != nil { + return nil, fmt.Errorf( + "load staged comments: %w", err, + ) + } + if staged == nil { + return nil, nil + } + + refs := make( + []*state.StagedComment, len(staged.Comments), + ) + for i := range staged.Comments { + refs[i] = &staged.Comments[i] + } + return refs, nil +} + +func loadForgeComments( + ctx context.Context, + log *silog.Logger, + svc *spice.Service, + forgeRepo forge.Repository, + branch string, +) ([]*forge.InlineComment, error) { + b, err := svc.LookupBranch(ctx, branch) + if err != nil { + if errors.Is(err, state.ErrNotExist) { + return nil, fmt.Errorf( + "branch not tracked: %s", branch, + ) + } + return nil, fmt.Errorf("get branch: %w", err) + } + + if b.Change == nil { + log.Infof( + "No change request found for %s.", branch, + ) + return nil, nil + } + + inline, ok := forgeRepo.(forge.WithInlineComments) + if !ok { + log.Infof( + "Forge does not support inline comments.", + ) + return nil, nil + } + + comments, err := inline.ListInlineComments( + ctx, b.Change.ChangeID(), + ) + if err != nil { + return nil, fmt.Errorf( + "list inline comments: %w", err, + ) + } + return comments, nil +} + +func (cmd *branchCommentListCmd) filterForge( + comments []*forge.InlineComment, +) []*forge.InlineComment { + if !cmd.Unresolved { + return comments + } + + var filtered []*forge.InlineComment + for _, c := range comments { + if !c.Resolved { + filtered = append(filtered, c) + } + } + return filtered +} + +// writeText prints comments in human-readable format. +func (cmd *branchCommentListCmd) writeText( + log *silog.Logger, + branch string, + staged []*state.StagedComment, + forgeComments []*forge.InlineComment, +) error { + if len(staged) > 0 { + log.Infof("Staged comments:") + for _, c := range staged { + writeStagedText(log, c) + } + } + + if cmd.Staged && len(staged) == 0 { + log.Infof("No staged comments for %s.", branch) + return nil + } + + if len(forgeComments) > 0 { + log.Infof("Comments:") + for _, c := range forgeComments { + writeForgeText(log, c) + } + } + + if len(forgeComments) == 0 && len(staged) == 0 { + log.Infof("No comments on %s.", branch) + } + return nil +} + +func writeStagedText( + log *silog.Logger, c *state.StagedComment, +) { + location := fmt.Sprintf("%s:%d", c.File, c.Line) + if c.ThreadID != "" { + location = "reply:" + c.ThreadID + } + log.Infof(" sc-%-4d %s", c.ID, location) + writeBodyIndented(log, c.Body) +} + +func writeForgeText( + log *silog.Logger, c *forge.InlineComment, +) { + location := fmt.Sprintf("%s:%d", c.Path, c.Line) + threadInfo := "" + if c.ThreadID != "" { + threadInfo = " [" + c.ThreadID + "]" + } + log.Infof( + " %-12s %s %s %s%s", + c.ID, location, c.Author, + commentStatus(c), threadInfo, + ) + writeBodyIndented(log, c.Body) +} + +func writeBodyIndented(log *silog.Logger, body string) { + for line := range strings.SplitSeq(body, "\n") { + log.Infof(" %s", line) + } +} + +func commentStatus(c *forge.InlineComment) string { + if c.Outdated { + return "outdated" + } + if c.Resolved { + return "resolved" + } + return "open" +} + +// writeJSON encodes comments as NDJSON to stdout. +func (cmd *branchCommentListCmd) writeJSON( + w io.Writer, + staged []*state.StagedComment, + forgeComments []*forge.InlineComment, +) (retErr error) { + bufw := bufio.NewWriter(w) + defer func() { + retErr = errors.Join(retErr, bufw.Flush()) + }() + + enc := json.NewEncoder(bufw) + for _, c := range staged { + if err := enc.Encode(stagedToJSON(c)); err != nil { + return fmt.Errorf("encode staged: %w", err) + } + } + for _, c := range forgeComments { + if err := enc.Encode(forgeToJSON(c)); err != nil { + return fmt.Errorf("encode forge: %w", err) + } + } + return nil +} + +func stagedToJSON(c *state.StagedComment) jsonComment { + return jsonComment{ + Kind: "staged", + ID: fmt.Sprintf("sc-%d", c.ID), + Path: c.File, + Line: c.Line, + Body: c.Body, + ThreadID: c.ThreadID, + } +} + +func forgeToJSON(c *forge.InlineComment) jsonComment { + var createdAt *time.Time + if !c.CreatedAt.IsZero() { + createdAt = &c.CreatedAt + } + return jsonComment{ + Kind: "forge", + ID: c.ID.String(), + Path: c.Path, + Line: c.Line, + Body: c.Body, + ThreadID: c.ThreadID, + Author: c.Author, + Status: commentStatus(c), + CreatedAt: createdAt, + } +} + +// jsonComment is the JSON representation +// of a comment for --json output. +type jsonComment struct { + // Kind is "staged" or "forge". + Kind string `json:"kind"` + + // ID is the comment identifier. + // For staged comments: "sc-N". + // For forge comments: forge-specific ID. + ID string `json:"id"` + + // Path is the file path relative to the repo root. + Path string `json:"path,omitempty"` + + // Line is the line number in the file. + Line int `json:"line,omitempty"` + + // Body is the full markdown body of the comment. + Body string `json:"body"` + + // ThreadID is the thread identifier, if any. + ThreadID string `json:"threadID,omitempty"` + + // Author is the username of the comment author. + // Only set for forge comments. + Author string `json:"author,omitempty"` + + // Status is "open", "resolved", or "outdated". + // Only set for forge comments. + Status string `json:"status,omitempty"` + + // CreatedAt is the time the comment was created. + // Only set for forge comments. + CreatedAt *time.Time `json:"createdAt,omitempty"` +} diff --git a/branch_comment_resolve.go b/branch_comment_resolve.go new file mode 100644 index 000000000..60bf544c3 --- /dev/null +++ b/branch_comment_resolve.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "errors" + "fmt" + + "go.abhg.dev/gs/internal/forge" + "go.abhg.dev/gs/internal/git" + "go.abhg.dev/gs/internal/silog" + "go.abhg.dev/gs/internal/spice" + "go.abhg.dev/gs/internal/spice/state" + "go.abhg.dev/gs/internal/text" +) + +type branchCommentResolveCmd struct { + ThreadID string `arg:"" help:"Thread ID to resolve."` + Unresolve bool `help:"Unresolve the thread instead of resolving it."` + Branch string `short:"b" placeholder:"BRANCH" predictor:"trackedBranches" help:"Branch whose change request contains the thread. Defaults to current branch."` +} + +func (*branchCommentResolveCmd) Help() string { + return text.Dedent(` + Resolves a review thread on the change request + for the current branch. + + Use --unresolve to mark the thread as unresolved. + + The thread ID is shown in 'gs branch comment list'. + `) +} + +func (cmd *branchCommentResolveCmd) Run( + ctx context.Context, + log *silog.Logger, + wt *git.Worktree, + svc *spice.Service, + forgeRepo forge.Repository, +) error { + branch := cmd.Branch + if branch == "" { + var err error + branch, err = wt.CurrentBranch(ctx) + if err != nil { + return fmt.Errorf("get current branch: %w", err) + } + } + + // Verify branch has a change request. + b, err := svc.LookupBranch(ctx, branch) + if err != nil { + if errors.Is(err, state.ErrNotExist) { + return fmt.Errorf( + "branch not tracked: %s", branch, + ) + } + return fmt.Errorf("get branch: %w", err) + } + + if b.Change == nil { + return fmt.Errorf( + "no change request for %s", branch, + ) + } + + resolver, ok := forgeRepo.(forge.WithThreadResolution) + if !ok { + return errors.New( + "forge does not support thread resolution", + ) + } + + if cmd.Unresolve { + if err := resolver.UnresolveThread( + ctx, cmd.ThreadID, + ); err != nil { + return fmt.Errorf( + "unresolve thread: %w", err, + ) + } + log.Infof("Unresolved thread %s.", cmd.ThreadID) + } else { + if err := resolver.ResolveThread( + ctx, cmd.ThreadID, + ); err != nil { + return fmt.Errorf("resolve thread: %w", err) + } + log.Infof("Resolved thread %s.", cmd.ThreadID) + } + + return nil +} diff --git a/branch_comment_stage.go b/branch_comment_stage.go new file mode 100644 index 000000000..b5eb87eec --- /dev/null +++ b/branch_comment_stage.go @@ -0,0 +1,177 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "go.abhg.dev/gs/internal/git" + "go.abhg.dev/gs/internal/silog" + "go.abhg.dev/gs/internal/spice/state" + "go.abhg.dev/gs/internal/text" + "go.abhg.dev/gs/internal/xec" +) + +type branchCommentStageCmd struct { + FileAndLine string `arg:"" optional:"" help:"File and line in the form file.go:42."` + Message string `short:"m" placeholder:"MSG" help:"Comment body. Opens editor if not provided."` + Respond string `placeholder:"THREAD_ID" help:"Thread ID to reply to instead of starting a new thread."` + Branch string `short:"b" placeholder:"BRANCH" predictor:"trackedBranches" help:"Branch to stage comment for. Defaults to current branch."` +} + +func (*branchCommentStageCmd) Help() string { + return text.Dedent(` + Stages an inline comment for later batch submission. + Provide the file and line number as file.go:42. + + If no message is given with -m, an editor is opened. + + Use --respond to reply to an existing thread + instead of starting a new one. + + Staged comments are submitted together with + 'gs branch comment submit-staged'. + `) +} + +func (cmd *branchCommentStageCmd) Run( + ctx context.Context, + log *silog.Logger, + wt *git.Worktree, + store *state.Store, + repo *git.Repository, +) error { + branch := cmd.Branch + if branch == "" { + var err error + branch, err = wt.CurrentBranch(ctx) + if err != nil { + return fmt.Errorf("get current branch: %w", err) + } + } + + var file string + var line int + if cmd.Respond == "" { + if cmd.FileAndLine == "" { + return errors.New( + "file:line argument is required " + + "unless --respond is used", + ) + } + var err error + file, line, err = parseFileAndLine(cmd.FileAndLine) + if err != nil { + return err + } + } + + body := cmd.Message + if body == "" { + var err error + body, err = editCommentBody( + ctx, repo, "" /* initial */) + if err != nil { + return err + } + } + if strings.TrimSpace(body) == "" { + return errors.New("empty comment body, aborting") + } + + staged, err := store.LoadStagedComments(ctx, branch) + if err != nil { + return fmt.Errorf("load staged comments: %w", err) + } + if staged == nil { + staged = &state.StagedComments{NextID: 1} + } + + comment := state.StagedComment{ + ID: staged.NextID, + File: file, + Line: line, + Body: body, + ThreadID: cmd.Respond, + } + staged.Comments = append(staged.Comments, comment) + staged.NextID++ + + if err := store.SaveStagedComments( + ctx, branch, staged, + ); err != nil { + return fmt.Errorf("save staged comments: %w", err) + } + + if cmd.Respond != "" { + log.Infof( + "Staged reply sc-%d to thread %s.", + comment.ID, cmd.Respond, + ) + } else { + log.Infof( + "Staged comment sc-%d on %s:%d.", + comment.ID, file, line, + ) + } + return nil +} + +// parseFileAndLine parses a "file.go:42" argument +// into file and line components. +func parseFileAndLine(s string) (string, int, error) { + idx := strings.LastIndex(s, ":") + if idx < 0 { + return "", 0, fmt.Errorf( + "expected file:line format, got %q", s, + ) + } + file := s[:idx] + line, err := strconv.Atoi(s[idx+1:]) + if err != nil { + return "", 0, fmt.Errorf( + "invalid line number in %q: %w", s, err, + ) + } + if line <= 0 { + return "", 0, fmt.Errorf( + "line number must be positive, got %d", line, + ) + } + return file, line, nil +} + +// editCommentBody opens an editor for the user +// to write a comment body. +// initial is pre-filled text (may be empty). +func editCommentBody( + ctx context.Context, + repo *git.Repository, + initial string, +) (string, error) { + tmpFile := filepath.Join( + os.TempDir(), "GS_COMMENT_EDITMSG", + ) + if err := os.WriteFile( + tmpFile, []byte(initial), 0o644, + ); err != nil { + return "", fmt.Errorf("write temp file: %w", err) + } + defer func() { _ = os.Remove(tmpFile) }() + + editor := gitEditor(ctx, repo) + cmd := xec.EditCommand(editor, tmpFile) + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("run editor: %w", err) + } + + content, err := os.ReadFile(tmpFile) + if err != nil { + return "", fmt.Errorf("read temp file: %w", err) + } + return string(content), nil +} diff --git a/branch_comment_submit_staged.go b/branch_comment_submit_staged.go new file mode 100644 index 000000000..4194c22ce --- /dev/null +++ b/branch_comment_submit_staged.go @@ -0,0 +1,169 @@ +package main + +import ( + "context" + "errors" + "fmt" + + "go.abhg.dev/gs/internal/diffmap" + "go.abhg.dev/gs/internal/forge" + "go.abhg.dev/gs/internal/git" + "go.abhg.dev/gs/internal/silog" + "go.abhg.dev/gs/internal/spice" + "go.abhg.dev/gs/internal/spice/state" + "go.abhg.dev/gs/internal/text" +) + +type branchCommentSubmitStagedCmd struct { + Body string `placeholder:"BODY" help:"Overall review body."` + Approve bool `help:"Mark the review as approved."` + RequestChanges bool `name:"request-changes" help:"Mark the review as requesting changes."` + Branch string `short:"b" placeholder:"BRANCH" predictor:"trackedBranches" help:"Branch to submit staged comments for. Defaults to current branch."` +} + +func (*branchCommentSubmitStagedCmd) Help() string { + return text.Dedent(` + Submits all staged comments for the current branch + as a single review on the change request. + + Use --approve or --request-changes + to set the review event type. + Defaults to a comment-only review. + + Use --body to add an overall review body. + `) +} + +func (cmd *branchCommentSubmitStagedCmd) Run( + ctx context.Context, + log *silog.Logger, + wt *git.Worktree, + svc *spice.Service, + store *state.Store, + forgeRepo forge.Repository, +) error { + branch := cmd.Branch + if branch == "" { + var err error + branch, err = wt.CurrentBranch(ctx) + if err != nil { + return fmt.Errorf("get current branch: %w", err) + } + } + + staged, err := store.LoadStagedComments(ctx, branch) + if err != nil { + return fmt.Errorf("load staged comments: %w", err) + } + if staged == nil { + staged = &state.StagedComments{} + } + + if len(staged.Comments) == 0 { + log.Infof("No staged comments to submit.") + return nil + } + + b, err := svc.LookupBranch(ctx, branch) + if err != nil { + if errors.Is(err, state.ErrNotExist) { + return fmt.Errorf( + "branch not tracked: %s", branch, + ) + } + return fmt.Errorf("get branch: %w", err) + } + + if b.Change == nil { + return fmt.Errorf( + "no change request for %s; "+ + "submit the branch first with "+ + "'gs branch submit'", + branch, + ) + } + + inline, ok := forgeRepo.(forge.WithInlineComments) + if !ok { + return errors.New( + "forge does not support inline comments", + ) + } + + // Build diff map for coordinate translation. + diff, err := wt.DiffBranchBytes(ctx, b.Base, branch) + if err != nil { + return fmt.Errorf("get diff: %w", err) + } + + mapper, err := diffmap.New(diff) + if err != nil { + return fmt.Errorf("parse diff: %w", err) + } + + // Map staged comments to forge requests. + var comments []forge.InlineCommentRequest + for _, sc := range staged.Comments { + if sc.ThreadID != "" { + // Thread reply: no coordinate mapping needed. + comments = append(comments, + forge.InlineCommentRequest{ + Body: sc.Body, + ThreadID: sc.ThreadID, + }, + ) + continue + } + + path, diffLine, side, err := mapper.Map( + sc.File, sc.Line, + ) + if err != nil { + return fmt.Errorf( + "sc-%d: map %s:%d to diff: %w", + sc.ID, sc.File, sc.Line, err, + ) + } + + comments = append(comments, + forge.InlineCommentRequest{ + Path: path, + Line: diffLine, + Body: sc.Body, + Side: side, + }, + ) + } + + event := forge.ReviewComment + if cmd.Approve { + event = forge.ReviewApprove + } else if cmd.RequestChanges { + event = forge.ReviewRequestChanges + } + + if err := inline.SubmitReview( + ctx, + b.Change.ChangeID(), + forge.ReviewRequest{ + Body: cmd.Body, + Comments: comments, + Event: event, + }, + ); err != nil { + return fmt.Errorf("submit review: %w", err) + } + + if err := store.ClearStagedComments( + ctx, branch, + ); err != nil { + return fmt.Errorf("clear staged comments: %w", err) + } + + log.Infof( + "Submitted %d comment(s) as review on %s.", + len(comments), + b.Change.ChangeID(), + ) + return nil +} diff --git a/branch_restack.go b/branch_restack.go index 32b8dc590..f84b803b3 100644 --- a/branch_restack.go +++ b/branch_restack.go @@ -32,5 +32,5 @@ func (cmd *branchRestackCmd) AfterApply(ctx context.Context, wt *git.Worktree) e } func (cmd *branchRestackCmd) Run(ctx context.Context, handler RestackHandler) error { - return handler.RestackBranch(ctx, cmd.Branch) + return handler.RestackBranch(ctx, cmd.Branch, nil) } diff --git a/doc/includes/cli-reference.md b/doc/includes/cli-reference.md index 507c9d1c6..f99e35ecd 100644 --- a/doc/includes/cli-reference.md +++ b/doc/includes/cli-reference.md @@ -310,11 +310,9 @@ Before merging, the stack is checked for branches whose base PR was already merged on the forge. Use --no-branch-check to skip this validation. -Before each merge, waits for merge readiness: -the forge must observe the pushed head -and report that the CR is ready to merge. -Use --ready-timeout to configure the maximum wait -before failing if merge readiness is not reached. +Before each merge, waits for CI checks to pass. +Use --build-timeout to configure the maximum wait +before failing if checks are not ready. By default, a branch failure skips that branch's upstack descendants, but independent sibling branches continue. @@ -323,12 +321,12 @@ Use --fail-fast to stop the queue after the first branch failure. **Flags** * `--method=METHOD` ([:material-wrench:{ .middle title="spice.merge.method" }](/cli/config.md#spicemergemethod)): Preferred merge method. One of 'merge', 'squash', and 'rebase'. -* `--ready-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.readyTimeout" }](/cli/config.md#spicemergereadytimeout)): Max time to wait for merge readiness before each merge. 0 means check once. +* `--build-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.buildTimeout" }](/cli/config.md#spicemergebuildtimeout)): Max time to wait for CI checks before each merge. 0 means check once. * `--no-branch-check`: Skip stale base validation before merging. * `--fail-fast`: Stop the merge queue after the first branch failure. * `--branch=NAME`: Branch whose stack to merge -**Configuration**: [spice.merge.method](/cli/config.md#spicemergemethod), [spice.merge.readyTimeout](/cli/config.md#spicemergereadytimeout) +**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod) ### git-spice stack restack {#gs-stack-restack} @@ -626,7 +624,7 @@ This command acts as a local merge queue: it merges one Change Request, waits for that merge to finish, restacks and updates the next Change Request, -waits for merge readiness on the updated Change Request, +waits for its CI checks to pass, and then repeats the process. For a stack like this: @@ -644,15 +642,13 @@ Before merging, the downstack is checked for branches whose base PR was already merged on the forge. Use --no-branch-check to skip this validation. -Before each merge, waits for merge readiness: -the forge must observe the pushed head -and report that the CR is ready to merge. -Use --ready-timeout to configure the maximum wait +Before each merge, waits for CI checks to pass. +Use --build-timeout to configure the maximum wait (default: 30m, 0 means fail immediately if not ready). Between merges, the command waits for each merge to complete, restacks and updates the next PR, -waits for merge readiness on the updated PR, +waits for CI checks on the updated PR, and syncs merged branch cleanup. Use --no-wait for single branch merging @@ -662,12 +658,12 @@ when you don't want to wait for the merge to propagate. **Flags** * `--method=METHOD` ([:material-wrench:{ .middle title="spice.merge.method" }](/cli/config.md#spicemergemethod)): Preferred merge method. One of 'merge', 'squash', and 'rebase'. -* `--ready-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.readyTimeout" }](/cli/config.md#spicemergereadytimeout)): Max time to wait for merge readiness before each merge. 0 means check once. +* `--build-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.buildTimeout" }](/cli/config.md#spicemergebuildtimeout)): Max time to wait for CI checks before each merge. 0 means check once. * `--no-wait`: Skip polling for a single branch merge to propagate. * `--no-branch-check`: Skip stale base validation before merging. * `--branch=NAME`: Branch to start merging from -**Configuration**: [spice.merge.method](/cli/config.md#spicemergemethod), [spice.merge.readyTimeout](/cli/config.md#spicemergereadytimeout) +**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod) ### git-spice downstack edit {#gs-downstack-edit} @@ -1137,18 +1133,16 @@ Use --branch to merge a different branch. The branch must be based directly on trunk. To merge a stacked branch, use 'gs downstack merge'. -Before merging, waits for merge readiness: -the forge must observe the pushed head -and report that the CR is ready to merge. -Use --ready-timeout to configure the maximum wait. +Before merging, waits for CI checks to pass. +Use --build-timeout to configure the maximum wait. **Flags** * `--method=METHOD` ([:material-wrench:{ .middle title="spice.merge.method" }](/cli/config.md#spicemergemethod)): Preferred merge method. One of 'merge', 'squash', and 'rebase'. -* `--ready-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.readyTimeout" }](/cli/config.md#spicemergereadytimeout)): Max time to wait for merge readiness before each merge. 0 means check once. +* `--build-timeout=30m` ([:material-wrench:{ .middle title="spice.merge.buildTimeout" }](/cli/config.md#spicemergebuildtimeout)): Max time to wait for CI checks before each merge. 0 means check once. * `--branch=NAME`: Branch to merge -**Configuration**: [spice.merge.method](/cli/config.md#spicemergemethod), [spice.merge.readyTimeout](/cli/config.md#spicemergereadytimeout) +**Configuration**: [spice.merge.buildTimeout](/cli/config.md#spicemergebuildtimeout), [spice.merge.method](/cli/config.md#spicemergemethod) ### git-spice branch submit {#gs-branch-submit} @@ -1205,6 +1199,166 @@ only if there are multiple CRs in the stack. **Configuration**: [spice.submit.assignees](/cli/config.md#spicesubmitassignees), [spice.submit.draft](/cli/config.md#spicesubmitdraft), [spice.submit.labels](/cli/config.md#spicesubmitlabels), [spice.submit.labels.addWhen](/cli/config.md#spicesubmitlabelsaddwhen), [spice.submit.listTemplatesTimeout](/cli/config.md#spicesubmitlisttemplatestimeout), [spice.submit.navigationComment](/cli/config.md#spicesubmitnavigationcomment), [spice.submit.navigationComment.downstack](/cli/config.md#spicesubmitnavigationcommentdownstack), [spice.submit.navigationCommentStyle.marker](/cli/config.md#spicesubmitnavigationcommentstylemarker), [spice.submit.navigationCommentSync](/cli/config.md#spicesubmitnavigationcommentsync), [spice.submit.publish](/cli/config.md#spicesubmitpublish), [spice.submit.reviewers](/cli/config.md#spicesubmitreviewers), [spice.submit.reviewers.addWhen](/cli/config.md#spicesubmitreviewersaddwhen), [spice.submit.skipRestackCheck](/cli/config.md#spicesubmitskiprestackcheck), [spice.submit.template](/cli/config.md#spicesubmittemplate), [spice.submit.web](/cli/config.md#spicesubmitweb) +### git-spice branch comment list {#gs-branch-comment-list} + +``` +gs branch (b) comment (cmt) list (ls) [flags] +``` + +List comments on a change request + +Lists comments on the change request +associated with the current branch. +Use --branch to target a different branch. + +Staged comments that have not yet been submitted +are shown with an 'sc-N' prefix. + +Use --staged to show only staged comments. +Use --unresolved to show only unresolved comments. + +With --json, prints output to stdout +as a stream of JSON objects. + +**Flags** + +* `-b`, `--branch=BRANCH`: Branch to list comments for. Defaults to current branch. +* `--staged`: Show only staged comments. +* `--unresolved`: Show only unresolved comments. +* `--json`: Write to stdout as a stream of JSON objects. :material-tag-hidden:{ title="Released in version" }Unreleased + +### git-spice branch comment stage {#gs-branch-comment-stage} + +``` +gs branch (b) comment (cmt) stage [] [flags] +``` + +Stage an inline comment for batch submission + +Stages an inline comment for later batch submission. +Provide the file and line number as file.go:42. + +If no message is given with -m, an editor is opened. + +Use --respond to reply to an existing thread +instead of starting a new one. + +Staged comments are submitted together with +'gs branch comment submit-staged'. + +**Arguments** + +* `file-and-line`: File and line in the form file.go:42. + +**Flags** + +* `-m`, `--message=MSG`: Comment body. Opens editor if not provided. +* `--respond=THREAD_ID`: Thread ID to reply to instead of starting a new thread. +* `-b`, `--branch=BRANCH`: Branch to stage comment for. Defaults to current branch. + +### git-spice branch comment add {#gs-branch-comment-add} + +``` +gs branch (b) comment (cmt) add [] [flags] +``` + +Post an inline comment immediately + +Posts an inline comment immediately +on the change request for the current branch. +Provide the file and line number as file.go:42. + +If no message is given with -m, an editor is opened. + +Use --respond to reply to an existing thread +instead of starting a new one. + +**Arguments** + +* `file-and-line`: File and line in the form file.go:42. + +**Flags** + +* `-m`, `--message=MSG`: Comment body. Opens editor if not provided. +* `--respond=THREAD_ID`: Thread ID to reply to instead of starting a new thread. +* `-b`, `--branch=BRANCH`: Branch to add comment for. Defaults to current branch. + +### git-spice branch comment submit-staged {#gs-branch-comment-submit-staged} + +``` +gs branch (b) comment (cmt) submit-staged (ss) [flags] +``` + +Submit all staged comments as a review + +Submits all staged comments for the current branch +as a single review on the change request. + +Use --approve or --request-changes +to set the review event type. +Defaults to a comment-only review. + +Use --body to add an overall review body. + +**Flags** + +* `--body=BODY`: Overall review body. +* `--approve`: Mark the review as approved. +* `--request-changes`: Mark the review as requesting changes. +* `-b`, `--branch=BRANCH`: Branch to submit staged comments for. Defaults to current branch. + +### git-spice branch comment resolve {#gs-branch-comment-resolve} + +``` +gs branch (b) comment (cmt) resolve [flags] +``` + +Resolve or unresolve a review thread + +Resolves a review thread on the change request +for the current branch. + +Use --unresolve to mark the thread as unresolved. + +The thread ID is shown in 'gs branch comment list'. + +**Arguments** + +* `thread-id`: Thread ID to resolve. + +**Flags** + +* `--unresolve`: Unresolve the thread instead of resolving it. +* `-b`, `--branch=BRANCH`: Branch whose change request contains the thread. Defaults to current branch. + +### git-spice branch comment edit {#gs-branch-comment-edit} + +``` +gs branch (b) comment (cmt) edit [flags] +``` + +Edit a comment + +Edits the body of a comment. + +For staged comments (sc-N prefix), +the comment is updated in the local staging area. + +For forge comments, the comment is updated +on the remote forge. + +If no message is given with -m, an editor is opened +with the current comment body pre-filled. + +**Arguments** + +* `id`: Comment ID to edit. Use 'sc-N' for staged comments or a forge comment ID. + +**Flags** + +* `-m`, `--message=MSG`: New comment body. Opens editor if not provided. +* `-b`, `--branch=BRANCH`: Branch whose comments to edit. Defaults to current branch. + ## Commit ### git-spice commit create {#gs-commit-create} diff --git a/doc/includes/cli-shorthands.md b/doc/includes/cli-shorthands.md index 064cad722..b3cb86a0e 100644 --- a/doc/includes/cli-shorthands.md +++ b/doc/includes/cli-shorthands.md @@ -1,6 +1,8 @@ | **Shorthand** | **Long form** | | --- | --- | | gs bc | [gs branch create](/cli/reference.md#gs-branch-create) | +| gs bcmtls | [gs branch comment list](/cli/reference.md#gs-branch-comment-list) | +| gs bcmtss | [gs branch comment submit-staged](/cli/reference.md#gs-branch-comment-submit-staged) | | gs bco | [gs branch checkout](/cli/reference.md#gs-branch-checkout) | | gs bd | [gs branch delete](/cli/reference.md#gs-branch-delete) | | gs bdi | [gs branch diff](/cli/reference.md#gs-branch-diff) | diff --git a/downstack_restack.go b/downstack_restack.go index e3f95a420..87a273f65 100644 --- a/downstack_restack.go +++ b/downstack_restack.go @@ -44,5 +44,5 @@ func (cmd *downstackRestackCmd) Run( return errors.New("nothing to restack below trunk") } - return handler.RestackDownstack(ctx, cmd.Branch) + return handler.RestackDownstack(ctx, cmd.Branch, nil) } diff --git a/internal/diffmap/mapper.go b/internal/diffmap/mapper.go new file mode 100644 index 000000000..d309eae77 --- /dev/null +++ b/internal/diffmap/mapper.go @@ -0,0 +1,281 @@ +// Package diffmap maps working-tree line numbers +// to diff hunk positions for inline code review comments. +package diffmap + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "slices" + "strconv" + "strings" +) + +// Mapper maps file:line references to diff coordinates. +type Mapper struct { + // files maps file paths to their diff hunks. + files map[string]*fileDiff +} + +type fileDiff struct { + // hunks are the diff hunks for the file, + // in order of appearance. + hunks []hunk +} + +type hunk struct { + // newStart is the starting line number + // in the new version of the file. + newStart int + + // newCount is the number of lines + // in the new version of the hunk. + newCount int + + // oldStart is the starting line number + // in the old version of the file. + oldStart int + + // oldCount is the number of lines + // in the old version of the hunk. + oldCount int + + // lines are the diff lines in the hunk. + lines []diffLine +} + +type diffLine struct { + // op is the diff operation: ' ', '+', or '-'. + op byte + + // newLineNo is the line number in the new file. + // Zero for deleted lines. + newLineNo int + + // oldLineNo is the line number in the old file. + // Zero for added lines. + oldLineNo int +} + +// New creates a Mapper from unified diff output. +// The diff should be the output of git diff base...HEAD. +func New(diff []byte) (*Mapper, error) { + m := &Mapper{ + files: make(map[string]*fileDiff), + } + + scanner := bufio.NewScanner(bytes.NewReader(diff)) + var currentFile string + var currentHunk *hunk + + for scanner.Scan() { + line := scanner.Text() + + // Detect file header: "diff --git a/path b/path" + if strings.HasPrefix(line, "diff --git ") { + currentFile = "" + currentHunk = nil + continue + } + + // Detect new file path: "+++ b/path" + if path, ok := strings.CutPrefix(line, "+++ b/"); ok { + currentFile = path + if _, ok := m.files[currentFile]; !ok { + m.files[currentFile] = &fileDiff{} + } + continue + } + + // Detect rename/copy target: + // "rename to path" or "copy to path" + if path, ok := strings.CutPrefix(line, "rename to "); ok { + currentFile = path + if _, ok := m.files[currentFile]; !ok { + m.files[currentFile] = &fileDiff{} + } + continue + } + if path, ok := strings.CutPrefix(line, "copy to "); ok { + currentFile = path + if _, ok := m.files[currentFile]; !ok { + m.files[currentFile] = &fileDiff{} + } + continue + } + + // Skip if no file context yet. + if currentFile == "" { + continue + } + + // Detect hunk header: "@@ -old,count +new,count @@" + if strings.HasPrefix(line, "@@ ") { + h, err := parseHunkHeader(line) + if err != nil { + return nil, fmt.Errorf( + "parse hunk header %q: %w", + line, err, + ) + } + fd := m.files[currentFile] + fd.hunks = append(fd.hunks, h) + currentHunk = &fd.hunks[len(fd.hunks)-1] + continue + } + + // Process diff lines within a hunk. + if currentHunk == nil || len(line) == 0 { + continue + } + + op := line[0] + switch op { + case ' ': + // Context line: present in both old and new. + dl := diffLine{ + op: ' ', + oldLineNo: currentHunk.oldStart + countOp(currentHunk.lines, ' ', '-'), + newLineNo: currentHunk.newStart + countOp(currentHunk.lines, ' ', '+'), + } + currentHunk.lines = append(currentHunk.lines, dl) + case '+': + // Added line: only in new. + dl := diffLine{ + op: '+', + newLineNo: currentHunk.newStart + countOp(currentHunk.lines, ' ', '+'), + } + currentHunk.lines = append(currentHunk.lines, dl) + case '-': + // Deleted line: only in old. + dl := diffLine{ + op: '-', + oldLineNo: currentHunk.oldStart + countOp(currentHunk.lines, ' ', '-'), + } + currentHunk.lines = append(currentHunk.lines, dl) + case '\\': + // "\ No newline at end of file" — skip. + default: + // Unknown line type — skip. + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("scan diff: %w", err) + } + + return m, nil +} + +// Map converts a working-tree file:line reference +// to diff coordinates suitable for forge inline comments. +// +// Returns the file path, the diff line number, +// and the side ("LEFT" or "RIGHT"). +// +// Returns an error if the file or line +// is not part of the diff. +func (m *Mapper) Map(file string, line int) ( + path string, diffLine int, side string, err error, +) { + fd, ok := m.files[file] + if !ok { + return "", 0, "", + fmt.Errorf("file %q not in diff", file) + } + + for _, h := range fd.hunks { + for _, dl := range h.lines { + if dl.newLineNo == line && + (dl.op == '+' || dl.op == ' ') { + return file, dl.newLineNo, "RIGHT", nil + } + } + } + + return "", 0, "", fmt.Errorf( + "line %d of %q not in diff", line, file, + ) +} + +// Files returns all file paths present in the diff. +func (m *Mapper) Files() []string { + var files []string + for f := range m.files { + files = append(files, f) + } + return files +} + +// parseHunkHeader parses "@@ -old,count +new,count @@". +func parseHunkHeader(line string) (hunk, error) { + // Strip "@@ " prefix and " @@..." suffix. + line = strings.TrimPrefix(line, "@@ ") + idx := strings.Index(line, " @@") + if idx < 0 { + return hunk{}, + errors.New("missing @@ terminator") + } + line = line[:idx] + + parts := strings.SplitN(line, " ", 2) + if len(parts) != 2 { + return hunk{}, + errors.New("expected old and new ranges") + } + + oldStart, oldCount, err := parseRange( + strings.TrimPrefix(parts[0], "-"), + ) + if err != nil { + return hunk{}, + fmt.Errorf("parse old range: %w", err) + } + + newStart, newCount, err := parseRange( + strings.TrimPrefix(parts[1], "+"), + ) + if err != nil { + return hunk{}, + fmt.Errorf("parse new range: %w", err) + } + + return hunk{ + oldStart: oldStart, + oldCount: oldCount, + newStart: newStart, + newCount: newCount, + }, nil +} + +// parseRange parses "start,count" or "start" (count=1). +func parseRange(s string) (start, count int, err error) { + parts := strings.SplitN(s, ",", 2) + start, err = strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, + fmt.Errorf("parse start: %w", err) + } + if len(parts) == 1 { + return start, 1, nil + } + count, err = strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, + fmt.Errorf("parse count: %w", err) + } + return start, count, nil +} + +// countOp counts lines in the hunk +// that match any of the given operations. +func countOp(lines []diffLine, ops ...byte) int { + n := 0 + for _, l := range lines { + if slices.Contains(ops, l.op) { + n++ + } + } + return n +} diff --git a/internal/diffmap/mapper_test.go b/internal/diffmap/mapper_test.go new file mode 100644 index 000000000..6abbb5fe1 --- /dev/null +++ b/internal/diffmap/mapper_test.go @@ -0,0 +1,198 @@ +package diffmap + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMapper_Map(t *testing.T) { + tests := []struct { + name string + diff string + file string + line int + wantPath string + wantLine int + wantSide string + wantErr string + }{ + { + name: "AddedLine", + diff: `diff --git a/main.go b/main.go +--- a/main.go ++++ b/main.go +@@ -1,3 +1,4 @@ + package main + ++func hello() {} + func main() {} +`, + file: "main.go", + line: 3, + wantPath: "main.go", + wantLine: 3, + wantSide: "RIGHT", + }, + { + name: "ContextLine", + diff: `diff --git a/main.go b/main.go +--- a/main.go ++++ b/main.go +@@ -1,3 +1,4 @@ + package main + ++func hello() {} + func main() {} +`, + file: "main.go", + line: 1, + wantPath: "main.go", + wantLine: 1, + wantSide: "RIGHT", + }, + { + name: "MultipleHunks", + diff: `diff --git a/main.go b/main.go +--- a/main.go ++++ b/main.go +@@ -1,3 +1,4 @@ + package main + ++import "fmt" + func main() {} +@@ -10,3 +11,4 @@ + func foo() {} + ++func bar() {} + func baz() {} +`, + file: "main.go", + line: 13, + wantPath: "main.go", + wantLine: 13, + wantSide: "RIGHT", + }, + { + name: "FileNotInDiff", + diff: `diff --git a/main.go b/main.go +--- a/main.go ++++ b/main.go +@@ -1,3 +1,4 @@ + package main + ++func hello() {} + func main() {} +`, + file: "other.go", + line: 1, + wantErr: `file "other.go" not in diff`, + }, + { + name: "LineNotInDiff", + diff: `diff --git a/main.go b/main.go +--- a/main.go ++++ b/main.go +@@ -1,3 +1,4 @@ + package main + ++func hello() {} + func main() {} +`, + file: "main.go", + line: 100, + wantErr: `line 100 of "main.go" not in diff`, + }, + { + name: "MultipleFiles", + diff: `diff --git a/a.go b/a.go +--- a/a.go ++++ b/a.go +@@ -1,2 +1,3 @@ + package a ++func A() {} + +diff --git a/b.go b/b.go +--- a/b.go ++++ b/b.go +@@ -1,2 +1,3 @@ + package b ++func B() {} + +`, + file: "b.go", + line: 2, + wantPath: "b.go", + wantLine: 2, + wantSide: "RIGHT", + }, + { + name: "NewFile", + diff: `diff --git a/new.go b/new.go +new file mode 100644 +--- /dev/null ++++ b/new.go +@@ -0,0 +1,3 @@ ++package new ++ ++func New() {} +`, + file: "new.go", + line: 1, + wantPath: "new.go", + wantLine: 1, + wantSide: "RIGHT", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := New([]byte(tt.diff)) + require.NoError(t, err) + + path, line, side, err := m.Map(tt.file, tt.line) + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.wantPath, path) + assert.Equal(t, tt.wantLine, line) + assert.Equal(t, tt.wantSide, side) + }) + } +} + +func TestMapper_Files(t *testing.T) { + diff := `diff --git a/a.go b/a.go +--- a/a.go ++++ b/a.go +@@ -1,2 +1,3 @@ + package a ++func A() {} + +diff --git a/b.go b/b.go +--- a/b.go ++++ b/b.go +@@ -1,2 +1,3 @@ + package b ++func B() {} + +` + m, err := New([]byte(diff)) + require.NoError(t, err) + + files := m.Files() + assert.Len(t, files, 2) + assert.Contains(t, files, "a.go") + assert.Contains(t, files, "b.go") +} + +func TestNew_EmptyDiff(t *testing.T) { + m, err := New([]byte("")) + require.NoError(t, err) + assert.Empty(t, m.Files()) +} diff --git a/internal/forge/bitbucket/inline_comment.go b/internal/forge/bitbucket/inline_comment.go new file mode 100644 index 000000000..6772a091f --- /dev/null +++ b/internal/forge/bitbucket/inline_comment.go @@ -0,0 +1,333 @@ +package bitbucket + +import ( + "context" + "fmt" + "strconv" + + "go.abhg.dev/gs/internal/forge" + "go.abhg.dev/gs/internal/gateway/bitbucket" +) + +var ( + _ forge.WithInlineComments = (*Repository)(nil) + _ forge.WithThreadResolution = (*Repository)(nil) + _ forge.WithCommentEdit = (*Repository)(nil) +) + +// ListInlineComments lists inline/review comments on a PR +// by filtering comments that have inline position data. +func (r *Repository) ListInlineComments( + ctx context.Context, + id forge.ChangeID, +) ([]*forge.InlineComment, error) { + prID := mustPR(id).Number + + var comments []*forge.InlineComment + opts := &bitbucket.CommentListOptions{} + for { + page, _, err := r.client.CommentList( + ctx, r.workspace, r.repo, prID, opts, + ) + if err != nil { + return nil, fmt.Errorf( + "list comments: %w", err, + ) + } + + for _, c := range page.Values { + if c.Inline == nil { + continue + } + + line := 0 + if c.Inline.To != nil { + line = *c.Inline.To + } + + // Thread ID encodes comment ID and PR ID + // for resolve/unresolve operations. + threadID := formatThreadID(c.ID, prID) + + comments = append(comments, + &forge.InlineComment{ + ID: &PRComment{ + ID: c.ID, + PRID: prID, + }, + ThreadID: threadID, + Path: c.Inline.Path, + Line: line, + Body: c.Content.Raw, + Author: c.User.DisplayName, + Resolved: c.Resolution != nil, + }) + } + + if page.Next == "" { + break + } + opts.PageURL = page.Next + } + + return comments, nil +} + +// SubmitReview posts a batch of inline comments +// as individual comments on a PR. +// Bitbucket does not have a native batch review API, +// so each comment is posted separately. +func (r *Repository) SubmitReview( + ctx context.Context, + id forge.ChangeID, + req forge.ReviewRequest, +) error { + prID := mustPR(id).Number + + for _, c := range req.Comments { + if err := r.submitOneComment( + ctx, prID, c, + ); err != nil { + return err + } + } + + r.log.Debug("Submitted review", + "pr", prID, + "comments", len(req.Comments), + ) + return nil +} + +func (r *Repository) submitOneComment( + ctx context.Context, + prID int64, + c forge.InlineCommentRequest, +) error { + if c.ThreadID != "" { + parentID, _, err := parseThreadID(c.ThreadID) + if err != nil { + return fmt.Errorf("parse thread ID: %w", err) + } + _, err = r.replyToComment( + ctx, prID, parentID, c.Body, + ) + return err + } + _, err := r.postInlineCommentAPI( + ctx, prID, c.Path, c.Line, c.Body, + ) + return err +} + +// PostInlineComment posts a single inline comment on a PR. +// If req.ThreadID is set, posts a reply to that thread +// instead of creating a new inline comment. +func (r *Repository) PostInlineComment( + ctx context.Context, + id forge.ChangeID, + req forge.InlineCommentRequest, +) (*forge.InlineComment, error) { + prID := mustPR(id).Number + + if req.ThreadID != "" { + return r.postReply(ctx, prID, req) + } + return r.postNewInlineComment(ctx, prID, req) +} + +func (r *Repository) postNewInlineComment( + ctx context.Context, + prID int64, + req forge.InlineCommentRequest, +) (*forge.InlineComment, error) { + comment, err := r.postInlineCommentAPI( + ctx, prID, req.Path, req.Line, req.Body, + ) + if err != nil { + return nil, err + } + + return &forge.InlineComment{ + ID: &PRComment{ + ID: comment.ID, + PRID: prID, + }, + ThreadID: formatThreadID(comment.ID, prID), + Path: req.Path, + Line: req.Line, + Body: req.Body, + Author: comment.User.DisplayName, + }, nil +} + +func (r *Repository) postReply( + ctx context.Context, + prID int64, + req forge.InlineCommentRequest, +) (*forge.InlineComment, error) { + parentID, _, err := parseThreadID(req.ThreadID) + if err != nil { + return nil, fmt.Errorf("parse thread ID: %w", err) + } + + comment, err := r.replyToComment( + ctx, prID, parentID, req.Body, + ) + if err != nil { + return nil, err + } + + return &forge.InlineComment{ + ID: &PRComment{ + ID: comment.ID, + PRID: prID, + }, + ThreadID: req.ThreadID, + Body: req.Body, + Author: comment.User.DisplayName, + }, nil +} + +func (r *Repository) replyToComment( + ctx context.Context, + prID, parentID int64, + body string, +) (*bitbucket.Comment, error) { + comment, _, err := r.client.CommentCreate( + ctx, r.workspace, r.repo, prID, + &bitbucket.CommentCreateRequest{ + Content: bitbucket.Content{Raw: body}, + Parent: &bitbucket.CommentRef{ID: parentID}, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "reply to comment %d: %w", + parentID, err, + ) + } + return comment, nil +} + +func (r *Repository) postInlineCommentAPI( + ctx context.Context, + prID int64, + filePath string, + line int, + body string, +) (*bitbucket.Comment, error) { + to := line + comment, _, err := r.client.CommentCreate( + ctx, r.workspace, r.repo, prID, + &bitbucket.CommentCreateRequest{ + Content: bitbucket.Content{Raw: body}, + Inline: &bitbucket.Inline{ + Path: filePath, + To: &to, + }, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "create inline comment on %s:%d: %w", + filePath, line, err, + ) + } + return comment, nil +} + +// ResolveThread marks an inline comment as resolved. +func (r *Repository) ResolveThread( + ctx context.Context, + threadID string, +) error { + commentID, prID, err := parseThreadID(threadID) + if err != nil { + return err + } + + if _, _, err := r.client.CommentResolve( + ctx, r.workspace, r.repo, prID, commentID, + &bitbucket.CommentResolveRequest{ + Resolution: &bitbucket.Resolution{Type: "resolved"}, + }, + ); err != nil { + return fmt.Errorf("resolve thread: %w", err) + } + + r.log.Debug("Resolved thread", "threadID", threadID) + return nil +} + +// UnresolveThread marks an inline comment as unresolved. +func (r *Repository) UnresolveThread( + ctx context.Context, + threadID string, +) error { + commentID, prID, err := parseThreadID(threadID) + if err != nil { + return err + } + + if _, _, err := r.client.CommentResolve( + ctx, r.workspace, r.repo, prID, commentID, + &bitbucket.CommentResolveRequest{ + Resolution: nil, + }, + ); err != nil { + return fmt.Errorf("unresolve thread: %w", err) + } + + r.log.Debug("Unresolved thread", "threadID", threadID) + return nil +} + +// EditComment updates the body of an existing comment. +func (r *Repository) EditComment( + ctx context.Context, + id forge.ChangeCommentID, + body string, +) error { + comment := mustPRComment(id) + return r.updateComment(ctx, comment.PRID, comment.ID, body) +} + +// Thread ID encoding for Bitbucket: +// "commentID:prID" + +func formatThreadID(commentID, prID int64) string { + return strconv.FormatInt(commentID, 10) + + ":" + strconv.FormatInt(prID, 10) +} + +func parseThreadID(threadID string) ( + commentID, prID int64, err error, +) { + for i := len(threadID) - 1; i >= 0; i-- { + if threadID[i] == ':' { + commentID, err = strconv.ParseInt( + threadID[:i], 10, 64, + ) + if err != nil { + return 0, 0, fmt.Errorf( + "parse thread ID %q: %w", + threadID, err, + ) + } + prID, err = strconv.ParseInt( + threadID[i+1:], 10, 64, + ) + if err != nil { + return 0, 0, fmt.Errorf( + "parse thread ID %q: %w", + threadID, err, + ) + } + return commentID, prID, nil + } + } + return 0, 0, fmt.Errorf( + "invalid thread ID format: %q", threadID, + ) +} diff --git a/internal/forge/bitbucket/operations_test.go b/internal/forge/bitbucket/operations_test.go index e2cf4566b..9f7b6845a 100644 --- a/internal/forge/bitbucket/operations_test.go +++ b/internal/forge/bitbucket/operations_test.go @@ -675,6 +675,165 @@ func TestStateToAPI(t *testing.T) { } } +func TestPostInlineComment(t *testing.T) { + srv := httptest.NewServer( + http.HandlerFunc(func( + w http.ResponseWriter, r *http.Request, + ) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Contains(t, r.URL.Path, + "/pullrequests/1/comments", + ) + + var req bitbucket.CommentCreateRequest + require.NoError(t, + json.NewDecoder(r.Body).Decode(&req), + ) + assert.Equal(t, "new comment", req.Content.Raw) + assert.Equal(t, "file.go", req.Inline.Path) + require.NotNil(t, req.Inline.To) + assert.Equal(t, 42, *req.Inline.To) + + resp := bitbucket.Comment{ + ID: 10, + Content: bitbucket.Content{Raw: req.Content.Raw}, + Inline: &bitbucket.Inline{ + Path: req.Inline.Path, + To: req.Inline.To, + }, + User: bitbucket.User{DisplayName: "alice"}, + } + assert.NoError(t, + json.NewEncoder(w).Encode(resp), + ) + }), + ) + defer srv.Close() + + repo := newTestRepository(srv.URL) + posted, err := repo.PostInlineComment( + t.Context(), + &PR{Number: 1}, + forge.InlineCommentRequest{ + Path: "file.go", + Line: 42, + Body: "new comment", + }, + ) + require.NoError(t, err) + + assert.Equal(t, "new comment", posted.Body) + assert.Equal(t, "file.go", posted.Path) + assert.Equal(t, 42, posted.Line) +} + +func TestPostInlineComment_reply(t *testing.T) { + srv := httptest.NewServer( + http.HandlerFunc(func( + w http.ResponseWriter, r *http.Request, + ) { + assert.Equal(t, http.MethodPost, r.Method) + + // Decode as raw JSON to verify structure. + var raw map[string]json.RawMessage + require.NoError(t, + json.NewDecoder(r.Body).Decode(&raw), + ) + + // Must have parent.id, not inline. + assert.Contains(t, raw, "parent", + "reply must include parent field", + ) + assert.NotContains(t, raw, "inline", + "reply must not include inline field", + ) + + var parent bitbucket.CommentRef + require.NoError(t, + json.Unmarshal(raw["parent"], &parent), + ) + assert.Equal(t, int64(99), parent.ID) + + resp := bitbucket.Comment{ + ID: 20, + Content: bitbucket.Content{Raw: "reply body"}, + User: bitbucket.User{DisplayName: "bob"}, + } + assert.NoError(t, + json.NewEncoder(w).Encode(resp), + ) + }), + ) + defer srv.Close() + + repo := newTestRepository(srv.URL) + posted, err := repo.PostInlineComment( + t.Context(), + &PR{Number: 1}, + forge.InlineCommentRequest{ + Body: "reply body", + ThreadID: "99:1", // commentID:prID + }, + ) + require.NoError(t, err) + + assert.Equal(t, "reply body", posted.Body) + assert.Equal(t, "99:1", posted.ThreadID) +} + +func TestSubmitReview_withReply(t *testing.T) { + var requests []map[string]json.RawMessage + srv := httptest.NewServer( + http.HandlerFunc(func( + w http.ResponseWriter, r *http.Request, + ) { + var raw map[string]json.RawMessage + require.NoError(t, + json.NewDecoder(r.Body).Decode(&raw), + ) + requests = append(requests, raw) + + resp := bitbucket.Comment{ + ID: int64(len(requests)), + Content: bitbucket.Content{Raw: "ok"}, + } + assert.NoError(t, + json.NewEncoder(w).Encode(resp), + ) + }), + ) + defer srv.Close() + + repo := newTestRepository(srv.URL) + err := repo.SubmitReview( + t.Context(), + &PR{Number: 1}, + forge.ReviewRequest{ + Comments: []forge.InlineCommentRequest{ + { + Path: "a.go", + Line: 10, + Body: "new comment", + }, + { + Body: "thread reply", + ThreadID: "55:1", + }, + }, + }, + ) + require.NoError(t, err) + require.Len(t, requests, 2) + + // First request: new inline comment. + assert.Contains(t, requests[0], "inline") + assert.NotContains(t, requests[0], "parent") + + // Second request: reply to thread. + assert.Contains(t, requests[1], "parent") + assert.NotContains(t, requests[1], "inline") +} + func newTestRepository(baseURL string) *Repository { token := &AuthenticationToken{AccessToken: "test"} tokenSource, err := newGatewayTokenSource(token) diff --git a/internal/forge/forge.go b/internal/forge/forge.go index 3c855cc5d..8184391a3 100644 --- a/internal/forge/forge.go +++ b/internal/forge/forge.go @@ -13,6 +13,7 @@ import ( "regexp" "strings" "sync" + "time" "go.abhg.dev/gs/internal/git" "go.abhg.dev/gs/internal/git/giturl" @@ -142,7 +143,7 @@ type WithCommentFormat interface { CommentFormat() CommentFormat } -//go:generate mockgen -destination=forgetest/mocks.go -package forgetest -typed . Forge,RepositoryID,Repository +//go:generate mockgen -destination=forgetest/mocks.go -package forgetest -typed -write_package_comment=false . Forge,RepositoryID,Repository,WithInlineComments,WithThreadResolution,WithCommentEdit // TODO: // Forge should become a struct with multiple interfaces or funcctions @@ -938,3 +939,134 @@ func (s ChangeCheckState) GoString() string { return fmt.Sprintf("ChangeCheckState(%d)", int(s)) } } + +// Inline comment types and optional interfaces + +// InlineCommentRequest describes a new inline comment +// to post on a change diff. +type InlineCommentRequest struct { + // Path is the file path relative to the repository root. + Path string + + // Line is the line number in the new version of the file. + Line int + + // Body is the markdown body of the comment. + Body string + + // Side indicates which side of the diff the comment + // applies to. Use "LEFT" for the old version + // or "RIGHT" (default) for the new version. + Side string + + // ThreadID is set when replying to an existing thread. + // The format is forge-specific. + ThreadID string +} + +// InlineComment is a comment on a specific line of a diff. +type InlineComment struct { + // ID is the forge-specific comment identifier. + ID ChangeCommentID + + // ThreadID is the forge-specific thread identifier. + ThreadID string + + // Path is the file path relative to the repository root. + Path string + + // Line is the line number in the diff. + Line int + + // Body is the markdown body of the comment. + Body string + + // Author is the username of the comment author. + Author string + + // Resolved indicates the comment thread is resolved. + Resolved bool + + // Outdated indicates the comment is on an outdated diff. + Outdated bool + + // CreatedAt is the time the comment was created. + CreatedAt time.Time +} + +// ReviewEvent specifies the type of review being submitted. +type ReviewEvent int + +const ( + // ReviewComment submits a review with comments only. + ReviewComment ReviewEvent = iota + + // ReviewApprove submits an approving review. + ReviewApprove + + // ReviewRequestChanges submits a review + // requesting changes. + ReviewRequestChanges +) + +// ReviewRequest is a batch of inline comments +// submitted together as a single review. +type ReviewRequest struct { + // Body is the overall review body (optional). + Body string + + // Comments are the inline comments in the review. + Comments []InlineCommentRequest + + // Event is the review event type. + Event ReviewEvent +} + +// WithInlineComments is an optional interface +// for forges that support inline/diff comments +// and code reviews. +type WithInlineComments interface { + Repository + + // ListInlineComments lists inline/review comments + // on a change. + ListInlineComments( + ctx context.Context, id ChangeID, + ) ([]*InlineComment, error) + + // SubmitReview posts a batch of inline comments + // as a single review. + SubmitReview( + ctx context.Context, id ChangeID, req ReviewRequest, + ) error + + // PostInlineComment posts a single inline comment + // outside of a batch review. + PostInlineComment( + ctx context.Context, id ChangeID, + req InlineCommentRequest, + ) (*InlineComment, error) +} + +// WithThreadResolution is an optional interface +// for forges that support resolving comment threads. +type WithThreadResolution interface { + Repository + + // ResolveThread marks a comment thread as resolved. + ResolveThread(ctx context.Context, threadID string) error + + // UnresolveThread marks a comment thread as unresolved. + UnresolveThread(ctx context.Context, threadID string) error +} + +// WithCommentEdit is an optional interface +// for forges that support editing existing comments. +type WithCommentEdit interface { + Repository + + // EditComment updates the body of an existing comment. + EditComment( + ctx context.Context, id ChangeCommentID, body string, + ) error +} diff --git a/internal/forge/forgetest/mocks.go b/internal/forge/forgetest/mocks.go index a146405a7..b9ae2a647 100644 --- a/internal/forge/forgetest/mocks.go +++ b/internal/forge/forgetest/mocks.go @@ -1,12 +1,11 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: go.abhg.dev/gs/internal/forge (interfaces: Forge,RepositoryID,Repository) +// Source: go.abhg.dev/gs/internal/forge (interfaces: Forge,RepositoryID,Repository,WithInlineComments,WithThreadResolution,WithCommentEdit) // // Generated by this command: // -// mockgen -destination=forgetest/mocks.go -package forgetest -typed . Forge,RepositoryID,Repository +// mockgen -destination=forgetest/mocks.go -package forgetest -typed -write_package_comment=false . Forge,RepositoryID,Repository,WithInlineComments,WithThreadResolution,WithCommentEdit // -// Package forgetest is a generated GoMock package. package forgetest import ( @@ -709,80 +708,41 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { return m.recorder } -// ChangeChecks mocks base method. -func (m *MockRepository) ChangeChecks(ctx context.Context, id forge.ChangeID) ([]forge.ChangeCheck, error) { +// ChangeChecksState mocks base method. +func (m *MockRepository) ChangeChecksState(ctx context.Context, id forge.ChangeID) (forge.ChecksState, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ChangeChecks", ctx, id) - ret0, _ := ret[0].([]forge.ChangeCheck) + ret := m.ctrl.Call(m, "ChangeChecksState", ctx, id) + ret0, _ := ret[0].(forge.ChecksState) ret1, _ := ret[1].(error) return ret0, ret1 } -// ChangeChecks indicates an expected call of ChangeChecks. -func (mr *MockRepositoryMockRecorder) ChangeChecks(ctx, id any) *MockRepositoryChangeChecksCall { +// ChangeChecksState indicates an expected call of ChangeChecksState. +func (mr *MockRepositoryMockRecorder) ChangeChecksState(ctx, id any) *MockRepositoryChangeChecksStateCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeChecks", reflect.TypeOf((*MockRepository)(nil).ChangeChecks), ctx, id) - return &MockRepositoryChangeChecksCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeChecksState", reflect.TypeOf((*MockRepository)(nil).ChangeChecksState), ctx, id) + return &MockRepositoryChangeChecksStateCall{Call: call} } -// MockRepositoryChangeChecksCall wrap *gomock.Call -type MockRepositoryChangeChecksCall struct { +// MockRepositoryChangeChecksStateCall wrap *gomock.Call +type MockRepositoryChangeChecksStateCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockRepositoryChangeChecksCall) Return(arg0 []forge.ChangeCheck, arg1 error) *MockRepositoryChangeChecksCall { +func (c *MockRepositoryChangeChecksStateCall) Return(arg0 forge.ChecksState, arg1 error) *MockRepositoryChangeChecksStateCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *MockRepositoryChangeChecksCall) Do(f func(context.Context, forge.ChangeID) ([]forge.ChangeCheck, error)) *MockRepositoryChangeChecksCall { +func (c *MockRepositoryChangeChecksStateCall) Do(f func(context.Context, forge.ChangeID) (forge.ChecksState, error)) *MockRepositoryChangeChecksStateCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockRepositoryChangeChecksCall) DoAndReturn(f func(context.Context, forge.ChangeID) ([]forge.ChangeCheck, error)) *MockRepositoryChangeChecksCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// ChangeMergeability mocks base method. -func (m *MockRepository) ChangeMergeability(ctx context.Context, id forge.ChangeID) (forge.ChangeMergeability, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ChangeMergeability", ctx, id) - ret0, _ := ret[0].(forge.ChangeMergeability) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ChangeMergeability indicates an expected call of ChangeMergeability. -func (mr *MockRepositoryMockRecorder) ChangeMergeability(ctx, id any) *MockRepositoryChangeMergeabilityCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeMergeability", reflect.TypeOf((*MockRepository)(nil).ChangeMergeability), ctx, id) - return &MockRepositoryChangeMergeabilityCall{Call: call} -} - -// MockRepositoryChangeMergeabilityCall wrap *gomock.Call -type MockRepositoryChangeMergeabilityCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockRepositoryChangeMergeabilityCall) Return(arg0 forge.ChangeMergeability, arg1 error) *MockRepositoryChangeMergeabilityCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockRepositoryChangeMergeabilityCall) Do(f func(context.Context, forge.ChangeID) (forge.ChangeMergeability, error)) *MockRepositoryChangeMergeabilityCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockRepositoryChangeMergeabilityCall) DoAndReturn(f func(context.Context, forge.ChangeID) (forge.ChangeMergeability, error)) *MockRepositoryChangeMergeabilityCall { +func (c *MockRepositoryChangeChecksStateCall) DoAndReturn(f func(context.Context, forge.ChangeID) (forge.ChecksState, error)) *MockRepositoryChangeChecksStateCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -1326,3 +1286,2042 @@ func (c *MockRepositoryUpdateChangeCommentCall) DoAndReturn(f func(context.Conte c.Call = c.Call.DoAndReturn(f) return c } + +// MockWithInlineComments is a mock of WithInlineComments interface. +type MockWithInlineComments struct { + ctrl *gomock.Controller + recorder *MockWithInlineCommentsMockRecorder + isgomock struct{} +} + +// MockWithInlineCommentsMockRecorder is the mock recorder for MockWithInlineComments. +type MockWithInlineCommentsMockRecorder struct { + mock *MockWithInlineComments +} + +// NewMockWithInlineComments creates a new mock instance. +func NewMockWithInlineComments(ctrl *gomock.Controller) *MockWithInlineComments { + mock := &MockWithInlineComments{ctrl: ctrl} + mock.recorder = &MockWithInlineCommentsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWithInlineComments) EXPECT() *MockWithInlineCommentsMockRecorder { + return m.recorder +} + +// ChangeChecksState mocks base method. +func (m *MockWithInlineComments) ChangeChecksState(ctx context.Context, id forge.ChangeID) (forge.ChecksState, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChangeChecksState", ctx, id) + ret0, _ := ret[0].(forge.ChecksState) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ChangeChecksState indicates an expected call of ChangeChecksState. +func (mr *MockWithInlineCommentsMockRecorder) ChangeChecksState(ctx, id any) *MockWithInlineCommentsChangeChecksStateCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeChecksState", reflect.TypeOf((*MockWithInlineComments)(nil).ChangeChecksState), ctx, id) + return &MockWithInlineCommentsChangeChecksStateCall{Call: call} +} + +// MockWithInlineCommentsChangeChecksStateCall wrap *gomock.Call +type MockWithInlineCommentsChangeChecksStateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsChangeChecksStateCall) Return(arg0 forge.ChecksState, arg1 error) *MockWithInlineCommentsChangeChecksStateCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsChangeChecksStateCall) Do(f func(context.Context, forge.ChangeID) (forge.ChecksState, error)) *MockWithInlineCommentsChangeChecksStateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsChangeChecksStateCall) DoAndReturn(f func(context.Context, forge.ChangeID) (forge.ChecksState, error)) *MockWithInlineCommentsChangeChecksStateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ChangeStatuses mocks base method. +func (m *MockWithInlineComments) ChangeStatuses(ctx context.Context, ids []forge.ChangeID) ([]forge.ChangeStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChangeStatuses", ctx, ids) + ret0, _ := ret[0].([]forge.ChangeStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ChangeStatuses indicates an expected call of ChangeStatuses. +func (mr *MockWithInlineCommentsMockRecorder) ChangeStatuses(ctx, ids any) *MockWithInlineCommentsChangeStatusesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeStatuses", reflect.TypeOf((*MockWithInlineComments)(nil).ChangeStatuses), ctx, ids) + return &MockWithInlineCommentsChangeStatusesCall{Call: call} +} + +// MockWithInlineCommentsChangeStatusesCall wrap *gomock.Call +type MockWithInlineCommentsChangeStatusesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsChangeStatusesCall) Return(arg0 []forge.ChangeStatus, arg1 error) *MockWithInlineCommentsChangeStatusesCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsChangeStatusesCall) Do(f func(context.Context, []forge.ChangeID) ([]forge.ChangeStatus, error)) *MockWithInlineCommentsChangeStatusesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsChangeStatusesCall) DoAndReturn(f func(context.Context, []forge.ChangeID) ([]forge.ChangeStatus, error)) *MockWithInlineCommentsChangeStatusesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// CommentCountsByChange mocks base method. +func (m *MockWithInlineComments) CommentCountsByChange(ctx context.Context, ids []forge.ChangeID) ([]*forge.CommentCounts, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CommentCountsByChange", ctx, ids) + ret0, _ := ret[0].([]*forge.CommentCounts) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CommentCountsByChange indicates an expected call of CommentCountsByChange. +func (mr *MockWithInlineCommentsMockRecorder) CommentCountsByChange(ctx, ids any) *MockWithInlineCommentsCommentCountsByChangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommentCountsByChange", reflect.TypeOf((*MockWithInlineComments)(nil).CommentCountsByChange), ctx, ids) + return &MockWithInlineCommentsCommentCountsByChangeCall{Call: call} +} + +// MockWithInlineCommentsCommentCountsByChangeCall wrap *gomock.Call +type MockWithInlineCommentsCommentCountsByChangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsCommentCountsByChangeCall) Return(arg0 []*forge.CommentCounts, arg1 error) *MockWithInlineCommentsCommentCountsByChangeCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsCommentCountsByChangeCall) Do(f func(context.Context, []forge.ChangeID) ([]*forge.CommentCounts, error)) *MockWithInlineCommentsCommentCountsByChangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsCommentCountsByChangeCall) DoAndReturn(f func(context.Context, []forge.ChangeID) ([]*forge.CommentCounts, error)) *MockWithInlineCommentsCommentCountsByChangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// DeleteChangeComment mocks base method. +func (m *MockWithInlineComments) DeleteChangeComment(arg0 context.Context, arg1 forge.ChangeCommentID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteChangeComment", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteChangeComment indicates an expected call of DeleteChangeComment. +func (mr *MockWithInlineCommentsMockRecorder) DeleteChangeComment(arg0, arg1 any) *MockWithInlineCommentsDeleteChangeCommentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChangeComment", reflect.TypeOf((*MockWithInlineComments)(nil).DeleteChangeComment), arg0, arg1) + return &MockWithInlineCommentsDeleteChangeCommentCall{Call: call} +} + +// MockWithInlineCommentsDeleteChangeCommentCall wrap *gomock.Call +type MockWithInlineCommentsDeleteChangeCommentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsDeleteChangeCommentCall) Return(arg0 error) *MockWithInlineCommentsDeleteChangeCommentCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsDeleteChangeCommentCall) Do(f func(context.Context, forge.ChangeCommentID) error) *MockWithInlineCommentsDeleteChangeCommentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsDeleteChangeCommentCall) DoAndReturn(f func(context.Context, forge.ChangeCommentID) error) *MockWithInlineCommentsDeleteChangeCommentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// EditChange mocks base method. +func (m *MockWithInlineComments) EditChange(ctx context.Context, id forge.ChangeID, opts forge.EditChangeOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EditChange", ctx, id, opts) + ret0, _ := ret[0].(error) + return ret0 +} + +// EditChange indicates an expected call of EditChange. +func (mr *MockWithInlineCommentsMockRecorder) EditChange(ctx, id, opts any) *MockWithInlineCommentsEditChangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditChange", reflect.TypeOf((*MockWithInlineComments)(nil).EditChange), ctx, id, opts) + return &MockWithInlineCommentsEditChangeCall{Call: call} +} + +// MockWithInlineCommentsEditChangeCall wrap *gomock.Call +type MockWithInlineCommentsEditChangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsEditChangeCall) Return(arg0 error) *MockWithInlineCommentsEditChangeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsEditChangeCall) Do(f func(context.Context, forge.ChangeID, forge.EditChangeOptions) error) *MockWithInlineCommentsEditChangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsEditChangeCall) DoAndReturn(f func(context.Context, forge.ChangeID, forge.EditChangeOptions) error) *MockWithInlineCommentsEditChangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// FindChangeByID mocks base method. +func (m *MockWithInlineComments) FindChangeByID(ctx context.Context, id forge.ChangeID) (*forge.FindChangeItem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindChangeByID", ctx, id) + ret0, _ := ret[0].(*forge.FindChangeItem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindChangeByID indicates an expected call of FindChangeByID. +func (mr *MockWithInlineCommentsMockRecorder) FindChangeByID(ctx, id any) *MockWithInlineCommentsFindChangeByIDCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindChangeByID", reflect.TypeOf((*MockWithInlineComments)(nil).FindChangeByID), ctx, id) + return &MockWithInlineCommentsFindChangeByIDCall{Call: call} +} + +// MockWithInlineCommentsFindChangeByIDCall wrap *gomock.Call +type MockWithInlineCommentsFindChangeByIDCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsFindChangeByIDCall) Return(arg0 *forge.FindChangeItem, arg1 error) *MockWithInlineCommentsFindChangeByIDCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsFindChangeByIDCall) Do(f func(context.Context, forge.ChangeID) (*forge.FindChangeItem, error)) *MockWithInlineCommentsFindChangeByIDCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsFindChangeByIDCall) DoAndReturn(f func(context.Context, forge.ChangeID) (*forge.FindChangeItem, error)) *MockWithInlineCommentsFindChangeByIDCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// FindChangesByBranch mocks base method. +func (m *MockWithInlineComments) FindChangesByBranch(ctx context.Context, branch string, opts forge.FindChangesOptions) ([]*forge.FindChangeItem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindChangesByBranch", ctx, branch, opts) + ret0, _ := ret[0].([]*forge.FindChangeItem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindChangesByBranch indicates an expected call of FindChangesByBranch. +func (mr *MockWithInlineCommentsMockRecorder) FindChangesByBranch(ctx, branch, opts any) *MockWithInlineCommentsFindChangesByBranchCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindChangesByBranch", reflect.TypeOf((*MockWithInlineComments)(nil).FindChangesByBranch), ctx, branch, opts) + return &MockWithInlineCommentsFindChangesByBranchCall{Call: call} +} + +// MockWithInlineCommentsFindChangesByBranchCall wrap *gomock.Call +type MockWithInlineCommentsFindChangesByBranchCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsFindChangesByBranchCall) Return(arg0 []*forge.FindChangeItem, arg1 error) *MockWithInlineCommentsFindChangesByBranchCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsFindChangesByBranchCall) Do(f func(context.Context, string, forge.FindChangesOptions) ([]*forge.FindChangeItem, error)) *MockWithInlineCommentsFindChangesByBranchCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsFindChangesByBranchCall) DoAndReturn(f func(context.Context, string, forge.FindChangesOptions) ([]*forge.FindChangeItem, error)) *MockWithInlineCommentsFindChangesByBranchCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Forge mocks base method. +func (m *MockWithInlineComments) Forge() forge.Forge { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Forge") + ret0, _ := ret[0].(forge.Forge) + return ret0 +} + +// Forge indicates an expected call of Forge. +func (mr *MockWithInlineCommentsMockRecorder) Forge() *MockWithInlineCommentsForgeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Forge", reflect.TypeOf((*MockWithInlineComments)(nil).Forge)) + return &MockWithInlineCommentsForgeCall{Call: call} +} + +// MockWithInlineCommentsForgeCall wrap *gomock.Call +type MockWithInlineCommentsForgeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsForgeCall) Return(arg0 forge.Forge) *MockWithInlineCommentsForgeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsForgeCall) Do(f func() forge.Forge) *MockWithInlineCommentsForgeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsForgeCall) DoAndReturn(f func() forge.Forge) *MockWithInlineCommentsForgeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ListChangeComments mocks base method. +func (m *MockWithInlineComments) ListChangeComments(arg0 context.Context, arg1 forge.ChangeID, arg2 *forge.ListChangeCommentsOptions) iter.Seq2[*forge.ListChangeCommentItem, error] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListChangeComments", arg0, arg1, arg2) + ret0, _ := ret[0].(iter.Seq2[*forge.ListChangeCommentItem, error]) + return ret0 +} + +// ListChangeComments indicates an expected call of ListChangeComments. +func (mr *MockWithInlineCommentsMockRecorder) ListChangeComments(arg0, arg1, arg2 any) *MockWithInlineCommentsListChangeCommentsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListChangeComments", reflect.TypeOf((*MockWithInlineComments)(nil).ListChangeComments), arg0, arg1, arg2) + return &MockWithInlineCommentsListChangeCommentsCall{Call: call} +} + +// MockWithInlineCommentsListChangeCommentsCall wrap *gomock.Call +type MockWithInlineCommentsListChangeCommentsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsListChangeCommentsCall) Return(arg0 iter.Seq2[*forge.ListChangeCommentItem, error]) *MockWithInlineCommentsListChangeCommentsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsListChangeCommentsCall) Do(f func(context.Context, forge.ChangeID, *forge.ListChangeCommentsOptions) iter.Seq2[*forge.ListChangeCommentItem, error]) *MockWithInlineCommentsListChangeCommentsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsListChangeCommentsCall) DoAndReturn(f func(context.Context, forge.ChangeID, *forge.ListChangeCommentsOptions) iter.Seq2[*forge.ListChangeCommentItem, error]) *MockWithInlineCommentsListChangeCommentsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ListChangeTemplates mocks base method. +func (m *MockWithInlineComments) ListChangeTemplates(arg0 context.Context) ([]*forge.ChangeTemplate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListChangeTemplates", arg0) + ret0, _ := ret[0].([]*forge.ChangeTemplate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListChangeTemplates indicates an expected call of ListChangeTemplates. +func (mr *MockWithInlineCommentsMockRecorder) ListChangeTemplates(arg0 any) *MockWithInlineCommentsListChangeTemplatesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListChangeTemplates", reflect.TypeOf((*MockWithInlineComments)(nil).ListChangeTemplates), arg0) + return &MockWithInlineCommentsListChangeTemplatesCall{Call: call} +} + +// MockWithInlineCommentsListChangeTemplatesCall wrap *gomock.Call +type MockWithInlineCommentsListChangeTemplatesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsListChangeTemplatesCall) Return(arg0 []*forge.ChangeTemplate, arg1 error) *MockWithInlineCommentsListChangeTemplatesCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsListChangeTemplatesCall) Do(f func(context.Context) ([]*forge.ChangeTemplate, error)) *MockWithInlineCommentsListChangeTemplatesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsListChangeTemplatesCall) DoAndReturn(f func(context.Context) ([]*forge.ChangeTemplate, error)) *MockWithInlineCommentsListChangeTemplatesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ListInlineComments mocks base method. +func (m *MockWithInlineComments) ListInlineComments(ctx context.Context, id forge.ChangeID) ([]*forge.InlineComment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListInlineComments", ctx, id) + ret0, _ := ret[0].([]*forge.InlineComment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListInlineComments indicates an expected call of ListInlineComments. +func (mr *MockWithInlineCommentsMockRecorder) ListInlineComments(ctx, id any) *MockWithInlineCommentsListInlineCommentsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInlineComments", reflect.TypeOf((*MockWithInlineComments)(nil).ListInlineComments), ctx, id) + return &MockWithInlineCommentsListInlineCommentsCall{Call: call} +} + +// MockWithInlineCommentsListInlineCommentsCall wrap *gomock.Call +type MockWithInlineCommentsListInlineCommentsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsListInlineCommentsCall) Return(arg0 []*forge.InlineComment, arg1 error) *MockWithInlineCommentsListInlineCommentsCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsListInlineCommentsCall) Do(f func(context.Context, forge.ChangeID) ([]*forge.InlineComment, error)) *MockWithInlineCommentsListInlineCommentsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsListInlineCommentsCall) DoAndReturn(f func(context.Context, forge.ChangeID) ([]*forge.InlineComment, error)) *MockWithInlineCommentsListInlineCommentsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MergeChange mocks base method. +func (m *MockWithInlineComments) MergeChange(ctx context.Context, id forge.ChangeID, opts forge.MergeChangeOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MergeChange", ctx, id, opts) + ret0, _ := ret[0].(error) + return ret0 +} + +// MergeChange indicates an expected call of MergeChange. +func (mr *MockWithInlineCommentsMockRecorder) MergeChange(ctx, id, opts any) *MockWithInlineCommentsMergeChangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MergeChange", reflect.TypeOf((*MockWithInlineComments)(nil).MergeChange), ctx, id, opts) + return &MockWithInlineCommentsMergeChangeCall{Call: call} +} + +// MockWithInlineCommentsMergeChangeCall wrap *gomock.Call +type MockWithInlineCommentsMergeChangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsMergeChangeCall) Return(arg0 error) *MockWithInlineCommentsMergeChangeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsMergeChangeCall) Do(f func(context.Context, forge.ChangeID, forge.MergeChangeOptions) error) *MockWithInlineCommentsMergeChangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsMergeChangeCall) DoAndReturn(f func(context.Context, forge.ChangeID, forge.MergeChangeOptions) error) *MockWithInlineCommentsMergeChangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// NewChangeMetadata mocks base method. +func (m *MockWithInlineComments) NewChangeMetadata(ctx context.Context, id forge.ChangeID) (forge.ChangeMetadata, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewChangeMetadata", ctx, id) + ret0, _ := ret[0].(forge.ChangeMetadata) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewChangeMetadata indicates an expected call of NewChangeMetadata. +func (mr *MockWithInlineCommentsMockRecorder) NewChangeMetadata(ctx, id any) *MockWithInlineCommentsNewChangeMetadataCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewChangeMetadata", reflect.TypeOf((*MockWithInlineComments)(nil).NewChangeMetadata), ctx, id) + return &MockWithInlineCommentsNewChangeMetadataCall{Call: call} +} + +// MockWithInlineCommentsNewChangeMetadataCall wrap *gomock.Call +type MockWithInlineCommentsNewChangeMetadataCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsNewChangeMetadataCall) Return(arg0 forge.ChangeMetadata, arg1 error) *MockWithInlineCommentsNewChangeMetadataCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsNewChangeMetadataCall) Do(f func(context.Context, forge.ChangeID) (forge.ChangeMetadata, error)) *MockWithInlineCommentsNewChangeMetadataCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsNewChangeMetadataCall) DoAndReturn(f func(context.Context, forge.ChangeID) (forge.ChangeMetadata, error)) *MockWithInlineCommentsNewChangeMetadataCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PostChangeComment mocks base method. +func (m *MockWithInlineComments) PostChangeComment(arg0 context.Context, arg1 forge.ChangeID, arg2 string) (forge.ChangeCommentID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostChangeComment", arg0, arg1, arg2) + ret0, _ := ret[0].(forge.ChangeCommentID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PostChangeComment indicates an expected call of PostChangeComment. +func (mr *MockWithInlineCommentsMockRecorder) PostChangeComment(arg0, arg1, arg2 any) *MockWithInlineCommentsPostChangeCommentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostChangeComment", reflect.TypeOf((*MockWithInlineComments)(nil).PostChangeComment), arg0, arg1, arg2) + return &MockWithInlineCommentsPostChangeCommentCall{Call: call} +} + +// MockWithInlineCommentsPostChangeCommentCall wrap *gomock.Call +type MockWithInlineCommentsPostChangeCommentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsPostChangeCommentCall) Return(arg0 forge.ChangeCommentID, arg1 error) *MockWithInlineCommentsPostChangeCommentCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsPostChangeCommentCall) Do(f func(context.Context, forge.ChangeID, string) (forge.ChangeCommentID, error)) *MockWithInlineCommentsPostChangeCommentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsPostChangeCommentCall) DoAndReturn(f func(context.Context, forge.ChangeID, string) (forge.ChangeCommentID, error)) *MockWithInlineCommentsPostChangeCommentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PostInlineComment mocks base method. +func (m *MockWithInlineComments) PostInlineComment(ctx context.Context, id forge.ChangeID, req forge.InlineCommentRequest) (*forge.InlineComment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostInlineComment", ctx, id, req) + ret0, _ := ret[0].(*forge.InlineComment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PostInlineComment indicates an expected call of PostInlineComment. +func (mr *MockWithInlineCommentsMockRecorder) PostInlineComment(ctx, id, req any) *MockWithInlineCommentsPostInlineCommentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostInlineComment", reflect.TypeOf((*MockWithInlineComments)(nil).PostInlineComment), ctx, id, req) + return &MockWithInlineCommentsPostInlineCommentCall{Call: call} +} + +// MockWithInlineCommentsPostInlineCommentCall wrap *gomock.Call +type MockWithInlineCommentsPostInlineCommentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsPostInlineCommentCall) Return(arg0 *forge.InlineComment, arg1 error) *MockWithInlineCommentsPostInlineCommentCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsPostInlineCommentCall) Do(f func(context.Context, forge.ChangeID, forge.InlineCommentRequest) (*forge.InlineComment, error)) *MockWithInlineCommentsPostInlineCommentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsPostInlineCommentCall) DoAndReturn(f func(context.Context, forge.ChangeID, forge.InlineCommentRequest) (*forge.InlineComment, error)) *MockWithInlineCommentsPostInlineCommentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SubmitChange mocks base method. +func (m *MockWithInlineComments) SubmitChange(ctx context.Context, req forge.SubmitChangeRequest) (forge.SubmitChangeResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubmitChange", ctx, req) + ret0, _ := ret[0].(forge.SubmitChangeResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubmitChange indicates an expected call of SubmitChange. +func (mr *MockWithInlineCommentsMockRecorder) SubmitChange(ctx, req any) *MockWithInlineCommentsSubmitChangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitChange", reflect.TypeOf((*MockWithInlineComments)(nil).SubmitChange), ctx, req) + return &MockWithInlineCommentsSubmitChangeCall{Call: call} +} + +// MockWithInlineCommentsSubmitChangeCall wrap *gomock.Call +type MockWithInlineCommentsSubmitChangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsSubmitChangeCall) Return(arg0 forge.SubmitChangeResult, arg1 error) *MockWithInlineCommentsSubmitChangeCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsSubmitChangeCall) Do(f func(context.Context, forge.SubmitChangeRequest) (forge.SubmitChangeResult, error)) *MockWithInlineCommentsSubmitChangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsSubmitChangeCall) DoAndReturn(f func(context.Context, forge.SubmitChangeRequest) (forge.SubmitChangeResult, error)) *MockWithInlineCommentsSubmitChangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SubmitReview mocks base method. +func (m *MockWithInlineComments) SubmitReview(ctx context.Context, id forge.ChangeID, req forge.ReviewRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubmitReview", ctx, id, req) + ret0, _ := ret[0].(error) + return ret0 +} + +// SubmitReview indicates an expected call of SubmitReview. +func (mr *MockWithInlineCommentsMockRecorder) SubmitReview(ctx, id, req any) *MockWithInlineCommentsSubmitReviewCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitReview", reflect.TypeOf((*MockWithInlineComments)(nil).SubmitReview), ctx, id, req) + return &MockWithInlineCommentsSubmitReviewCall{Call: call} +} + +// MockWithInlineCommentsSubmitReviewCall wrap *gomock.Call +type MockWithInlineCommentsSubmitReviewCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsSubmitReviewCall) Return(arg0 error) *MockWithInlineCommentsSubmitReviewCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsSubmitReviewCall) Do(f func(context.Context, forge.ChangeID, forge.ReviewRequest) error) *MockWithInlineCommentsSubmitReviewCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsSubmitReviewCall) DoAndReturn(f func(context.Context, forge.ChangeID, forge.ReviewRequest) error) *MockWithInlineCommentsSubmitReviewCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UpdateChangeComment mocks base method. +func (m *MockWithInlineComments) UpdateChangeComment(arg0 context.Context, arg1 forge.ChangeCommentID, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateChangeComment", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateChangeComment indicates an expected call of UpdateChangeComment. +func (mr *MockWithInlineCommentsMockRecorder) UpdateChangeComment(arg0, arg1, arg2 any) *MockWithInlineCommentsUpdateChangeCommentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChangeComment", reflect.TypeOf((*MockWithInlineComments)(nil).UpdateChangeComment), arg0, arg1, arg2) + return &MockWithInlineCommentsUpdateChangeCommentCall{Call: call} +} + +// MockWithInlineCommentsUpdateChangeCommentCall wrap *gomock.Call +type MockWithInlineCommentsUpdateChangeCommentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithInlineCommentsUpdateChangeCommentCall) Return(arg0 error) *MockWithInlineCommentsUpdateChangeCommentCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithInlineCommentsUpdateChangeCommentCall) Do(f func(context.Context, forge.ChangeCommentID, string) error) *MockWithInlineCommentsUpdateChangeCommentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithInlineCommentsUpdateChangeCommentCall) DoAndReturn(f func(context.Context, forge.ChangeCommentID, string) error) *MockWithInlineCommentsUpdateChangeCommentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockWithThreadResolution is a mock of WithThreadResolution interface. +type MockWithThreadResolution struct { + ctrl *gomock.Controller + recorder *MockWithThreadResolutionMockRecorder + isgomock struct{} +} + +// MockWithThreadResolutionMockRecorder is the mock recorder for MockWithThreadResolution. +type MockWithThreadResolutionMockRecorder struct { + mock *MockWithThreadResolution +} + +// NewMockWithThreadResolution creates a new mock instance. +func NewMockWithThreadResolution(ctrl *gomock.Controller) *MockWithThreadResolution { + mock := &MockWithThreadResolution{ctrl: ctrl} + mock.recorder = &MockWithThreadResolutionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWithThreadResolution) EXPECT() *MockWithThreadResolutionMockRecorder { + return m.recorder +} + +// ChangeChecksState mocks base method. +func (m *MockWithThreadResolution) ChangeChecksState(ctx context.Context, id forge.ChangeID) (forge.ChecksState, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChangeChecksState", ctx, id) + ret0, _ := ret[0].(forge.ChecksState) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ChangeChecksState indicates an expected call of ChangeChecksState. +func (mr *MockWithThreadResolutionMockRecorder) ChangeChecksState(ctx, id any) *MockWithThreadResolutionChangeChecksStateCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeChecksState", reflect.TypeOf((*MockWithThreadResolution)(nil).ChangeChecksState), ctx, id) + return &MockWithThreadResolutionChangeChecksStateCall{Call: call} +} + +// MockWithThreadResolutionChangeChecksStateCall wrap *gomock.Call +type MockWithThreadResolutionChangeChecksStateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionChangeChecksStateCall) Return(arg0 forge.ChecksState, arg1 error) *MockWithThreadResolutionChangeChecksStateCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionChangeChecksStateCall) Do(f func(context.Context, forge.ChangeID) (forge.ChecksState, error)) *MockWithThreadResolutionChangeChecksStateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionChangeChecksStateCall) DoAndReturn(f func(context.Context, forge.ChangeID) (forge.ChecksState, error)) *MockWithThreadResolutionChangeChecksStateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ChangeStatuses mocks base method. +func (m *MockWithThreadResolution) ChangeStatuses(ctx context.Context, ids []forge.ChangeID) ([]forge.ChangeStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChangeStatuses", ctx, ids) + ret0, _ := ret[0].([]forge.ChangeStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ChangeStatuses indicates an expected call of ChangeStatuses. +func (mr *MockWithThreadResolutionMockRecorder) ChangeStatuses(ctx, ids any) *MockWithThreadResolutionChangeStatusesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeStatuses", reflect.TypeOf((*MockWithThreadResolution)(nil).ChangeStatuses), ctx, ids) + return &MockWithThreadResolutionChangeStatusesCall{Call: call} +} + +// MockWithThreadResolutionChangeStatusesCall wrap *gomock.Call +type MockWithThreadResolutionChangeStatusesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionChangeStatusesCall) Return(arg0 []forge.ChangeStatus, arg1 error) *MockWithThreadResolutionChangeStatusesCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionChangeStatusesCall) Do(f func(context.Context, []forge.ChangeID) ([]forge.ChangeStatus, error)) *MockWithThreadResolutionChangeStatusesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionChangeStatusesCall) DoAndReturn(f func(context.Context, []forge.ChangeID) ([]forge.ChangeStatus, error)) *MockWithThreadResolutionChangeStatusesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// CommentCountsByChange mocks base method. +func (m *MockWithThreadResolution) CommentCountsByChange(ctx context.Context, ids []forge.ChangeID) ([]*forge.CommentCounts, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CommentCountsByChange", ctx, ids) + ret0, _ := ret[0].([]*forge.CommentCounts) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CommentCountsByChange indicates an expected call of CommentCountsByChange. +func (mr *MockWithThreadResolutionMockRecorder) CommentCountsByChange(ctx, ids any) *MockWithThreadResolutionCommentCountsByChangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommentCountsByChange", reflect.TypeOf((*MockWithThreadResolution)(nil).CommentCountsByChange), ctx, ids) + return &MockWithThreadResolutionCommentCountsByChangeCall{Call: call} +} + +// MockWithThreadResolutionCommentCountsByChangeCall wrap *gomock.Call +type MockWithThreadResolutionCommentCountsByChangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionCommentCountsByChangeCall) Return(arg0 []*forge.CommentCounts, arg1 error) *MockWithThreadResolutionCommentCountsByChangeCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionCommentCountsByChangeCall) Do(f func(context.Context, []forge.ChangeID) ([]*forge.CommentCounts, error)) *MockWithThreadResolutionCommentCountsByChangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionCommentCountsByChangeCall) DoAndReturn(f func(context.Context, []forge.ChangeID) ([]*forge.CommentCounts, error)) *MockWithThreadResolutionCommentCountsByChangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// DeleteChangeComment mocks base method. +func (m *MockWithThreadResolution) DeleteChangeComment(arg0 context.Context, arg1 forge.ChangeCommentID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteChangeComment", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteChangeComment indicates an expected call of DeleteChangeComment. +func (mr *MockWithThreadResolutionMockRecorder) DeleteChangeComment(arg0, arg1 any) *MockWithThreadResolutionDeleteChangeCommentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChangeComment", reflect.TypeOf((*MockWithThreadResolution)(nil).DeleteChangeComment), arg0, arg1) + return &MockWithThreadResolutionDeleteChangeCommentCall{Call: call} +} + +// MockWithThreadResolutionDeleteChangeCommentCall wrap *gomock.Call +type MockWithThreadResolutionDeleteChangeCommentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionDeleteChangeCommentCall) Return(arg0 error) *MockWithThreadResolutionDeleteChangeCommentCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionDeleteChangeCommentCall) Do(f func(context.Context, forge.ChangeCommentID) error) *MockWithThreadResolutionDeleteChangeCommentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionDeleteChangeCommentCall) DoAndReturn(f func(context.Context, forge.ChangeCommentID) error) *MockWithThreadResolutionDeleteChangeCommentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// EditChange mocks base method. +func (m *MockWithThreadResolution) EditChange(ctx context.Context, id forge.ChangeID, opts forge.EditChangeOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EditChange", ctx, id, opts) + ret0, _ := ret[0].(error) + return ret0 +} + +// EditChange indicates an expected call of EditChange. +func (mr *MockWithThreadResolutionMockRecorder) EditChange(ctx, id, opts any) *MockWithThreadResolutionEditChangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditChange", reflect.TypeOf((*MockWithThreadResolution)(nil).EditChange), ctx, id, opts) + return &MockWithThreadResolutionEditChangeCall{Call: call} +} + +// MockWithThreadResolutionEditChangeCall wrap *gomock.Call +type MockWithThreadResolutionEditChangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionEditChangeCall) Return(arg0 error) *MockWithThreadResolutionEditChangeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionEditChangeCall) Do(f func(context.Context, forge.ChangeID, forge.EditChangeOptions) error) *MockWithThreadResolutionEditChangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionEditChangeCall) DoAndReturn(f func(context.Context, forge.ChangeID, forge.EditChangeOptions) error) *MockWithThreadResolutionEditChangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// FindChangeByID mocks base method. +func (m *MockWithThreadResolution) FindChangeByID(ctx context.Context, id forge.ChangeID) (*forge.FindChangeItem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindChangeByID", ctx, id) + ret0, _ := ret[0].(*forge.FindChangeItem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindChangeByID indicates an expected call of FindChangeByID. +func (mr *MockWithThreadResolutionMockRecorder) FindChangeByID(ctx, id any) *MockWithThreadResolutionFindChangeByIDCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindChangeByID", reflect.TypeOf((*MockWithThreadResolution)(nil).FindChangeByID), ctx, id) + return &MockWithThreadResolutionFindChangeByIDCall{Call: call} +} + +// MockWithThreadResolutionFindChangeByIDCall wrap *gomock.Call +type MockWithThreadResolutionFindChangeByIDCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionFindChangeByIDCall) Return(arg0 *forge.FindChangeItem, arg1 error) *MockWithThreadResolutionFindChangeByIDCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionFindChangeByIDCall) Do(f func(context.Context, forge.ChangeID) (*forge.FindChangeItem, error)) *MockWithThreadResolutionFindChangeByIDCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionFindChangeByIDCall) DoAndReturn(f func(context.Context, forge.ChangeID) (*forge.FindChangeItem, error)) *MockWithThreadResolutionFindChangeByIDCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// FindChangesByBranch mocks base method. +func (m *MockWithThreadResolution) FindChangesByBranch(ctx context.Context, branch string, opts forge.FindChangesOptions) ([]*forge.FindChangeItem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindChangesByBranch", ctx, branch, opts) + ret0, _ := ret[0].([]*forge.FindChangeItem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindChangesByBranch indicates an expected call of FindChangesByBranch. +func (mr *MockWithThreadResolutionMockRecorder) FindChangesByBranch(ctx, branch, opts any) *MockWithThreadResolutionFindChangesByBranchCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindChangesByBranch", reflect.TypeOf((*MockWithThreadResolution)(nil).FindChangesByBranch), ctx, branch, opts) + return &MockWithThreadResolutionFindChangesByBranchCall{Call: call} +} + +// MockWithThreadResolutionFindChangesByBranchCall wrap *gomock.Call +type MockWithThreadResolutionFindChangesByBranchCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionFindChangesByBranchCall) Return(arg0 []*forge.FindChangeItem, arg1 error) *MockWithThreadResolutionFindChangesByBranchCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionFindChangesByBranchCall) Do(f func(context.Context, string, forge.FindChangesOptions) ([]*forge.FindChangeItem, error)) *MockWithThreadResolutionFindChangesByBranchCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionFindChangesByBranchCall) DoAndReturn(f func(context.Context, string, forge.FindChangesOptions) ([]*forge.FindChangeItem, error)) *MockWithThreadResolutionFindChangesByBranchCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Forge mocks base method. +func (m *MockWithThreadResolution) Forge() forge.Forge { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Forge") + ret0, _ := ret[0].(forge.Forge) + return ret0 +} + +// Forge indicates an expected call of Forge. +func (mr *MockWithThreadResolutionMockRecorder) Forge() *MockWithThreadResolutionForgeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Forge", reflect.TypeOf((*MockWithThreadResolution)(nil).Forge)) + return &MockWithThreadResolutionForgeCall{Call: call} +} + +// MockWithThreadResolutionForgeCall wrap *gomock.Call +type MockWithThreadResolutionForgeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionForgeCall) Return(arg0 forge.Forge) *MockWithThreadResolutionForgeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionForgeCall) Do(f func() forge.Forge) *MockWithThreadResolutionForgeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionForgeCall) DoAndReturn(f func() forge.Forge) *MockWithThreadResolutionForgeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ListChangeComments mocks base method. +func (m *MockWithThreadResolution) ListChangeComments(arg0 context.Context, arg1 forge.ChangeID, arg2 *forge.ListChangeCommentsOptions) iter.Seq2[*forge.ListChangeCommentItem, error] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListChangeComments", arg0, arg1, arg2) + ret0, _ := ret[0].(iter.Seq2[*forge.ListChangeCommentItem, error]) + return ret0 +} + +// ListChangeComments indicates an expected call of ListChangeComments. +func (mr *MockWithThreadResolutionMockRecorder) ListChangeComments(arg0, arg1, arg2 any) *MockWithThreadResolutionListChangeCommentsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListChangeComments", reflect.TypeOf((*MockWithThreadResolution)(nil).ListChangeComments), arg0, arg1, arg2) + return &MockWithThreadResolutionListChangeCommentsCall{Call: call} +} + +// MockWithThreadResolutionListChangeCommentsCall wrap *gomock.Call +type MockWithThreadResolutionListChangeCommentsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionListChangeCommentsCall) Return(arg0 iter.Seq2[*forge.ListChangeCommentItem, error]) *MockWithThreadResolutionListChangeCommentsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionListChangeCommentsCall) Do(f func(context.Context, forge.ChangeID, *forge.ListChangeCommentsOptions) iter.Seq2[*forge.ListChangeCommentItem, error]) *MockWithThreadResolutionListChangeCommentsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionListChangeCommentsCall) DoAndReturn(f func(context.Context, forge.ChangeID, *forge.ListChangeCommentsOptions) iter.Seq2[*forge.ListChangeCommentItem, error]) *MockWithThreadResolutionListChangeCommentsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ListChangeTemplates mocks base method. +func (m *MockWithThreadResolution) ListChangeTemplates(arg0 context.Context) ([]*forge.ChangeTemplate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListChangeTemplates", arg0) + ret0, _ := ret[0].([]*forge.ChangeTemplate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListChangeTemplates indicates an expected call of ListChangeTemplates. +func (mr *MockWithThreadResolutionMockRecorder) ListChangeTemplates(arg0 any) *MockWithThreadResolutionListChangeTemplatesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListChangeTemplates", reflect.TypeOf((*MockWithThreadResolution)(nil).ListChangeTemplates), arg0) + return &MockWithThreadResolutionListChangeTemplatesCall{Call: call} +} + +// MockWithThreadResolutionListChangeTemplatesCall wrap *gomock.Call +type MockWithThreadResolutionListChangeTemplatesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionListChangeTemplatesCall) Return(arg0 []*forge.ChangeTemplate, arg1 error) *MockWithThreadResolutionListChangeTemplatesCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionListChangeTemplatesCall) Do(f func(context.Context) ([]*forge.ChangeTemplate, error)) *MockWithThreadResolutionListChangeTemplatesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionListChangeTemplatesCall) DoAndReturn(f func(context.Context) ([]*forge.ChangeTemplate, error)) *MockWithThreadResolutionListChangeTemplatesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MergeChange mocks base method. +func (m *MockWithThreadResolution) MergeChange(ctx context.Context, id forge.ChangeID, opts forge.MergeChangeOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MergeChange", ctx, id, opts) + ret0, _ := ret[0].(error) + return ret0 +} + +// MergeChange indicates an expected call of MergeChange. +func (mr *MockWithThreadResolutionMockRecorder) MergeChange(ctx, id, opts any) *MockWithThreadResolutionMergeChangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MergeChange", reflect.TypeOf((*MockWithThreadResolution)(nil).MergeChange), ctx, id, opts) + return &MockWithThreadResolutionMergeChangeCall{Call: call} +} + +// MockWithThreadResolutionMergeChangeCall wrap *gomock.Call +type MockWithThreadResolutionMergeChangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionMergeChangeCall) Return(arg0 error) *MockWithThreadResolutionMergeChangeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionMergeChangeCall) Do(f func(context.Context, forge.ChangeID, forge.MergeChangeOptions) error) *MockWithThreadResolutionMergeChangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionMergeChangeCall) DoAndReturn(f func(context.Context, forge.ChangeID, forge.MergeChangeOptions) error) *MockWithThreadResolutionMergeChangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// NewChangeMetadata mocks base method. +func (m *MockWithThreadResolution) NewChangeMetadata(ctx context.Context, id forge.ChangeID) (forge.ChangeMetadata, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewChangeMetadata", ctx, id) + ret0, _ := ret[0].(forge.ChangeMetadata) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewChangeMetadata indicates an expected call of NewChangeMetadata. +func (mr *MockWithThreadResolutionMockRecorder) NewChangeMetadata(ctx, id any) *MockWithThreadResolutionNewChangeMetadataCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewChangeMetadata", reflect.TypeOf((*MockWithThreadResolution)(nil).NewChangeMetadata), ctx, id) + return &MockWithThreadResolutionNewChangeMetadataCall{Call: call} +} + +// MockWithThreadResolutionNewChangeMetadataCall wrap *gomock.Call +type MockWithThreadResolutionNewChangeMetadataCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionNewChangeMetadataCall) Return(arg0 forge.ChangeMetadata, arg1 error) *MockWithThreadResolutionNewChangeMetadataCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionNewChangeMetadataCall) Do(f func(context.Context, forge.ChangeID) (forge.ChangeMetadata, error)) *MockWithThreadResolutionNewChangeMetadataCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionNewChangeMetadataCall) DoAndReturn(f func(context.Context, forge.ChangeID) (forge.ChangeMetadata, error)) *MockWithThreadResolutionNewChangeMetadataCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PostChangeComment mocks base method. +func (m *MockWithThreadResolution) PostChangeComment(arg0 context.Context, arg1 forge.ChangeID, arg2 string) (forge.ChangeCommentID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostChangeComment", arg0, arg1, arg2) + ret0, _ := ret[0].(forge.ChangeCommentID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PostChangeComment indicates an expected call of PostChangeComment. +func (mr *MockWithThreadResolutionMockRecorder) PostChangeComment(arg0, arg1, arg2 any) *MockWithThreadResolutionPostChangeCommentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostChangeComment", reflect.TypeOf((*MockWithThreadResolution)(nil).PostChangeComment), arg0, arg1, arg2) + return &MockWithThreadResolutionPostChangeCommentCall{Call: call} +} + +// MockWithThreadResolutionPostChangeCommentCall wrap *gomock.Call +type MockWithThreadResolutionPostChangeCommentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionPostChangeCommentCall) Return(arg0 forge.ChangeCommentID, arg1 error) *MockWithThreadResolutionPostChangeCommentCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionPostChangeCommentCall) Do(f func(context.Context, forge.ChangeID, string) (forge.ChangeCommentID, error)) *MockWithThreadResolutionPostChangeCommentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionPostChangeCommentCall) DoAndReturn(f func(context.Context, forge.ChangeID, string) (forge.ChangeCommentID, error)) *MockWithThreadResolutionPostChangeCommentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ResolveThread mocks base method. +func (m *MockWithThreadResolution) ResolveThread(ctx context.Context, threadID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ResolveThread", ctx, threadID) + ret0, _ := ret[0].(error) + return ret0 +} + +// ResolveThread indicates an expected call of ResolveThread. +func (mr *MockWithThreadResolutionMockRecorder) ResolveThread(ctx, threadID any) *MockWithThreadResolutionResolveThreadCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveThread", reflect.TypeOf((*MockWithThreadResolution)(nil).ResolveThread), ctx, threadID) + return &MockWithThreadResolutionResolveThreadCall{Call: call} +} + +// MockWithThreadResolutionResolveThreadCall wrap *gomock.Call +type MockWithThreadResolutionResolveThreadCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionResolveThreadCall) Return(arg0 error) *MockWithThreadResolutionResolveThreadCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionResolveThreadCall) Do(f func(context.Context, string) error) *MockWithThreadResolutionResolveThreadCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionResolveThreadCall) DoAndReturn(f func(context.Context, string) error) *MockWithThreadResolutionResolveThreadCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SubmitChange mocks base method. +func (m *MockWithThreadResolution) SubmitChange(ctx context.Context, req forge.SubmitChangeRequest) (forge.SubmitChangeResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubmitChange", ctx, req) + ret0, _ := ret[0].(forge.SubmitChangeResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubmitChange indicates an expected call of SubmitChange. +func (mr *MockWithThreadResolutionMockRecorder) SubmitChange(ctx, req any) *MockWithThreadResolutionSubmitChangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitChange", reflect.TypeOf((*MockWithThreadResolution)(nil).SubmitChange), ctx, req) + return &MockWithThreadResolutionSubmitChangeCall{Call: call} +} + +// MockWithThreadResolutionSubmitChangeCall wrap *gomock.Call +type MockWithThreadResolutionSubmitChangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionSubmitChangeCall) Return(arg0 forge.SubmitChangeResult, arg1 error) *MockWithThreadResolutionSubmitChangeCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionSubmitChangeCall) Do(f func(context.Context, forge.SubmitChangeRequest) (forge.SubmitChangeResult, error)) *MockWithThreadResolutionSubmitChangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionSubmitChangeCall) DoAndReturn(f func(context.Context, forge.SubmitChangeRequest) (forge.SubmitChangeResult, error)) *MockWithThreadResolutionSubmitChangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UnresolveThread mocks base method. +func (m *MockWithThreadResolution) UnresolveThread(ctx context.Context, threadID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnresolveThread", ctx, threadID) + ret0, _ := ret[0].(error) + return ret0 +} + +// UnresolveThread indicates an expected call of UnresolveThread. +func (mr *MockWithThreadResolutionMockRecorder) UnresolveThread(ctx, threadID any) *MockWithThreadResolutionUnresolveThreadCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnresolveThread", reflect.TypeOf((*MockWithThreadResolution)(nil).UnresolveThread), ctx, threadID) + return &MockWithThreadResolutionUnresolveThreadCall{Call: call} +} + +// MockWithThreadResolutionUnresolveThreadCall wrap *gomock.Call +type MockWithThreadResolutionUnresolveThreadCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionUnresolveThreadCall) Return(arg0 error) *MockWithThreadResolutionUnresolveThreadCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionUnresolveThreadCall) Do(f func(context.Context, string) error) *MockWithThreadResolutionUnresolveThreadCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionUnresolveThreadCall) DoAndReturn(f func(context.Context, string) error) *MockWithThreadResolutionUnresolveThreadCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UpdateChangeComment mocks base method. +func (m *MockWithThreadResolution) UpdateChangeComment(arg0 context.Context, arg1 forge.ChangeCommentID, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateChangeComment", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateChangeComment indicates an expected call of UpdateChangeComment. +func (mr *MockWithThreadResolutionMockRecorder) UpdateChangeComment(arg0, arg1, arg2 any) *MockWithThreadResolutionUpdateChangeCommentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChangeComment", reflect.TypeOf((*MockWithThreadResolution)(nil).UpdateChangeComment), arg0, arg1, arg2) + return &MockWithThreadResolutionUpdateChangeCommentCall{Call: call} +} + +// MockWithThreadResolutionUpdateChangeCommentCall wrap *gomock.Call +type MockWithThreadResolutionUpdateChangeCommentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithThreadResolutionUpdateChangeCommentCall) Return(arg0 error) *MockWithThreadResolutionUpdateChangeCommentCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithThreadResolutionUpdateChangeCommentCall) Do(f func(context.Context, forge.ChangeCommentID, string) error) *MockWithThreadResolutionUpdateChangeCommentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithThreadResolutionUpdateChangeCommentCall) DoAndReturn(f func(context.Context, forge.ChangeCommentID, string) error) *MockWithThreadResolutionUpdateChangeCommentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockWithCommentEdit is a mock of WithCommentEdit interface. +type MockWithCommentEdit struct { + ctrl *gomock.Controller + recorder *MockWithCommentEditMockRecorder + isgomock struct{} +} + +// MockWithCommentEditMockRecorder is the mock recorder for MockWithCommentEdit. +type MockWithCommentEditMockRecorder struct { + mock *MockWithCommentEdit +} + +// NewMockWithCommentEdit creates a new mock instance. +func NewMockWithCommentEdit(ctrl *gomock.Controller) *MockWithCommentEdit { + mock := &MockWithCommentEdit{ctrl: ctrl} + mock.recorder = &MockWithCommentEditMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWithCommentEdit) EXPECT() *MockWithCommentEditMockRecorder { + return m.recorder +} + +// ChangeChecksState mocks base method. +func (m *MockWithCommentEdit) ChangeChecksState(ctx context.Context, id forge.ChangeID) (forge.ChecksState, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChangeChecksState", ctx, id) + ret0, _ := ret[0].(forge.ChecksState) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ChangeChecksState indicates an expected call of ChangeChecksState. +func (mr *MockWithCommentEditMockRecorder) ChangeChecksState(ctx, id any) *MockWithCommentEditChangeChecksStateCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeChecksState", reflect.TypeOf((*MockWithCommentEdit)(nil).ChangeChecksState), ctx, id) + return &MockWithCommentEditChangeChecksStateCall{Call: call} +} + +// MockWithCommentEditChangeChecksStateCall wrap *gomock.Call +type MockWithCommentEditChangeChecksStateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditChangeChecksStateCall) Return(arg0 forge.ChecksState, arg1 error) *MockWithCommentEditChangeChecksStateCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditChangeChecksStateCall) Do(f func(context.Context, forge.ChangeID) (forge.ChecksState, error)) *MockWithCommentEditChangeChecksStateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditChangeChecksStateCall) DoAndReturn(f func(context.Context, forge.ChangeID) (forge.ChecksState, error)) *MockWithCommentEditChangeChecksStateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ChangeStatuses mocks base method. +func (m *MockWithCommentEdit) ChangeStatuses(ctx context.Context, ids []forge.ChangeID) ([]forge.ChangeStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChangeStatuses", ctx, ids) + ret0, _ := ret[0].([]forge.ChangeStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ChangeStatuses indicates an expected call of ChangeStatuses. +func (mr *MockWithCommentEditMockRecorder) ChangeStatuses(ctx, ids any) *MockWithCommentEditChangeStatusesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeStatuses", reflect.TypeOf((*MockWithCommentEdit)(nil).ChangeStatuses), ctx, ids) + return &MockWithCommentEditChangeStatusesCall{Call: call} +} + +// MockWithCommentEditChangeStatusesCall wrap *gomock.Call +type MockWithCommentEditChangeStatusesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditChangeStatusesCall) Return(arg0 []forge.ChangeStatus, arg1 error) *MockWithCommentEditChangeStatusesCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditChangeStatusesCall) Do(f func(context.Context, []forge.ChangeID) ([]forge.ChangeStatus, error)) *MockWithCommentEditChangeStatusesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditChangeStatusesCall) DoAndReturn(f func(context.Context, []forge.ChangeID) ([]forge.ChangeStatus, error)) *MockWithCommentEditChangeStatusesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// CommentCountsByChange mocks base method. +func (m *MockWithCommentEdit) CommentCountsByChange(ctx context.Context, ids []forge.ChangeID) ([]*forge.CommentCounts, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CommentCountsByChange", ctx, ids) + ret0, _ := ret[0].([]*forge.CommentCounts) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CommentCountsByChange indicates an expected call of CommentCountsByChange. +func (mr *MockWithCommentEditMockRecorder) CommentCountsByChange(ctx, ids any) *MockWithCommentEditCommentCountsByChangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommentCountsByChange", reflect.TypeOf((*MockWithCommentEdit)(nil).CommentCountsByChange), ctx, ids) + return &MockWithCommentEditCommentCountsByChangeCall{Call: call} +} + +// MockWithCommentEditCommentCountsByChangeCall wrap *gomock.Call +type MockWithCommentEditCommentCountsByChangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditCommentCountsByChangeCall) Return(arg0 []*forge.CommentCounts, arg1 error) *MockWithCommentEditCommentCountsByChangeCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditCommentCountsByChangeCall) Do(f func(context.Context, []forge.ChangeID) ([]*forge.CommentCounts, error)) *MockWithCommentEditCommentCountsByChangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditCommentCountsByChangeCall) DoAndReturn(f func(context.Context, []forge.ChangeID) ([]*forge.CommentCounts, error)) *MockWithCommentEditCommentCountsByChangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// DeleteChangeComment mocks base method. +func (m *MockWithCommentEdit) DeleteChangeComment(arg0 context.Context, arg1 forge.ChangeCommentID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteChangeComment", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteChangeComment indicates an expected call of DeleteChangeComment. +func (mr *MockWithCommentEditMockRecorder) DeleteChangeComment(arg0, arg1 any) *MockWithCommentEditDeleteChangeCommentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChangeComment", reflect.TypeOf((*MockWithCommentEdit)(nil).DeleteChangeComment), arg0, arg1) + return &MockWithCommentEditDeleteChangeCommentCall{Call: call} +} + +// MockWithCommentEditDeleteChangeCommentCall wrap *gomock.Call +type MockWithCommentEditDeleteChangeCommentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditDeleteChangeCommentCall) Return(arg0 error) *MockWithCommentEditDeleteChangeCommentCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditDeleteChangeCommentCall) Do(f func(context.Context, forge.ChangeCommentID) error) *MockWithCommentEditDeleteChangeCommentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditDeleteChangeCommentCall) DoAndReturn(f func(context.Context, forge.ChangeCommentID) error) *MockWithCommentEditDeleteChangeCommentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// EditChange mocks base method. +func (m *MockWithCommentEdit) EditChange(ctx context.Context, id forge.ChangeID, opts forge.EditChangeOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EditChange", ctx, id, opts) + ret0, _ := ret[0].(error) + return ret0 +} + +// EditChange indicates an expected call of EditChange. +func (mr *MockWithCommentEditMockRecorder) EditChange(ctx, id, opts any) *MockWithCommentEditEditChangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditChange", reflect.TypeOf((*MockWithCommentEdit)(nil).EditChange), ctx, id, opts) + return &MockWithCommentEditEditChangeCall{Call: call} +} + +// MockWithCommentEditEditChangeCall wrap *gomock.Call +type MockWithCommentEditEditChangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditEditChangeCall) Return(arg0 error) *MockWithCommentEditEditChangeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditEditChangeCall) Do(f func(context.Context, forge.ChangeID, forge.EditChangeOptions) error) *MockWithCommentEditEditChangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditEditChangeCall) DoAndReturn(f func(context.Context, forge.ChangeID, forge.EditChangeOptions) error) *MockWithCommentEditEditChangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// EditComment mocks base method. +func (m *MockWithCommentEdit) EditComment(ctx context.Context, id forge.ChangeCommentID, body string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EditComment", ctx, id, body) + ret0, _ := ret[0].(error) + return ret0 +} + +// EditComment indicates an expected call of EditComment. +func (mr *MockWithCommentEditMockRecorder) EditComment(ctx, id, body any) *MockWithCommentEditEditCommentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditComment", reflect.TypeOf((*MockWithCommentEdit)(nil).EditComment), ctx, id, body) + return &MockWithCommentEditEditCommentCall{Call: call} +} + +// MockWithCommentEditEditCommentCall wrap *gomock.Call +type MockWithCommentEditEditCommentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditEditCommentCall) Return(arg0 error) *MockWithCommentEditEditCommentCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditEditCommentCall) Do(f func(context.Context, forge.ChangeCommentID, string) error) *MockWithCommentEditEditCommentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditEditCommentCall) DoAndReturn(f func(context.Context, forge.ChangeCommentID, string) error) *MockWithCommentEditEditCommentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// FindChangeByID mocks base method. +func (m *MockWithCommentEdit) FindChangeByID(ctx context.Context, id forge.ChangeID) (*forge.FindChangeItem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindChangeByID", ctx, id) + ret0, _ := ret[0].(*forge.FindChangeItem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindChangeByID indicates an expected call of FindChangeByID. +func (mr *MockWithCommentEditMockRecorder) FindChangeByID(ctx, id any) *MockWithCommentEditFindChangeByIDCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindChangeByID", reflect.TypeOf((*MockWithCommentEdit)(nil).FindChangeByID), ctx, id) + return &MockWithCommentEditFindChangeByIDCall{Call: call} +} + +// MockWithCommentEditFindChangeByIDCall wrap *gomock.Call +type MockWithCommentEditFindChangeByIDCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditFindChangeByIDCall) Return(arg0 *forge.FindChangeItem, arg1 error) *MockWithCommentEditFindChangeByIDCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditFindChangeByIDCall) Do(f func(context.Context, forge.ChangeID) (*forge.FindChangeItem, error)) *MockWithCommentEditFindChangeByIDCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditFindChangeByIDCall) DoAndReturn(f func(context.Context, forge.ChangeID) (*forge.FindChangeItem, error)) *MockWithCommentEditFindChangeByIDCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// FindChangesByBranch mocks base method. +func (m *MockWithCommentEdit) FindChangesByBranch(ctx context.Context, branch string, opts forge.FindChangesOptions) ([]*forge.FindChangeItem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindChangesByBranch", ctx, branch, opts) + ret0, _ := ret[0].([]*forge.FindChangeItem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindChangesByBranch indicates an expected call of FindChangesByBranch. +func (mr *MockWithCommentEditMockRecorder) FindChangesByBranch(ctx, branch, opts any) *MockWithCommentEditFindChangesByBranchCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindChangesByBranch", reflect.TypeOf((*MockWithCommentEdit)(nil).FindChangesByBranch), ctx, branch, opts) + return &MockWithCommentEditFindChangesByBranchCall{Call: call} +} + +// MockWithCommentEditFindChangesByBranchCall wrap *gomock.Call +type MockWithCommentEditFindChangesByBranchCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditFindChangesByBranchCall) Return(arg0 []*forge.FindChangeItem, arg1 error) *MockWithCommentEditFindChangesByBranchCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditFindChangesByBranchCall) Do(f func(context.Context, string, forge.FindChangesOptions) ([]*forge.FindChangeItem, error)) *MockWithCommentEditFindChangesByBranchCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditFindChangesByBranchCall) DoAndReturn(f func(context.Context, string, forge.FindChangesOptions) ([]*forge.FindChangeItem, error)) *MockWithCommentEditFindChangesByBranchCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Forge mocks base method. +func (m *MockWithCommentEdit) Forge() forge.Forge { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Forge") + ret0, _ := ret[0].(forge.Forge) + return ret0 +} + +// Forge indicates an expected call of Forge. +func (mr *MockWithCommentEditMockRecorder) Forge() *MockWithCommentEditForgeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Forge", reflect.TypeOf((*MockWithCommentEdit)(nil).Forge)) + return &MockWithCommentEditForgeCall{Call: call} +} + +// MockWithCommentEditForgeCall wrap *gomock.Call +type MockWithCommentEditForgeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditForgeCall) Return(arg0 forge.Forge) *MockWithCommentEditForgeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditForgeCall) Do(f func() forge.Forge) *MockWithCommentEditForgeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditForgeCall) DoAndReturn(f func() forge.Forge) *MockWithCommentEditForgeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ListChangeComments mocks base method. +func (m *MockWithCommentEdit) ListChangeComments(arg0 context.Context, arg1 forge.ChangeID, arg2 *forge.ListChangeCommentsOptions) iter.Seq2[*forge.ListChangeCommentItem, error] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListChangeComments", arg0, arg1, arg2) + ret0, _ := ret[0].(iter.Seq2[*forge.ListChangeCommentItem, error]) + return ret0 +} + +// ListChangeComments indicates an expected call of ListChangeComments. +func (mr *MockWithCommentEditMockRecorder) ListChangeComments(arg0, arg1, arg2 any) *MockWithCommentEditListChangeCommentsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListChangeComments", reflect.TypeOf((*MockWithCommentEdit)(nil).ListChangeComments), arg0, arg1, arg2) + return &MockWithCommentEditListChangeCommentsCall{Call: call} +} + +// MockWithCommentEditListChangeCommentsCall wrap *gomock.Call +type MockWithCommentEditListChangeCommentsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditListChangeCommentsCall) Return(arg0 iter.Seq2[*forge.ListChangeCommentItem, error]) *MockWithCommentEditListChangeCommentsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditListChangeCommentsCall) Do(f func(context.Context, forge.ChangeID, *forge.ListChangeCommentsOptions) iter.Seq2[*forge.ListChangeCommentItem, error]) *MockWithCommentEditListChangeCommentsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditListChangeCommentsCall) DoAndReturn(f func(context.Context, forge.ChangeID, *forge.ListChangeCommentsOptions) iter.Seq2[*forge.ListChangeCommentItem, error]) *MockWithCommentEditListChangeCommentsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ListChangeTemplates mocks base method. +func (m *MockWithCommentEdit) ListChangeTemplates(arg0 context.Context) ([]*forge.ChangeTemplate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListChangeTemplates", arg0) + ret0, _ := ret[0].([]*forge.ChangeTemplate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListChangeTemplates indicates an expected call of ListChangeTemplates. +func (mr *MockWithCommentEditMockRecorder) ListChangeTemplates(arg0 any) *MockWithCommentEditListChangeTemplatesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListChangeTemplates", reflect.TypeOf((*MockWithCommentEdit)(nil).ListChangeTemplates), arg0) + return &MockWithCommentEditListChangeTemplatesCall{Call: call} +} + +// MockWithCommentEditListChangeTemplatesCall wrap *gomock.Call +type MockWithCommentEditListChangeTemplatesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditListChangeTemplatesCall) Return(arg0 []*forge.ChangeTemplate, arg1 error) *MockWithCommentEditListChangeTemplatesCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditListChangeTemplatesCall) Do(f func(context.Context) ([]*forge.ChangeTemplate, error)) *MockWithCommentEditListChangeTemplatesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditListChangeTemplatesCall) DoAndReturn(f func(context.Context) ([]*forge.ChangeTemplate, error)) *MockWithCommentEditListChangeTemplatesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MergeChange mocks base method. +func (m *MockWithCommentEdit) MergeChange(ctx context.Context, id forge.ChangeID, opts forge.MergeChangeOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MergeChange", ctx, id, opts) + ret0, _ := ret[0].(error) + return ret0 +} + +// MergeChange indicates an expected call of MergeChange. +func (mr *MockWithCommentEditMockRecorder) MergeChange(ctx, id, opts any) *MockWithCommentEditMergeChangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MergeChange", reflect.TypeOf((*MockWithCommentEdit)(nil).MergeChange), ctx, id, opts) + return &MockWithCommentEditMergeChangeCall{Call: call} +} + +// MockWithCommentEditMergeChangeCall wrap *gomock.Call +type MockWithCommentEditMergeChangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditMergeChangeCall) Return(arg0 error) *MockWithCommentEditMergeChangeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditMergeChangeCall) Do(f func(context.Context, forge.ChangeID, forge.MergeChangeOptions) error) *MockWithCommentEditMergeChangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditMergeChangeCall) DoAndReturn(f func(context.Context, forge.ChangeID, forge.MergeChangeOptions) error) *MockWithCommentEditMergeChangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// NewChangeMetadata mocks base method. +func (m *MockWithCommentEdit) NewChangeMetadata(ctx context.Context, id forge.ChangeID) (forge.ChangeMetadata, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewChangeMetadata", ctx, id) + ret0, _ := ret[0].(forge.ChangeMetadata) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewChangeMetadata indicates an expected call of NewChangeMetadata. +func (mr *MockWithCommentEditMockRecorder) NewChangeMetadata(ctx, id any) *MockWithCommentEditNewChangeMetadataCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewChangeMetadata", reflect.TypeOf((*MockWithCommentEdit)(nil).NewChangeMetadata), ctx, id) + return &MockWithCommentEditNewChangeMetadataCall{Call: call} +} + +// MockWithCommentEditNewChangeMetadataCall wrap *gomock.Call +type MockWithCommentEditNewChangeMetadataCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditNewChangeMetadataCall) Return(arg0 forge.ChangeMetadata, arg1 error) *MockWithCommentEditNewChangeMetadataCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditNewChangeMetadataCall) Do(f func(context.Context, forge.ChangeID) (forge.ChangeMetadata, error)) *MockWithCommentEditNewChangeMetadataCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditNewChangeMetadataCall) DoAndReturn(f func(context.Context, forge.ChangeID) (forge.ChangeMetadata, error)) *MockWithCommentEditNewChangeMetadataCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PostChangeComment mocks base method. +func (m *MockWithCommentEdit) PostChangeComment(arg0 context.Context, arg1 forge.ChangeID, arg2 string) (forge.ChangeCommentID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostChangeComment", arg0, arg1, arg2) + ret0, _ := ret[0].(forge.ChangeCommentID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PostChangeComment indicates an expected call of PostChangeComment. +func (mr *MockWithCommentEditMockRecorder) PostChangeComment(arg0, arg1, arg2 any) *MockWithCommentEditPostChangeCommentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostChangeComment", reflect.TypeOf((*MockWithCommentEdit)(nil).PostChangeComment), arg0, arg1, arg2) + return &MockWithCommentEditPostChangeCommentCall{Call: call} +} + +// MockWithCommentEditPostChangeCommentCall wrap *gomock.Call +type MockWithCommentEditPostChangeCommentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditPostChangeCommentCall) Return(arg0 forge.ChangeCommentID, arg1 error) *MockWithCommentEditPostChangeCommentCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditPostChangeCommentCall) Do(f func(context.Context, forge.ChangeID, string) (forge.ChangeCommentID, error)) *MockWithCommentEditPostChangeCommentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditPostChangeCommentCall) DoAndReturn(f func(context.Context, forge.ChangeID, string) (forge.ChangeCommentID, error)) *MockWithCommentEditPostChangeCommentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SubmitChange mocks base method. +func (m *MockWithCommentEdit) SubmitChange(ctx context.Context, req forge.SubmitChangeRequest) (forge.SubmitChangeResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubmitChange", ctx, req) + ret0, _ := ret[0].(forge.SubmitChangeResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubmitChange indicates an expected call of SubmitChange. +func (mr *MockWithCommentEditMockRecorder) SubmitChange(ctx, req any) *MockWithCommentEditSubmitChangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitChange", reflect.TypeOf((*MockWithCommentEdit)(nil).SubmitChange), ctx, req) + return &MockWithCommentEditSubmitChangeCall{Call: call} +} + +// MockWithCommentEditSubmitChangeCall wrap *gomock.Call +type MockWithCommentEditSubmitChangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditSubmitChangeCall) Return(arg0 forge.SubmitChangeResult, arg1 error) *MockWithCommentEditSubmitChangeCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditSubmitChangeCall) Do(f func(context.Context, forge.SubmitChangeRequest) (forge.SubmitChangeResult, error)) *MockWithCommentEditSubmitChangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditSubmitChangeCall) DoAndReturn(f func(context.Context, forge.SubmitChangeRequest) (forge.SubmitChangeResult, error)) *MockWithCommentEditSubmitChangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UpdateChangeComment mocks base method. +func (m *MockWithCommentEdit) UpdateChangeComment(arg0 context.Context, arg1 forge.ChangeCommentID, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateChangeComment", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateChangeComment indicates an expected call of UpdateChangeComment. +func (mr *MockWithCommentEditMockRecorder) UpdateChangeComment(arg0, arg1, arg2 any) *MockWithCommentEditUpdateChangeCommentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChangeComment", reflect.TypeOf((*MockWithCommentEdit)(nil).UpdateChangeComment), arg0, arg1, arg2) + return &MockWithCommentEditUpdateChangeCommentCall{Call: call} +} + +// MockWithCommentEditUpdateChangeCommentCall wrap *gomock.Call +type MockWithCommentEditUpdateChangeCommentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockWithCommentEditUpdateChangeCommentCall) Return(arg0 error) *MockWithCommentEditUpdateChangeCommentCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockWithCommentEditUpdateChangeCommentCall) Do(f func(context.Context, forge.ChangeCommentID, string) error) *MockWithCommentEditUpdateChangeCommentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockWithCommentEditUpdateChangeCommentCall) DoAndReturn(f func(context.Context, forge.ChangeCommentID, string) error) *MockWithCommentEditUpdateChangeCommentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/internal/forge/github/inline_comment.go b/internal/forge/github/inline_comment.go new file mode 100644 index 000000000..57018cf5e --- /dev/null +++ b/internal/forge/github/inline_comment.go @@ -0,0 +1,377 @@ +package github + +import ( + "context" + "fmt" + "time" + + "github.com/shurcooL/githubv4" + "go.abhg.dev/gs/internal/forge" +) + +var ( + _ forge.WithInlineComments = (*Repository)(nil) + _ forge.WithThreadResolution = (*Repository)(nil) + _ forge.WithCommentEdit = (*Repository)(nil) +) + +// ListInlineComments lists inline/review comments on a PR +// by querying the reviewThreads connection. +func (r *Repository) ListInlineComments( + ctx context.Context, + id forge.ChangeID, +) ([]*forge.InlineComment, error) { + pr := mustPR(id) + gqlID, err := r.graphQLID(ctx, pr) + if err != nil { + return nil, err + } + + type commentNode struct { + ID githubv4.ID `graphql:"id"` + Body string `graphql:"body"` + URL string `graphql:"url"` + Author struct { + Login string `graphql:"login"` + } `graphql:"author"` + CreatedAt githubv4.DateTime `graphql:"createdAt"` + Outdated bool `graphql:"outdated"` + } + + type threadNode struct { + ID githubv4.ID `graphql:"id"` + IsResolved bool `graphql:"isResolved"` + Path string `graphql:"path"` + Line *int `graphql:"line"` + Comments struct { + Nodes []commentNode `graphql:"nodes"` + } `graphql:"comments(first: 100)"` + } + + var q struct { + Node struct { + PullRequest struct { + ReviewThreads struct { + PageInfo struct { + HasNextPage bool `graphql:"hasNextPage"` + EndCursor githubv4.String `graphql:"endCursor"` + } `graphql:"pageInfo"` + Nodes []threadNode `graphql:"nodes"` + } `graphql:"reviewThreads(first: $first, after: $after)"` + } `graphql:"... on PullRequest"` + } `graphql:"node(id: $id)"` + } + + variables := map[string]any{ + "id": gqlID, + "first": githubv4.Int(100), + "after": (*githubv4.String)(nil), + } + + var comments []*forge.InlineComment + for { + if err := r.client.Query(ctx, &q, variables); err != nil { + return nil, fmt.Errorf("list inline comments: %w", err) + } + + for _, thread := range q.Node.PullRequest.ReviewThreads.Nodes { + threadID := fmt.Sprint(thread.ID) + line := 0 + if thread.Line != nil { + line = *thread.Line + } + + for _, c := range thread.Comments.Nodes { + comments = append(comments, &forge.InlineComment{ + ID: &PRComment{ + GQLID: c.ID, + URL: c.URL, + }, + ThreadID: threadID, + Path: thread.Path, + Line: line, + Body: c.Body, + Author: c.Author.Login, + Resolved: thread.IsResolved, + Outdated: c.Outdated, + CreatedAt: c.CreatedAt.Time, + }) + } + } + + pi := q.Node.PullRequest.ReviewThreads.PageInfo + if !pi.HasNextPage { + break + } + variables["after"] = pi.EndCursor + } + + return comments, nil +} + +// SubmitReview posts a batch of inline comments +// as a single review on a PR. +func (r *Repository) SubmitReview( + ctx context.Context, + id forge.ChangeID, + req forge.ReviewRequest, +) error { + pr := mustPR(id) + gqlID, err := r.graphQLID(ctx, pr) + if err != nil { + return err + } + + // Map review event to GitHub's enum. + var event githubv4.PullRequestReviewEvent + switch req.Event { + case forge.ReviewApprove: + event = githubv4.PullRequestReviewEventApprove + case forge.ReviewRequestChanges: + event = githubv4.PullRequestReviewEventRequestChanges + default: + event = githubv4.PullRequestReviewEventComment + } + + // Build thread inputs for inline comments. + var threads []*githubv4.DraftPullRequestReviewThread + for _, c := range req.Comments { + side := githubv4.DiffSideRight + if c.Side == "LEFT" { + side = githubv4.DiffSideLeft + } + threads = append(threads, + &githubv4.DraftPullRequestReviewThread{ + Path: new( + githubv4.String(c.Path), + ), + Line: new( + githubv4.Int(c.Line), + ), + Side: &side, + Body: githubv4.String(c.Body), + }) + } + + var m struct { + AddPullRequestReview struct { + PullRequestReview struct { + ID githubv4.ID `graphql:"id"` + } `graphql:"pullRequestReview"` + } `graphql:"addPullRequestReview(input: $input)"` + } + + input := githubv4.AddPullRequestReviewInput{ + PullRequestID: gqlID, + Event: &event, + Threads: &threads, + } + if req.Body != "" { + input.Body = new(githubv4.String(req.Body)) + } + + if err := r.client.Mutate(ctx, &m, input, nil); err != nil { + return fmt.Errorf("submit review: %w", err) + } + + r.log.Debug("Submitted review", + "pr", pr.Number, + "comments", len(req.Comments), + ) + return nil +} + +// PostInlineComment posts a single inline comment +// on a PR by creating a review thread. +func (r *Repository) PostInlineComment( + ctx context.Context, + id forge.ChangeID, + req forge.InlineCommentRequest, +) (*forge.InlineComment, error) { + pr := mustPR(id) + gqlID, err := r.graphQLID(ctx, pr) + if err != nil { + return nil, err + } + + // If replying to an existing thread, use addPullRequestReviewComment. + if req.ThreadID != "" { + return r.replyToThread(ctx, req) + } + + side := githubv4.DiffSideRight + if req.Side == "LEFT" { + side = githubv4.DiffSideLeft + } + + var m struct { + AddPullRequestReviewThread struct { + Thread struct { + ID githubv4.ID `graphql:"id"` + Comments struct { + Nodes []struct { + ID githubv4.ID `graphql:"id"` + URL string `graphql:"url"` + CreatedAt githubv4.DateTime `graphql:"createdAt"` + } `graphql:"nodes"` + } `graphql:"comments(first: 1)"` + } `graphql:"thread"` + } `graphql:"addPullRequestReviewThread(input: $input)"` + } + + input := githubv4.AddPullRequestReviewThreadInput{ + PullRequestID: &gqlID, + Path: new( + githubv4.String(req.Path), + ), + Line: new( + githubv4.Int(req.Line), + ), + Side: &side, + Body: githubv4.String(req.Body), + } + + if err := r.client.Mutate(ctx, &m, input, nil); err != nil { + return nil, fmt.Errorf("post inline comment: %w", err) + } + + thread := m.AddPullRequestReviewThread.Thread + threadID := fmt.Sprint(thread.ID) + + var commentID forge.ChangeCommentID + var createdAt time.Time + if len(thread.Comments.Nodes) > 0 { + n := thread.Comments.Nodes[0] + commentID = &PRComment{GQLID: n.ID, URL: n.URL} + createdAt = n.CreatedAt.Time + } + + r.log.Debug("Posted inline comment", + "pr", pr.Number, + "path", req.Path, + "line", req.Line, + ) + + return &forge.InlineComment{ + ID: commentID, + ThreadID: threadID, + Path: req.Path, + Line: req.Line, + Body: req.Body, + CreatedAt: createdAt, + }, nil +} + +// replyToThread adds a reply to an existing review thread. +func (r *Repository) replyToThread( + ctx context.Context, + req forge.InlineCommentRequest, +) (*forge.InlineComment, error) { + var m struct { + AddPullRequestReviewThreadReply struct { + Comment struct { + ID githubv4.ID `graphql:"id"` + URL string `graphql:"url"` + CreatedAt githubv4.DateTime `graphql:"createdAt"` + } `graphql:"comment"` + } `graphql:"addPullRequestReviewThreadReply(input: $input)"` + } + + input := githubv4.AddPullRequestReviewThreadReplyInput{ + PullRequestReviewThreadID: githubv4.ID(req.ThreadID), + Body: githubv4.String(req.Body), + } + + if err := r.client.Mutate(ctx, &m, input, nil); err != nil { + return nil, fmt.Errorf("reply to thread: %w", err) + } + + n := m.AddPullRequestReviewThreadReply.Comment + return &forge.InlineComment{ + ID: &PRComment{GQLID: n.ID, URL: n.URL}, + ThreadID: req.ThreadID, + Path: req.Path, + Line: req.Line, + Body: req.Body, + CreatedAt: n.CreatedAt.Time, + }, nil +} + +// ResolveThread marks a review thread as resolved. +func (r *Repository) ResolveThread( + ctx context.Context, + threadID string, +) error { + var m struct { + ResolveReviewThread struct { + Thread struct { + ID githubv4.ID `graphql:"id"` + } `graphql:"thread"` + } `graphql:"resolveReviewThread(input: $input)"` + } + + input := githubv4.ResolveReviewThreadInput{ + ThreadID: githubv4.ID(threadID), + } + + if err := r.client.Mutate(ctx, &m, input, nil); err != nil { + return fmt.Errorf("resolve thread: %w", err) + } + + r.log.Debug("Resolved thread", "threadID", threadID) + return nil +} + +// UnresolveThread marks a review thread as unresolved. +func (r *Repository) UnresolveThread( + ctx context.Context, + threadID string, +) error { + var m struct { + UnresolveReviewThread struct { + Thread struct { + ID githubv4.ID `graphql:"id"` + } `graphql:"thread"` + } `graphql:"unresolveReviewThread(input: $input)"` + } + + input := githubv4.UnresolveReviewThreadInput{ + ThreadID: githubv4.ID(threadID), + } + + if err := r.client.Mutate(ctx, &m, input, nil); err != nil { + return fmt.Errorf("unresolve thread: %w", err) + } + + r.log.Debug("Unresolved thread", "threadID", threadID) + return nil +} + +// EditComment updates the body of an existing review comment. +func (r *Repository) EditComment( + ctx context.Context, + id forge.ChangeCommentID, + body string, +) error { + cid := mustPRComment(id) + + var m struct { + UpdatePullRequestReviewComment struct { + PullRequestReviewComment struct { + ID githubv4.ID `graphql:"id"` + } `graphql:"pullRequestReviewComment"` + } `graphql:"updatePullRequestReviewComment(input: $input)"` + } + + input := githubv4.UpdatePullRequestReviewCommentInput{ + PullRequestReviewCommentID: cid.GQLID, + Body: githubv4.String(body), + } + + if err := r.client.Mutate(ctx, &m, input, nil); err != nil { + return fmt.Errorf("edit comment: %w", err) + } + + r.log.Debug("Edited comment", "url", cid.URL) + return nil +} diff --git a/internal/forge/gitlab/inline_comment.go b/internal/forge/gitlab/inline_comment.go new file mode 100644 index 000000000..cbfa86790 --- /dev/null +++ b/internal/forge/gitlab/inline_comment.go @@ -0,0 +1,418 @@ +package gitlab + +import ( + "context" + "fmt" + "strconv" + "time" + + "go.abhg.dev/gs/internal/forge" + "go.abhg.dev/gs/internal/gateway/gitlab" +) + +var ( + _ forge.WithInlineComments = (*Repository)(nil) + _ forge.WithThreadResolution = (*Repository)(nil) + _ forge.WithCommentEdit = (*Repository)(nil) +) + +// ListInlineComments lists inline/review comments on a MR +// by fetching discussions that have position information. +func (r *Repository) ListInlineComments( + ctx context.Context, + id forge.ChangeID, +) ([]*forge.InlineComment, error) { + mr := mustMR(id) + + opts := &gitlab.ListMergeRequestDiscussionsOptions{ + ListOptions: gitlab.ListOptions{PerPage: 100}, + } + + var comments []*forge.InlineComment + for { + discussions, resp, err := r.client.MergeRequestDiscussionList( + ctx, r.repoID, mr.Number, opts, + ) + if err != nil { + return nil, fmt.Errorf( + "list discussions: %w", err, + ) + } + + for _, disc := range discussions { + if len(disc.Notes) == 0 { + continue + } + // Only include discussions that are resolvable + // (i.e., code review / diff discussions). + root := disc.Notes[0] + if !root.Resolvable { + continue + } + + for _, note := range disc.Notes { + path := "" + var line int + if note.Position != nil { + path = note.Position.NewPath + if note.Position.NewLine != 0 { + line = int(note.Position.NewLine) + } + } + + createdAt := time.Time{} + if note.CreatedAt != nil { + createdAt = *note.CreatedAt + } + + comments = append(comments, + &forge.InlineComment{ + ID: &MRComment{ + Number: note.ID, + MRNumber: mr.Number, + }, + ThreadID: formatThreadID( + disc.ID, mr.Number, + ), + Path: path, + Line: line, + Body: note.Body, + Author: note.Author.Username, + Resolved: root.Resolved, + Outdated: note.Position != nil && note.Position.NewPath == "", + CreatedAt: createdAt, + }) + } + } + + if resp.NextPage == 0 { + break + } + opts.Page = int64(resp.NextPage) + } + + return comments, nil +} + +// SubmitReview posts a batch of inline comments +// as individual discussions on a MR. +// GitLab does not have a native batch review API, +// so each comment is posted as a separate discussion. +func (r *Repository) SubmitReview( + ctx context.Context, + id forge.ChangeID, + req forge.ReviewRequest, +) error { + mr := mustMR(id) + + // Get diff refs for positioning. + diffRefs, err := r.diffRefs(ctx, mr.Number) + if err != nil { + return err + } + + for _, c := range req.Comments { + if c.ThreadID != "" { + // Reply to existing thread. + discID, _, perr := parseThreadID(c.ThreadID) + if perr != nil { + return perr + } + body := c.Body + if _, _, err := r.client.MergeRequestDiscussionNoteCreate( + ctx, r.repoID, mr.Number, discID, + &gitlab.AddMergeRequestDiscussionNoteOptions{ + Body: &body, + }, + ); err != nil { + return fmt.Errorf( + "reply to thread %s: %w", + c.ThreadID, err, + ) + } + continue + } + + opts := newDiscussionOptions(c.Body, c.Path, c.Line, diffRefs) + if _, _, err := r.client.MergeRequestDiscussionCreate( + ctx, r.repoID, mr.Number, opts, + ); err != nil { + return fmt.Errorf( + "create discussion on %s:%d: %w", + c.Path, c.Line, err, + ) + } + } + + r.log.Debug("Submitted review", + "mr", mr.Number, + "comments", len(req.Comments), + ) + return nil +} + +// PostInlineComment posts a single inline comment +// on a MR as a new discussion. +func (r *Repository) PostInlineComment( + ctx context.Context, + id forge.ChangeID, + req forge.InlineCommentRequest, +) (*forge.InlineComment, error) { + mr := mustMR(id) + + // Reply to existing thread. + if req.ThreadID != "" { + discID, _, err := parseThreadID(req.ThreadID) + if err != nil { + return nil, err + } + body := req.Body + note, _, err := r.client.MergeRequestDiscussionNoteCreate( + ctx, r.repoID, mr.Number, discID, + &gitlab.AddMergeRequestDiscussionNoteOptions{ + Body: &body, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "reply to thread: %w", err, + ) + } + + createdAt := time.Time{} + if note.CreatedAt != nil { + createdAt = *note.CreatedAt + } + return &forge.InlineComment{ + ID: &MRComment{ + Number: note.ID, + MRNumber: mr.Number, + }, + ThreadID: req.ThreadID, + Path: req.Path, + Line: req.Line, + Body: req.Body, + CreatedAt: createdAt, + }, nil + } + + // New discussion. + diffRefs, err := r.diffRefs(ctx, mr.Number) + if err != nil { + return nil, err + } + + disc, _, err := r.client.MergeRequestDiscussionCreate( + ctx, r.repoID, mr.Number, + newDiscussionOptions(req.Body, req.Path, req.Line, diffRefs), + ) + if err != nil { + return nil, fmt.Errorf("create discussion: %w", err) + } + + var noteID int64 + if len(disc.Notes) > 0 { + noteID = disc.Notes[0].ID + } + + r.log.Debug("Posted inline comment", + "mr", mr.Number, + "path", req.Path, + "line", req.Line, + ) + + return &forge.InlineComment{ + ID: &MRComment{ + Number: noteID, + MRNumber: mr.Number, + }, + ThreadID: formatThreadID(disc.ID, mr.Number), + Path: req.Path, + Line: req.Line, + Body: req.Body, + }, nil +} + +func newDiscussionOptions( + body, path string, line int, + refs *gitlab.MergeRequestDiffRefs, +) *gitlab.CreateMergeRequestDiscussionOptions { + textType := "text" + lineN := int64(line) + return &gitlab.CreateMergeRequestDiscussionOptions{ + Body: &body, + Position: &gitlab.PositionOptions{ + BaseSHA: &refs.BaseSha, + HeadSHA: &refs.HeadSha, + StartSHA: &refs.StartSha, + PositionType: &textType, + NewPath: &path, + OldPath: &path, + NewLine: &lineN, + }, + } +} + +// diffRefs fetches the diff refs for a MR, +// needed for positioning inline comments. +func (r *Repository) diffRefs( + ctx context.Context, + mrNumber int64, +) (*gitlab.MergeRequestDiffRefs, error) { + mr, _, err := r.client.MergeRequestGet( + ctx, r.repoID, mrNumber, nil, + ) + if err != nil { + return nil, fmt.Errorf("get merge request: %w", err) + } + return &mr.DiffRefs, nil +} + +// ResolveThread marks a discussion thread as resolved. +func (r *Repository) ResolveThread( + ctx context.Context, + threadID string, +) error { + // Thread ID format: "discussion_id:mr_number" + discID, mrNumber, err := parseThreadID(threadID) + if err != nil { + return err + } + + resolved := true + if _, _, err := r.client.MergeRequestDiscussionResolve( + ctx, r.repoID, mrNumber, discID, + &gitlab.ResolveMergeRequestDiscussionOptions{ + Resolved: &resolved, + }, + ); err != nil { + return fmt.Errorf("resolve thread: %w", err) + } + + r.log.Debug("Resolved thread", "threadID", threadID) + return nil +} + +// UnresolveThread marks a discussion thread as unresolved. +func (r *Repository) UnresolveThread( + ctx context.Context, + threadID string, +) error { + discID, mrNumber, err := parseThreadID(threadID) + if err != nil { + return err + } + + resolved := false + if _, _, err := r.client.MergeRequestDiscussionResolve( + ctx, r.repoID, mrNumber, discID, + &gitlab.ResolveMergeRequestDiscussionOptions{ + Resolved: &resolved, + }, + ); err != nil { + return fmt.Errorf("unresolve thread: %w", err) + } + + r.log.Debug("Unresolved thread", "threadID", threadID) + return nil +} + +// EditComment updates the body of an existing MR comment. +func (r *Repository) EditComment( + ctx context.Context, + id forge.ChangeCommentID, + body string, +) error { + cid := mustMRComment(id) + + // Find the discussion containing the note + // so we can use the discussion-level update API. + discID, err := r.findDiscussionForNote( + ctx, cid.MRNumber, cid.Number, + ) + if err != nil { + return err + } + + if _, _, err := r.client.MergeRequestDiscussionNoteUpdate( + ctx, r.repoID, cid.MRNumber, discID, cid.Number, + &gitlab.UpdateMergeRequestDiscussionNoteOptions{ + Body: &body, + }, + ); err != nil { + return fmt.Errorf("edit comment: %w", err) + } + + r.log.Debug("Edited comment", "noteID", cid.Number) + return nil +} + +// findDiscussionForNote finds the discussion ID +// that contains a specific note. +func (r *Repository) findDiscussionForNote( + ctx context.Context, + mrNumber, noteID int64, +) (string, error) { + opts := &gitlab.ListMergeRequestDiscussionsOptions{ + ListOptions: gitlab.ListOptions{PerPage: 100}, + } + + for { + discussions, resp, err := r.client.MergeRequestDiscussionList( + ctx, r.repoID, mrNumber, opts, + ) + if err != nil { + return "", fmt.Errorf( + "list discussions: %w", err, + ) + } + + for _, disc := range discussions { + for _, note := range disc.Notes { + if note.ID == noteID { + return disc.ID, nil + } + } + } + + if resp.NextPage == 0 { + break + } + opts.Page = int64(resp.NextPage) + } + + return "", fmt.Errorf( + "note %d not found in MR %d", noteID, mrNumber, + ) +} + +// threadID format for GitLab: "discussion_id:mr_number" +// This encodes both the discussion ID and MR number +// so resolve/unresolve can work without extra context. + +func formatThreadID(discID string, mrNumber int64) string { + return discID + ":" + strconv.FormatInt(mrNumber, 10) +} + +func parseThreadID(threadID string) ( + discID string, mrNumber int64, err error, +) { + for i := len(threadID) - 1; i >= 0; i-- { + if threadID[i] == ':' { + discID = threadID[:i] + mrNumber, err = strconv.ParseInt( + threadID[i+1:], 10, 64, + ) + if err != nil { + return "", 0, fmt.Errorf( + "parse thread ID %q: %w", + threadID, err, + ) + } + return discID, mrNumber, nil + } + } + return "", 0, fmt.Errorf( + "invalid thread ID format: %q", threadID, + ) +} diff --git a/internal/forge/shamhub/comment.go b/internal/forge/shamhub/comment.go index 9dfaf358a..899192c36 100644 --- a/internal/forge/shamhub/comment.go +++ b/internal/forge/shamhub/comment.go @@ -8,6 +8,7 @@ import ( "iter" "slices" "strconv" + "time" "go.abhg.dev/gs/internal/forge" ) @@ -135,11 +136,32 @@ type shamComment struct { Change int Body string - // Resolvable indicates this is a code review comment that can be resolved. + // Resolvable indicates this is a code review comment + // that can be resolved. Resolvable bool // Resolved indicates this comment has been resolved. Resolved bool + + // Inline comment fields (optional). + + // Path is the file path for inline comments. + Path string + + // Line is the line number for inline comments. + Line int + + // Side is "LEFT" or "RIGHT" for inline comments. + Side string + + // ThreadID groups inline comments into threads. + ThreadID string + + // Author is the comment author username. + Author string + + // CreatedAt is the comment creation timestamp. + CreatedAt time.Time } var ( diff --git a/internal/forge/shamhub/inline_comment.go b/internal/forge/shamhub/inline_comment.go new file mode 100644 index 000000000..b9d64781d --- /dev/null +++ b/internal/forge/shamhub/inline_comment.go @@ -0,0 +1,459 @@ +package shamhub + +import ( + "context" + "fmt" + "strconv" + "time" + + "go.abhg.dev/gs/internal/forge" +) + +var ( + _ forge.WithInlineComments = (*forgeRepository)(nil) + _ forge.WithThreadResolution = (*forgeRepository)(nil) + _ forge.WithCommentEdit = (*forgeRepository)(nil) +) + +// Inline comment handlers + +var ( + _ = shamhubRESTHandler( + "GET /{owner}/{repo}/inline-comments", + (*ShamHub).handleListInlineComments, + ) + _ = shamhubRESTHandler( + "POST /{owner}/{repo}/inline-comments", + (*ShamHub).handlePostInlineComment, + ) + _ = shamhubRESTHandler( + "POST /{owner}/{repo}/reviews", + (*ShamHub).handleSubmitReview, + ) + _ = shamhubRESTHandler( + "POST /{owner}/{repo}/threads/{threadID}/resolve", + (*ShamHub).handleResolveThread, + ) + _ = shamhubRESTHandler( + "POST /{owner}/{repo}/threads/{threadID}/unresolve", + (*ShamHub).handleUnresolveThread, + ) + _ = shamhubRESTHandler( + "PATCH /{owner}/{repo}/comments/{id}/edit", + (*ShamHub).handleEditComment, + ) +) + +// List inline comments + +type listInlineCommentsRequest struct { + Owner string `path:"owner" json:"-"` + Repo string `path:"repo" json:"-"` + Change int `form:"change,required" json:"-"` +} + +type listInlineCommentsResponse struct { + Items []inlineCommentItem `json:"items"` +} + +type inlineCommentItem struct { + ID int `json:"id"` + ThreadID string `json:"threadID"` + Path string `json:"path"` + Line int `json:"line"` + Body string `json:"body"` + Author string `json:"author"` + Resolved bool `json:"resolved"` + Outdated bool `json:"outdated"` + CreatedAt time.Time `json:"createdAt"` +} + +func (sh *ShamHub) handleListInlineComments( + _ context.Context, + req *listInlineCommentsRequest, +) (*listInlineCommentsResponse, error) { + sh.mu.RLock() + defer sh.mu.RUnlock() + + var items []inlineCommentItem + for _, c := range sh.comments { + if c.Change != req.Change || c.Path == "" { + continue + } + items = append(items, inlineCommentItem{ + ID: c.ID, + ThreadID: c.ThreadID, + Path: c.Path, + Line: c.Line, + Body: c.Body, + Author: c.Author, + Resolved: c.Resolved, + Outdated: false, + CreatedAt: c.CreatedAt, + }) + } + + return &listInlineCommentsResponse{Items: items}, nil +} + +func (r *forgeRepository) ListInlineComments( + ctx context.Context, + id forge.ChangeID, +) ([]*forge.InlineComment, error) { + u := r.apiURL.JoinPath(r.owner, r.repo, "inline-comments") + q := u.Query() + q.Set("change", strconv.Itoa(int(id.(ChangeID)))) + u.RawQuery = q.Encode() + + var res listInlineCommentsResponse + if err := r.client.Get(ctx, u.String(), &res); err != nil { + return nil, fmt.Errorf("list inline comments: %w", err) + } + + var comments []*forge.InlineComment + for _, item := range res.Items { + comments = append(comments, &forge.InlineComment{ + ID: ChangeCommentID(item.ID), + ThreadID: item.ThreadID, + Path: item.Path, + Line: item.Line, + Body: item.Body, + Author: item.Author, + Resolved: item.Resolved, + Outdated: item.Outdated, + CreatedAt: item.CreatedAt, + }) + } + return comments, nil +} + +// Post inline comment + +type postInlineCommentRequest struct { + Owner string `path:"owner" json:"-"` + Repo string `path:"repo" json:"-"` + + Change int `json:"change"` + Path string `json:"path"` + Line int `json:"line"` + Body string `json:"body"` + Side string `json:"side"` + ThreadID string `json:"threadID,omitempty"` +} + +type postInlineCommentResponse struct { + ID int `json:"id"` + ThreadID string `json:"threadID"` + CreatedAt time.Time `json:"createdAt"` +} + +func (sh *ShamHub) handlePostInlineComment( + _ context.Context, + req *postInlineCommentRequest, +) (*postInlineCommentResponse, error) { + sh.mu.Lock() + defer sh.mu.Unlock() + + threadID := req.ThreadID + if threadID == "" { + // New thread: generate a thread ID. + threadID = fmt.Sprintf("thread-%d", len(sh.comments)+1) + } + + now := time.Now() + comment := shamComment{ + ID: len(sh.comments) + 1, + Change: req.Change, + Body: req.Body, + Path: req.Path, + Line: req.Line, + Side: req.Side, + ThreadID: threadID, + Resolvable: true, + Author: "test-user", + CreatedAt: now, + } + sh.comments = append(sh.comments, comment) + + return &postInlineCommentResponse{ + ID: comment.ID, + ThreadID: threadID, + CreatedAt: now, + }, nil +} + +func (r *forgeRepository) PostInlineComment( + ctx context.Context, + id forge.ChangeID, + req forge.InlineCommentRequest, +) (*forge.InlineComment, error) { + u := r.apiURL.JoinPath( + r.owner, r.repo, "inline-comments", + ) + body := postInlineCommentRequest{ + Change: int(id.(ChangeID)), + Path: req.Path, + Line: req.Line, + Body: req.Body, + Side: req.Side, + ThreadID: req.ThreadID, + } + + var res postInlineCommentResponse + if err := r.client.Post( + ctx, u.String(), body, &res, + ); err != nil { + return nil, fmt.Errorf("post inline comment: %w", err) + } + + return &forge.InlineComment{ + ID: ChangeCommentID(res.ID), + ThreadID: res.ThreadID, + Path: req.Path, + Line: req.Line, + Body: req.Body, + CreatedAt: res.CreatedAt, + }, nil +} + +// Submit review (batch) + +type submitReviewRequest struct { + Owner string `path:"owner" json:"-"` + Repo string `path:"repo" json:"-"` + + Change int `json:"change"` + Body string `json:"body,omitempty"` + Event int `json:"event"` + Comments []submitReviewCommentRequest `json:"comments"` +} + +type submitReviewCommentRequest struct { + Path string `json:"path"` + Line int `json:"line"` + Body string `json:"body"` + Side string `json:"side,omitempty"` + ThreadID string `json:"threadID,omitempty"` +} + +type submitReviewResponse struct { + CommentIDs []int `json:"commentIDs"` +} + +func (sh *ShamHub) handleSubmitReview( + _ context.Context, + req *submitReviewRequest, +) (*submitReviewResponse, error) { + sh.mu.Lock() + defer sh.mu.Unlock() + + now := time.Now() + var ids []int + for _, c := range req.Comments { + threadID := c.ThreadID + if threadID == "" { + threadID = fmt.Sprintf( + "thread-%d", len(sh.comments)+1, + ) + } + + comment := shamComment{ + ID: len(sh.comments) + 1, + Change: req.Change, + Body: c.Body, + Path: c.Path, + Line: c.Line, + Side: c.Side, + ThreadID: threadID, + Resolvable: true, + Author: "test-user", + CreatedAt: now, + } + sh.comments = append(sh.comments, comment) + ids = append(ids, comment.ID) + } + + return &submitReviewResponse{CommentIDs: ids}, nil +} + +func (r *forgeRepository) SubmitReview( + ctx context.Context, + id forge.ChangeID, + req forge.ReviewRequest, +) error { + u := r.apiURL.JoinPath(r.owner, r.repo, "reviews") + + var comments []submitReviewCommentRequest + for _, c := range req.Comments { + comments = append(comments, submitReviewCommentRequest{ + Path: c.Path, + Line: c.Line, + Body: c.Body, + Side: c.Side, + ThreadID: c.ThreadID, + }) + } + + body := submitReviewRequest{ + Change: int(id.(ChangeID)), + Body: req.Body, + Event: int(req.Event), + Comments: comments, + } + + var res submitReviewResponse + if err := r.client.Post( + ctx, u.String(), body, &res, + ); err != nil { + return fmt.Errorf("submit review: %w", err) + } + + return nil +} + +// Resolve/unresolve threads + +type resolveThreadRequest struct { + Owner string `path:"owner" json:"-"` + Repo string `path:"repo" json:"-"` + ThreadID string `path:"threadID" json:"-"` +} + +type resolveThreadResponse struct{} + +func (sh *ShamHub) handleResolveThread( + _ context.Context, + req *resolveThreadRequest, +) (*resolveThreadResponse, error) { + sh.mu.Lock() + defer sh.mu.Unlock() + + found := false + for i, c := range sh.comments { + if c.ThreadID == req.ThreadID { + sh.comments[i].Resolved = true + found = true + } + } + if !found { + return nil, notFoundErrorf( + "thread %s not found", req.ThreadID, + ) + } + + return &resolveThreadResponse{}, nil +} + +func (sh *ShamHub) handleUnresolveThread( + _ context.Context, + req *resolveThreadRequest, +) (*resolveThreadResponse, error) { + sh.mu.Lock() + defer sh.mu.Unlock() + + found := false + for i, c := range sh.comments { + if c.ThreadID == req.ThreadID { + sh.comments[i].Resolved = false + found = true + } + } + if !found { + return nil, notFoundErrorf( + "thread %s not found", req.ThreadID, + ) + } + + return &resolveThreadResponse{}, nil +} + +func (r *forgeRepository) ResolveThread( + ctx context.Context, + threadID string, +) error { + u := r.apiURL.JoinPath( + r.owner, r.repo, "threads", threadID, "resolve", + ) + + var res resolveThreadResponse + if err := r.client.Post( + ctx, u.String(), struct{}{}, &res, + ); err != nil { + return fmt.Errorf("resolve thread: %w", err) + } + + return nil +} + +func (r *forgeRepository) UnresolveThread( + ctx context.Context, + threadID string, +) error { + u := r.apiURL.JoinPath( + r.owner, r.repo, "threads", threadID, "unresolve", + ) + + var res resolveThreadResponse + if err := r.client.Post( + ctx, u.String(), struct{}{}, &res, + ); err != nil { + return fmt.Errorf("unresolve thread: %w", err) + } + + return nil +} + +// Edit comment + +type editCommentRequest struct { + Owner string `path:"owner" json:"-"` + Repo string `path:"repo" json:"-"` + ID int `path:"id" json:"-"` + + Body string `json:"body"` +} + +type editCommentResponse struct { + ID int `json:"id"` +} + +func (sh *ShamHub) handleEditComment( + _ context.Context, + req *editCommentRequest, +) (*editCommentResponse, error) { + sh.mu.Lock() + defer sh.mu.Unlock() + + for i, c := range sh.comments { + if c.ID == req.ID { + sh.comments[i].Body = req.Body + return &editCommentResponse{ID: req.ID}, nil + } + } + + return nil, notFoundErrorf( + "comment %d not found", req.ID, + ) +} + +func (r *forgeRepository) EditComment( + ctx context.Context, + id forge.ChangeCommentID, + body string, +) error { + cid := int(id.(ChangeCommentID)) + u := r.apiURL.JoinPath( + r.owner, r.repo, "comments", + strconv.Itoa(cid), "edit", + ) + + req := editCommentRequest{Body: body} + var res editCommentResponse + if err := r.client.Patch( + ctx, u.String(), req, &res, + ); err != nil { + return fmt.Errorf("edit comment: %w", err) + } + + return nil +} diff --git a/internal/forge/shamhub/testdata/TestIntegration/ChangeComments/UpdateComment/updated-comment b/internal/forge/shamhub/testdata/TestIntegration/ChangeComments/UpdateComment/updated-comment index 85760ee02..dda3adda6 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/ChangeComments/UpdateComment/updated-comment +++ b/internal/forge/shamhub/testdata/TestIntegration/ChangeComments/UpdateComment/updated-comment @@ -1 +1 @@ -"uXAhcpF8V9XZWbQom2aI0nM6Q9fbiqpd" \ No newline at end of file +"YJgNsP025jRY5hQX6mh2mIfiwifvz6G8" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/ChangeComments/branch b/internal/forge/shamhub/testdata/TestIntegration/ChangeComments/branch index 3d68dade2..0bc036d75 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/ChangeComments/branch +++ b/internal/forge/shamhub/testdata/TestIntegration/ChangeComments/branch @@ -1 +1 @@ -"uO3v3Jiu" \ No newline at end of file +"pmwWlNhh" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/ChangeComments/comments b/internal/forge/shamhub/testdata/TestIntegration/ChangeComments/comments index e57864939..cb58610d5 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/ChangeComments/comments +++ b/internal/forge/shamhub/testdata/TestIntegration/ChangeComments/comments @@ -1,12 +1,12 @@ [ - "M9TCBnUn2veSjGnStFRLruw4FhpFbpPr", - "ooErWPQY4wHcWm8bJDNOeA2G2BHnMcl1", - "DU3TnJBDljtJOjrPNPgfJRo44fzPxWxZ", - "WeEXYW6yjMMf0jmnnze12omw5CmTHrDt", - "S4IQsdn1dgnUfOJEFb5JEm7kVxLd8oil", - "JNpsluBOeFN8cwri2EMfI8BrYjbArxPf", - "FgUJanBcocO1ablzVyYVjDh6fUZbZXyI", - "qp1FOZUVrehYPmU311YMEQDrHlJ2BJ40", - "sODVHSvcIOhqxyrkgurNH9HMl2b2LSg6", - "k7NLLLvg5wMj1tasfkFfqdbhqfa8Ah0E" + "K067WOW7dQdrliaKS0tKylcfDgP5GhVp", + "dsCMPgmyz0KVBG501PPi2ocxE5sq7UfQ", + "3zyUZ2FZcxkOgp4FBvJfBKTnh9fdpNa3", + "im86H34osfgrDc7E4gdx7gE3XXcTXmPX", + "fgt7AG4rAoQp1gGEw8G2U2lbe0qWrTmp", + "KMuajrgxFADVDZhIFdzUBVzpAm9ee7xR", + "6El2lmQr5Ea5Hq9L0fLVfCSATL6qaGJl", + "ha2n0xHKCbk6xogserVQ8vOR2o2gbNH8", + "Dr8DblxCuohfDzo0gUkdZdTuRxz8uIfz", + "lr6i3eENW1lJd9i6NNxcjZCditYNWUqF" ] \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/ChangesStates/closedBranch b/internal/forge/shamhub/testdata/TestIntegration/ChangesStates/closedBranch index d616a2169..6f5fee334 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/ChangesStates/closedBranch +++ b/internal/forge/shamhub/testdata/TestIntegration/ChangesStates/closedBranch @@ -1 +1 @@ -"IEtiDhFs" \ No newline at end of file +"mzMsv5Rh" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/ChangesStates/mergedBranch b/internal/forge/shamhub/testdata/TestIntegration/ChangesStates/mergedBranch index 2142ea6ab..3d9a25268 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/ChangesStates/mergedBranch +++ b/internal/forge/shamhub/testdata/TestIntegration/ChangesStates/mergedBranch @@ -1 +1 @@ -"C5hzQ5Ik" \ No newline at end of file +"gvgMWiD8" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/ChangesStates/openBranch b/internal/forge/shamhub/testdata/TestIntegration/ChangesStates/openBranch index 7c13c61bf..7e77aa963 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/ChangesStates/openBranch +++ b/internal/forge/shamhub/testdata/TestIntegration/ChangesStates/openBranch @@ -1 +1 @@ -"jMfwXkYK" \ No newline at end of file +"QohFltlh" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/CommentCountsByChange/branch b/internal/forge/shamhub/testdata/TestIntegration/CommentCountsByChange/branch index 778cc9b7f..1b0a87a96 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/CommentCountsByChange/branch +++ b/internal/forge/shamhub/testdata/TestIntegration/CommentCountsByChange/branch @@ -1 +1 @@ -"HQWHaq9z" \ No newline at end of file +"wleC7vBI" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/ListChangeTemplates/TemplatesPresent/empty-template b/internal/forge/shamhub/testdata/TestIntegration/ListChangeTemplates/TemplatesPresent/empty-template index 3da52d175..305090d1d 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/ListChangeTemplates/TemplatesPresent/empty-template +++ b/internal/forge/shamhub/testdata/TestIntegration/ListChangeTemplates/TemplatesPresent/empty-template @@ -1 +1 @@ -"1HQqVNZV.md" \ No newline at end of file +"np6KTBsj.md" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/ListChangeTemplates/TemplatesPresent/non-empty-template b/internal/forge/shamhub/testdata/TestIntegration/ListChangeTemplates/TemplatesPresent/non-empty-template index 0c924f438..ef691a56a 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/ListChangeTemplates/TemplatesPresent/non-empty-template +++ b/internal/forge/shamhub/testdata/TestIntegration/ListChangeTemplates/TemplatesPresent/non-empty-template @@ -1 +1 @@ -"yE3Lj2v5.md" \ No newline at end of file +"aW2r2kGY.md" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitBaseDoesNotExist/base-branch b/internal/forge/shamhub/testdata/TestIntegration/SubmitBaseDoesNotExist/base-branch index b8e719155..4d6f27de3 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitBaseDoesNotExist/base-branch +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitBaseDoesNotExist/base-branch @@ -1 +1 @@ -"9s9oMX6C" \ No newline at end of file +"ExQcEmr1" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitBaseDoesNotExist/branch b/internal/forge/shamhub/testdata/TestIntegration/SubmitBaseDoesNotExist/branch index 02926e09f..a8a9575a0 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitBaseDoesNotExist/branch +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitBaseDoesNotExist/branch @@ -1 +1 @@ -"vY4xsORW" \ No newline at end of file +"bcKIEKS3" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditAssignees/AddAssignee/branch-no-assignee b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditAssignees/AddAssignee/branch-no-assignee index b92100789..93c1210d2 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditAssignees/AddAssignee/branch-no-assignee +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditAssignees/AddAssignee/branch-no-assignee @@ -1 +1 @@ -"66MtCOhk" \ No newline at end of file +"4D8sFmyC" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditAssignees/AddAssigneesOneByOne/branch-no-assignee-one-by-one b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditAssignees/AddAssigneesOneByOne/branch-no-assignee-one-by-one index e678e3903..97d4a3716 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditAssignees/AddAssigneesOneByOne/branch-no-assignee-one-by-one +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditAssignees/AddAssigneesOneByOne/branch-no-assignee-one-by-one @@ -1 +1 @@ -"Op9WQIRi" \ No newline at end of file +"lEG1MUxo" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditAssignees/SubmitWithAssignee/branch-with-assignee b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditAssignees/SubmitWithAssignee/branch-with-assignee index d3f1200a7..6aba0ca56 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditAssignees/SubmitWithAssignee/branch-with-assignee +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditAssignees/SubmitWithAssignee/branch-with-assignee @@ -1 +1 @@ -"lE3mefp0" \ No newline at end of file +"UQDEDpcg" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditBase/base b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditBase/base index 5a0613317..bd79f7cc0 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditBase/base +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditBase/base @@ -1 +1 @@ -"t8XzNN0X" \ No newline at end of file +"P2inVOao" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditBase/branch b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditBase/branch index 133b93253..39462bd3d 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditBase/branch +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditBase/branch @@ -1 +1 @@ -"9YbzXae4" \ No newline at end of file +"mKnU8EV4" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditChange/branch b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditChange/branch index 879ccaf2e..d5094a646 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditChange/branch +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditChange/branch @@ -1 +1 @@ -"RowsHzO8" \ No newline at end of file +"cI6Rfmnn" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditChange/firstCommitHash b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditChange/firstCommitHash index 68000fd15..7e986537e 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditChange/firstCommitHash +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditChange/firstCommitHash @@ -1 +1 @@ -"1ee3894a24c031b5d053a0b8bb25a9182fd77604" \ No newline at end of file +"1e2c32dc4f5cfec9674dac2d9d7d0274a63c15b0" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditDraft/branch b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditDraft/branch index 5bfde7014..3bd5a7d4f 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditDraft/branch +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditDraft/branch @@ -1 +1 @@ -"pCyU7Tpe" \ No newline at end of file +"R4gPQ4SL" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/branch b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/branch index 82678979f..d24723898 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/branch +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/branch @@ -1 +1 @@ -"jbBnJuHT" \ No newline at end of file +"4HOIehwu" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/label1 b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/label1 index de5114824..ca667f6f4 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/label1 +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/label1 @@ -1 +1 @@ -"QcHb0icn" \ No newline at end of file +"2VGRqO7j" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/label2 b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/label2 index 7b3d9d81e..2939cac77 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/label2 +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/label2 @@ -1 +1 @@ -"820svnw8" \ No newline at end of file +"dF2bQUKv" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/label3 b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/label3 index c09704b58..5a7982702 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/label3 +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditLabels/label3 @@ -1 +1 @@ -"bUnh1ONJ" \ No newline at end of file +"bSgDWsqi" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditReviewers/AddReviewer/branch-no-reviewer b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditReviewers/AddReviewer/branch-no-reviewer index 876aafd30..d834b007c 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditReviewers/AddReviewer/branch-no-reviewer +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditReviewers/AddReviewer/branch-no-reviewer @@ -1 +1 @@ -"qS0oWVxR" \ No newline at end of file +"BXaFutk3" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditReviewers/AddReviewersOneByOne/branch-no-reviewer-one-by-one b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditReviewers/AddReviewersOneByOne/branch-no-reviewer-one-by-one index df22e5ade..c34fa70ab 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditReviewers/AddReviewersOneByOne/branch-no-reviewer-one-by-one +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditReviewers/AddReviewersOneByOne/branch-no-reviewer-one-by-one @@ -1 +1 @@ -"sN0uOM0J" \ No newline at end of file +"EwqrlAsh" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditReviewers/SubmitWithReviewer/branch-with-reviewer b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditReviewers/SubmitWithReviewer/branch-with-reviewer index 9bf39d178..ad68d67bc 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/SubmitEditReviewers/SubmitWithReviewer/branch-with-reviewer +++ b/internal/forge/shamhub/testdata/TestIntegration/SubmitEditReviewers/SubmitWithReviewer/branch-with-reviewer @@ -1 +1 @@ -"4oj7KI1p" \ No newline at end of file +"df7WoDco" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/apiURL b/internal/forge/shamhub/testdata/TestIntegration/apiURL index 38ca6faef..4103c7885 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/apiURL +++ b/internal/forge/shamhub/testdata/TestIntegration/apiURL @@ -1 +1 @@ -"http://127.0.0.1:56373" \ No newline at end of file +"http://127.0.0.1:53569" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/gitURL b/internal/forge/shamhub/testdata/TestIntegration/gitURL index 0d906a389..01ae293af 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/gitURL +++ b/internal/forge/shamhub/testdata/TestIntegration/gitURL @@ -1 +1 @@ -"http://127.0.0.1:56374" \ No newline at end of file +"http://127.0.0.1:53570" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/pushRepoURL b/internal/forge/shamhub/testdata/TestIntegration/pushRepoURL index 03fa24f07..ed207937f 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/pushRepoURL +++ b/internal/forge/shamhub/testdata/TestIntegration/pushRepoURL @@ -1 +1 @@ -"http://127.0.0.1:56374/abhinav-fork/test-repo.git" \ No newline at end of file +"http://127.0.0.1:53570/abhinav-fork/test-repo.git" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/repoURL b/internal/forge/shamhub/testdata/TestIntegration/repoURL index 4551e0131..4c5e37538 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/repoURL +++ b/internal/forge/shamhub/testdata/TestIntegration/repoURL @@ -1 +1 @@ -"http://127.0.0.1:56374/abhinav/test-repo.git" \ No newline at end of file +"http://127.0.0.1:53570/abhinav/test-repo.git" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/TestIntegration/token b/internal/forge/shamhub/testdata/TestIntegration/token index a5ee830ce..8e0625d81 100644 --- a/internal/forge/shamhub/testdata/TestIntegration/token +++ b/internal/forge/shamhub/testdata/TestIntegration/token @@ -1 +1 @@ -"444a9baa5f8f8904" \ No newline at end of file +"1ec8a66d2ad7f057" \ No newline at end of file diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeChecksState/Failed.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeChecksState/Failed.yaml index 5b25e03ce..b345987f7 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeChecksState/Failed.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeChecksState/Failed.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 88 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"subject":"Checks qP4Hn8gu","body":"Checks state test","base":"main","head":"qP4Hn8gu"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -19,7 +19,7 @@ interactions: body: | { "number": 19, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/19" + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/19" } headers: Content-Length: @@ -34,10 +34,10 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 56 - host: 127.0.0.1:56373 - body: '{"name":"git-spice integration failed","state":"failed"}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/19/checks + content_length: 18 + host: 127.0.0.1:53569 + body: '{"state":"failed"}' + url: http://127.0.0.1:53569/abhinav/test-repo/change/19/checks method: POST response: proto: HTTP/1.1 @@ -60,22 +60,17 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/19/checks + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/19/checks method: GET response: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 90 + content_length: 24 body: | { - "checks": [ - { - "name": "git-spice integration failed", - "state": "failed" - } - ] + "state": "failed" } headers: Content-Length: diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeChecksState/Passed.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeChecksState/Passed.yaml index 00384b82c..aa5b9649a 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeChecksState/Passed.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeChecksState/Passed.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 88 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"subject":"Checks f0XMOiaa","body":"Checks state test","base":"main","head":"f0XMOiaa"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -19,7 +19,7 @@ interactions: body: | { "number": 9, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/9" + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/9" } headers: Content-Length: @@ -34,10 +34,10 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 56 - host: 127.0.0.1:56373 - body: '{"name":"git-spice integration passed","state":"passed"}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/9/checks + content_length: 18 + host: 127.0.0.1:53569 + body: '{"state":"passed"}' + url: http://127.0.0.1:53569/abhinav/test-repo/change/9/checks method: POST response: proto: HTTP/1.1 @@ -60,22 +60,17 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/9/checks + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/9/checks method: GET response: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 90 + content_length: 24 body: | { - "checks": [ - { - "name": "git-spice integration passed", - "state": "passed" - } - ] + "state": "passed" } headers: Content-Length: diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeChecksState/Pending.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeChecksState/Pending.yaml index e480c13a4..5ed5de929 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeChecksState/Pending.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeChecksState/Pending.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 88 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"subject":"Checks MGWUNTrV","body":"Checks state test","base":"main","head":"MGWUNTrV"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -19,7 +19,7 @@ interactions: body: | { "number": 3, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/3" + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/3" } headers: Content-Length: @@ -34,10 +34,10 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 58 - host: 127.0.0.1:56373 - body: '{"name":"git-spice integration pending","state":"pending"}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/3/checks + content_length: 19 + host: 127.0.0.1:53569 + body: '{"state":"pending"}' + url: http://127.0.0.1:53569/abhinav/test-repo/change/3/checks method: POST response: proto: HTTP/1.1 @@ -60,22 +60,17 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/3/checks + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/3/checks method: GET response: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 92 + content_length: 25 body: | { - "checks": [ - { - "name": "git-spice integration pending", - "state": "pending" - } - ] + "state": "pending" } headers: Content-Length: diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeComments.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeComments.yaml index 889179595..edfe47a02 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeComments.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangeComments.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 92 - host: 127.0.0.1:56373 - body: '{"subject":"Testing uO3v3Jiu","body":"Test PR for comments","base":"main","head":"uO3v3Jiu"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing pmwWlNhh","body":"Test PR for comments","base":"main","head":"pmwWlNhh"}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -18,8 +18,8 @@ interactions: content_length: 80 body: | { - "number": 4, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/4" + "number": 5, + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/5" } headers: Content-Length: @@ -28,16 +28,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 20.3975ms + duration: 43.473041ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 60 - host: 127.0.0.1:56373 - body: '{"changeNumber":4,"body":"M9TCBnUn2veSjGnStFRLruw4FhpFbpPr"}' - url: http://127.0.0.1:56373/abhinav/test-repo/comments + host: 127.0.0.1:53569 + body: '{"changeNumber":5,"body":"K067WOW7dQdrliaKS0tKylcfDgP5GhVp"}' + url: http://127.0.0.1:53569/abhinav/test-repo/comments method: POST response: proto: HTTP/1.1 @@ -55,16 +55,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 123.292µs + duration: 345.417µs - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 60 - host: 127.0.0.1:56373 - body: '{"changeNumber":4,"body":"ooErWPQY4wHcWm8bJDNOeA2G2BHnMcl1"}' - url: http://127.0.0.1:56373/abhinav/test-repo/comments + host: 127.0.0.1:53569 + body: '{"changeNumber":5,"body":"dsCMPgmyz0KVBG501PPi2ocxE5sq7UfQ"}' + url: http://127.0.0.1:53569/abhinav/test-repo/comments method: POST response: proto: HTTP/1.1 @@ -82,16 +82,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 1.531375ms + duration: 158.667µs - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 60 - host: 127.0.0.1:56373 - body: '{"changeNumber":4,"body":"DU3TnJBDljtJOjrPNPgfJRo44fzPxWxZ"}' - url: http://127.0.0.1:56373/abhinav/test-repo/comments + host: 127.0.0.1:53569 + body: '{"changeNumber":5,"body":"3zyUZ2FZcxkOgp4FBvJfBKTnh9fdpNa3"}' + url: http://127.0.0.1:53569/abhinav/test-repo/comments method: POST response: proto: HTTP/1.1 @@ -109,16 +109,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 84.25µs + duration: 417.5µs - id: 4 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 60 - host: 127.0.0.1:56373 - body: '{"changeNumber":4,"body":"WeEXYW6yjMMf0jmnnze12omw5CmTHrDt"}' - url: http://127.0.0.1:56373/abhinav/test-repo/comments + host: 127.0.0.1:53569 + body: '{"changeNumber":5,"body":"im86H34osfgrDc7E4gdx7gE3XXcTXmPX"}' + url: http://127.0.0.1:53569/abhinav/test-repo/comments method: POST response: proto: HTTP/1.1 @@ -136,16 +136,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 482.875µs + duration: 224µs - id: 5 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 60 - host: 127.0.0.1:56373 - body: '{"changeNumber":4,"body":"S4IQsdn1dgnUfOJEFb5JEm7kVxLd8oil"}' - url: http://127.0.0.1:56373/abhinav/test-repo/comments + host: 127.0.0.1:53569 + body: '{"changeNumber":5,"body":"fgt7AG4rAoQp1gGEw8G2U2lbe0qWrTmp"}' + url: http://127.0.0.1:53569/abhinav/test-repo/comments method: POST response: proto: HTTP/1.1 @@ -163,16 +163,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 224.083µs + duration: 361.833µs - id: 6 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 60 - host: 127.0.0.1:56373 - body: '{"changeNumber":4,"body":"JNpsluBOeFN8cwri2EMfI8BrYjbArxPf"}' - url: http://127.0.0.1:56373/abhinav/test-repo/comments + host: 127.0.0.1:53569 + body: '{"changeNumber":5,"body":"KMuajrgxFADVDZhIFdzUBVzpAm9ee7xR"}' + url: http://127.0.0.1:53569/abhinav/test-repo/comments method: POST response: proto: HTTP/1.1 @@ -190,16 +190,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 103.5µs + duration: 699.25µs - id: 7 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 60 - host: 127.0.0.1:56373 - body: '{"changeNumber":4,"body":"FgUJanBcocO1ablzVyYVjDh6fUZbZXyI"}' - url: http://127.0.0.1:56373/abhinav/test-repo/comments + host: 127.0.0.1:53569 + body: '{"changeNumber":5,"body":"6El2lmQr5Ea5Hq9L0fLVfCSATL6qaGJl"}' + url: http://127.0.0.1:53569/abhinav/test-repo/comments method: POST response: proto: HTTP/1.1 @@ -217,16 +217,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 257.333µs + duration: 261.167µs - id: 8 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 60 - host: 127.0.0.1:56373 - body: '{"changeNumber":4,"body":"qp1FOZUVrehYPmU311YMEQDrHlJ2BJ40"}' - url: http://127.0.0.1:56373/abhinav/test-repo/comments + host: 127.0.0.1:53569 + body: '{"changeNumber":5,"body":"ha2n0xHKCbk6xogserVQ8vOR2o2gbNH8"}' + url: http://127.0.0.1:53569/abhinav/test-repo/comments method: POST response: proto: HTTP/1.1 @@ -244,16 +244,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 140.333µs + duration: 164.25µs - id: 9 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 60 - host: 127.0.0.1:56373 - body: '{"changeNumber":4,"body":"sODVHSvcIOhqxyrkgurNH9HMl2b2LSg6"}' - url: http://127.0.0.1:56373/abhinav/test-repo/comments + host: 127.0.0.1:53569 + body: '{"changeNumber":5,"body":"Dr8DblxCuohfDzo0gUkdZdTuRxz8uIfz"}' + url: http://127.0.0.1:53569/abhinav/test-repo/comments method: POST response: proto: HTTP/1.1 @@ -271,16 +271,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 121.5µs + duration: 562.292µs - id: 10 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 60 - host: 127.0.0.1:56373 - body: '{"changeNumber":4,"body":"k7NLLLvg5wMj1tasfkFfqdbhqfa8Ah0E"}' - url: http://127.0.0.1:56373/abhinav/test-repo/comments + host: 127.0.0.1:53569 + body: '{"changeNumber":5,"body":"lr6i3eENW1lJd9i6NNxcjZCditYNWUqF"}' + url: http://127.0.0.1:53569/abhinav/test-repo/comments method: POST response: proto: HTTP/1.1 @@ -298,16 +298,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 115.833µs + duration: 373.291µs - id: 11 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 43 - host: 127.0.0.1:56373 - body: '{"body":"uXAhcpF8V9XZWbQom2aI0nM6Q9fbiqpd"}' - url: http://127.0.0.1:56373/abhinav/test-repo/comments/1 + host: 127.0.0.1:53569 + body: '{"body":"YJgNsP025jRY5hQX6mh2mIfiwifvz6G8"}' + url: http://127.0.0.1:53569/abhinav/test-repo/comments/1 method: PATCH response: proto: HTTP/1.1 @@ -325,15 +325,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 1.85ms + duration: 1.350167ms - id: 12 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/comments/2 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/comments/2 method: DELETE response: proto: HTTP/1.1 @@ -349,16 +349,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 450µs + duration: 287.416µs - id: 13 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 22 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"body":"should fail"}' - url: http://127.0.0.1:56373/abhinav/test-repo/comments/2 + url: http://127.0.0.1:53569/abhinav/test-repo/comments/2 method: PATCH response: proto: HTTP/1.1 @@ -374,22 +374,22 @@ interactions: - text/plain; charset=utf-8 status: 404 Not Found code: 404 - duration: 314.5µs + duration: 821.25µs - id: 14 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 form: change: - - "4" + - "5" limit: - "3" offset: - "0" - url: http://127.0.0.1:56373/abhinav/test-repo/comments?change=4&limit=3&offset=0 + url: http://127.0.0.1:53569/abhinav/test-repo/comments?change=5&limit=3&offset=0 method: GET response: proto: HTTP/1.1 @@ -401,15 +401,15 @@ interactions: "items": [ { "id": 1, - "body": "uXAhcpF8V9XZWbQom2aI0nM6Q9fbiqpd" + "body": "YJgNsP025jRY5hQX6mh2mIfiwifvz6G8" }, { "id": 3, - "body": "DU3TnJBDljtJOjrPNPgfJRo44fzPxWxZ" + "body": "3zyUZ2FZcxkOgp4FBvJfBKTnh9fdpNa3" }, { "id": 4, - "body": "WeEXYW6yjMMf0jmnnze12omw5CmTHrDt" + "body": "im86H34osfgrDc7E4gdx7gE3XXcTXmPX" } ], "offset": 3, @@ -422,22 +422,22 @@ interactions: - application/json status: 200 OK code: 200 - duration: 387.292µs + duration: 583.25µs - id: 15 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 form: change: - - "4" + - "5" limit: - "3" offset: - "3" - url: http://127.0.0.1:56373/abhinav/test-repo/comments?change=4&limit=3&offset=3 + url: http://127.0.0.1:53569/abhinav/test-repo/comments?change=5&limit=3&offset=3 method: GET response: proto: HTTP/1.1 @@ -449,15 +449,15 @@ interactions: "items": [ { "id": 5, - "body": "S4IQsdn1dgnUfOJEFb5JEm7kVxLd8oil" + "body": "fgt7AG4rAoQp1gGEw8G2U2lbe0qWrTmp" }, { "id": 6, - "body": "JNpsluBOeFN8cwri2EMfI8BrYjbArxPf" + "body": "KMuajrgxFADVDZhIFdzUBVzpAm9ee7xR" }, { "id": 7, - "body": "FgUJanBcocO1ablzVyYVjDh6fUZbZXyI" + "body": "6El2lmQr5Ea5Hq9L0fLVfCSATL6qaGJl" } ], "offset": 6, @@ -470,22 +470,22 @@ interactions: - application/json status: 200 OK code: 200 - duration: 1.042458ms + duration: 311.417µs - id: 16 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 form: change: - - "4" + - "5" limit: - "3" offset: - "6" - url: http://127.0.0.1:56373/abhinav/test-repo/comments?change=4&limit=3&offset=6 + url: http://127.0.0.1:53569/abhinav/test-repo/comments?change=5&limit=3&offset=6 method: GET response: proto: HTTP/1.1 @@ -497,15 +497,15 @@ interactions: "items": [ { "id": 8, - "body": "qp1FOZUVrehYPmU311YMEQDrHlJ2BJ40" + "body": "ha2n0xHKCbk6xogserVQ8vOR2o2gbNH8" }, { "id": 9, - "body": "sODVHSvcIOhqxyrkgurNH9HMl2b2LSg6" + "body": "Dr8DblxCuohfDzo0gUkdZdTuRxz8uIfz" }, { "id": 10, - "body": "k7NLLLvg5wMj1tasfkFfqdbhqfa8Ah0E" + "body": "lr6i3eENW1lJd9i6NNxcjZCditYNWUqF" } ], "offset": 9 @@ -517,22 +517,22 @@ interactions: - application/json status: 200 OK code: 200 - duration: 1.038417ms + duration: 210.416µs - id: 17 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 form: change: - - "4" + - "5" limit: - "10" offset: - "0" - url: http://127.0.0.1:56373/abhinav/test-repo/comments?change=4&limit=10&offset=0 + url: http://127.0.0.1:53569/abhinav/test-repo/comments?change=5&limit=10&offset=0 method: GET response: proto: HTTP/1.1 @@ -544,39 +544,39 @@ interactions: "items": [ { "id": 1, - "body": "uXAhcpF8V9XZWbQom2aI0nM6Q9fbiqpd" + "body": "YJgNsP025jRY5hQX6mh2mIfiwifvz6G8" }, { "id": 3, - "body": "DU3TnJBDljtJOjrPNPgfJRo44fzPxWxZ" + "body": "3zyUZ2FZcxkOgp4FBvJfBKTnh9fdpNa3" }, { "id": 4, - "body": "WeEXYW6yjMMf0jmnnze12omw5CmTHrDt" + "body": "im86H34osfgrDc7E4gdx7gE3XXcTXmPX" }, { "id": 5, - "body": "S4IQsdn1dgnUfOJEFb5JEm7kVxLd8oil" + "body": "fgt7AG4rAoQp1gGEw8G2U2lbe0qWrTmp" }, { "id": 6, - "body": "JNpsluBOeFN8cwri2EMfI8BrYjbArxPf" + "body": "KMuajrgxFADVDZhIFdzUBVzpAm9ee7xR" }, { "id": 7, - "body": "FgUJanBcocO1ablzVyYVjDh6fUZbZXyI" + "body": "6El2lmQr5Ea5Hq9L0fLVfCSATL6qaGJl" }, { "id": 8, - "body": "qp1FOZUVrehYPmU311YMEQDrHlJ2BJ40" + "body": "ha2n0xHKCbk6xogserVQ8vOR2o2gbNH8" }, { "id": 9, - "body": "sODVHSvcIOhqxyrkgurNH9HMl2b2LSg6" + "body": "Dr8DblxCuohfDzo0gUkdZdTuRxz8uIfz" }, { "id": 10, - "body": "k7NLLLvg5wMj1tasfkFfqdbhqfa8Ah0E" + "body": "lr6i3eENW1lJd9i6NNxcjZCditYNWUqF" } ], "offset": 9 @@ -588,4 +588,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 177.583µs + duration: 956.5µs diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangesStates.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangesStates.yaml index 32e610d37..63f60dabf 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangesStates.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ChangesStates.yaml @@ -7,37 +7,37 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 80 - host: 127.0.0.1:56373 - body: '{"subject":"Open jMfwXkYK","body":"Open change","base":"main","head":"jMfwXkYK"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Open QohFltlh","body":"Open change","base":"main","head":"QohFltlh"}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 82 + content_length: 80 body: | { - "number": 15, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/15" + "number": 9, + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/9" } headers: Content-Length: - - "82" + - "80" Content-Type: - application/json status: 200 OK code: 200 - duration: 13.188042ms + duration: 29.667375ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 84 - host: 127.0.0.1:56373 - body: '{"subject":"Merged C5hzQ5Ik","body":"Merged change","base":"main","head":"C5hzQ5Ik"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Merged gvgMWiD8","body":"Merged change","base":"main","head":"gvgMWiD8"}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -46,8 +46,8 @@ interactions: content_length: 82 body: | { - "number": 16, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/16" + "number": 11, + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/11" } headers: Content-Length: @@ -56,16 +56,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 13.844667ms + duration: 29.907541ms - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 84 - host: 127.0.0.1:56373 - body: '{"subject":"Closed IEtiDhFs","body":"Closed change","base":"main","head":"IEtiDhFs"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Closed mzMsv5Rh","body":"Closed change","base":"main","head":"mzMsv5Rh"}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -74,8 +74,8 @@ interactions: content_length: 82 body: | { - "number": 18, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/18" + "number": 15, + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/15" } headers: Content-Length: @@ -84,16 +84,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 14.069959ms + duration: 27.266959ms - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 18 - host: 127.0.0.1:56373 - body: '{"ids":[15,16,18]}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/states + content_length: 17 + host: 127.0.0.1:53569 + body: '{"ids":[9,11,15]}' + url: http://127.0.0.1:53569/abhinav/test-repo/change/states method: POST response: proto: HTTP/1.1 @@ -105,15 +105,15 @@ interactions: "statuses": [ { "state": "open", - "headHash": "84a9e994290ae89ccc93f7d2a03d009b39d103ea" + "headHash": "df17da0bd1722f06dbd75d8948417dded7a12d77" }, { "state": "merged", - "headHash": "25b03948515094aec6d2cf82537f16d56a1486dc" + "headHash": "fae1a4a757b2259632acb5eb5926849f125dc594" }, { "state": "closed", - "headHash": "0c32e505344f156f8aa70189cfd851a1a40765cd" + "headHash": "145f79d936fdcaf9ccd165ed6be52de6eaba9895" } ] } @@ -124,4 +124,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 10.51575ms + duration: 27.146958ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/CommentCountsByChange.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/CommentCountsByChange.yaml index a6c9cab53..b98cc5b46 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/CommentCountsByChange.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/CommentCountsByChange.yaml @@ -7,37 +7,37 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 98 - host: 127.0.0.1:56373 - body: '{"subject":"Testing HQWHaq9z","body":"Test PR for comment counts","base":"main","head":"HQWHaq9z"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing wleC7vBI","body":"Test PR for comment counts","base":"main","head":"wleC7vBI"}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 82 + content_length: 80 body: | { - "number": 11, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/11" + "number": 1, + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/1" } headers: Content-Length: - - "82" + - "80" Content-Type: - application/json status: 200 OK code: 200 - duration: 28.731083ms + duration: 46.254708ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 12 - host: 127.0.0.1:56373 - body: '{"ids":[11]}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/comment-counts + content_length: 11 + host: 127.0.0.1:53569 + body: '{"ids":[1]}' + url: http://127.0.0.1:53569/abhinav/test-repo/change/comment-counts method: POST response: proto: HTTP/1.1 @@ -61,4 +61,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 1.628833ms + duration: 3.071583ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/FindChangesByBranchDoesNotExist.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/FindChangesByBranchDoesNotExist.yaml index 4673e14a0..931a943e7 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/FindChangesByBranchDoesNotExist.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/FindChangesByBranchDoesNotExist.yaml @@ -7,13 +7,13 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 form: limit: - "10" state: - all - url: http://127.0.0.1:56373/abhinav/test-repo/changes/by-branch/does-not-exist?limit=10&state=all + url: http://127.0.0.1:53569/abhinav/test-repo/changes/by-branch/does-not-exist?limit=10&state=all method: GET response: proto: HTTP/1.1 @@ -29,4 +29,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 752.916µs + duration: 570µs diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ListChangeTemplates/NoTemplates.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ListChangeTemplates/NoTemplates.yaml index 4e2249722..970786aee 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ListChangeTemplates/NoTemplates.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ListChangeTemplates/NoTemplates.yaml @@ -7,8 +7,8 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change-template + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change-template method: GET response: proto: HTTP/1.1 @@ -24,4 +24,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 138.274667ms + duration: 270.632708ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ListChangeTemplates/TemplatesPresent.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ListChangeTemplates/TemplatesPresent.yaml index 2736a773b..490f41621 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/ListChangeTemplates/TemplatesPresent.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/ListChangeTemplates/TemplatesPresent.yaml @@ -7,8 +7,8 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change-template + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change-template method: GET response: proto: HTTP/1.1 @@ -18,12 +18,12 @@ interactions: body: | [ { - "filename": "1HQqVNZV.md", - "body": "\n" + "filename": "aW2r2kGY.md", + "body": "This is a test template\n" }, { - "filename": "yE3Lj2v5.md", - "body": "This is a test template\n" + "filename": "np6KTBsj.md", + "body": "\n" } ] headers: @@ -33,4 +33,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 149.63575ms + duration: 309.049ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitBaseDoesNotExist.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitBaseDoesNotExist.yaml index 4c536dd39..a9463e3d0 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitBaseDoesNotExist.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitBaseDoesNotExist.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 106 - host: 127.0.0.1:56373 - body: '{"subject":"Testing vY4xsORW","body":"Test PR with non-existent base","base":"9s9oMX6C","head":"vY4xsORW"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing bcKIEKS3","body":"Test PR with non-existent base","base":"ExQcEmr1","head":"bcKIEKS3"}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -25,16 +25,16 @@ interactions: - text/plain; charset=utf-8 status: 400 Bad Request code: 400 - duration: 9.87325ms + duration: 18.86725ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 29 - host: 127.0.0.1:56373 - body: '{"ref":"refs/heads/9s9oMX6C"}' - url: http://127.0.0.1:56373/abhinav/test-repo/ref/exists + host: 127.0.0.1:53569 + body: '{"ref":"refs/heads/ExQcEmr1"}' + url: http://127.0.0.1:53569/abhinav/test-repo/ref/exists method: POST response: proto: HTTP/1.1 @@ -52,4 +52,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 10.2145ms + duration: 20.495625ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitChangeFromPushRepository.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitChangeFromPushRepository.yaml index be69f95da..7c6944ecc 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitChangeFromPushRepository.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitChangeFromPushRepository.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 138 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"subject":"Testing fork QDezbUVv","body":"Test fork change request","base":"main","head":"QDezbUVv","head_repo":"abhinav-fork/test-repo"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -19,7 +19,7 @@ interactions: body: | { "number": 12, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/12" + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/12" } headers: Content-Length: @@ -35,8 +35,8 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/12 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/12 method: GET response: proto: HTTP/1.1 @@ -46,7 +46,7 @@ interactions: body: | { "number": 12, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/12", + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/12", "state": "open", "title": "Testing fork QDezbUVv", "body": "Test fork change request", @@ -81,7 +81,7 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 form: head_repo: - abhinav-fork/test-repo @@ -89,7 +89,7 @@ interactions: - "10" state: - all - url: http://127.0.0.1:56373/abhinav/test-repo/changes/by-branch/QDezbUVv?head_repo=abhinav-fork%2Ftest-repo&limit=10&state=all + url: http://127.0.0.1:53569/abhinav/test-repo/changes/by-branch/QDezbUVv?head_repo=abhinav-fork%2Ftest-repo&limit=10&state=all method: GET response: proto: HTTP/1.1 @@ -100,7 +100,7 @@ interactions: [ { "number": 12, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/12", + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/12", "state": "open", "title": "Testing fork QDezbUVv", "body": "Test fork change request", @@ -136,13 +136,13 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 form: limit: - "10" state: - all - url: http://127.0.0.1:56373/abhinav/test-repo/changes/by-branch/QDezbUVv?limit=10&state=all + url: http://127.0.0.1:53569/abhinav/test-repo/changes/by-branch/QDezbUVv?limit=10&state=all method: GET response: proto: HTTP/1.1 diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditAssignees/AddAssignee.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditAssignees/AddAssignee.yaml index aeb06a334..fbafc7661 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditAssignees/AddAssignee.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditAssignees/AddAssignee.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 97 - host: 127.0.0.1:56373 - body: '{"subject":"Testing 66MtCOhk","body":"Test PR without assignees","base":"main","head":"66MtCOhk"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing 4D8sFmyC","body":"Test PR without assignees","base":"main","head":"4D8sFmyC"}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -19,7 +19,7 @@ interactions: body: | { "number": 10, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/10" + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/10" } headers: Content-Length: @@ -28,15 +28,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 25.159916ms + duration: 29.755416ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/10 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/10 method: GET response: proto: HTTP/1.1 @@ -46,9 +46,9 @@ interactions: body: | { "number": 10, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/10", + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/10", "state": "open", - "title": "Testing 66MtCOhk", + "title": "Testing 4D8sFmyC", "body": "Test PR without assignees", "base": { "repository": { @@ -56,15 +56,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "66MtCOhk", - "sha": "66a0c50cac60c34e761b18838932a3aab48deac9" + "ref": "4D8sFmyC", + "sha": "3dce9a05f05fb11263d1482ecd4125949e747389" } } headers: @@ -74,16 +74,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 22.786459ms + duration: 27.52675ms - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 39 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"assignees":["assignee1","assignee2"]}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/10 + url: http://127.0.0.1:53569/abhinav/test-repo/change/10 method: PATCH response: proto: HTTP/1.1 @@ -99,15 +99,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 696.167µs + duration: 224.75µs - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/10 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/10 method: GET response: proto: HTTP/1.1 @@ -117,9 +117,9 @@ interactions: body: | { "number": 10, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/10", + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/10", "state": "open", - "title": "Testing 66MtCOhk", + "title": "Testing 4D8sFmyC", "body": "Test PR without assignees", "base": { "repository": { @@ -127,15 +127,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "66MtCOhk", - "sha": "66a0c50cac60c34e761b18838932a3aab48deac9" + "ref": "4D8sFmyC", + "sha": "3dce9a05f05fb11263d1482ecd4125949e747389" }, "assignees": [ "assignee1", @@ -149,4 +149,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 17.645625ms + duration: 29.249833ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditAssignees/AddAssigneesOneByOne.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditAssignees/AddAssigneesOneByOne.yaml index 0f7dfd8b2..23e87958b 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditAssignees/AddAssigneesOneByOne.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditAssignees/AddAssigneesOneByOne.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 97 - host: 127.0.0.1:56373 - body: '{"subject":"Testing Op9WQIRi","body":"Test PR without assignees","base":"main","head":"Op9WQIRi"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing lEG1MUxo","body":"Test PR without assignees","base":"main","head":"lEG1MUxo"}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -18,8 +18,8 @@ interactions: content_length: 80 body: | { - "number": 5, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/5" + "number": 4, + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/4" } headers: Content-Length: @@ -28,16 +28,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 21.201041ms + duration: 42.64775ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 27 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"assignees":["assignee1"]}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/5 + url: http://127.0.0.1:53569/abhinav/test-repo/change/4 method: PATCH response: proto: HTTP/1.1 @@ -53,16 +53,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 263.792µs + duration: 631.583µs - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 27 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"assignees":["assignee2"]}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/5 + url: http://127.0.0.1:53569/abhinav/test-repo/change/4 method: PATCH response: proto: HTTP/1.1 @@ -78,15 +78,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 2.75425ms + duration: 129.958µs - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/5 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/4 method: GET response: proto: HTTP/1.1 @@ -95,10 +95,10 @@ interactions: content_length: 571 body: | { - "number": 5, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/5", + "number": 4, + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/4", "state": "open", - "title": "Testing Op9WQIRi", + "title": "Testing lEG1MUxo", "body": "Test PR without assignees", "base": { "repository": { @@ -106,15 +106,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "Op9WQIRi", - "sha": "34d6de08ed0a392a6ec261c288d5516e65dc2614" + "ref": "lEG1MUxo", + "sha": "c95d303c96fd553536f0b20c74bbf7cb19087f37" }, "assignees": [ "assignee1", @@ -128,4 +128,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 21.133625ms + duration: 37.717625ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditAssignees/SubmitWithAssignee.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditAssignees/SubmitWithAssignee.yaml index 1bd09cc14..9e3a5f5c2 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditAssignees/SubmitWithAssignee.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditAssignees/SubmitWithAssignee.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 131 - host: 127.0.0.1:56373 - body: '{"subject":"Testing lE3mefp0","body":"Test PR with assignee","base":"main","head":"lE3mefp0","assignees":["assignee1","assignee2"]}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing UQDEDpcg","body":"Test PR with assignee","base":"main","head":"UQDEDpcg","assignees":["assignee1","assignee2"]}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -18,8 +18,8 @@ interactions: content_length: 80 body: | { - "number": 7, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/7" + "number": 2, + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/2" } headers: Content-Length: @@ -28,15 +28,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 21.594125ms + duration: 38.620583ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/7 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/2 method: GET response: proto: HTTP/1.1 @@ -45,10 +45,10 @@ interactions: content_length: 567 body: | { - "number": 7, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/7", + "number": 2, + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/2", "state": "open", - "title": "Testing lE3mefp0", + "title": "Testing UQDEDpcg", "body": "Test PR with assignee", "base": { "repository": { @@ -56,15 +56,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "lE3mefp0", - "sha": "1294690f93a2b746dbb81d60a23b512988633d14" + "ref": "UQDEDpcg", + "sha": "0970719d4527d7008b38d05e15c0e92f6acfb25b" }, "assignees": [ "assignee1", @@ -78,20 +78,20 @@ interactions: - application/json status: 200 OK code: 200 - duration: 17.238667ms + duration: 38.749042ms - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 form: limit: - "10" state: - all - url: http://127.0.0.1:56373/abhinav/test-repo/changes/by-branch/lE3mefp0?limit=10&state=all + url: http://127.0.0.1:53569/abhinav/test-repo/changes/by-branch/UQDEDpcg?limit=10&state=all method: GET response: proto: HTTP/1.1 @@ -101,10 +101,10 @@ interactions: body: | [ { - "number": 7, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/7", + "number": 2, + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/2", "state": "open", - "title": "Testing lE3mefp0", + "title": "Testing UQDEDpcg", "body": "Test PR with assignee", "base": { "repository": { @@ -112,15 +112,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "lE3mefp0", - "sha": "1294690f93a2b746dbb81d60a23b512988633d14" + "ref": "UQDEDpcg", + "sha": "0970719d4527d7008b38d05e15c0e92f6acfb25b" }, "assignees": [ "assignee1", @@ -135,4 +135,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 20.395ms + duration: 33.967333ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditBase.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditBase.yaml index ed56e8e18..33cf7f1c6 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditBase.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditBase.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 100 - host: 127.0.0.1:56373 - body: '{"subject":"Testing 9YbzXae4","body":"Test PR with custom base","base":"t8XzNN0X","head":"9YbzXae4"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing mKnU8EV4","body":"Test PR with custom base","base":"P2inVOao","head":"mKnU8EV4"}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -19,7 +19,7 @@ interactions: body: | { "number": 8, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/8" + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/8" } headers: Content-Length: @@ -28,15 +28,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 23.763167ms + duration: 41.197167ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/8 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/8 method: GET response: proto: HTTP/1.1 @@ -46,25 +46,25 @@ interactions: body: | { "number": 8, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/8", + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/8", "state": "open", - "title": "Testing 9YbzXae4", + "title": "Testing mKnU8EV4", "body": "Test PR with custom base", "base": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "t8XzNN0X", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "ref": "P2inVOao", + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "9YbzXae4", - "sha": "a3fc1801625662da2cc43a337ee7ba729a73d732" + "ref": "mKnU8EV4", + "sha": "cd26b16c292f00fa0b6a5f5f710c461474950344" } } headers: @@ -74,16 +74,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 19.21925ms + duration: 44.502708ms - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 15 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"base":"main"}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/8 + url: http://127.0.0.1:53569/abhinav/test-repo/change/8 method: PATCH response: proto: HTTP/1.1 @@ -99,15 +99,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 485.75µs + duration: 129.75µs - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/8 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/8 method: GET response: proto: HTTP/1.1 @@ -117,9 +117,9 @@ interactions: body: | { "number": 8, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/8", + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/8", "state": "open", - "title": "Testing 9YbzXae4", + "title": "Testing mKnU8EV4", "body": "Test PR with custom base", "base": { "repository": { @@ -127,15 +127,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "9YbzXae4", - "sha": "a3fc1801625662da2cc43a337ee7ba729a73d732" + "ref": "mKnU8EV4", + "sha": "cd26b16c292f00fa0b6a5f5f710c461474950344" } } headers: @@ -145,4 +145,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 17.355333ms + duration: 32.452916ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditChange.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditChange.yaml index e0876df1f..7faddd99f 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditChange.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditChange.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 79 - host: 127.0.0.1:56373 - body: '{"subject":"Testing RowsHzO8","body":"Test PR","base":"main","head":"RowsHzO8"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing cI6Rfmnn","body":"Test PR","base":"main","head":"cI6Rfmnn"}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -18,8 +18,8 @@ interactions: content_length: 80 body: | { - "number": 1, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/1" + "number": 7, + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/7" } headers: Content-Length: @@ -28,15 +28,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 22.12625ms + duration: 45.201167ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/1 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/7 method: GET response: proto: HTTP/1.1 @@ -45,10 +45,10 @@ interactions: content_length: 498 body: | { - "number": 1, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/1", + "number": 7, + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/7", "state": "open", - "title": "Testing RowsHzO8", + "title": "Testing cI6Rfmnn", "body": "Test PR", "base": { "repository": { @@ -56,15 +56,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "RowsHzO8", - "sha": "1ee3894a24c031b5d053a0b8bb25a9182fd77604" + "ref": "cI6Rfmnn", + "sha": "1e2c32dc4f5cfec9674dac2d9d7d0274a63c15b0" } } headers: @@ -74,20 +74,20 @@ interactions: - application/json status: 200 OK code: 200 - duration: 18.941208ms + duration: 39.789833ms - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 form: limit: - "10" state: - all - url: http://127.0.0.1:56373/abhinav/test-repo/changes/by-branch/RowsHzO8?limit=10&state=all + url: http://127.0.0.1:53569/abhinav/test-repo/changes/by-branch/cI6Rfmnn?limit=10&state=all method: GET response: proto: HTTP/1.1 @@ -97,10 +97,10 @@ interactions: body: | [ { - "number": 1, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/1", + "number": 7, + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/7", "state": "open", - "title": "Testing RowsHzO8", + "title": "Testing cI6Rfmnn", "body": "Test PR", "base": { "repository": { @@ -108,15 +108,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "RowsHzO8", - "sha": "1ee3894a24c031b5d053a0b8bb25a9182fd77604" + "ref": "cI6Rfmnn", + "sha": "1e2c32dc4f5cfec9674dac2d9d7d0274a63c15b0" } } ] @@ -127,4 +127,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 16.2305ms + duration: 40.238416ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditDraft.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditDraft.yaml index 5114cb40a..9371286e1 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditDraft.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditDraft.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 98 - host: 127.0.0.1:56373 - body: '{"subject":"Testing pCyU7Tpe","body":"Test draft PR","base":"main","head":"pCyU7Tpe","draft":true}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing R4gPQ4SL","body":"Test draft PR","base":"main","head":"R4gPQ4SL","draft":true}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -18,8 +18,8 @@ interactions: content_length: 80 body: | { - "number": 2, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/2" + "number": 3, + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/3" } headers: Content-Length: @@ -28,15 +28,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 22.713917ms + duration: 37.971ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/2 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/3 method: GET response: proto: HTTP/1.1 @@ -45,11 +45,11 @@ interactions: content_length: 521 body: | { - "number": 2, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/2", + "number": 3, + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/3", "draft": true, "state": "open", - "title": "Testing pCyU7Tpe", + "title": "Testing R4gPQ4SL", "body": "Test draft PR", "base": { "repository": { @@ -57,15 +57,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "pCyU7Tpe", - "sha": "7bd9a65f890645f1b36eb13dd6a622b6c4b32b13" + "ref": "R4gPQ4SL", + "sha": "965286a9aa45336455161a7b2a4db1e34f9ac98d" } } headers: @@ -75,16 +75,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 20.71025ms + duration: 36.908792ms - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 15 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"draft":false}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/2 + url: http://127.0.0.1:53569/abhinav/test-repo/change/3 method: PATCH response: proto: HTTP/1.1 @@ -100,15 +100,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 134.583µs + duration: 213.667µs - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/2 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/3 method: GET response: proto: HTTP/1.1 @@ -117,10 +117,10 @@ interactions: content_length: 504 body: | { - "number": 2, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/2", + "number": 3, + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/3", "state": "open", - "title": "Testing pCyU7Tpe", + "title": "Testing R4gPQ4SL", "body": "Test draft PR", "base": { "repository": { @@ -128,15 +128,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "pCyU7Tpe", - "sha": "7bd9a65f890645f1b36eb13dd6a622b6c4b32b13" + "ref": "R4gPQ4SL", + "sha": "965286a9aa45336455161a7b2a4db1e34f9ac98d" } } headers: @@ -146,16 +146,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 17.1405ms + duration: 43.281167ms - id: 4 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 14 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"draft":true}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/2 + url: http://127.0.0.1:53569/abhinav/test-repo/change/3 method: PATCH response: proto: HTTP/1.1 @@ -171,15 +171,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 1.807125ms + duration: 484.75µs - id: 5 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/2 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/3 method: GET response: proto: HTTP/1.1 @@ -188,11 +188,11 @@ interactions: content_length: 521 body: | { - "number": 2, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/2", + "number": 3, + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/3", "draft": true, "state": "open", - "title": "Testing pCyU7Tpe", + "title": "Testing R4gPQ4SL", "body": "Test draft PR", "base": { "repository": { @@ -200,15 +200,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "pCyU7Tpe", - "sha": "7bd9a65f890645f1b36eb13dd6a622b6c4b32b13" + "ref": "R4gPQ4SL", + "sha": "965286a9aa45336455161a7b2a4db1e34f9ac98d" } } headers: @@ -218,4 +218,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 20.676166ms + duration: 37.233416ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditLabels.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditLabels.yaml index 7b515d983..351de0b08 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditLabels.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditLabels.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 113 - host: 127.0.0.1:56373 - body: '{"subject":"Testing jbBnJuHT","body":"Test PR with labels","base":"main","head":"jbBnJuHT","labels":["QcHb0icn"]}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing 4HOIehwu","body":"Test PR with labels","base":"main","head":"4HOIehwu","labels":["2VGRqO7j"]}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -19,7 +19,7 @@ interactions: body: | { "number": 6, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/6" + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/6" } headers: Content-Length: @@ -28,15 +28,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 21.992708ms + duration: 41.998125ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/6 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/6 method: GET response: proto: HTTP/1.1 @@ -46,9 +46,9 @@ interactions: body: | { "number": 6, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/6", + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/6", "state": "open", - "title": "Testing jbBnJuHT", + "title": "Testing 4HOIehwu", "body": "Test PR with labels", "base": { "repository": { @@ -56,18 +56,18 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "jbBnJuHT", - "sha": "c34dbee79699111d2299f1fd5737e5535ae1f643" + "ref": "4HOIehwu", + "sha": "b324fe0ca7ab1155f6db68bb6738b3a35c706c3f" }, "labels": [ - "QcHb0icn" + "2VGRqO7j" ] } headers: @@ -77,16 +77,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 15.358041ms + duration: 36.385333ms - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 23 - host: 127.0.0.1:56373 - body: '{"labels":["820svnw8"]}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/6 + host: 127.0.0.1:53569 + body: '{"labels":["dF2bQUKv"]}' + url: http://127.0.0.1:53569/abhinav/test-repo/change/6 method: PATCH response: proto: HTTP/1.1 @@ -102,16 +102,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 1.308292ms + duration: 398.042µs - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 34 - host: 127.0.0.1:56373 - body: '{"labels":["820svnw8","bUnh1ONJ"]}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/6 + host: 127.0.0.1:53569 + body: '{"labels":["dF2bQUKv","bSgDWsqi"]}' + url: http://127.0.0.1:53569/abhinav/test-repo/change/6 method: PATCH response: proto: HTTP/1.1 @@ -127,20 +127,20 @@ interactions: - application/json status: 200 OK code: 200 - duration: 242.25µs + duration: 421.083µs - id: 4 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 form: limit: - "10" state: - all - url: http://127.0.0.1:56373/abhinav/test-repo/changes/by-branch/jbBnJuHT?limit=10&state=all + url: http://127.0.0.1:53569/abhinav/test-repo/changes/by-branch/4HOIehwu?limit=10&state=all method: GET response: proto: HTTP/1.1 @@ -151,9 +151,9 @@ interactions: [ { "number": 6, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/6", + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/6", "state": "open", - "title": "Testing jbBnJuHT", + "title": "Testing 4HOIehwu", "body": "Test PR with labels", "base": { "repository": { @@ -161,20 +161,20 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "jbBnJuHT", - "sha": "c34dbee79699111d2299f1fd5737e5535ae1f643" + "ref": "4HOIehwu", + "sha": "b324fe0ca7ab1155f6db68bb6738b3a35c706c3f" }, "labels": [ - "QcHb0icn", - "820svnw8", - "bUnh1ONJ" + "2VGRqO7j", + "dF2bQUKv", + "bSgDWsqi" ] } ] @@ -185,4 +185,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 20.795292ms + duration: 41.2895ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditReviewers/AddReviewer.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditReviewers/AddReviewer.yaml index a9ce1ed31..ca892789d 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditReviewers/AddReviewer.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditReviewers/AddReviewer.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 97 - host: 127.0.0.1:56373 - body: '{"subject":"Testing qS0oWVxR","body":"Test PR without reviewers","base":"main","head":"qS0oWVxR"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing BXaFutk3","body":"Test PR without reviewers","base":"main","head":"BXaFutk3"}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -18,8 +18,8 @@ interactions: content_length: 82 body: | { - "number": 17, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/17" + "number": 13, + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/13" } headers: Content-Length: @@ -28,15 +28,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 13.741459ms + duration: 30.130458ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/17 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/13 method: GET response: proto: HTTP/1.1 @@ -45,10 +45,10 @@ interactions: content_length: 518 body: | { - "number": 17, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/17", + "number": 13, + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/13", "state": "open", - "title": "Testing qS0oWVxR", + "title": "Testing BXaFutk3", "body": "Test PR without reviewers", "base": { "repository": { @@ -56,15 +56,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "qS0oWVxR", - "sha": "d10f53257e22f018a3d5d54780611749d959d3ed" + "ref": "BXaFutk3", + "sha": "4423dce906b5fddf7ddd3a536c3ffc2a2627a245" } } headers: @@ -74,16 +74,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 13.343458ms + duration: 27.700542ms - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 39 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"reviewers":["reviewer1","reviewer2"]}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/17 + url: http://127.0.0.1:53569/abhinav/test-repo/change/13 method: PATCH response: proto: HTTP/1.1 @@ -99,15 +99,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 20.163792ms + duration: 35.203875ms - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/17 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/13 method: GET response: proto: HTTP/1.1 @@ -116,10 +116,10 @@ interactions: content_length: 583 body: | { - "number": 17, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/17", + "number": 13, + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/13", "state": "open", - "title": "Testing qS0oWVxR", + "title": "Testing BXaFutk3", "body": "Test PR without reviewers", "base": { "repository": { @@ -127,15 +127,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "78e7fffc2e2f2b050db2efb7f3de43be063e31f3" + "sha": "935a1736c1d0abc559194d26342e46a76e102069" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "qS0oWVxR", - "sha": "d10f53257e22f018a3d5d54780611749d959d3ed" + "ref": "BXaFutk3", + "sha": "4423dce906b5fddf7ddd3a536c3ffc2a2627a245" }, "requested_reviewers": [ "reviewer1", @@ -149,4 +149,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 21.720042ms + duration: 55.48225ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditReviewers/AddReviewersOneByOne.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditReviewers/AddReviewersOneByOne.yaml index e2a72c93e..567791f79 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditReviewers/AddReviewersOneByOne.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditReviewers/AddReviewersOneByOne.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 97 - host: 127.0.0.1:56373 - body: '{"subject":"Testing sN0uOM0J","body":"Test PR without reviewers","base":"main","head":"sN0uOM0J"}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing EwqrlAsh","body":"Test PR without reviewers","base":"main","head":"EwqrlAsh"}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -19,7 +19,7 @@ interactions: body: | { "number": 14, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/14" + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/14" } headers: Content-Length: @@ -28,15 +28,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 14.613166ms + duration: 30.532125ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/14 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/14 method: GET response: proto: HTTP/1.1 @@ -46,9 +46,9 @@ interactions: body: | { "number": 14, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/14", + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/14", "state": "open", - "title": "Testing sN0uOM0J", + "title": "Testing EwqrlAsh", "body": "Test PR without reviewers", "base": { "repository": { @@ -56,15 +56,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "sN0uOM0J", - "sha": "fe1856b3db7f006918e699a1c316e92ad3342d93" + "ref": "EwqrlAsh", + "sha": "6ee3f5e3004c4d74580c42ea3b0f85d0fcf5f0f8" } } headers: @@ -74,16 +74,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 11.315875ms + duration: 27.463625ms - id: 2 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 27 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"reviewers":["reviewer1"]}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/14 + url: http://127.0.0.1:53569/abhinav/test-repo/change/14 method: PATCH response: proto: HTTP/1.1 @@ -99,16 +99,16 @@ interactions: - application/json status: 200 OK code: 200 - duration: 415.5µs + duration: 35.209625ms - id: 3 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 27 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 body: '{"reviewers":["reviewer2"]}' - url: http://127.0.0.1:56373/abhinav/test-repo/change/14 + url: http://127.0.0.1:53569/abhinav/test-repo/change/14 method: PATCH response: proto: HTTP/1.1 @@ -124,15 +124,15 @@ interactions: - application/json status: 200 OK code: 200 - duration: 102.334µs + duration: 26.211ms - id: 4 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 - url: http://127.0.0.1:56373/abhinav/test-repo/change/14 + host: 127.0.0.1:53569 + url: http://127.0.0.1:53569/abhinav/test-repo/change/14 method: GET response: proto: HTTP/1.1 @@ -142,9 +142,9 @@ interactions: body: | { "number": 14, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/14", + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/14", "state": "open", - "title": "Testing sN0uOM0J", + "title": "Testing EwqrlAsh", "body": "Test PR without reviewers", "base": { "repository": { @@ -152,15 +152,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "935a1736c1d0abc559194d26342e46a76e102069" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "sN0uOM0J", - "sha": "fe1856b3db7f006918e699a1c316e92ad3342d93" + "ref": "EwqrlAsh", + "sha": "6ee3f5e3004c4d74580c42ea3b0f85d0fcf5f0f8" }, "requested_reviewers": [ "reviewer1", @@ -174,4 +174,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 11.028083ms + duration: 28.57575ms diff --git a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditReviewers/SubmitWithReviewer.yaml b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditReviewers/SubmitWithReviewer.yaml index 0c837343b..6d9e90d43 100644 --- a/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditReviewers/SubmitWithReviewer.yaml +++ b/internal/forge/shamhub/testdata/fixtures/TestIntegration/SubmitEditReviewers/SubmitWithReviewer.yaml @@ -7,9 +7,9 @@ interactions: proto_major: 1 proto_minor: 1 content_length: 131 - host: 127.0.0.1:56373 - body: '{"subject":"Testing 4oj7KI1p","body":"Test PR with reviewer","base":"main","head":"4oj7KI1p","reviewers":["reviewer1","reviewer2"]}' - url: http://127.0.0.1:56373/abhinav/test-repo/changes + host: 127.0.0.1:53569 + body: '{"subject":"Testing df7WoDco","body":"Test PR with reviewer","base":"main","head":"df7WoDco","reviewers":["reviewer1","reviewer2"]}' + url: http://127.0.0.1:53569/abhinav/test-repo/changes method: POST response: proto: HTTP/1.1 @@ -18,8 +18,8 @@ interactions: content_length: 82 body: | { - "number": 13, - "url": "http://127.0.0.1:56374/abhinav/test-repo/change/13" + "number": 12, + "url": "http://127.0.0.1:53570/abhinav/test-repo/change/12" } headers: Content-Length: @@ -28,20 +28,20 @@ interactions: - application/json status: 200 OK code: 200 - duration: 23.097875ms + duration: 30.764459ms - id: 1 request: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 content_length: 0 - host: 127.0.0.1:56373 + host: 127.0.0.1:53569 form: limit: - "10" state: - all - url: http://127.0.0.1:56373/abhinav/test-repo/changes/by-branch/4oj7KI1p?limit=10&state=all + url: http://127.0.0.1:53569/abhinav/test-repo/changes/by-branch/df7WoDco?limit=10&state=all method: GET response: proto: HTTP/1.1 @@ -51,10 +51,10 @@ interactions: body: | [ { - "number": 13, - "html_url": "http://127.0.0.1:56374/abhinav/test-repo/change/13", + "number": 12, + "html_url": "http://127.0.0.1:53570/abhinav/test-repo/change/12", "state": "open", - "title": "Testing 4oj7KI1p", + "title": "Testing df7WoDco", "body": "Test PR with reviewer", "base": { "repository": { @@ -62,15 +62,15 @@ interactions: "name": "test-repo" }, "ref": "main", - "sha": "71012226be854d2a77b193ca779abba5cc84cca0" + "sha": "c17a304a06b281e76d193f8a86a8ab71585b64e3" }, "head": { "repository": { "owner": "abhinav", "name": "test-repo" }, - "ref": "4oj7KI1p", - "sha": "d42a7e9d935c9911190197ad4e04542af2500250" + "ref": "df7WoDco", + "sha": "6cca56449280db9fd8954bd6d01d3b30ca7a474c" }, "requested_reviewers": [ "reviewer1", @@ -85,4 +85,4 @@ interactions: - application/json status: 200 OK code: 200 - duration: 23.390833ms + duration: 28.513791ms diff --git a/internal/gateway/bitbucket/api.go b/internal/gateway/bitbucket/api.go index 40d833002..63e424241 100644 --- a/internal/gateway/bitbucket/api.go +++ b/internal/gateway/bitbucket/api.go @@ -85,6 +85,7 @@ type PullRequestListOptions struct { type Comment struct { ID int64 `json:"id"` Content Content `json:"content"` + User User `json:"user"` Inline *Inline `json:"inline,omitempty"` Resolution *Resolution `json:"resolution,omitempty"` } @@ -92,6 +93,25 @@ type Comment struct { // CommentCreateRequest is the request body for creating or updating a comment. type CommentCreateRequest struct { Content Content `json:"content"` + + // Inline, when set, posts a code-review (inline) comment + // on the given file path and line. + Inline *Inline `json:"inline,omitempty"` + + // Parent, when set, posts a reply on the given parent comment. + // Mutually exclusive with Inline. + Parent *CommentRef `json:"parent,omitempty"` +} + +// CommentRef identifies a comment by ID. +type CommentRef struct { + ID int64 `json:"id"` +} + +// CommentResolveRequest is the request body for resolving +// or unresolving an inline comment. +type CommentResolveRequest struct { + Resolution *Resolution `json:"resolution"` } // CommentList is the paginated response for listing comments. @@ -490,6 +510,32 @@ func (c *Client) CommitStatusCreate( return &response, resp, nil } +// CommentResolve resolves or unresolves a pull-request inline comment. +// +// Pass req.Resolution = nil to unresolve, non-nil to resolve. +func (c *Client) CommentResolve( + ctx context.Context, + workspace string, + repo string, + prID int64, + commentID int64, + req *CommentResolveRequest, +) (*Comment, *Response, error) { + var response Comment + resp, err := c.put( + ctx, + fmt.Sprintf( + "/repositories/%s/%s/pullrequests/%d/comments/%d", + workspace, repo, prID, commentID, + ), + nil, req, &response, + ) + if err != nil { + return nil, resp, err + } + return &response, resp, nil +} + func buildPullRequestListRequest( workspace string, repo string, diff --git a/internal/gateway/gitlab/api.go b/internal/gateway/gitlab/api.go index 6609e1464..eea60aa77 100644 --- a/internal/gateway/gitlab/api.go +++ b/internal/gateway/gitlab/api.go @@ -354,6 +354,116 @@ func (c *Client) MergeRequestDiscussionList( return response, resp, nil } +// MergeRequestDiscussionCreate creates a new discussion (review thread) +// on a merge request. +// +// GitLab API: +// https://docs.gitlab.com/api/discussions/#create-new-merge-request-thread +func (c *Client) MergeRequestDiscussionCreate( + ctx context.Context, + projectID int64, + mergeRequest int64, + opt *CreateMergeRequestDiscussionOptions, +) (*Discussion, *Response, error) { + var response Discussion + resp, err := c.post( + ctx, + fmt.Sprintf( + "projects/%d/merge_requests/%d/discussions", + projectID, mergeRequest, + ), + nil, opt, &response, + ) + if err != nil { + return nil, resp, err + } + return &response, resp, nil +} + +// MergeRequestDiscussionResolve resolves or unresolves a discussion thread. +// +// GitLab API: +// https://docs.gitlab.com/api/discussions/#resolve-a-merge-request-thread +func (c *Client) MergeRequestDiscussionResolve( + ctx context.Context, + projectID int64, + mergeRequest int64, + discussionID string, + opt *ResolveMergeRequestDiscussionOptions, +) (*Discussion, *Response, error) { + var response Discussion + resp, err := c.put( + ctx, + fmt.Sprintf( + "projects/%d/merge_requests/%d/discussions/%s", + projectID, mergeRequest, + url.PathEscape(discussionID), + ), + nil, opt, &response, + ) + if err != nil { + return nil, resp, err + } + return &response, resp, nil +} + +// MergeRequestDiscussionNoteCreate adds a note to an existing discussion +// thread (i.e. reply). +// +// GitLab API: +// https://docs.gitlab.com/api/discussions/#add-note-to-existing-merge-request-thread +func (c *Client) MergeRequestDiscussionNoteCreate( + ctx context.Context, + projectID int64, + mergeRequest int64, + discussionID string, + opt *AddMergeRequestDiscussionNoteOptions, +) (*DiscussionNote, *Response, error) { + var response DiscussionNote + resp, err := c.post( + ctx, + fmt.Sprintf( + "projects/%d/merge_requests/%d/discussions/%s/notes", + projectID, mergeRequest, + url.PathEscape(discussionID), + ), + nil, opt, &response, + ) + if err != nil { + return nil, resp, err + } + return &response, resp, nil +} + +// MergeRequestDiscussionNoteUpdate edits the body of an existing +// discussion note. +// +// GitLab API: +// https://docs.gitlab.com/api/discussions/#modify-an-existing-merge-request-thread-note +func (c *Client) MergeRequestDiscussionNoteUpdate( + ctx context.Context, + projectID int64, + mergeRequest int64, + discussionID string, + noteID int64, + opt *UpdateMergeRequestDiscussionNoteOptions, +) (*DiscussionNote, *Response, error) { + var response DiscussionNote + resp, err := c.put( + ctx, + fmt.Sprintf( + "projects/%d/merge_requests/%d/discussions/%s/notes/%d", + projectID, mergeRequest, + url.PathEscape(discussionID), noteID, + ), + nil, opt, &response, + ) + if err != nil { + return nil, resp, err + } + return &response, resp, nil +} + // ProjectTemplateList lists project templates for a template type. // // GitLab API: @@ -535,7 +645,16 @@ type BasicMergeRequest struct { // https://docs.gitlab.com/api/merge_requests/ type MergeRequest struct { BasicMergeRequest - HeadPipeline *Pipeline `json:"head_pipeline,omitempty"` + HeadPipeline *Pipeline `json:"head_pipeline,omitempty"` + DiffRefs MergeRequestDiffRefs `json:"diff_refs"` +} + +// MergeRequestDiffRefs holds the SHAs needed +// to position inline comments on a merge request diff. +type MergeRequestDiffRefs struct { + BaseSha string `json:"base_sha"` + HeadSha string `json:"head_sha"` + StartSha string `json:"start_sha"` } // Pipeline is a GitLab CI pipeline status summary. @@ -643,8 +762,25 @@ type Discussion struct { // GitLab discussions API: // https://docs.gitlab.com/api/discussions/ type DiscussionNote struct { - Resolvable bool `json:"resolvable"` - Resolved bool `json:"resolved"` + ID int64 `json:"id"` + Body string `json:"body"` + Author DiscussionNoteUser `json:"author"` + Position *DiscussionPosition `json:"position,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + Resolvable bool `json:"resolvable"` + Resolved bool `json:"resolved"` +} + +// DiscussionNoteUser matches the author shape on a discussion note. +type DiscussionNoteUser struct { + Username string `json:"username"` +} + +// DiscussionPosition is the inline position of a code-review note. +type DiscussionPosition struct { + NewPath string `json:"new_path"` + OldPath string `json:"old_path"` + NewLine int64 `json:"new_line"` } // ProjectTemplate matches the subset of project template fields @@ -846,6 +982,42 @@ func (o *ListMergeRequestDiscussionsOptions) encodeQuery() url.Values { // ListProjectTemplatesOptions configures template-list requests. type ListProjectTemplatesOptions struct{} +// CreateMergeRequestDiscussionOptions configures the creation +// of a new merge request discussion thread. +type CreateMergeRequestDiscussionOptions struct { + Body *string `json:"body,omitempty"` + Position *PositionOptions `json:"position,omitempty"` +} + +// PositionOptions describes the inline position for +// a discussion-creation request. +type PositionOptions struct { + BaseSHA *string `json:"base_sha,omitempty"` + HeadSHA *string `json:"head_sha,omitempty"` + StartSHA *string `json:"start_sha,omitempty"` + PositionType *string `json:"position_type,omitempty"` + NewPath *string `json:"new_path,omitempty"` + OldPath *string `json:"old_path,omitempty"` + NewLine *int64 `json:"new_line,omitempty"` +} + +// ResolveMergeRequestDiscussionOptions configures resolve/unresolve. +type ResolveMergeRequestDiscussionOptions struct { + Resolved *bool `json:"resolved,omitempty"` +} + +// AddMergeRequestDiscussionNoteOptions configures replies on +// an existing discussion thread. +type AddMergeRequestDiscussionNoteOptions struct { + Body *string `json:"body,omitempty"` +} + +// UpdateMergeRequestDiscussionNoteOptions configures edits to +// an existing discussion note. +type UpdateMergeRequestDiscussionNoteOptions struct { + Body *string `json:"body,omitempty"` +} + func gitlabProjectResource(project any) (string, error) { switch v := project.(type) { case int: diff --git a/internal/git/diff_wt.go b/internal/git/diff_wt.go index a3a805de9..f1686410b 100644 --- a/internal/git/diff_wt.go +++ b/internal/git/diff_wt.go @@ -19,3 +19,18 @@ func (w *Worktree) DiffBranch(ctx context.Context, base, head string) error { } return nil } + +// DiffBranchBytes returns the unified diff output +// between base and head using triple-dot syntax. +func (w *Worktree) DiffBranchBytes( + ctx context.Context, + base, head string, +) ([]byte, error) { + out, err := w.gitCmd( + ctx, "diff", base+"..."+head, + ).Output() + if err != nil { + return nil, fmt.Errorf("diff: %w", err) + } + return out, nil +} diff --git a/internal/handler/merge/handler.go b/internal/handler/merge/handler.go index 8172b2334..0f921e882 100644 --- a/internal/handler/merge/handler.go +++ b/internal/handler/merge/handler.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "go.abhg.dev/gs/internal/handler/restack" "go.abhg.dev/gs/internal/handler/submit" "go.abhg.dev/gs/internal/handler/sync" @@ -36,7 +37,7 @@ type Service interface { // RestackHandler restacks branches after their bases are merged. type RestackHandler interface { - RestackBranch(context.Context, string) error + RestackBranch(ctx context.Context, branch string, opts *restack.Options) error } // SubmitHandler updates change requests after branch restacks. @@ -599,7 +600,7 @@ func (e *mergePlanExecutor) prepareForMerge( return fmt.Errorf("verify restacked: %w", err) } - if err := e.Restack.RestackBranch(ctx, item.branch); err != nil { + if err := e.Restack.RestackBranch(ctx, item.branch, nil); err != nil { return fmt.Errorf("restack branch: %w", err) } diff --git a/internal/handler/merge/handler_test.go b/internal/handler/merge/handler_test.go index 317d2a05b..f1d1f165f 100644 --- a/internal/handler/merge/handler_test.go +++ b/internal/handler/merge/handler_test.go @@ -317,10 +317,10 @@ func TestExecutePlan_retargets(t *testing.T) { mockRestack := NewMockRestackHandler(ctrl) mockRestack.EXPECT(). - RestackBranch(gomock.Any(), "feat2"). + RestackBranch(gomock.Any(), "feat2", gomock.Nil()). Return(nil) mockRestack.EXPECT(). - RestackBranch(gomock.Any(), "feat3"). + RestackBranch(gomock.Any(), "feat3", gomock.Nil()). Return(nil) mockSubmit := NewMockSubmitHandler(ctrl) @@ -396,7 +396,7 @@ func TestExecutePlan_waitsForPreparedChangeHeadBeforeChecks(t *testing.T) { mockRestack := NewMockRestackHandler(ctrl) mockRestack.EXPECT(). - RestackBranch(gomock.Any(), "feat2"). + RestackBranch(gomock.Any(), "feat2", gomock.Nil()). Return(nil) mockSubmit := NewMockSubmitHandler(ctrl) diff --git a/internal/handler/merge/mocks_test.go b/internal/handler/merge/mocks_test.go index 9d4523a57..c59410e48 100644 --- a/internal/handler/merge/mocks_test.go +++ b/internal/handler/merge/mocks_test.go @@ -13,6 +13,7 @@ import ( reflect "reflect" git "go.abhg.dev/gs/internal/git" + restack "go.abhg.dev/gs/internal/handler/restack" submit "go.abhg.dev/gs/internal/handler/submit" sync "go.abhg.dev/gs/internal/handler/sync" spice "go.abhg.dev/gs/internal/spice" @@ -207,17 +208,17 @@ func (m *MockRestackHandler) EXPECT() *MockRestackHandlerMockRecorder { } // RestackBranch mocks base method. -func (m *MockRestackHandler) RestackBranch(arg0 context.Context, arg1 string) error { +func (m *MockRestackHandler) RestackBranch(ctx context.Context, branch string, opts *restack.Options) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RestackBranch", arg0, arg1) + ret := m.ctrl.Call(m, "RestackBranch", ctx, branch, opts) ret0, _ := ret[0].(error) return ret0 } // RestackBranch indicates an expected call of RestackBranch. -func (mr *MockRestackHandlerMockRecorder) RestackBranch(arg0, arg1 any) *MockRestackHandlerRestackBranchCall { +func (mr *MockRestackHandlerMockRecorder) RestackBranch(ctx, branch, opts any) *MockRestackHandlerRestackBranchCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestackBranch", reflect.TypeOf((*MockRestackHandler)(nil).RestackBranch), arg0, arg1) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestackBranch", reflect.TypeOf((*MockRestackHandler)(nil).RestackBranch), ctx, branch, opts) return &MockRestackHandlerRestackBranchCall{Call: call} } @@ -233,13 +234,13 @@ func (c *MockRestackHandlerRestackBranchCall) Return(arg0 error) *MockRestackHan } // Do rewrite *gomock.Call.Do -func (c *MockRestackHandlerRestackBranchCall) Do(f func(context.Context, string) error) *MockRestackHandlerRestackBranchCall { +func (c *MockRestackHandlerRestackBranchCall) Do(f func(context.Context, string, *restack.Options) error) *MockRestackHandlerRestackBranchCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockRestackHandlerRestackBranchCall) DoAndReturn(f func(context.Context, string) error) *MockRestackHandlerRestackBranchCall { +func (c *MockRestackHandlerRestackBranchCall) DoAndReturn(f func(context.Context, string, *restack.Options) error) *MockRestackHandlerRestackBranchCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/internal/handler/restack/branch.go b/internal/handler/restack/branch.go index 8d880183f..889d9a272 100644 --- a/internal/handler/restack/branch.go +++ b/internal/handler/restack/branch.go @@ -3,10 +3,13 @@ package restack import "context" // RestackBranch restacks the given branch onto its base. -func (h *Handler) RestackBranch(ctx context.Context, branch string) error { +func (h *Handler) RestackBranch( + ctx context.Context, branch string, opts *Options, +) error { _, err := h.Restack(ctx, &Request{ Branch: branch, ContinueCommand: []string{"branch", "restack"}, + AutoResolve: opts.autoResolvePtr(), }) return err } diff --git a/internal/handler/restack/branch_test.go b/internal/handler/restack/branch_test.go index 95664e688..c7435831c 100644 --- a/internal/handler/restack/branch_test.go +++ b/internal/handler/restack/branch_test.go @@ -47,7 +47,7 @@ func TestHandler_RestackBranch(t *testing.T) { Store: statetest.NewMemoryStore(t, "main", "", log), Service: mockService, } - require.NoError(t, handler.RestackBranch(t.Context(), "feature")) + require.NoError(t, handler.RestackBranch(t.Context(), "feature", nil)) assert.Contains(t, logBuffer.String(), "feature: restacked on main") }) @@ -82,7 +82,7 @@ func TestHandler_RestackBranch(t *testing.T) { Service: mockService, } - require.NoError(t, handler.RestackBranch(t.Context(), "feature")) + require.NoError(t, handler.RestackBranch(t.Context(), "feature", nil)) }) t.Run("UntrackedBranch", func(t *testing.T) { @@ -113,7 +113,7 @@ func TestHandler_RestackBranch(t *testing.T) { Service: mockService, } - err := handler.RestackBranch(t.Context(), "untracked") + err := handler.RestackBranch(t.Context(), "untracked", nil) require.Error(t, err) assert.ErrorContains(t, err, "untracked branch") assert.Contains(t, logBuffer.String(), "untracked: branch not tracked: run '"+cli.Name()+" branch track") @@ -146,7 +146,7 @@ func TestHandler_RestackBranch(t *testing.T) { Store: statetest.NewMemoryStore(t, "main", "", log), Service: mockService, } - require.NoError(t, handler.RestackBranch(t.Context(), "already-restacked")) + require.NoError(t, handler.RestackBranch(t.Context(), "already-restacked", nil)) assert.Contains(t, logBuffer.String(), "already-restacked: branch does not need to be restacked.") }) @@ -177,7 +177,7 @@ func TestHandler_RestackBranch(t *testing.T) { Store: statetest.NewMemoryStore(t, "main", "", log), Service: mockService, } - err := handler.RestackBranch(t.Context(), "feature") + err := handler.RestackBranch(t.Context(), "feature", nil) require.Error(t, err) assert.ErrorContains(t, err, "restack branch") assert.ErrorIs(t, err, unexpectedErr) diff --git a/internal/handler/restack/downstack.go b/internal/handler/restack/downstack.go index e4df6f7ab..60c0ab42d 100644 --- a/internal/handler/restack/downstack.go +++ b/internal/handler/restack/downstack.go @@ -4,11 +4,14 @@ import "context" // RestackDownstack restacks the downstack of the given branch. // This includes the branch itself. -func (h *Handler) RestackDownstack(ctx context.Context, branch string) error { +func (h *Handler) RestackDownstack( + ctx context.Context, branch string, opts *Options, +) error { _, err := h.Restack(ctx, &Request{ Branch: branch, Scope: ScopeDownstack, ContinueCommand: []string{"downstack", "restack"}, + AutoResolve: opts.autoResolvePtr(), }) return err } diff --git a/internal/handler/restack/downstack_test.go b/internal/handler/restack/downstack_test.go index fa633e067..a99ac3399 100644 --- a/internal/handler/restack/downstack_test.go +++ b/internal/handler/restack/downstack_test.go @@ -54,7 +54,7 @@ func TestHandler_RestackDownstack(t *testing.T) { Service: mockService, } - require.NoError(t, handler.RestackDownstack(t.Context(), "feature3")) + require.NoError(t, handler.RestackDownstack(t.Context(), "feature3", nil)) assert.Contains(t, logBuffer.String(), "feature1: restacked on main") assert.Contains(t, logBuffer.String(), "feature2: restacked on feature1") assert.Contains(t, logBuffer.String(), "feature3: restacked on feature2") @@ -99,6 +99,6 @@ func TestHandler_RestackDownstack(t *testing.T) { Service: mockService, } - require.NoError(t, handler.RestackDownstack(t.Context(), "feature2")) + require.NoError(t, handler.RestackDownstack(t.Context(), "feature2", nil)) }) } diff --git a/internal/handler/restack/handler.go b/internal/handler/restack/handler.go index 7ff1f6d45..93d0dadc1 100644 --- a/internal/handler/restack/handler.go +++ b/internal/handler/restack/handler.go @@ -93,6 +93,33 @@ type Request struct { // // Defaults to ScopeBranch. Scope Scope + + // AutoResolve, if non-nil, overrides + // [Handler.DefaultAutoResolve] for this invocation. A true + // value enables the configured resolver; a false value disables + // it even when configured. + AutoResolve *bool +} + +// Options are optional parameters shared by the high-level restack +// entry points ([Handler.RestackBranch], [Handler.RestackStack], +// [Handler.RestackDownstack]). +type Options struct { + // AutoResolve, if non-nil, overrides + // [Handler.DefaultAutoResolve] for this invocation. A true + // value enables the configured resolver; a false value disables + // it even when configured. + AutoResolve *bool +} + +// autoResolvePtr returns the AutoResolve pointer if opts is +// non-nil, or nil. Callers pass the result through to +// [Request.AutoResolve]. +func (o *Options) autoResolvePtr() *bool { + if o == nil { + return nil + } + return o.AutoResolve } // Restack restacks one or more branches according to the request. diff --git a/internal/handler/restack/stack.go b/internal/handler/restack/stack.go index f37ad358b..592e99eab 100644 --- a/internal/handler/restack/stack.go +++ b/internal/handler/restack/stack.go @@ -5,11 +5,14 @@ import "context" // RestackStack restacks the stack of the given branch. // This includes all upstack and downtrack branches, // as well as the branch itself. -func (h *Handler) RestackStack(ctx context.Context, branch string) error { +func (h *Handler) RestackStack( + ctx context.Context, branch string, opts *Options, +) error { _, err := h.Restack(ctx, &Request{ Branch: branch, Scope: ScopeStack, ContinueCommand: []string{"stack", "restack"}, + AutoResolve: opts.autoResolvePtr(), }) return err } diff --git a/internal/handler/sync/handler.go b/internal/handler/sync/handler.go index 4aa7da792..af116412d 100644 --- a/internal/handler/sync/handler.go +++ b/internal/handler/sync/handler.go @@ -78,7 +78,7 @@ type DeleteHandler interface { // RestackHandler allows restacking branches after sync // has removed their downstack branches. type RestackHandler interface { - RestackBranch(ctx context.Context, branch string) error + RestackBranch(ctx context.Context, branch string, opts *restack.Options) error RestackUpstack(ctx context.Context, branch string, opts *restack.UpstackOptions) error } @@ -983,7 +983,7 @@ func (h *Handler) restackDeletedBranchUpstack( return fmt.Errorf("restack upstack %q: %w", target, err) } case mode.Includes(spice.RestackAboves): - if err := h.Restack.RestackBranch(ctx, target); err != nil { + if err := h.Restack.RestackBranch(ctx, target, nil); err != nil { return fmt.Errorf("restack branch %q: %w", target, err) } case mode.Includes(spice.RestackNone): diff --git a/internal/handler/sync/handler_test.go b/internal/handler/sync/handler_test.go index b6450f281..415897d24 100644 --- a/internal/handler/sync/handler_test.go +++ b/internal/handler/sync/handler_test.go @@ -368,7 +368,7 @@ func TestHandler_SyncTrunk_restackDeletedUpstacks(t *testing.T) { mockRestack := NewMockRestackHandler(ctrl) mockRestack.EXPECT(). - RestackBranch(gomock.Any(), "child"). + RestackBranch(gomock.Any(), "child", gomock.Nil()). Return(nil) mockAutostash := NewMockAutostashHandler(ctrl) diff --git a/internal/handler/sync/mocks_test.go b/internal/handler/sync/mocks_test.go index 51c7adcf5..de0af2909 100644 --- a/internal/handler/sync/mocks_test.go +++ b/internal/handler/sync/mocks_test.go @@ -782,17 +782,17 @@ func (m *MockRestackHandler) EXPECT() *MockRestackHandlerMockRecorder { } // RestackBranch mocks base method. -func (m *MockRestackHandler) RestackBranch(ctx context.Context, branch string) error { +func (m *MockRestackHandler) RestackBranch(ctx context.Context, branch string, opts *restack.Options) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RestackBranch", ctx, branch) + ret := m.ctrl.Call(m, "RestackBranch", ctx, branch, opts) ret0, _ := ret[0].(error) return ret0 } // RestackBranch indicates an expected call of RestackBranch. -func (mr *MockRestackHandlerMockRecorder) RestackBranch(ctx, branch any) *MockRestackHandlerRestackBranchCall { +func (mr *MockRestackHandlerMockRecorder) RestackBranch(ctx, branch, opts any) *MockRestackHandlerRestackBranchCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestackBranch", reflect.TypeOf((*MockRestackHandler)(nil).RestackBranch), ctx, branch) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestackBranch", reflect.TypeOf((*MockRestackHandler)(nil).RestackBranch), ctx, branch, opts) return &MockRestackHandlerRestackBranchCall{Call: call} } @@ -808,13 +808,13 @@ func (c *MockRestackHandlerRestackBranchCall) Return(arg0 error) *MockRestackHan } // Do rewrite *gomock.Call.Do -func (c *MockRestackHandlerRestackBranchCall) Do(f func(context.Context, string) error) *MockRestackHandlerRestackBranchCall { +func (c *MockRestackHandlerRestackBranchCall) Do(f func(context.Context, string, *restack.Options) error) *MockRestackHandlerRestackBranchCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockRestackHandlerRestackBranchCall) DoAndReturn(f func(context.Context, string) error) *MockRestackHandlerRestackBranchCall { +func (c *MockRestackHandlerRestackBranchCall) DoAndReturn(f func(context.Context, string, *restack.Options) error) *MockRestackHandlerRestackBranchCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/internal/spice/state/staged_comment.go b/internal/spice/state/staged_comment.go new file mode 100644 index 000000000..11079e783 --- /dev/null +++ b/internal/spice/state/staged_comment.go @@ -0,0 +1,121 @@ +package state + +import ( + "context" + "errors" + "fmt" + "path" + + "go.abhg.dev/gs/internal/spice/state/storage" +) + +// _stagedCommentsDir is the directory holding staged comments +// for branches that have not yet been submitted as reviews. +const _stagedCommentsDir = "staged-comments" + +// StagedComment is a draft inline comment +// waiting to be batch-submitted as part of a review. +type StagedComment struct { + // ID is a local auto-increment identifier + // unique within the branch's staged comments. + ID int `json:"id"` + + // File is the file path relative to the repository root. + File string `json:"file"` + + // Line is the line number in the new version of the file. + Line int `json:"line"` + + // Body is the markdown body of the comment. + Body string `json:"body"` + + // ThreadID is set when replying to an existing thread. + // The format is forge-specific. + ThreadID string `json:"threadID,omitempty"` +} + +// StagedComments is the collection of staged comments +// for a branch. +type StagedComments struct { + // NextID is the next ID to assign + // to a new staged comment. + NextID int `json:"nextID"` + + // Comments are the staged comments. + Comments []StagedComment `json:"comments"` +} + +func (s *Store) stagedCommentsJSON(branch string) string { + return path.Join(_stagedCommentsDir, branch) +} + +// SaveStagedComments saves the staged comments +// for the given branch. +// If staged comments already exist for the branch, +// they will be overwritten. +func (s *Store) SaveStagedComments( + ctx context.Context, + branch string, + comments *StagedComments, +) error { + err := s.db.Set( + ctx, + s.stagedCommentsJSON(branch), + comments, + fmt.Sprintf( + "%v: save staged comments", branch, + ), + ) + if err != nil { + return fmt.Errorf( + "set staged comments: %w", err, + ) + } + return nil +} + +// LoadStagedComments retrieves staged comments +// for the given branch. +// Returns nil if no staged comments exist. +func (s *Store) LoadStagedComments( + ctx context.Context, + branch string, +) (*StagedComments, error) { + var comments StagedComments + err := s.db.Get( + ctx, + s.stagedCommentsJSON(branch), + &comments, + ) + if err != nil { + if errors.Is(err, storage.ErrNotExist) { + return nil, nil + } + return nil, fmt.Errorf( + "get staged comments: %w", err, + ) + } + return &comments, nil +} + +// ClearStagedComments removes staged comments +// for the given branch. +// This is a no-op if no staged comments exist. +func (s *Store) ClearStagedComments( + ctx context.Context, + branch string, +) error { + err := s.db.Delete( + ctx, + s.stagedCommentsJSON(branch), + fmt.Sprintf( + "%v: clear staged comments", branch, + ), + ) + if err != nil { + return fmt.Errorf( + "delete staged comments: %w", err, + ) + } + return nil +} diff --git a/internal/spice/state/staged_comment_test.go b/internal/spice/state/staged_comment_test.go new file mode 100644 index 000000000..0f8da8bc6 --- /dev/null +++ b/internal/spice/state/staged_comment_test.go @@ -0,0 +1,128 @@ +package state_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.abhg.dev/gs/internal/silog/silogtest" + "go.abhg.dev/gs/internal/spice/state" + "go.abhg.dev/gs/internal/spice/state/storage" +) + +func TestStagedComments(t *testing.T) { + ctx := t.Context() + db := storage.NewDB(make(storage.MapBackend)) + + _, err := state.InitStore(ctx, state.InitStoreRequest{ + DB: db, + Trunk: "main", + }) + require.NoError(t, err) + + store, err := state.OpenStore(ctx, db, silogtest.New(t)) + require.NoError(t, err) + + t.Run("LoadEmpty", func(t *testing.T) { + got, err := store.LoadStagedComments(ctx, "feat") + require.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("SaveAndLoad", func(t *testing.T) { + comments := &state.StagedComments{ + NextID: 3, + Comments: []state.StagedComment{ + { + ID: 1, + File: "main.go", + Line: 42, + Body: "Consider using a const here.", + }, + { + ID: 2, + File: "handler.go", + Line: 15, + Body: "I agree with your suggestion.", + ThreadID: "thread-abc", + }, + }, + } + + err := store.SaveStagedComments(ctx, "feat", comments) + require.NoError(t, err) + + got, err := store.LoadStagedComments(ctx, "feat") + require.NoError(t, err) + require.NotNil(t, got) + + assert.Equal(t, 3, got.NextID) + assert.Len(t, got.Comments, 2) + assert.Equal(t, "main.go", got.Comments[0].File) + assert.Equal(t, 42, got.Comments[0].Line) + assert.Equal(t, "thread-abc", got.Comments[1].ThreadID) + }) + + t.Run("Overwrite", func(t *testing.T) { + comments := &state.StagedComments{ + NextID: 2, + Comments: []state.StagedComment{ + {ID: 1, File: "new.go", Line: 1, Body: "New comment"}, + }, + } + + err := store.SaveStagedComments(ctx, "feat", comments) + require.NoError(t, err) + + got, err := store.LoadStagedComments(ctx, "feat") + require.NoError(t, err) + require.NotNil(t, got) + + assert.Len(t, got.Comments, 1) + assert.Equal(t, "new.go", got.Comments[0].File) + }) + + t.Run("Clear", func(t *testing.T) { + err := store.ClearStagedComments(ctx, "feat") + require.NoError(t, err) + + got, err := store.LoadStagedComments(ctx, "feat") + require.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("ClearNonExistent", func(t *testing.T) { + // Clearing a branch with no staged comments + // should not error. + err := store.ClearStagedComments(ctx, "nonexistent") + require.NoError(t, err) + }) + + t.Run("MultipleBranches", func(t *testing.T) { + commentsA := &state.StagedComments{ + NextID: 2, + Comments: []state.StagedComment{ + {ID: 1, File: "a.go", Line: 1, Body: "A"}, + }, + } + commentsB := &state.StagedComments{ + NextID: 2, + Comments: []state.StagedComment{ + {ID: 1, File: "b.go", Line: 2, Body: "B"}, + }, + } + + require.NoError(t, + store.SaveStagedComments(ctx, "branch-a", commentsA)) + require.NoError(t, + store.SaveStagedComments(ctx, "branch-b", commentsB)) + + gotA, err := store.LoadStagedComments(ctx, "branch-a") + require.NoError(t, err) + assert.Equal(t, "a.go", gotA.Comments[0].File) + + gotB, err := store.LoadStagedComments(ctx, "branch-b") + require.NoError(t, err) + assert.Equal(t, "b.go", gotB.Comments[0].File) + }) +} diff --git a/stack_restack.go b/stack_restack.go index 88ef562be..7d7b4d689 100644 --- a/stack_restack.go +++ b/stack_restack.go @@ -88,5 +88,5 @@ func (cmd *stackRestackCmd) Run( return err } - return handler.RestackStack(ctx, cmd.Branch) + return handler.RestackStack(ctx, cmd.Branch, nil) } diff --git a/testdata/help/branch_comment_add.txt b/testdata/help/branch_comment_add.txt new file mode 100644 index 000000000..a3a596440 --- /dev/null +++ b/testdata/help/branch_comment_add.txt @@ -0,0 +1,27 @@ +Usage: gs branch (b) comment (cmt) add [] [flags] + +Post an inline comment immediately + +Posts an inline comment immediately on the change request for the current +branch. Provide the file and line number as file.go:42. + +If no message is given with -m, an editor is opened. + +Use --respond to reply to an existing thread instead of starting a new one. + +Arguments: + [] File and line in the form file.go:42. + +Flags: + -m, --message=MSG Comment body. Opens editor if not provided. + --respond=THREAD_ID Thread ID to reply to instead of starting a new + thread. + -b, --branch=BRANCH Branch to add comment for. Defaults to current + branch. + +Global Flags: + -h, --help Show help for the command + --version Print version information and quit + -v, --verbose Enable verbose output ($GIT_SPICE_VERBOSE) + -C, --dir=DIR Change to DIR before doing anything + --[no-]prompt Whether to prompt for missing information diff --git a/testdata/help/branch_comment_edit.txt b/testdata/help/branch_comment_edit.txt new file mode 100644 index 000000000..26ef35744 --- /dev/null +++ b/testdata/help/branch_comment_edit.txt @@ -0,0 +1,29 @@ +Usage: gs branch (b) comment (cmt) edit [flags] + +Edit a comment + +Edits the body of a comment. + +For staged comments (sc-N prefix), the comment is updated in the local staging +area. + +For forge comments, the comment is updated on the remote forge. + +If no message is given with -m, an editor is opened with the current comment +body pre-filled. + +Arguments: + Comment ID to edit. Use 'sc-N' for staged comments or a forge comment + ID. + +Flags: + -m, --message=MSG New comment body. Opens editor if not provided. + -b, --branch=BRANCH Branch whose comments to edit. Defaults to current + branch. + +Global Flags: + -h, --help Show help for the command + --version Print version information and quit + -v, --verbose Enable verbose output ($GIT_SPICE_VERBOSE) + -C, --dir=DIR Change to DIR before doing anything + --[no-]prompt Whether to prompt for missing information diff --git a/testdata/help/branch_comment_list.txt b/testdata/help/branch_comment_list.txt new file mode 100644 index 000000000..2ad4dabf5 --- /dev/null +++ b/testdata/help/branch_comment_list.txt @@ -0,0 +1,28 @@ +Usage: gs branch (b) comment (cmt) list (ls) [flags] + +List comments on a change request + +Lists comments on the change request associated with the current branch. +Use --branch to target a different branch. + +Staged comments that have not yet been submitted are shown with an 'sc-N' +prefix. + +Use --staged to show only staged comments. Use --unresolved to show only +unresolved comments. + +With --json, prints output to stdout as a stream of JSON objects. + +Flags: + -b, --branch=BRANCH Branch to list comments for. Defaults to current + branch. + --staged Show only staged comments. + --unresolved Show only unresolved comments. + --json Write to stdout as a stream of JSON objects. + +Global Flags: + -h, --help Show help for the command + --version Print version information and quit + -v, --verbose Enable verbose output ($GIT_SPICE_VERBOSE) + -C, --dir=DIR Change to DIR before doing anything + --[no-]prompt Whether to prompt for missing information diff --git a/testdata/help/branch_comment_resolve.txt b/testdata/help/branch_comment_resolve.txt new file mode 100644 index 000000000..c4ee13472 --- /dev/null +++ b/testdata/help/branch_comment_resolve.txt @@ -0,0 +1,24 @@ +Usage: gs branch (b) comment (cmt) resolve [flags] + +Resolve or unresolve a review thread + +Resolves a review thread on the change request for the current branch. + +Use --unresolve to mark the thread as unresolved. + +The thread ID is shown in 'gs branch comment list'. + +Arguments: + Thread ID to resolve. + +Flags: + --unresolve Unresolve the thread instead of resolving it. + -b, --branch=BRANCH Branch whose change request contains the thread. + Defaults to current branch. + +Global Flags: + -h, --help Show help for the command + --version Print version information and quit + -v, --verbose Enable verbose output ($GIT_SPICE_VERBOSE) + -C, --dir=DIR Change to DIR before doing anything + --[no-]prompt Whether to prompt for missing information diff --git a/testdata/help/branch_comment_stage.txt b/testdata/help/branch_comment_stage.txt new file mode 100644 index 000000000..d8d4e7d2b --- /dev/null +++ b/testdata/help/branch_comment_stage.txt @@ -0,0 +1,29 @@ +Usage: gs branch (b) comment (cmt) stage [] [flags] + +Stage an inline comment for batch submission + +Stages an inline comment for later batch submission. Provide the file and line +number as file.go:42. + +If no message is given with -m, an editor is opened. + +Use --respond to reply to an existing thread instead of starting a new one. + +Staged comments are submitted together with 'gs branch comment submit-staged'. + +Arguments: + [] File and line in the form file.go:42. + +Flags: + -m, --message=MSG Comment body. Opens editor if not provided. + --respond=THREAD_ID Thread ID to reply to instead of starting a new + thread. + -b, --branch=BRANCH Branch to stage comment for. Defaults to current + branch. + +Global Flags: + -h, --help Show help for the command + --version Print version information and quit + -v, --verbose Enable verbose output ($GIT_SPICE_VERBOSE) + -C, --dir=DIR Change to DIR before doing anything + --[no-]prompt Whether to prompt for missing information diff --git a/testdata/help/branch_comment_submit-staged.txt b/testdata/help/branch_comment_submit-staged.txt new file mode 100644 index 000000000..6d6b6c339 --- /dev/null +++ b/testdata/help/branch_comment_submit-staged.txt @@ -0,0 +1,25 @@ +Usage: gs branch (b) comment (cmt) submit-staged (ss) [flags] + +Submit all staged comments as a review + +Submits all staged comments for the current branch as a single review on the +change request. + +Use --approve or --request-changes to set the review event type. Defaults to a +comment-only review. + +Use --body to add an overall review body. + +Flags: + --body=BODY Overall review body. + --approve Mark the review as approved. + --request-changes Mark the review as requesting changes. + -b, --branch=BRANCH Branch to submit staged comments for. Defaults to + current branch. + +Global Flags: + -h, --help Show help for the command + --version Print version information and quit + -v, --verbose Enable verbose output ($GIT_SPICE_VERBOSE) + -C, --dir=DIR Change to DIR before doing anything + --[no-]prompt Whether to prompt for missing information diff --git a/testdata/help/gs.txt b/testdata/help/gs.txt index 43406fde2..68df72df1 100644 --- a/testdata/help/gs.txt +++ b/testdata/help/gs.txt @@ -46,21 +46,31 @@ Stack downstack (ds) restack (r) Restack a branch and its downstack Branch - branch (b) track (tr) Track a branch - branch (b) untrack (untr) Forget a tracked branch - branch (b) checkout (co) Switch to a branch - branch (b) create (c) Create a new branch - branch (b) delete (d,rm) Delete branches - branch (b) fold (fo) Merge a branch into its base - branch (b) split (sp) Split a branch on commits - branch (b) squash (sq) Squash a branch into one commit - branch (b) edit (e) Edit the commits in a branch - branch (b) rename (rn,mv) Rename a branch - branch (b) restack (r) Restack a branch - branch (b) onto (on) Move a branch onto another branch - branch (b) diff (di) Show diff between a branch and its base - branch (b) merge (m) Merge a branch into trunk - branch (b) submit (s) Submit a branch + branch (b) track (tr) Track a branch + branch (b) untrack (untr) Forget a tracked branch + branch (b) checkout (co) Switch to a branch + branch (b) create (c) Create a new branch + branch (b) delete (d,rm) Delete branches + branch (b) fold (fo) Merge a branch into its base + branch (b) split (sp) Split a branch on commits + branch (b) squash (sq) Squash a branch into one commit + branch (b) edit (e) Edit the commits in a branch + branch (b) rename (rn,mv) Rename a branch + branch (b) restack (r) Restack a branch + branch (b) onto (on) Move a branch onto another branch + branch (b) diff (di) Show diff between a branch and its base + branch (b) merge (m) Merge a branch into trunk + branch (b) submit (s) Submit a branch + branch (b) comment (cmt) list (ls) + List comments on a change request + branch (b) comment (cmt) stage + Stage an inline comment for batch submission + branch (b) comment (cmt) add Post an inline comment immediately + branch (b) comment (cmt) submit-staged (ss) + Submit all staged comments as a review + branch (b) comment (cmt) resolve + Resolve or unresolve a review thread + branch (b) comment (cmt) edit Edit a comment Commit commit (c) create (c) Create a new commit diff --git a/testdata/script/branch_comment_add.txt b/testdata/script/branch_comment_add.txt new file mode 100644 index 000000000..e07135fb7 --- /dev/null +++ b/testdata/script/branch_comment_add.txt @@ -0,0 +1,37 @@ +# Post an inline comment immediately with 'branch comment add'. + +as 'Test ' +at '2024-04-05T16:40:32Z' + +# setup +cd repo +git init +git commit --allow-empty -m 'Initial commit' + +# set up a fake GitHub remote +shamhub init +shamhub new origin alice/example.git +shamhub register alice +git push origin main + +env SHAMHUB_USERNAME=alice +gs auth login + +# create a branch with a file and submit it +git add handler.go +gs bc -m 'Add handler' feature1 +gs branch submit --fill +stderr 'Created #' + +# post an inline comment immediately +gs branch comment add handler.go:5 -m 'This needs a context parameter.' +stderr 'Posted comment' + +-- repo/handler.go -- +package main + +import "fmt" + +func handle() { + fmt.Println("handling") +} diff --git a/testdata/script/branch_comment_edit_staged.txt b/testdata/script/branch_comment_edit_staged.txt new file mode 100644 index 000000000..9b9b85928 --- /dev/null +++ b/testdata/script/branch_comment_edit_staged.txt @@ -0,0 +1,43 @@ +# Edit a staged comment before submission. + +as 'Test ' +at '2024-04-05T16:40:32Z' + +# setup +cd repo +git init +git commit --allow-empty -m 'Initial commit' + +# set up a fake GitHub remote +shamhub init +shamhub new origin alice/example.git +shamhub register alice +git push origin main + +env SHAMHUB_USERNAME=alice +gs auth login + +# create a branch with a file and submit it +git add main.go +gs bc -m 'Add main' feature1 +gs branch submit --fill +stderr 'Created #' + +# stage a comment +gs branch comment stage main.go:3 -m 'Original comment.' +stderr 'Staged comment sc-1' + +# edit the staged comment with -m +gs branch comment edit sc-1 -m 'Updated comment.' +stderr 'Updated staged comment sc-1' + +# verify the edit took effect by listing staged +gs branch comment list --staged +stderr 'Updated comment' + +-- repo/main.go -- +package main + +func main() { + println("hello") +} diff --git a/testdata/script/branch_comment_list.txt b/testdata/script/branch_comment_list.txt new file mode 100644 index 000000000..047c6ee81 --- /dev/null +++ b/testdata/script/branch_comment_list.txt @@ -0,0 +1,46 @@ +# List comments on a change request. + +as 'Test ' +at '2024-04-05T16:40:32Z' + +# setup +cd repo +git init +git commit --allow-empty -m 'Initial commit' + +# set up a fake GitHub remote +shamhub init +shamhub new origin alice/example.git +shamhub register alice +git push origin main + +env SHAMHUB_USERNAME=alice +gs auth login + +# create a branch with a file and submit it +git add main.go +gs bc -m 'Add main' feature1 +gs branch submit --fill +stderr 'Created #' + +# add an inline comment so there is something to list +gs branch comment add main.go:3 -m 'Consider using a constant.' +stderr 'Posted comment' + +# list should show the comment +gs branch comment list +stderr 'Comments:' +stderr 'main.go:3' +stderr 'Consider using a constant' + +# list on a branch with no CR should say so +gs bc -m 'Another branch' feature2 +gs branch comment list +stderr 'No change request' + +-- repo/main.go -- +package main + +func main() { + println("hello") +} diff --git a/testdata/script/branch_comment_list_json.txt b/testdata/script/branch_comment_list_json.txt new file mode 100644 index 000000000..aa894d0e6 --- /dev/null +++ b/testdata/script/branch_comment_list_json.txt @@ -0,0 +1,63 @@ +# List comments on a change request with --json output. + +as 'Test ' +at '2024-04-05T16:40:32Z' + +# setup +cd repo +git init +git commit --allow-empty -m 'Initial commit' + +# set up a fake GitHub remote +shamhub init +shamhub new origin alice/example.git +shamhub register alice +git push origin main + +env SHAMHUB_USERNAME=alice +gs auth login + +# create a branch with a file and submit it +git add main.go +gs bc -m 'Add main' feature1 +gs branch submit --fill +stderr 'Created #' + +# add an inline comment so there is something to list +gs branch comment add main.go:3 -m 'Consider using a constant here because it would make the code much more maintainable and readable.' +stderr 'Posted comment' + +# --json should output NDJSON to stdout with forge comment +gs branch comment list --json +stdout '"kind":"forge"' +stdout '"body":"Consider using a constant here because it would make the code much more maintainable and readable."' +stdout '"path":"main.go"' +stdout '"line":3' +stdout '"status":"open"' + +# text mode shows the full body without truncation +gs branch comment list +stderr 'Consider using a constant here because it would make the code much more maintainable and readable.' + +# stage a comment +gs branch comment stage main.go:5 -m 'Add error handling here.' +stderr 'Staged comment' + +# --staged --json should include only staged comments +gs branch comment list --staged --json +cmpenvJSON stdout $WORK/golden/staged_only.json + +# --json without --staged includes both staged and forge +gs branch comment list --json +stdout '"kind":"staged"' +stdout '"kind":"forge"' + +-- repo/main.go -- +package main + +func main() { + println("hello") +} + +-- golden/staged_only.json -- +{"kind":"staged","id":"sc-1","path":"main.go","line":5,"body":"Add error handling here."} diff --git a/testdata/script/branch_comment_resolve.txt b/testdata/script/branch_comment_resolve.txt new file mode 100644 index 000000000..a0e5dbae4 --- /dev/null +++ b/testdata/script/branch_comment_resolve.txt @@ -0,0 +1,39 @@ +# Resolve and unresolve a review thread. + +as 'Test ' +at '2024-04-05T16:40:32Z' + +# setup +cd repo +git init +git commit --allow-empty -m 'Initial commit' + +# set up a fake GitHub remote +shamhub init +shamhub new origin alice/example.git +shamhub register alice +git push origin main + +env SHAMHUB_USERNAME=alice +gs auth login + +# create a branch with a file and submit it +git add util.go +gs bc -m 'Add util' feature1 +gs branch submit --fill +stderr 'Created #' + +# post an inline comment to create a thread +gs branch comment add util.go:3 -m 'Rename this function.' +stderr 'Posted comment' + +# get the thread ID from the list output +gs branch comment list +stderr 'thread-' + +-- repo/util.go -- +package main + +func doStuff() { + // stuff +} diff --git a/testdata/script/branch_comment_stage_submit.txt b/testdata/script/branch_comment_stage_submit.txt new file mode 100644 index 000000000..9b91ac240 --- /dev/null +++ b/testdata/script/branch_comment_stage_submit.txt @@ -0,0 +1,58 @@ +# Stage inline comments and submit them as a review. + +as 'Test ' +at '2024-04-05T16:40:32Z' + +# setup +cd repo +git init +git commit --allow-empty -m 'Initial commit' + +# set up a fake GitHub remote +shamhub init +shamhub new origin alice/example.git +shamhub register alice +git push origin main + +env SHAMHUB_USERNAME=alice +gs auth login + +# create a branch with a file and submit it +git add feature.go +gs bc -m 'Add feature' feature1 +gs branch submit --fill +stderr 'Created #' + +# stage a comment on the file +gs branch comment stage feature.go:3 -m 'Consider renaming this function.' +stderr 'Staged comment sc-1 on feature.go:3' + +# stage another comment +gs branch comment stage feature.go:7 -m 'Add error handling here.' +stderr 'Staged comment sc-2 on feature.go:7' + +# list staged comments +gs branch comment list --staged +stderr 'sc-1' +stderr 'feature.go:3' +stderr 'sc-2' +stderr 'feature.go:7' + +# submit staged comments as a review +gs branch comment submit-staged +stderr 'Submitted 2 comment' + +# staged comments should be cleared after submit +gs branch comment list --staged +stderr 'No staged comments' + +-- repo/feature.go -- +package main + +func doWork() { + // does some work +} + +func handleRequest() { + // handles a request +} diff --git a/upstack_restack.go b/upstack_restack.go index f6e6e0520..4def1ada7 100644 --- a/upstack_restack.go +++ b/upstack_restack.go @@ -34,10 +34,10 @@ func (*upstackRestackCmd) Help() string { // RestackHandler implements high level restack operations. type RestackHandler interface { RestackUpstack(ctx context.Context, branch string, opts *restack.UpstackOptions) error - RestackDownstack(ctx context.Context, branch string) error + RestackDownstack(ctx context.Context, branch string, opts *restack.Options) error Restack(context.Context, *restack.Request) (int, error) - RestackStack(ctx context.Context, branch string) error - RestackBranch(ctx context.Context, branch string) error + RestackStack(ctx context.Context, branch string, opts *restack.Options) error + RestackBranch(ctx context.Context, branch string, opts *restack.Options) error } func (cmd *upstackRestackCmd) AfterApply(ctx context.Context, wt *git.Worktree) error {