Merge pull request #1720 from entireio/fix/enable-confirm-defaults-1717 · Entire
Merge pull request #1720 from entireio/fix/enable-confirm-defaults-1717
5c0b576→main·
computermode·4d ago·3 files·+348 added/-78 removed
fix(enable): confirm before init/create/push in a non-repo directory
Changes
3
cmd/entire/cli
Msetup.go+4/-1
Msetup_github.go+122/-43
Msetup_github_test.go+222/-34
934 unmodified lines
935
936
937
938
938
939
940
941
3 unmodified lines
945
946
947
948
949
950
951
952
953
954
955
956
957
934 unmodified lines
cmd.Flags().BoolVar(&opts.AbsoluteGitHookPath, flagAbsoluteGitHookPath, false, "Embed full binary path in git hooks (for GUI git clients that don't source shell profiles)")
cmd.Flags().BoolVar(&opts.SearchSkill, flagSearchSkill, false, "Install the optional Entire search skill for selected agent(s)")
cmd.Flags().BoolVar(&opts.AgentHelpSkill, flagAgentHelpSkill, false, "Install the stable Entire agent-help skill (points agents at `entire agent-help`) for selected agent(s)")
cmd.Flags().BoolVarP(&opts.Yes, "yes", "y", false, "Accept all defaults without prompting (in a non-repo directory: init git, create private GitHub repo, commit; then enable all agents and accept telemetry)")
cmd.Flags().BoolVarP(&opts.Yes, "yes", "y", false, "Accept all defaults without prompting (in a non-repo directory: init git, create private GitHub repo, commit, and push; then enable all agents and accept telemetry)")
addInsecureHTTPAuthFlag(cmd, &insecureHTTPAuth)
// Bootstrap flags for non-git-repo folders.
3 unmodified lines
cmd.Flags().StringVar(&bootstrapOpts.RepoOwner, "repo-owner", "", "GitHub user or organization login for the new repo")
cmd.Flags().StringVar(&bootstrapOpts.RepoVisibility, "repo-visibility", "", "GitHub repository visibility: public, private, or internal")
cmd.Flags().BoolVar(&bootstrapOpts.NoGitHub, "no-github", false, "Initialize local git repo only; skip creating a GitHub remote")
cmd.Flags().BoolVar(&bootstrapOpts.Push, "push", false, "When bootstrapping a new repo, push the initial commit to the created GitHub remote (implies creating the remote; without it the repo is created but not pushed)")
cmd.Flags().StringVar(&bootstrapOpts.InitialCommitMessage, "initial-commit-message", "", "Commit message for the initial commit when bootstrapping a new repo")
cmd.Flags().BoolVar(&bootstrapOpts.SkipInitialCommit, "skip-initial-commit", false, "Don't create the initial commit when bootstrapping a new repo")
cmd.MarkFlagsMutuallyExclusive("init-repo", "no-init-repo")
cmd.MarkFlagsMutuallyExclusive("initial-commit-message", "skip-initial-commit")
cmd.MarkFlagsMutuallyExclusive("push", "no-github")
cmd.MarkFlagsMutuallyExclusive("push", "skip-initial-commit")
// Provide a helpful error when --agent is used without a value
defaultFlagErr := cmd.FlagErrorFunc()
Mcmd/entire/cli/setup.go+4/-1
41 unmodified lines
42
43
44
45
46
45
46
47
48
49
50
51
52
53
54
67 unmodified lines
122
123
124
125
126
127
128
36 unmodified lines
165
166
167
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
26 unmodified lines
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
2 unmodified lines
253
254
255
256
257
258
259
22 unmodified lines
282
283
284
258
285
286
287
288
15 unmodified lines
304
305
306
307
308
309
281
310
311
312
313
314
286
315
316
317
318
319
320
321
322
323
324
325
16 unmodified lines
342
343
344
345
346
347
348
349
350
351
352
353
354
312
313
314
355
356
357
358
359
360
361
362
363
364
365
318
366
367
368
369
6 unmodified lines
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
10 unmodified lines
418
419
420
347
348
421
422
423
424
425
426
427
428
429
352
430
431
432
433
508 unmodified lines
942
943
944
867
868
869
945
946
947
948
949
950
951
12 unmodified lines
964
965
966
888
967
968
969
970
41 unmodified lines
// still created, but nothing is pushed.
SkipInitialCommit bool
// Yes accepts all defaults without prompting: init repo, create GitHub
// repo under the user's account (private), default commit message.
// Explicit flags (--no-github, --repo-owner, etc.) take precedence.
// repo under the user's account (private), default commit message, and
// push. Explicit flags (--no-github, --repo-owner, etc.) take precedence.
Yes bool
// Push opts into pushing the initial commit to the created GitHub remote
// without prompting. Pushing is otherwise an explicit, separate opt-in
// (interactive "yes" or --yes). Implies creating the remote.
Push bool
}
// bootstrapRunner executes external commands. Tests override this to avoid
67 unmodified lines
visibility string // public/private/internal, if useGitHub
commit bool // false means the user opted out of the initial commit
message string // resolved initial commit message (empty when !commit)
push bool // false means create the GitHub repo but don't push to it
}
// runGitHubBootstrapInit handles the pre-setup half of "enable on a non-git
36 unmodified lines
paths.ClearWorktreeRootCache()
fmt.Fprintln(w, " ✓ Initialized empty git repository")
// Step 3: decide whether to create a GitHub repo. If gh is missing or the
// user passed --no-github, we skip that branch but still bootstrap the
// local repo.
useGitHub := !opts.NoGitHub
if useGitHub {
if !ghAvailable(ctx, runner) {
fmt.Fprintln(errW, "gh CLI not found. Install it from https://cli.github.com/ and run `gh auth login` to add a GitHub remote.")
fmt.Fprintln(errW, "Continuing with local initialization only.")
useGitHub = false
} else if !ghAuthenticated(ctx, runner) {
fmt.Fprintln(errW, "gh CLI is not authenticated. Run `gh auth login` to add a GitHub remote.")
fmt.Fprintln(errW, "Continuing with local initialization only.")
useGitHub = false
}
}
// Step 3b: ask a simple yes/no before diving into owner/name/visibility
// prompts. Skip the confirm when any gh-specific flag is set (the flag
// implies intent) or when we're non-interactive (keep the documented
// happy path: default to yes).
if useGitHub && !opts.Yes && !ghFlagsProvided(opts) && interactive.CanPromptInteractively() {
confirmed, err := confirmCreateGitHubRepo()
if err != nil {
return nil, err
}
if !confirmed {
useGitHub = false
}
}
// Step 3: decide whether to create a GitHub repo. Creating a remote is an
// explicit opt-in: it happens only on an explicit signal (repo flags,
// --push, or --yes) or an interactive "yes". A non-interactive run with no
// such signal stays local-only — we never create a repo on the user's
// behalf. --no-github always wins.
useGitHub := false
if !opts.NoGitHub {
explicit := ghCreateRequested(opts)
// Only probe gh (and warn about a missing/unauthenticated CLI) when the
// user actually wants a GitHub repo — explicitly, or via the confirm
// prompt we're about to show interactively.
if explicit || interactive.CanPromptInteractively() {
switch {
case !ghAvailable(ctx, runner):
fmt.Fprintln(errW, "gh CLI not found. Install it from https://cli.github.com/ and run `gh auth login` to add a GitHub remote.")
fmt.Fprintln(errW, "Continuing with local initialization only.")
case !ghAuthenticated(ctx, runner):
fmt.Fprintln(errW, "gh CLI is not authenticated. Run `gh auth login` to add a GitHub remote.")
fmt.Fprintln(errW, "Continuing with local initialization only.")
case explicit:
useGitHub = true
default:
// Interactive with no explicit signal: prompt, defaulting to No.
confirmed, err := confirmCreateGitHubRepo(cwd)
if err != nil {
return nil, err
}
useGitHub = confirmed
}
}
}
26 unmodified lines
}
}
// Step 6: pushing is also an explicit opt-in, separate from creating the
// repo. Publishing the directory's contents is a distinct outward-facing
// action, so it happens only on an explicit signal (--push or --yes) or an
// interactive "yes". Otherwise the repo is created but left unpushed. Only
// relevant when we'll create a GitHub repo and have a commit to push.
push := false
if useGitHub && commit {
switch {
case opts.Yes || opts.Push:
push = true
case interactive.CanPromptInteractively():
confirmed, err := confirmPushToRemote(fullName)
if err != nil {
return nil, err
}
push = confirmed
}
}
return &bootstrapState{
runner: runner,
cwd: cwd,
visibility: visibility,
commit: commit,
message: message,
push: push,
}, nil
}
22 unmodified lines
// Pick a single section title for this phase based on what we'll do.
if s.useGitHub || s.commit {
switch {
case s.useGitHub && s.commit:
case s.useGitHub && s.commit && s.push:
printBootstrapSection(w, "Publishing to GitHub")
case s.useGitHub:
printBootstrapSection(w, "Creating GitHub repository")
15 unmodified lines
fmt.Fprintln(w, " ✓ Nothing to commit — the folder has no files yet")
}
}
// Push only when there's a commit AND the user opted into pushing.
pushed := committed && s.push
if s.useGitHub {
if err := ghRepoCreate(ctx, s.runner, s.cwd, s.fullName, s.visibility, committed); err != nil {
if err := ghRepoCreate(ctx, s.runner, s.cwd, s.fullName, s.visibility, pushed); err != nil {
return fmt.Errorf("gh repo create: %w", err)
}
fmt.Fprintf(w, " ✓ Created %s (%s)\n", s.fullName, s.visibility)
fmt.Fprintf(w, " https://github.com/%s\n", s.fullName)
if committed {
if pushed {
fmt.Fprintln(w, " ✓ Pushed initial commit to origin")
} else if committed {
// Repo created and origin configured, but the user declined the
// push. Tell them how to publish when ready.
fmt.Fprintln(w)
fmt.Fprintln(w, " Skipped push — nothing was published. When you're ready:")
fmt.Fprintln(w, " git push -u origin HEAD")
}
}
}
}
if !s.commit {
16 unmodified lines
return opts.RepoName != "" || opts.RepoOwner != "" || opts.RepoVisibility != ""
}
// ghCreateRequested reports whether the caller has explicitly opted into
// creating a GitHub repo without an interactive prompt: --yes, --push (which
// needs a remote to push to), or any repo-targeting flag. When false and the
// session is non-interactive, the bootstrap stays local-only.
func ghCreateRequested(opts GitHubBootstrapOptions) bool {
return opts.Yes || opts.Push || ghFlagsProvided(opts)
}
// confirmCreateGitHubRepo asks the user whether they want to also create
// a matching GitHub repository. Interactive-only; callers gate on
// interactive.CanPromptInteractively.
func confirmCreateGitHubRepo() (bool, error) {
confirmed := true
// interactive.CanPromptInteractively. Pushing to the repo is confirmed
// separately (see confirmPushToRemote).
//
// Defaults to No: creating a remote repository on the user's behalf must
// never happen just because the user pressed Enter. The absolute path is in
// the title so it's clear which directory is the source.
func confirmCreateGitHubRepo(cwd string) (bool, error) {
confirmed := false
form := NewAccessibleForm(
huh.NewGroup(
huh.NewConfirm().
Title("Create a matching repository on GitHub?").
Title(fmt.Sprintf("Create a GitHub repository for %q?", cwd)).
Value(&confirmed),
),
)
6 unmodified lines
return confirmed, nil
}
// confirmPushToRemote asks the user whether to push the initial commit to
// the newly-created GitHub repository. Interactive-only; callers gate on
// interactive.CanPromptInteractively.
//
// Defaults to No: pushing publishes the directory's contents to the remote,
// a distinct outward-facing action from creating the repo, so it must never
// happen just because the user pressed Enter. Declining leaves the repo
// created with origin configured but nothing pushed.
func confirmPushToRemote(fullName string) (bool, error) {
confirmed := false
form := NewAccessibleForm(
huh.NewGroup(
huh.NewConfirm().
Title(fmt.Sprintf("Push the initial commit to %q?", fullName)).
Value(&confirmed),
),
)
if err := form.Run(); err != nil {
if errors.Is(err, huh.ErrUserAborted) {
return false, errBootstrapInterrupted
}
return false, fmt.Errorf("push confirm prompt: %w", err)
}
return confirmed, nil
}
// confirmInitRepo returns true if we should proceed with `git init`. It
// respects --init-repo / --no-init-repo; otherwise prompts. In
// non-interactive mode we return false without printing anything so
10 unmodified lines
return false, nil
}
folder := filepath.Base(cwd)
confirmed := true
// Default to No: `entire enable` is often run reflexively inside an
// existing project, so a stray run in the wrong (non-repo) directory
// must not initialize a repo just because the user pressed Enter. The
// absolute path is in the title so it's clear which directory is the source.
confirmed := false
form := NewAccessibleForm(
huh.NewGroup(
huh.NewConfirm().
Title(fmt.Sprintf("No git repository in %q. Initialize one here?", folder)).
Title(fmt.Sprintf("Warning: Not a git repository. Initialize a new one in %q?", cwd)).
Value(&confirmed),
),
)
508 unmodified lines
return false, fmt.Errorf("gh repo view: %w", err)
}
// ghRepoCreate creates a GitHub repo from the local source directory, adds
// origin as its remote, and pushes if there's anything to push.
func ghRepoCreate(ctx context.Context, runner bootstrapRunner, dir, fullName, visibility string, hasCommits bool) error {
// ghRepoCreate creates a GitHub repo from the local source directory and
// adds origin as its remote. It pushes only when push is true; callers gate
// this on both having a commit and the user opting into the push.
func ghRepoCreate(ctx context.Context, runner bootstrapRunner, dir, fullName, visibility string, push bool) error {
// Create the remote repo and add origin, but don't push yet. We push
// separately below with --no-verify so the pre-push hook doesn't run
// on this first push: the entire/checkpoints/v1 branch has nothing to
12 unmodified lines
if _, err := runner.RunInDir(ctx, dir, "gh", args...); err != nil {
return fmt.Errorf("gh repo create: %w", ghRunnerErr(err))
}
if hasCommits {
if push {
// -q silences "Enumerating objects..." etc. --no-verify bypasses
// the pre-push hook so entire/checkpoints/v1 isn't pushed
// alongside the default branch.
Mcmd/entire/cli/setup_github.go+122/-43
384 unmodified lines
385
386
387
388
388
389
390
391
392
393
33 unmodified lines
427
428
429
430
431
432
433
157 unmodified lines
591
592
593
591
592
593
594
594
595
596
597
598
599
600
601
602
603
600
601
602
603
604
605
606
604
605
606
607
608
609
613
614
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
30 unmodified lines
700
701
702
703
704
705
706
322 unmodified lines
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
28 unmodified lines
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
89 unmodified lines
1310
1311
1312
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1313
1314
1315
1316
1317
1318
384 unmodified lines
r.set("git", []string{"add", "-A"}, "", nil)
r.set("git", []string{"status", "--porcelain"}, "", nil)
opts := GitHubBootstrapOptions{InitRepo: true}
// A repo flag is an explicit GitHub request, so gh is probed; since it's
// missing we warn and fall back to local-only.
opts := GitHubBootstrapOptions{InitRepo: true, RepoName: "wanted"}
var errBuf bytes.Buffer
err := runGitHubBootstrapWith(context.Background(), io.Discard, &errBuf, opts, r)
if err != nil {
t.Fatalf("init failed: %v", err)
}
if !r.hasCall(func(c fakeCall) bool { return c.name == "gh" }) {
t.Fatal("must not invoke gh when no GitHub repo was requested")
}
// TestRunGitHubBootstrap_NonInteractive_NoFlagsDefaultsToGitHub confirms the
// non-interactive happy path still creates a GitHub repo when the user
// didn't set any explicit flag (the confirm prompt is only interactive).
func TestRunGitHubBootstrap_NonInteractive_NoFlagsDefaultsToGitHub(t *testing.T) {
// TestRunGitHubBootstrap_NonInteractive_NoFlagsStaysLocal confirms that a
// non-interactive bootstrap with no explicit GitHub signal stays local-only:
// it does not probe gh, create a repo, or push. Creating and pushing are
// explicit opt-ins (--repo-*, --push, --yes, or an interactive "yes").
func TestRunGitHubBootstrap_NonInteractive_NoFlagsStaysLocal(t *testing.T) {
dir := t.TempDir()
restoreCwd(t, dir)
r := newFakeRunner()
r.setIdentityConfigured()
r.set("gh", []string{"--version"}, "gh", nil)
r.set("gh", []string{"auth", "status"}, "ok", nil)
r.set("gh", []string{"api", "user", "--jq", ".login"}, "octocat\n", nil)
r.set("gh", []string{"api", "user/orgs", "--jq", "[..login"}, "", nil)
// Default folder slug derived from t.TempDir().
suggested := slugifyRepoName(filepath.Base(dir))
r.set("gh", []string{"repo", "view", "octocat/" + suggested, "--json", "name"}, "", errors.New("not found"))
r.set("git", []string{"init"}, "", nil)
state, err := runGitHubBootstrapInitWith(context.Background(), io.Discard, io.Discard, GitHubBootstrapOptions{InitRepo: true}, r)
if err != nil {
t.Fatalf("init failed: %v", err)
}
if !state.useGitHub {
t.Fatal("non-interactive bootstrap should default to using GitHub")
}
if state.push {
t.Fatal("push must be false when staying local-only")
}
// gh must never be probed when no GitHub repo was requested.
if r.hasCall(func(c fakeCall) bool { return c.name == "gh" }) {
t.Fatal("must not invoke gh when no GitHub repo was requested")
}
}
// TestRunGitHubBootstrap_RepoFlagsCreateButDoNotPush confirms that repo flags
// opt into creating the GitHub repo but NOT into pushing. Non-interactively,
// the repo is created and origin configured, but nothing is pushed unless
// --push or --yes is also given; the user is told how to publish manually.
func TestRunGitHubBootstrap_RepoFlagsCreateButDoNotPush(t *testing.T) {
dir := t.TempDir()
restoreCwd(t, dir)
r := newFakeRunner()
r.setIdentityConfigured()
r.set("gh", []string{"--version"}, "gh 2.81.0", nil)
r.set("gh", []string{"auth", "status"}, "Logged in", nil)
r.set("gh", []string{"api", "user", "--jq", ".login"}, "octocat\n", nil)
r.set("gh", []string{"api", "user/orgs", "--jq", "[..login"}, "", nil)
r.set("gh", []string{"repo", "view", "octocat/create-only", "--json", "name"}, "", errors.New("not found"))
r.set("git", []string{"init"}, "", nil)
r.set("git", []string{"add", "-A"}, "", nil)
r.set("git", []string{"status", "--porcelain"}, " M f\n", nil)
r.set("git", []string{"-c", "commit.gpgsign=false", "commit", "-m", "Seed"}, "", nil)
r.set("gh", []string{"repo", "create", "octocat/create-only", "--private", "--source=.", "--remote=origin"}, "", nil)
opts := GitHubBootstrapOptions{
InitRepo: true,
RepoName: "create-only",
RepoVisibility: "private",
InitialCommitMessage: "Seed",
}
var out bytes.Buffer
if err := runGitHubBootstrapWith(context.Background(), &out, io.Discard, opts, r); err != nil {
t.Fatalf("bootstrap failed: %v", err)
}
if !r.hasCall(argsMatch("gh", []string{"repo", "create"})) {
t.Fatal("expected gh repo create when repo flags are given")
}
if r.hasCall(argsMatch("git", []string{"push"})) {
t.Fatal("must not push without --push or --yes")
}
if !strings.Contains(out.String(), "Skipped push") {
t.Fatalf("expected 'Skipped push' guidance, got: %s", out.String())
}
}
30 unmodified lines
RepoName: "phased",
RepoVisibility: "private",
InitialCommitMessage: "First",
Push: true,
}
// Phase 1: init. This must NOT call git add/commit/ gh repo create.
322 unmodified lines
}
}
func TestEnableCmd_PushNoGitHubMutuallyExclusive(t *testing.T) {
setupTestRepo(t)
cmd := newEnableCmd()
var stderr bytes.Buffer
cmd.SetErr(&stderr)
cmd.SetOut(&bytes.Buffer{})
cmd.SetArgs([]string{"--push", "--no-github"})
err := cmd.Execute()
if err == nil {
t.Fatal("expected error when both --push and --no-github are set")
}
if !strings.Contains(err.Error(), "push") || !strings.Contains(err.Error(), "no-github") {
t.Fatalf("expected error to mention both flags, got: %v", err)
}
}
// withInteractivePromptStdin forces interactive, accessible (text-based)
// prompt mode and feeds input to os.Stdin for the duration of the test, so a
// huh prompt reads a scripted answer instead of opening /dev/tty or blocking
// on a real terminal. ENTIRE_TEST_TTY makes CanPromptInteractively report
// true; ACCESSIBLE makes the form read os.Stdin rather than dial the terminal.
func withInteractivePromptStdin(t *testing.T, input string) {
t.Helper()
t.Setenv("ENTIRE_TEST_TTY", "1")
t.Setenv("ACCESSIBLE", "1")
pr, pw, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { pr.Close() })
go func() {
pw.WriteString(input) //nolint:errcheck // test helper
pw.Close()
}
old := os.Stdin
os.Stdin = pr
t.Cleanup(func() { os.Stdin = old })
}
// TestConfirmInitRepo_DefaultsToNo verifies that pressing Enter (empty
// input) at the init-repo prompt declines. `entire enable` is often run
// reflexively, so a stray run in a non-repo directory must not initialize
// a repo on the user's behalf. Regression guard for issue #1717.
func TestConfirmInitRepo_DefaultsToNo(t *testing.T) {
withInteractivePromptStdin(t, "\n")
proceed, err := confirmInitRepo(io.Discard, t.TempDir(), GitHubBootstrapOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if proceed {
t.Fatal("confirmInitRepo should default to No (decline) on empty input")
}
}
// TestConfirmInitRepo_ExplicitYesProceeds verifies an explicit "y" still
// opts in, so the safer default doesn't block intentional use.
func TestConfirmInitRepo_ExplicitYesProceeds(t *testing.T) {
withInteractivePromptStdin(t, "y\n")
proceed, err := confirmInitRepo(io.Discard, t.TempDir(), GitHubBootstrapOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !proceed {
t.Fatal("confirmInitRepo should proceed when the user explicitly answers yes")
}
}
// TestConfirmCreateGitHubRepo_DefaultsToNo verifies that pressing Enter at
// the GitHub-repo prompt declines. Creating and pushing a remote repository
// publishes the directory's contents, so it must never happen just because
// the user pressed Enter. Regression guard for issue #1717.
func TestConfirmCreateGitHubRepo_DefaultsToNo(t *testing.T) {
withInteractivePromptStdin(t, "\n")
confirmed, err := confirmCreateGitHubRepo(t.TempDir())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if confirmed {
t.Fatal("confirmCreateGitHubRepo should default to No on empty input")
}
}
// TestConfirmPushToRemote_DefaultsToNo verifies that pressing Enter at the
// push prompt declines. Pushing publishes the directory's contents, so it
// must never happen just because the user pressed Enter, even after they
// opted into creating the repo. Regression guard for issue #1717.
func TestConfirmPushToRemote_DefaultsToNo(t *testing.T) {
withInteractivePromptStdin(t, "\n")
confirmed, err := confirmPushToRemote("octocat/example")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if confirmed {
t.Fatal("confirmPushToRemote should default to No on empty input")
}
}
// TestRunGitHubBootstrapFinalize_HonorsPushFalse verifies that finalize
// respects state.push == false: the GitHub repo is still created and origin
// configured, but nothing is pushed and the user is told how to publish
// manually. The push *decision* (default No on Enter) is covered separately
// by TestConfirmPushToRemote_DefaultsToNo; this test covers finalize honoring
// that decision.
func TestRunGitHubBootstrapFinalize_HonorsPushFalse(t *testing.T) {
t.Parallel()
dir := t.TempDir()
r := newFakeRunner()
r.set("git", []string{"add", "-A"}, "", nil)
r.set("git", []string{"status", "--porcelain"}, " M f\n", nil)
r.set("git", []string{"-c", "commit.gpgsign=false", "commit", "-m", "Seed"}, "", nil)
r.set("gh", []string{"repo", "create", "octocat/no-push", "--private", "--source=.", "--remote=origin"}, "", nil)
s := &bootstrapState{
runner: r,
cwd: dir,
useGitHub: true,
fullName: "octocat/no-push",
visibility: "private",
commit: true,
message: "Seed",
push: false,
}
var out bytes.Buffer
if err := runGitHubBootstrapFinalize(context.Background(), &out, s); err != nil {
t.Fatalf("finalize failed: %v", err)
}
// The repo is still created (create guard was accepted)...
if !r.hasCall(argsMatch("gh", []string{"repo", "create"})) {
t.Fatal("expected gh repo create to run")
}
// ...but the push guard was declined, so nothing is pushed.
if r.hasCall(argsMatch("git", []string{"push"})) {
t.Fatal("git push must not run when the push guard was declined")
}
if !strings.Contains(out.String(), "Skipped push") {
t.Fatalf("expected 'Skipped push' guidance in output, got: %s", out.String())
}
}
// restoreCwd chdirs into dir for the duration of the test.
func restoreCwd(t *testing.T, dir string) {
t.Helper()
89 unmodified lines
// When --yes is set, the name is taken, and a TTY is available,
// resolveRepoName should print a conflict message and fall through
// to the interactive prompt. We verify the conflict message was
// printed (proving the fallback path was taken).
t.Setenv("ENTIRE_TEST_TTY", "1")
// Force accessible (text-based) mode so the huh form reads from
// os.Stdin instead of trying to open /dev/tty via bubbletea.
// Pipe a unique name so the form completes instead of blocking.
t.Setenv("ACCESSIBLE", "1")
pr, pw, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { pr.Close() })
go func() {
// The form reads one line; provide a unique name so it exits the loop.
pw.WriteString("unique-test-repo\n") //nolint:errcheck // test helper
pw.Close()
}
oldStdin := os.Stdin
os.Stdin = pr
t.Cleanup(func() { os.Stdin = oldStdin })
}