review: make the arg-count test real, and confirm before a transferUnverified
Two findings from the review of !3: - `TestRepoMoveArgCount` came over from 4a5d5d2 asserting only that the command errored — with no token configured, `newClient()` failed first, so it passed whether or not the `ExactArgs(2)` guard existed. Verified: relaxing `rename` to `MinimumNArgs(1)` left it green, while in real use that regression panics on `args[1]`. It now runs against the stub server with a token, asserts the error actually says "accepts 2 arg(s)", and asserts nothing reached the server — and it does fail on that relaxed guard. - `repo transfer` fired immediately, while `repo delete` right below it makes you type the repo name. A transfer is the stronger one-way door of the two: after the move the actor may hold no ADMIN in the destination, so transferring back answers `forbidden_owner`. It now confirms the same way, with `--yes` for scripts. `rename` deliberately stays unprompted — it breaks clone URLs, but the actor keeps ADMIN and can rename straight back, so there is no door to close. The prompt is a third divergence from the ported commit, on top of `clean()` and the bad-spec test; noted in the MR alongside the other two. Verified live: a mistyped confirmation aborts and the repo stays put, the right answer goes through, `--yes` skips the prompt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPPhRdTiUiiSvLjthotkT1
94ea6bb parent: aada7be modified
README.md +3 -1 | @@ -147,7 +147,9 @@ rickub repo create <name> [--org H] [--public|--private] [-d desc] | ||
| 147 | 147 | rickub repo view <owner/repo> |
| 148 | 148 | rickub repo edit <owner/repo> [--visibility public|private] [-d desc] [--default-branch B] |
| 149 | 149 | rickub repo rename <owner/repo> <new-name> # needs ADMIN; breaks every existing clone URL |
| 150 | -rickub repo transfer <owner/repo> <new-owner> # needs ADMIN here and create rights there | |
| 150 | +rickub repo transfer <owner/repo> <new-owner> [--yes] | |
| 151 | + # needs ADMIN here and create rights there; | |
| 152 | + # confirms first — you may not be able to move it back | |
| 151 | 153 | rickub repo delete <owner/repo> [--yes] |
| 152 | 154 | rickub repo clone <owner/repo> [dir] [--protocol ssh|https] [-- git-args…] |
| 153 | 155 | rickub repo files <owner/repo> [path] [--ref R] |
| @@ -147,7 +147,9 @@ rickub repo create <name> [--org H] [--public|--private] [-d desc] | |||
| 147 | rickub repo view <owner/repo> | 147 | rickub repo view <owner/repo> |
| 148 | rickub repo edit <owner/repo> [--visibility public|private] [-d desc] [--default-branch B] | 148 | rickub repo edit <owner/repo> [--visibility public|private] [-d desc] [--default-branch B] |
| 149 | rickub repo rename <owner/repo> <new-name> # needs ADMIN; breaks every existing clone URL | 149 | rickub repo rename <owner/repo> <new-name> # needs ADMIN; breaks every existing clone URL |
| 150 | -rickub repo transfer <owner/repo> <new-owner> # needs ADMIN here and create rights there | 150 | +rickub repo transfer <owner/repo> <new-owner> [--yes] |
| 151 | + # needs ADMIN here and create rights there; | ||
| 152 | + # confirms first — you may not be able to move it back | ||
| 151 | rickub repo delete <owner/repo> [--yes] | 153 | rickub repo delete <owner/repo> [--yes] |
| 152 | rickub repo clone <owner/repo> [dir] [--protocol ssh|https] [-- git-args…] | 154 | rickub repo clone <owner/repo> [dir] [--protocol ssh|https] [-- git-args…] |
| 153 | rickub repo files <owner/repo> [path] [--ref R] | 155 | rickub repo files <owner/repo> [path] [--ref R] |
modified
cmd/repo.go +18 -1 | @@ -27,6 +27,7 @@ var ( | ||
| 27 | 27 | repoEditDesc string |
| 28 | 28 | repoEditBranch string |
| 29 | 29 | repoDeleteYes bool |
| 30 | + repoTransferYes bool | |
| 30 | 31 | repoContentsRef string |
| 31 | 32 | pageFlag int |
| 32 | 33 | perPageFlag int |
| @@ -106,10 +107,15 @@ name instead).`, | ||
| 106 | 107 | |
| 107 | 108 | Requires ADMIN on the repository, a verified email address, and the right |
| 108 | 109 | to create repositories under the destination. A private repository counts |
| 109 | -against the destination's plan's private-repo limit.`, | |
| 110 | +against the destination's plan's private-repo limit. | |
| 111 | + | |
| 112 | +You are asked to confirm first: a transfer is not reversible by the person | |
| 113 | +who made it, since you may hold no ADMIN in the destination namespace | |
| 114 | +afterwards. Pass --yes to skip the prompt in a script.`, | |
| 110 | 115 | Args: cobra.ExactArgs(2), |
| 111 | 116 | RunE: runRepoTransfer, |
| 112 | 117 | } |
| 118 | + transferCmd.Flags().BoolVar(&repoTransferYes, "yes", false, "skip the confirmation prompt") | |
| 113 | 119 | |
| 114 | 120 | // delete |
| 115 | 121 | deleteCmd := &cobra.Command{ |
| @@ -343,6 +349,17 @@ func runRepoTransfer(cmd *cobra.Command, args []string) error { | ||
| 343 | 349 | if err != nil { |
| 344 | 350 | return err |
| 345 | 351 | } |
| 352 | + // A transfer is not reversible by the person who made it: once the repo is in | |
| 353 | + // the destination namespace the actor may hold no ADMIN there, and transferring | |
| 354 | + // it back answers forbidden_owner. That is a stronger one-way door than | |
| 355 | + // `repo delete`, which does prompt — so this one prompts too, with the same | |
| 356 | + // --yes escape for scripts. | |
| 357 | + if !repoTransferYes { | |
| 358 | + ans := prompt(cmd, fmt.Sprintf("Transfer %s/%s to %s? You may not be able to move it back. Type the repo name to confirm: ", owner, repo, args[1])) | |
| 359 | + if ans != repo { | |
| 360 | + return fmt.Errorf("confirmation did not match; aborted") | |
| 361 | + } | |
| 362 | + } | |
| 346 | 363 | r, err := client.TransferRepo(cmd.Context(), owner, repo, args[1]) |
| 347 | 364 | if err != nil { |
| 348 | 365 | return err |
| @@ -27,6 +27,7 @@ var ( | |||
| 27 | repoEditDesc string | 27 | repoEditDesc string |
| 28 | repoEditBranch string | 28 | repoEditBranch string |
| 29 | repoDeleteYes bool | 29 | repoDeleteYes bool |
| 30 | + repoTransferYes bool | ||
| 30 | repoContentsRef string | 31 | repoContentsRef string |
| 31 | pageFlag int | 32 | pageFlag int |
| 32 | perPageFlag int | 33 | perPageFlag int |
| @@ -106,10 +107,15 @@ name instead).`, | |||
| 106 | 107 | ||
| 107 | Requires ADMIN on the repository, a verified email address, and the right | 108 | Requires ADMIN on the repository, a verified email address, and the right |
| 108 | to create repositories under the destination. A private repository counts | 109 | to create repositories under the destination. A private repository counts |
| 109 | -against the destination's plan's private-repo limit.`, | 110 | +against the destination's plan's private-repo limit. |
| 111 | + | ||
| 112 | +You are asked to confirm first: a transfer is not reversible by the person | ||
| 113 | +who made it, since you may hold no ADMIN in the destination namespace | ||
| 114 | +afterwards. Pass --yes to skip the prompt in a script.`, | ||
| 110 | Args: cobra.ExactArgs(2), | 115 | Args: cobra.ExactArgs(2), |
| 111 | RunE: runRepoTransfer, | 116 | RunE: runRepoTransfer, |
| 112 | } | 117 | } |
| 118 | + transferCmd.Flags().BoolVar(&repoTransferYes, "yes", false, "skip the confirmation prompt") | ||
| 113 | 119 | ||
| 114 | // delete | 120 | // delete |
| 115 | deleteCmd := &cobra.Command{ | 121 | deleteCmd := &cobra.Command{ |
| @@ -343,6 +349,17 @@ func runRepoTransfer(cmd *cobra.Command, args []string) error { | |||
| 343 | if err != nil { | 349 | if err != nil { |
| 344 | return err | 350 | return err |
| 345 | } | 351 | } |
| 352 | + // A transfer is not reversible by the person who made it: once the repo is in | ||
| 353 | + // the destination namespace the actor may hold no ADMIN there, and transferring | ||
| 354 | + // it back answers forbidden_owner. That is a stronger one-way door than | ||
| 355 | + // `repo delete`, which does prompt — so this one prompts too, with the same | ||
| 356 | + // --yes escape for scripts. | ||
| 357 | + if !repoTransferYes { | ||
| 358 | + ans := prompt(cmd, fmt.Sprintf("Transfer %s/%s to %s? You may not be able to move it back. Type the repo name to confirm: ", owner, repo, args[1])) | ||
| 359 | + if ans != repo { | ||
| 360 | + return fmt.Errorf("confirmation did not match; aborted") | ||
| 361 | + } | ||
| 362 | + } | ||
| 346 | r, err := client.TransferRepo(cmd.Context(), owner, repo, args[1]) | 363 | r, err := client.TransferRepo(cmd.Context(), owner, repo, args[1]) |
| 347 | if err != nil { | 364 | if err != nil { |
| 348 | return err | 365 | return err |
modified
cmd/repo_test.go +96 -6 | @@ -22,6 +22,7 @@ func execute(t *testing.T, args ...string) (string, string, error) { | ||
| 22 | 22 | flagHost = "" |
| 23 | 23 | flagToken = "" |
| 24 | 24 | repoListUser = "" |
| 25 | + repoTransferYes = false | |
| 25 | 26 | repoListOrg = "" |
| 26 | 27 | pageFlag = 0 |
| 27 | 28 | perPageFlag = 0 |
| @@ -223,7 +224,9 @@ func TestRepoTransferCallsMoveEndpoint(t *testing.T) { | ||
| 223 | 224 | defer srv.Close() |
| 224 | 225 | t.Setenv("RICKUB_HOST", srv.URL) |
| 225 | 226 | |
| 226 | - out, _, err := execute(t, "repo", "transfer", "rick/portal", "acme") | |
| 227 | + // --yes: this test is about the wire call, not the confirmation, which | |
| 228 | + // TestRepoTransferPromptsBeforeMoving covers. | |
| 229 | + out, _, err := execute(t, "repo", "transfer", "rick/portal", "acme", "--yes") | |
| 227 | 230 | if err != nil { |
| 228 | 231 | t.Fatalf("execute: %v", err) |
| 229 | 232 | } |
| @@ -240,12 +243,99 @@ func TestRepoTransferCallsMoveEndpoint(t *testing.T) { | ||
| 240 | 243 | |
| 241 | 244 | func TestRepoMoveArgCount(t *testing.T) { |
| 242 | 245 | t.Setenv("XDG_CONFIG_HOME", t.TempDir()) |
| 243 | - // Both commands are argument-strict: no destination, no request. | |
| 244 | - if _, _, err := execute(t, "repo", "rename", "rick/portal"); err == nil { | |
| 245 | - t.Error("rename without a new name should fail arg parsing") | |
| 246 | + t.Setenv("RICKUB_TOKEN", "rickub_pat_test") | |
| 247 | + seen := &captured{} | |
| 248 | + srv := repoMoveServer(t, seen) | |
| 249 | + defer srv.Close() | |
| 250 | + t.Setenv("RICKUB_HOST", srv.URL) | |
| 251 | + | |
| 252 | + // Both commands are argument-strict: no destination, no request. The token and | |
| 253 | + // host are configured on purpose — without them `newClient()` would fail first | |
| 254 | + // and this would pass even with the ExactArgs guard removed, which is what the | |
| 255 | + // version ported from 4a5d5d2 did. | |
| 256 | + for _, args := range [][]string{ | |
| 257 | + {"repo", "rename", "rick/portal"}, | |
| 258 | + {"repo", "transfer", "rick/portal"}, | |
| 259 | + } { | |
| 260 | + _, _, err := execute(t, args...) | |
| 261 | + if err == nil { | |
| 262 | + t.Errorf("%v: a missing destination should fail arg parsing", args) | |
| 263 | + continue | |
| 264 | + } | |
| 265 | + if !strings.Contains(err.Error(), "accepts 2 arg(s)") { | |
| 266 | + t.Errorf("%v: want an arg-count error, got %v", args, err) | |
| 267 | + } | |
| 268 | + if seen.Path != "" { | |
| 269 | + t.Errorf("%v: reached the server at %q", args, seen.Path) | |
| 270 | + } | |
| 271 | + } | |
| 272 | +} | |
| 273 | + | |
| 274 | +// TestRepoTransferPromptsBeforeMoving: a transfer is a one-way door for the person | |
| 275 | +// making it — after the move the actor may hold no ADMIN in the destination, so | |
| 276 | +// transferring back answers forbidden_owner. It confirms like `repo delete` does. | |
| 277 | +func TestRepoTransferPromptsBeforeMoving(t *testing.T) { | |
| 278 | + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | |
| 279 | + t.Setenv("RICKUB_TOKEN", "rickub_pat_test") | |
| 280 | + seen := &captured{} | |
| 281 | + srv := repoMoveServer(t, seen) | |
| 282 | + defer srv.Close() | |
| 283 | + t.Setenv("RICKUB_HOST", srv.URL) | |
| 284 | + | |
| 285 | + // A wrong answer aborts, and nothing is sent. | |
| 286 | + rootCmd.SetIn(strings.NewReader("nope\n")) | |
| 287 | + _, _, err := execute(t, "repo", "transfer", "rick/portal", "acme") | |
| 288 | + rootCmd.SetIn(nil) | |
| 289 | + if err == nil { | |
| 290 | + t.Fatal("a mistyped confirmation was accepted") | |
| 291 | + } | |
| 292 | + if seen.Path != "" { | |
| 293 | + t.Fatalf("an unconfirmed transfer reached the server at %q", seen.Path) | |
| 294 | + } | |
| 295 | + | |
| 296 | + // Typing the repo name goes through. | |
| 297 | + rootCmd.SetIn(strings.NewReader("portal\n")) | |
| 298 | + out, _, err := execute(t, "repo", "transfer", "rick/portal", "acme") | |
| 299 | + rootCmd.SetIn(nil) | |
| 300 | + if err != nil { | |
| 301 | + t.Fatalf("execute: %v", err) | |
| 302 | + } | |
| 303 | + if seen.Path != "/api/v1/repos/rick/portal/transfer" { | |
| 304 | + t.Errorf("confirmed transfer did not reach the endpoint: %q", seen.Path) | |
| 305 | + } | |
| 306 | + if !strings.Contains(out, "Transferred to acme/portal") { | |
| 307 | + t.Errorf("output missing new full name:\n%s", out) | |
| 308 | + } | |
| 309 | + | |
| 310 | + // --yes skips the prompt: scripts keep working without a tty. | |
| 311 | + seen.Path = "" | |
| 312 | + if _, _, err := execute(t, "repo", "transfer", "rick/portal", "acme", "--yes"); err != nil { | |
| 313 | + t.Fatalf("--yes: %v", err) | |
| 314 | + } | |
| 315 | + if seen.Path != "/api/v1/repos/rick/portal/transfer" { | |
| 316 | + t.Errorf("--yes did not reach the endpoint: %q", seen.Path) | |
| 317 | + } | |
| 318 | +} | |
| 319 | + | |
| 320 | +// TestRepoRenameDoesNotPrompt: rename stays unprompted. It breaks clone URLs, but | |
| 321 | +// the actor keeps ADMIN and can rename straight back — a confirmation on every | |
| 322 | +// rename would be friction without a one-way door behind it. | |
| 323 | +func TestRepoRenameDoesNotPrompt(t *testing.T) { | |
| 324 | + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | |
| 325 | + t.Setenv("RICKUB_TOKEN", "rickub_pat_test") | |
| 326 | + seen := &captured{} | |
| 327 | + srv := repoMoveServer(t, seen) | |
| 328 | + defer srv.Close() | |
| 329 | + t.Setenv("RICKUB_HOST", srv.URL) | |
| 330 | + | |
| 331 | + rootCmd.SetIn(strings.NewReader("")) | |
| 332 | + _, _, err := execute(t, "repo", "rename", "rick/portal", "portal-gun") | |
| 333 | + rootCmd.SetIn(nil) | |
| 334 | + if err != nil { | |
| 335 | + t.Fatalf("execute: %v", err) | |
| 246 | 336 | } |
| 247 | - if _, _, err := execute(t, "repo", "transfer", "rick/portal"); err == nil { | |
| 248 | - t.Error("transfer without a destination owner should fail arg parsing") | |
| 337 | + if seen.Path != "/api/v1/repos/rick/portal/rename" { | |
| 338 | + t.Errorf("rename should not need a confirmation: %q", seen.Path) | |
| 249 | 339 | } |
| 250 | 340 | } |
| 251 | 341 | |
| @@ -22,6 +22,7 @@ func execute(t *testing.T, args ...string) (string, string, error) { | |||
| 22 | flagHost = "" | 22 | flagHost = "" |
| 23 | flagToken = "" | 23 | flagToken = "" |
| 24 | repoListUser = "" | 24 | repoListUser = "" |
| 25 | + repoTransferYes = false | ||
| 25 | repoListOrg = "" | 26 | repoListOrg = "" |
| 26 | pageFlag = 0 | 27 | pageFlag = 0 |
| 27 | perPageFlag = 0 | 28 | perPageFlag = 0 |
| @@ -223,7 +224,9 @@ func TestRepoTransferCallsMoveEndpoint(t *testing.T) { | |||
| 223 | defer srv.Close() | 224 | defer srv.Close() |
| 224 | t.Setenv("RICKUB_HOST", srv.URL) | 225 | t.Setenv("RICKUB_HOST", srv.URL) |
| 225 | 226 | ||
| 226 | - out, _, err := execute(t, "repo", "transfer", "rick/portal", "acme") | 227 | + // --yes: this test is about the wire call, not the confirmation, which |
| 228 | + // TestRepoTransferPromptsBeforeMoving covers. | ||
| 229 | + out, _, err := execute(t, "repo", "transfer", "rick/portal", "acme", "--yes") | ||
| 227 | if err != nil { | 230 | if err != nil { |
| 228 | t.Fatalf("execute: %v", err) | 231 | t.Fatalf("execute: %v", err) |
| 229 | } | 232 | } |
| @@ -240,12 +243,99 @@ func TestRepoTransferCallsMoveEndpoint(t *testing.T) { | |||
| 240 | 243 | ||
| 241 | func TestRepoMoveArgCount(t *testing.T) { | 244 | func TestRepoMoveArgCount(t *testing.T) { |
| 242 | t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | 245 | t.Setenv("XDG_CONFIG_HOME", t.TempDir()) |
| 243 | - // Both commands are argument-strict: no destination, no request. | 246 | + t.Setenv("RICKUB_TOKEN", "rickub_pat_test") |
| 244 | - if _, _, err := execute(t, "repo", "rename", "rick/portal"); err == nil { | 247 | + seen := &captured{} |
| 245 | - t.Error("rename without a new name should fail arg parsing") | 248 | + srv := repoMoveServer(t, seen) |
| 249 | + defer srv.Close() | ||
| 250 | + t.Setenv("RICKUB_HOST", srv.URL) | ||
| 251 | + | ||
| 252 | + // Both commands are argument-strict: no destination, no request. The token and | ||
| 253 | + // host are configured on purpose — without them `newClient()` would fail first | ||
| 254 | + // and this would pass even with the ExactArgs guard removed, which is what the | ||
| 255 | + // version ported from 4a5d5d2 did. | ||
| 256 | + for _, args := range [][]string{ | ||
| 257 | + {"repo", "rename", "rick/portal"}, | ||
| 258 | + {"repo", "transfer", "rick/portal"}, | ||
| 259 | + } { | ||
| 260 | + _, _, err := execute(t, args...) | ||
| 261 | + if err == nil { | ||
| 262 | + t.Errorf("%v: a missing destination should fail arg parsing", args) | ||
| 263 | + continue | ||
| 264 | + } | ||
| 265 | + if !strings.Contains(err.Error(), "accepts 2 arg(s)") { | ||
| 266 | + t.Errorf("%v: want an arg-count error, got %v", args, err) | ||
| 267 | + } | ||
| 268 | + if seen.Path != "" { | ||
| 269 | + t.Errorf("%v: reached the server at %q", args, seen.Path) | ||
| 270 | + } | ||
| 271 | + } | ||
| 272 | +} | ||
| 273 | + | ||
| 274 | +// TestRepoTransferPromptsBeforeMoving: a transfer is a one-way door for the person | ||
| 275 | +// making it — after the move the actor may hold no ADMIN in the destination, so | ||
| 276 | +// transferring back answers forbidden_owner. It confirms like `repo delete` does. | ||
| 277 | +func TestRepoTransferPromptsBeforeMoving(t *testing.T) { | ||
| 278 | + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | ||
| 279 | + t.Setenv("RICKUB_TOKEN", "rickub_pat_test") | ||
| 280 | + seen := &captured{} | ||
| 281 | + srv := repoMoveServer(t, seen) | ||
| 282 | + defer srv.Close() | ||
| 283 | + t.Setenv("RICKUB_HOST", srv.URL) | ||
| 284 | + | ||
| 285 | + // A wrong answer aborts, and nothing is sent. | ||
| 286 | + rootCmd.SetIn(strings.NewReader("nope\n")) | ||
| 287 | + _, _, err := execute(t, "repo", "transfer", "rick/portal", "acme") | ||
| 288 | + rootCmd.SetIn(nil) | ||
| 289 | + if err == nil { | ||
| 290 | + t.Fatal("a mistyped confirmation was accepted") | ||
| 291 | + } | ||
| 292 | + if seen.Path != "" { | ||
| 293 | + t.Fatalf("an unconfirmed transfer reached the server at %q", seen.Path) | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + // Typing the repo name goes through. | ||
| 297 | + rootCmd.SetIn(strings.NewReader("portal\n")) | ||
| 298 | + out, _, err := execute(t, "repo", "transfer", "rick/portal", "acme") | ||
| 299 | + rootCmd.SetIn(nil) | ||
| 300 | + if err != nil { | ||
| 301 | + t.Fatalf("execute: %v", err) | ||
| 302 | + } | ||
| 303 | + if seen.Path != "/api/v1/repos/rick/portal/transfer" { | ||
| 304 | + t.Errorf("confirmed transfer did not reach the endpoint: %q", seen.Path) | ||
| 305 | + } | ||
| 306 | + if !strings.Contains(out, "Transferred to acme/portal") { | ||
| 307 | + t.Errorf("output missing new full name:\n%s", out) | ||
| 308 | + } | ||
| 309 | + | ||
| 310 | + // --yes skips the prompt: scripts keep working without a tty. | ||
| 311 | + seen.Path = "" | ||
| 312 | + if _, _, err := execute(t, "repo", "transfer", "rick/portal", "acme", "--yes"); err != nil { | ||
| 313 | + t.Fatalf("--yes: %v", err) | ||
| 314 | + } | ||
| 315 | + if seen.Path != "/api/v1/repos/rick/portal/transfer" { | ||
| 316 | + t.Errorf("--yes did not reach the endpoint: %q", seen.Path) | ||
| 317 | + } | ||
| 318 | +} | ||
| 319 | + | ||
| 320 | +// TestRepoRenameDoesNotPrompt: rename stays unprompted. It breaks clone URLs, but | ||
| 321 | +// the actor keeps ADMIN and can rename straight back — a confirmation on every | ||
| 322 | +// rename would be friction without a one-way door behind it. | ||
| 323 | +func TestRepoRenameDoesNotPrompt(t *testing.T) { | ||
| 324 | + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | ||
| 325 | + t.Setenv("RICKUB_TOKEN", "rickub_pat_test") | ||
| 326 | + seen := &captured{} | ||
| 327 | + srv := repoMoveServer(t, seen) | ||
| 328 | + defer srv.Close() | ||
| 329 | + t.Setenv("RICKUB_HOST", srv.URL) | ||
| 330 | + | ||
| 331 | + rootCmd.SetIn(strings.NewReader("")) | ||
| 332 | + _, _, err := execute(t, "repo", "rename", "rick/portal", "portal-gun") | ||
| 333 | + rootCmd.SetIn(nil) | ||
| 334 | + if err != nil { | ||
| 335 | + t.Fatalf("execute: %v", err) | ||
| 246 | } | 336 | } |
| 247 | - if _, _, err := execute(t, "repo", "transfer", "rick/portal"); err == nil { | 337 | + if seen.Path != "/api/v1/repos/rick/portal/rename" { |
| 248 | - t.Error("transfer without a destination owner should fail arg parsing") | 338 | + t.Errorf("rename should not need a confirmation: %q", seen.Path) |
| 249 | } | 339 | } |
| 250 | } | 340 | } |
| 251 | 341 | ||