From a286579655b91b3df9776cdcd9734e6a43593f26 Mon Sep 17 00:00:00 2001 From: Nathan Nutter Date: Tue, 6 Oct 2026 14:24:36 -0500 Subject: [PATCH 1/2] Pin mise Go toolchain to 1.27.1 go.mod requires Go 1.27.1 while mise still provided 1.27.0, so every go command switched toolchains and compiled the standard library with a mismatched GOROOT. Aligning the pin fixes the mise fixers and pre-commit hook. --- mise.lock | 30 +++++++++++++++--------------- mise.toml | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/mise.lock b/mise.lock index 256707e..43a4dea 100644 --- a/mise.lock +++ b/mise.lock @@ -40,36 +40,36 @@ url = "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8 url_api = "https://api.github.com/repos/gitleaks/gitleaks/releases/assets/378333178" [[tools.go]] -version = "1.27.0" +version = "1.27.1" backend = "core:go" [tools.go."platforms.linux-arm64"] -checksum = "sha256:51798d2c42d0e1c6ed7fd9f48728b4193abac9e8aad6dbac2fe96a81f5909bda" -url = "https://dl.google.com/go/go1.27.0.linux-arm64.tar.gz" +checksum = "sha256:3450b45a3f9ee8568792736a5c5e70a1f2e9b36c35a8f74958c03e51d7d92bec" +url = "https://dl.google.com/go/go1.27.1.linux-arm64.tar.gz" [tools.go."platforms.linux-arm64-musl"] -checksum = "sha256:51798d2c42d0e1c6ed7fd9f48728b4193abac9e8aad6dbac2fe96a81f5909bda" -url = "https://dl.google.com/go/go1.27.0.linux-arm64.tar.gz" +checksum = "sha256:3450b45a3f9ee8568792736a5c5e70a1f2e9b36c35a8f74958c03e51d7d92bec" +url = "https://dl.google.com/go/go1.27.1.linux-arm64.tar.gz" [tools.go."platforms.linux-x64"] -checksum = "sha256:675c26c449cbb18fc24b74650de1eabbae6e16f64326fd85a283fb3b58280685" -url = "https://dl.google.com/go/go1.27.0.linux-amd64.tar.gz" +checksum = "sha256:63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445" +url = "https://dl.google.com/go/go1.27.1.linux-amd64.tar.gz" [tools.go."platforms.linux-x64-musl"] -checksum = "sha256:675c26c449cbb18fc24b74650de1eabbae6e16f64326fd85a283fb3b58280685" -url = "https://dl.google.com/go/go1.27.0.linux-amd64.tar.gz" +checksum = "sha256:63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445" +url = "https://dl.google.com/go/go1.27.1.linux-amd64.tar.gz" [tools.go."platforms.macos-arm64"] -checksum = "sha256:90493b3bbd5e10f91d12153198bf1994fd756399b4fec93b49b0c6e2acdeeb3e" -url = "https://dl.google.com/go/go1.27.0.darwin-arm64.tar.gz" +checksum = "sha256:ee215d57e0ec269c60cc9ceca68e6bda321ba9ee5afe24f4b0988703c2d87d12" +url = "https://dl.google.com/go/go1.27.1.darwin-arm64.tar.gz" [tools.go."platforms.macos-x64"] -checksum = "sha256:d3314e25496e4381d71a5c51d2907e7af655d199f6780b549f015bd85fef4986" -url = "https://dl.google.com/go/go1.27.0.darwin-amd64.tar.gz" +checksum = "sha256:8f8f52c6649542cf027bbc9b9c68d1ec042f9f34808a40413f0b8b3f66f3caa4" +url = "https://dl.google.com/go/go1.27.1.darwin-amd64.tar.gz" [tools.go."platforms.windows-x64"] -checksum = "sha256:f0c0a0d33ba94f4d2c5dbc887334ce678b21813504ddb3aafcb06e60a5a667c4" -url = "https://dl.google.com/go/go1.27.0.windows-amd64.zip" +checksum = "sha256:a3911b5e0e1b1053f25ed0675f4c1c6aad1e2bfcf253df2b9be4caabd2edd95d" +url = "https://dl.google.com/go/go1.27.1.windows-amd64.zip" [[tools."go:github.com/Antonboom/testifylint"]] version = "1.6.4" diff --git a/mise.toml b/mise.toml index b4c919d..f0068d2 100644 --- a/mise.toml +++ b/mise.toml @@ -1,5 +1,5 @@ [tools] -go = "1.27.0" +go = "1.27.1" ##################### # Development Tasks # From bb4efd2682519a1958f1c69ddf5c6ee27e56c765 Mon Sep 17 00:00:00 2001 From: Nathan Nutter Date: Tue, 6 Oct 2026 14:24:37 -0500 Subject: [PATCH 2/2] Support @repo selector in timber git Lets users run git directly against a registered bare repository, for example timber git @myrepo remote -v, without naming a worktree. --- internal/timber/git.go | 84 +++++++++++++++++++++++++++---------- internal/timber/git_test.go | 40 ++++++++++++++++++ 2 files changed, 101 insertions(+), 23 deletions(-) diff --git a/internal/timber/git.go b/internal/timber/git.go index 4e74938..5ef723a 100644 --- a/internal/timber/git.go +++ b/internal/timber/git.go @@ -17,12 +17,12 @@ func NewGitCommand(runtime Runtime) *cobra.Command { options := &gitCommandOptions{runtime: runtime} command := &cobra.Command{ - Use: "git [worktree[@repo]] [-- git-args...]", - Short: "Run git with --git-dir set to a managed worktree", + Use: "git [worktree[@repo]|@repo] [-- git-args...]", + Short: "Run git with --git-dir set to a managed worktree or repository", Args: cobra.ArbitraryArgs, DisableFlagParsing: true, RunE: options.Execute, - ValidArgsFunction: runtime.completeQualifiedWorktreeNames, + ValidArgsFunction: options.completeSelector, } return command @@ -30,16 +30,18 @@ func NewGitCommand(runtime Runtime) *cobra.Command { func (x *gitCommandOptions) Execute(command *cobra.Command, args []string) error { targetDirectory := x.runtime.CurrentDirectory + explicitGitDir := "" gitArgs := args if len(args) > 0 && args[0] == "--" { gitArgs = args[1:] } else if len(args) > 0 && !strings.HasPrefix(args[0], "-") { - selected, remaining, ok, err := x.resolveWorktreeSelector(command.InOrStdin(), args) + selectedDir, selectedGitDir, remaining, ok, err := x.resolveWorktreeSelector(command.InOrStdin(), args) if err != nil { return err } if ok { - targetDirectory = selected + targetDirectory = selectedDir + explicitGitDir = selectedGitDir gitArgs = remaining if len(gitArgs) > 0 && gitArgs[0] == "--" { gitArgs = gitArgs[1:] @@ -47,9 +49,13 @@ func (x *gitCommandOptions) Execute(command *cobra.Command, args []string) error } } - gitDir, err := gitDirForDirectory(x.runtime, targetDirectory) - if err != nil { - return err + gitDir := explicitGitDir + if gitDir == "" { + resolved, err := gitDirForDirectory(x.runtime, targetDirectory) + if err != nil { + return err + } + gitDir = resolved } gitArgs = append([]string{"--git-dir", gitDir}, gitArgs...) @@ -64,41 +70,73 @@ func (x *gitCommandOptions) Execute(command *cobra.Command, args []string) error return nil } -// resolveWorktreeSelector interprets args[0] as a or -// @ selector like the other worktree commands. A candidate -// containing @ always selects a worktree and returns its errors. A bare -// name selects a worktree only when it resolves to an existing managed -// worktree; otherwise ok is false and the caller treats all args as git -// args so subcommands such as `status` keep working. -func (x *gitCommandOptions) resolveWorktreeSelector(input io.Reader, args []string) (string, []string, bool, error) { +// resolveWorktreeSelector interprets args[0] as a , @, +// or @ selector. A candidate containing @ always selects a target and +// returns its errors: a bare worktree name selects the managed worktree, while +// a repo-only @ selects the bare repository. A bare name selects a +// worktree only when it resolves to an existing managed worktree; otherwise +// ok is false and the caller treats all args as git args so subcommands such +// as `status` keep working. The returned gitDir is set for the repo-only +// case; it is empty for worktrees so the caller resolves it from the +// worktree directory. +func (x *gitCommandOptions) resolveWorktreeSelector(input io.Reader, args []string) (string, string, []string, bool, error) { candidate := args[0] if strings.Contains(candidate, "@") { + qualified, err := x.runtime.parseQualifiedName(candidate) + if err != nil { + return "", "", nil, false, err + } + if qualified.Name == "" { + repo, err := x.runtime.registeredRepoByName(qualified.Repo) + if err != nil { + return "", "", nil, false, err + } + return repo.BarePath, repo.BarePath, args[1:], true, nil + } _, _, worktree, err := x.resolveQualifiedWorktree(input, candidate) if err != nil { - return "", nil, false, err + return "", "", nil, false, err } - return worktree.Path, args[1:], true, nil + return worktree.Path, "", args[1:], true, nil } repoName, err := x.runtime.inferUniqueRepoForWorktree(candidate) if err != nil { if strings.Contains(err.Error(), "not found") { - return "", nil, false, nil + return "", "", nil, false, nil } - return "", nil, false, err + return "", "", nil, false, err } x.RepoName = repoName _, _, worktree, err := x.resolveQualifiedWorktree(input, candidate) if err != nil { - return "", nil, false, err + return "", "", nil, false, err + } + return worktree.Path, "", args[1:], true, nil +} + +func (x *gitCommandOptions) completeSelector(command *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) > 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + if name, repoPrefix, found := strings.CutLast(toComplete, "@"); found { + if name == "" { + return x.runtime.completeRepoSuffix("", repoPrefix, false) + } + return x.runtime.completeRepoSuffix(name, repoPrefix, true) + } + if toComplete != "" && !strings.HasPrefix(toComplete, "@") { + return x.runtime.completeQualifiedWorktreeNames(command, args, toComplete) } - return worktree.Path, args[1:], true, nil + worktrees, _ := x.runtime.completeQualifiedWorktreeNames(command, args, toComplete) + repos, _ := x.runtime.completeRepoSuffix("", strings.TrimPrefix(toComplete, "@"), false) + return append(worktrees, repos...), cobra.ShellCompDirectiveNoFileComp } func gitDirForDirectory(runtime Runtime, directory string) (string, error) { gitDirResult, err := gitOutput(runtime, directory, "rev-parse", "--absolute-git-dir") if err != nil { - return "", fmt.Errorf("not inside a worktree: pass a worktree name or run inside a worktree") + return "", fmt.Errorf("not inside a worktree: pass a worktree name or @ or run inside a worktree") } bareResult, err := gitOutput(runtime, directory, "rev-parse", "--is-bare-repository") @@ -106,7 +144,7 @@ func gitDirForDirectory(runtime Runtime, directory string) (string, error) { return "", err } if bareResult.stdout == "true" { - return "", fmt.Errorf("not inside a worktree: pass a worktree name or run inside a worktree") + return "", fmt.Errorf("not inside a worktree: pass a worktree name or @ or run inside a worktree") } return filepath.Clean(gitDirResult.stdout), nil } diff --git a/internal/timber/git_test.go b/internal/timber/git_test.go index 7f6abe8..6d08b87 100644 --- a/internal/timber/git_test.go +++ b/internal/timber/git_test.go @@ -196,3 +196,43 @@ func TestGitFallsBackToCurrentForGitSubcommand(t *testing.T) { require.NoError(t, result.err, result.stderr) assert.Equal(t, currentPath, strings.TrimSpace(result.stdout)) } + +func TestGitRunsInRepoOnlySelector(t *testing.T) { + t.Parallel() + + testRepository := newTestRepository(t) + + result := runTimberFromWithRuntime(t, testRepository.runtime, testRepository.home, "git", at(testRepoName, ""), "rev-parse", "--absolute-git-dir") + require.NoError(t, result.err, result.stderr) + assert.Equal(t, testRepository.barePath, strings.TrimSpace(result.stdout)) +} + +func TestGitRepoOnlySelectorShowsRemotes(t *testing.T) { + t.Parallel() + + testRepository := newTestRepository(t) + + result := runTimberFromWithRuntime(t, testRepository.runtime, testRepository.home, "git", at(testRepoName, ""), "remote", "-v") + require.NoError(t, result.err, result.stderr) + assert.Contains(t, result.stdout, "origin") +} + +func TestGitRepoOnlySelectorStripsFollowingDashDash(t *testing.T) { + t.Parallel() + + testRepository := newTestRepository(t) + + result := runTimberFromWithRuntime(t, testRepository.runtime, testRepository.home, "git", at(testRepoName, ""), "--", "rev-parse", "--absolute-git-dir") + require.NoError(t, result.err, result.stderr) + assert.Equal(t, testRepository.barePath, strings.TrimSpace(result.stdout)) +} + +func TestGitRepoOnlySelectorUnknownRepo(t *testing.T) { + t.Parallel() + + testRepository := newTestRepository(t) + + result := runTimberFromWithRuntime(t, testRepository.runtime, testRepository.home, "git", "@missing", "status") + require.Error(t, result.err) + assert.Contains(t, result.err.Error(), `unknown repository "missing"`) +}