From ffc793d0efe4cb867fec02606ef453af99a1d2ed Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Wed, 25 Mar 2026 18:18:13 -0400 Subject: [PATCH 1/3] fix: resolve repo from remote during init --- internal/cmd/root.go | 2 ++ internal/cmd/root_test.go | 26 ++++++++++++++++++++++ internal/git/client.go | 47 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 9af37d2..b685045 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -99,6 +99,8 @@ stack init --trunk main --remote origin if trunk == "" { trunk = repoView.DefaultBranchRef.Name } + } else { + repo, _ = runtime.Git.RemoteRepoSlug(runtime.Context, chooseString(remote, "origin")) } if trunk == "" { diff --git a/internal/cmd/root_test.go b/internal/cmd/root_test.go index b9bb730..149b9b6 100644 --- a/internal/cmd/root_test.go +++ b/internal/cmd/root_test.go @@ -834,6 +834,32 @@ func TestVersionCommandPrintsBuildInfo(t *testing.T) { } } +func TestInitFallsBackToRemoteRepoWhenGHRepoViewFails(t *testing.T) { + repo := testutil.SetupGitRepo(t) + testutil.Run(t, repo, "git", "remote", "add", "origin", "git@github.com-ln:acme/new-repo.git") + runtime := newTestRuntime(repo) + + ghStubDir := t.TempDir() + ghStubPath := filepath.Join(ghStubDir, "gh") + if err := os.WriteFile(ghStubPath, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil { + t.Fatalf("write gh stub: %v", err) + } + t.Setenv("PATH", ghStubDir+string(os.PathListSeparator)+os.Getenv("PATH")) + + output := executeCommand(t, runtime, "init", "--remote", "origin", "--trunk", "main") + if !strings.Contains(output, "repo: acme/new-repo") { + t.Fatalf("expected init output to include remote repo slug, got %q", output) + } + + state, err := runtime.Store.ReadState(runtime.Context) + if err != nil { + t.Fatalf("read state: %v", err) + } + if state.Repo != "acme/new-repo" { + t.Fatalf("expected state repo acme/new-repo, got %q", state.Repo) + } +} + func TestQueueMergesHealthyBottomBranch(t *testing.T) { repo := testutil.SetupGitRepo(t) remote := filepath.Join(t.TempDir(), "remote.git") diff --git a/internal/git/client.go b/internal/git/client.go index 116629d..7de85a7 100644 --- a/internal/git/client.go +++ b/internal/git/client.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "net/url" "os" "os/exec" "path/filepath" @@ -191,6 +192,15 @@ func (c *Client) RemoteURL(ctx context.Context, remote string) (string, error) { return c.output(ctx, "remote", "get-url", remote) } +func (c *Client) RemoteRepoSlug(ctx context.Context, remote string) (string, error) { + remoteURL, err := c.RemoteURL(ctx, remote) + if err != nil { + return "", err + } + + return parseRemoteRepoSlug(remoteURL), nil +} + func (c *Client) RangeDiff(ctx context.Context, oldBase string, newBranch string) (string, error) { return c.output(ctx, "range-diff", fmt.Sprintf("%s...%s", oldBase, newBranch)) } @@ -232,3 +242,40 @@ func (c *Client) runWithEnv(ctx context.Context, env []string, args ...string) ( return stdout.String(), nil } + +func parseRemoteRepoSlug(remoteURL string) string { + trimmed := strings.TrimSpace(remoteURL) + if trimmed == "" { + return "" + } + + if parsed, err := url.Parse(trimmed); err == nil && parsed.Scheme != "" { + return repoSlugFromPath(parsed.Path) + } + + scpPrefix, scpPath, found := strings.Cut(trimmed, ":") + if !found || !strings.Contains(scpPrefix, "@") { + return "" + } + + return repoSlugFromPath(scpPath) +} + +func repoSlugFromPath(path string) string { + trimmed := strings.Trim(strings.TrimSpace(path), "/") + trimmed = strings.TrimSuffix(trimmed, ".git") + if trimmed == "" { + return "" + } + + parts := strings.Split(trimmed, "/") + if len(parts) != 2 { + return "" + } + + if parts[0] == "" || parts[1] == "" { + return "" + } + + return strings.Join(parts, "/") +} From 76f12f77b576040c1482f59fd233d35b92d0826e Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Wed, 25 Mar 2026 18:21:52 -0400 Subject: [PATCH 2/3] chore: codify repo workflow guidance --- .gitignore | 1 + AGENTS.md | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 AGENTS.md diff --git a/.gitignore b/.gitignore index 0b1c0e1..dd6266a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store dist/ bin/ +stack coverage.out *.test *.out diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e514bcd --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# Repository Agent Instructions + +This file adds repo-specific guidance for work in this repository. Follow it in +addition to the global baseline instructions. + +## Toolchain + +- Use `mise` for Go commands in this repo. +- Prefer `mise exec -- gofmt -w ` for formatting Go files. +- Prefer `mise exec -- go test ./...` for the main test suite. +- Prefer `mise exec -- go build ./...` for a repo-wide build check. +- Do not assume `go` or `gofmt` are on `PATH`; use `mise exec -- ...`. + +## Verification + +- Treat `mise exec -- go test ./...` and `mise exec -- go build ./...` as the + default verification loop for code changes. +- If you change queue, restack, submit, sync, or recovery behavior, read + [docs/testing.md](docs/testing.md) and use the strongest relevant loop from + that doc instead of stopping at unit tests. +- State clearly what was verified and what remains unverified if a stronger loop + is unavailable. + +## Commits And Releases + +- Use Conventional Commits for commit subjects. +- Keep pull request titles conventional as well; the release flow depends on + it. +- Before touching release behavior or release automation, read + [docs/releasing.md](docs/releasing.md). + +## Working Tree Hygiene + +- Do not commit local build outputs such as the root `stack` binary. +- If a generated or built artifact appears repeatedly during normal work, add it + to `.gitignore` in the same change that introduces the behavior. + From e75c3896ed6de3b3a078d5be65e2c40f18e7a957 Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Wed, 25 Mar 2026 18:28:04 -0400 Subject: [PATCH 3/3] fix: parse userless scp remote aliases --- internal/git/client.go | 25 +++++++++++++++++++++---- internal/git/client_test.go | 16 ++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/internal/git/client.go b/internal/git/client.go index 7de85a7..a3dfaa4 100644 --- a/internal/git/client.go +++ b/internal/git/client.go @@ -249,16 +249,33 @@ func parseRemoteRepoSlug(remoteURL string) string { return "" } + if slug, ok := parseSCPRepoSlug(trimmed); ok { + return slug + } + if parsed, err := url.Parse(trimmed); err == nil && parsed.Scheme != "" { return repoSlugFromPath(parsed.Path) } - scpPrefix, scpPath, found := strings.Cut(trimmed, ":") - if !found || !strings.Contains(scpPrefix, "@") { - return "" + return "" +} + +func parseSCPRepoSlug(remote string) (string, bool) { + if strings.Contains(remote, "://") { + return "", false + } + + scpPrefix, scpPath, found := strings.Cut(remote, ":") + if !found || scpPrefix == "" || scpPath == "" { + return "", false + } + + slug := repoSlugFromPath(scpPath) + if slug == "" { + return "", false } - return repoSlugFromPath(scpPath) + return slug, true } func repoSlugFromPath(path string) string { diff --git a/internal/git/client_test.go b/internal/git/client_test.go index df5dc7b..97935ae 100644 --- a/internal/git/client_test.go +++ b/internal/git/client_test.go @@ -126,6 +126,22 @@ func TestRebaseContinueDoesNotRequireInteractiveEditor(t *testing.T) { } } +func TestRemoteRepoSlugParsesUserlessSCPAlias(t *testing.T) { + t.Parallel() + + repo := setupGitRepo(t) + run(t, repo, "git", "remote", "add", "origin", "github-work:acme/new-repo.git") + + client := stackgit.NewClient(repo) + slug, err := client.RemoteRepoSlug(context.Background(), "origin") + if err != nil { + t.Fatalf("remote repo slug: %v", err) + } + if slug != "acme/new-repo" { + t.Fatalf("expected acme/new-repo, got %q", slug) + } +} + func setupGitRepo(t *testing.T) string { t.Helper()