Skip to content

feat(worktree): Git-first discovery + shared config in git common dir#48

Merged
samzong merged 3 commits intomainfrom
feat/worktree-discovery-share-common-dir
Mar 14, 2026
Merged

feat(worktree): Git-first discovery + shared config in git common dir#48
samzong merged 3 commits intomainfrom
feat/worktree-discovery-share-common-dir

Conversation

@samzong
Copy link
Copy Markdown
Owner

@samzong samzong commented Mar 14, 2026

Summary

Implements two improvements to gmc wt:

1. Git-first worktree discovery

  • gmc wt and gmc wt switch no longer reject non-bare repositories
  • Any directory belonging to a git repo/worktree family now shows the full worktree list
  • .bare remains the preferred layout for gmc wt clone / init, but it is no longer a hard gate for read operations

2. Shared config moves to git common dir

  • Config is now stored at <git-common-dir>/gmc-share.yml (e.g. .git/gmc-share.yml or .bare/gmc-share.yml)
  • Legacy .gmc-shared.yml / .gmc-shared.yaml are still read as fallback for backward compatibility
  • Sync targets are resolved from Git metadata (real wt.Path), no longer constructed by string concatenation
  • Shared resource paths are normalized to repo-relative paths; the old auto-prepend worktree-name behaviour is removed
  • SaveSharedConfig creates the parent directory if needed

3. Display name fix

  • Worktree names in wt list now prefer repo-relative paths; external worktrees fall back to basename instead of raw absolute paths

Tests added

  • cmd/worktree_test.go: default wt output works from a non-bare repo
  • internal/worktree/resource_nonbare_test.go: shared config path resolves to git common dir; sync writes to real worktree paths, not fabricated directories

Checklist

  • go test ./... all green
  • Backward compatible: legacy config filenames still read
  • .bare init/clone behaviour unchanged

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the flexibility and compatibility of gmc's worktree management and shared resource features. It shifts the core logic to be "Git-first" for worktree discovery, allowing gmc wt commands to function seamlessly within any Git repository, not just those following the .bare pattern. Furthermore, it centralizes shared configuration within the Git common directory, improving consistency and supporting diverse repository layouts, while also refining how shared resources are managed and synchronized across worktrees.

Highlights

  • Git-first Worktree Discovery: gmc wt and gmc wt switch commands now support non-bare repositories, allowing worktree discovery from any directory within a Git repository family.
  • Shared Configuration Location: The shared configuration for gmc wt share has moved to the Git common directory (/gmc-share.yml), with backward compatibility for legacy .gmc-shared.yml and .gmc-shared.yaml files.
  • Improved Shared Resource Management: Sync targets are now resolved using Git metadata (real wt.Path), and shared resource paths are normalized to be repository-relative, removing the old auto-prepend worktree-name behavior.
  • Worktree Display Names: Worktree names in gmc wt list now prioritize repo-relative paths, falling back to the basename for external worktrees.
  • Configuration Persistence: The SaveSharedConfig function now automatically creates the necessary parent directories if they do not exist.
  • New Tests: Added new tests to verify default wt output from non-bare repositories and ensure shared config paths resolve correctly to the Git common directory, with sync operations writing to real worktree paths.
Changelog
  • README.md
    • Updated documentation to reflect the new Git-first worktree discovery and shared config location.
  • cmd/worktree.go
    • Removed the check that rejected non-bare repositories from runWorktreeDefault.
    • Introduced displayWorktreeName function to determine worktree display names.
    • Modified printWorktreeTable to use displayWorktreeName for consistent worktree naming.
  • cmd/worktree_share.go
    • Updated the command's help text to specify the new shared config file location.
    • Removed the logic that automatically prepended the worktree name to relative paths when adding shared resources.
  • cmd/worktree_switch.go
    • Removed the IsBareWorktree check, allowing gmc wt switch to operate in any Git worktree setup.
  • cmd/worktree_test.go
    • Added a new test case to ensure gmc wt correctly lists worktrees when run from a non-bare repository.
  • docs/plans/2026-03-14-worktree-discovery-share-common-dir-design.md
    • Added a design document detailing the rationale and decisions behind the worktree discovery and shared config changes.
  • internal/worktree/resource.go
    • Refactored SyncSharedResources to use a new syncSharedResourcesToPath function, enabling direct syncing to specified paths.
    • Updated LoadSharedConfig to locate the shared config file in the Git common directory and support legacy config file names.
    • Modified SaveSharedConfig to create parent directories if they don't exist before writing the config file.
    • Introduced NormalizeSharedResourcePath to convert paths to be repository-relative and validate them.
    • Implemented resolveSharedPaths to determine source and target paths for shared resources, handling different worktree layouts.
    • Updated AddSharedResource to use normalized paths.
    • Added resolveWorktreePath to find the full path of a worktree by its name.
    • Added currentTopLevel to get the current Git repository's top-level directory.
  • internal/worktree/resource_nonbare_test.go
    • Added new tests to verify LoadSharedConfig correctly uses the Git common directory in non-bare worktrees.
    • Added tests to confirm SyncAllSharedResources functions correctly from non-bare worktree repositories.
  • internal/worktree/worktree.go
    • Renamed GetWorktreeRoot to GetGitCommonDir to accurately reflect its purpose of returning the shared Git directory.
    • Introduced GetRepoRoot to find the main repository root, prioritizing .bare discovery and falling back to Git common dir.
    • Updated GetWorktreeRoot to simply call GetRepoRoot.
Activity
  • No human activity recorded yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant improvements to the worktree management functionality, primarily by enabling git-first discovery (removing the hard requirement for a .bare repository layout for read operations) and relocating the shared configuration to the git common directory. This makes the feature more flexible and compatible with existing non-bare repositories. The changes are well-structured and include new tests and documentation. I've identified a couple of high-severity issues regarding ambiguous worktree resolution by basename, which could lead to unpredictable behavior in environments with similarly named worktrees. I also found a minor logging issue where a verbose warning was not being displayed. Addressing these points will enhance the robustness of this new feature.

Comment thread internal/worktree/resource.go Outdated
Comment on lines +309 to +311
if filepath.Base(wt.Path) == worktreeName {
return wt.Path, nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Matching worktrees by basename can be ambiguous and lead to unpredictable behavior. If a user has multiple worktrees with the same basename in different locations (e.g., ~/dev/project1/my-feature and ~/dev/project2/my-feature which are both worktrees of the same repo), this logic will match the first one it finds in the list from c.List(). The order is not guaranteed. This could result in operations being performed on the wrong worktree.

Consider detecting this ambiguity. You could collect all matches and if there's more than one, return an error asking the user to provide a more specific, unique path.

Comment on lines +377 to +380
for _, wt := range worktrees {
if filepath.Base(wt.Path) != parts[0] {
continue
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This logic for resolving legacy shared resource paths (e.g., worktree-name/path/to/file) has the same ambiguity as in resolveWorktreePath. It matches the source worktree by its basename (filepath.Base(wt.Path)).

If multiple worktrees share the same basename, this could resolve the source path from the wrong worktree, leading to incorrect files being synced. This is especially risky as it could overwrite files with incorrect content.

It would be safer to detect when parts[0] matches multiple worktree basenames and return an error to the user, prompting for a more specific configuration.

Comment thread internal/worktree/resource.go Outdated
Comment on lines +383 to +384
var report Report
report.Warn(fmt.Sprintf("Skipping %s: source worktree is target", res.Path))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This warning message will never be displayed. A new local report variable is created, a warning is added to it, and then the variable goes out of scope. The Warn method appends to the report's internal slice, but since the report is never used, the warning is lost.

If the intention is to log a verbose message, you should use a mechanism that ensures the message is displayed, such as writing directly to os.Stderr.

Suggested change
var report Report
report.Warn(fmt.Sprintf("Skipping %s: source worktree is target", res.Path))
report.Warn(fmt.Sprintf("Skipping %s: source worktree is target", res.Path))

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates gmc wt to discover worktrees using Git metadata (working in non-bare repos/worktrees) and relocates the shared-resource config to the repository’s git common dir so wt share works across both bare and non-bare layouts.

Changes:

  • Add Git-first worktree discovery and adjust display naming to prefer repo-relative paths (with sane fallback for external worktrees).
  • Move shared-resource config to <git-common-dir>/gmc-share.yml and update sync to target real worktree paths from Git metadata.
  • Add non-bare repo/worktree test coverage for wt default output and shared-resource sync/config resolution.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/worktree/worktree.go Adds GetGitCommonDir/GetRepoRoot and aligns “root” resolution with Git metadata.
internal/worktree/resource.go Moves shared config into git common dir, normalizes shared paths, and syncs to real worktree paths.
internal/worktree/resource_nonbare_test.go Tests config resolution and sync behavior from a non-bare linked worktree.
cmd/worktree.go Makes default wt output work in non-bare repos and improves worktree name display.
cmd/worktree_switch.go Removes bare-only gating so switching works in non-bare repos too.
cmd/worktree_share.go Updates help text and removes legacy auto-prepend behavior in interactive add.
cmd/worktree_test.go Tests default wt output from a non-bare linked worktree.
docs/plans/2026-03-14-worktree-discovery-share-common-dir-design.md Documents design decisions and verification plan.
README.md Documents Git-first discovery and new shared-config location/behavior.

Comment on lines +292 to +323
func (c *Client) resolveWorktreePath(worktreeName string) (string, error) {
if worktreeName == "" {
return "", errors.New("worktree name cannot be empty")
}

repoRoot, _ := c.GetRepoRoot()
worktrees, err := c.List()
if err != nil {
if repoRoot != "" {
return filepath.Join(repoRoot, worktreeName), nil
}
return "", err
}
for _, wt := range worktrees {
if wt.Path == worktreeName {
return wt.Path, nil
}
if filepath.Base(wt.Path) == worktreeName {
return wt.Path, nil
}
if repoRoot != "" {
if rel, relErr := filepath.Rel(repoRoot, wt.Path); relErr == nil && rel == worktreeName {
return wt.Path, nil
}
}
}

if repoRoot != "" {
return filepath.Join(repoRoot, worktreeName), nil
}
return worktreeName, nil
}
Comment thread internal/worktree/resource.go Outdated
Comment on lines +383 to +384
var report Report
report.Warn(fmt.Sprintf("Skipping %s: source worktree is target", res.Path))
Comment on lines 195 to 229
@@ -192,7 +216,7 @@ func (c *Client) AddSharedResource(path string, strategy ResourceStrategy) (Repo

if !found {
cfg.Resources = append(cfg.Resources, SharedResource{
Path: path,
Path: normalizedPath,
Strategy: strategy,
})
}
@@ -201,7 +225,7 @@ func (c *Client) AddSharedResource(path string, strategy ResourceStrategy) (Repo
return report, err
}

report.Info(fmt.Sprintf("Updated shared resource: %s (%s)", path, strategy))
report.Info(fmt.Sprintf("Updated shared resource: %s (%s)", normalizedPath, strategy))
return report, nil
Comment on lines +147 to 160
configPath := filepath.Join(commonDir, sharedConfigName)
legacyCandidates := []string{
filepath.Join(commonDir, legacySharedConfigYML),
filepath.Join(commonDir, legacySharedConfigYAML),
}

configPath := filepath.Join(root, ".gmc-shared.yml")
if _, err := os.Stat(configPath); os.IsNotExist(err) {
yamlPath := filepath.Join(root, ".gmc-shared.yaml")
if _, err := os.Stat(yamlPath); err == nil {
configPath = yamlPath
for _, candidate := range legacyCandidates {
if _, statErr := os.Stat(candidate); statErr == nil {
configPath = candidate
break
}
}
}
Comment thread internal/worktree/resource.go Outdated
Comment on lines +141 to +146
commonDir = root
} else {
return nil, "", err
}
}

Comment on lines +344 to +367
func (c *Client) NormalizeSharedResourcePath(path string) (string, error) {
trimmed := strings.TrimSpace(path)
if trimmed == "" {
return "", errors.New("shared resource path cannot be empty")
}

if filepath.IsAbs(trimmed) {
currentRoot := c.currentTopLevel()
if currentRoot != "" {
rel, err := filepath.Rel(currentRoot, trimmed)
if err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
trimmed = rel
}
}
}

trimmed = filepath.Clean(trimmed)
if trimmed == "." {
return "", errors.New("shared resource path cannot be '.'")
}
if strings.HasPrefix(trimmed, ".."+string(filepath.Separator)) || trimmed == ".." {
return "", fmt.Errorf("shared resource path must stay within the worktree: %s", path)
}
return trimmed, nil
@samzong
Copy link
Copy Markdown
Owner Author

samzong commented Mar 14, 2026

/gemini review

Addressed the previous review concerns in this update:

  • reject ambiguous basename worktree matches instead of picking the first match
  • stop fabricating unresolved worktree target paths unless an existing directory actually exists
  • enforce shared paths stay worktree-relative and reject unsafe absolute/escaping paths
  • normalize remove-path input just like add
  • restore legacy repo-root config fallback

Tests: go test ./... is green.

@samzong
Copy link
Copy Markdown
Owner Author

samzong commented Mar 14, 2026

@copilot-pull-request-reviewer please re-review the latest changes.

Addressed the shared-path resolution and compatibility issues from the previous pass, and go test ./... is green.

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 14, 2026

@samzong I've opened a new pull request, #49, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly improves the worktree functionality by removing the hard dependency on the .bare repository layout, making it usable with standard Git repositories that use worktrees. The changes are well-structured, moving the shared configuration to the git common directory (.git/ or .bare/) with backward compatibility, and resolving worktree paths directly from Git metadata. The introduction of new tests for non-bare repository scenarios is a great addition that increases confidence in these changes. I have a couple of suggestions for code simplification and to improve robustness.

Comment on lines +310 to +318
if err != nil {
if repoRoot != "" {
candidate := filepath.Join(repoRoot, worktreeName)
if info, statErr := os.Stat(candidate); statErr == nil && info.IsDir() {
return candidate, nil
}
}
return "", err
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The fallback logic for when c.List() fails is risky. c.List() should be the source of truth for what Git considers a worktree. If it fails, falling back to a filesystem check with os.Stat could lead to operations on a directory that isn't a valid worktree from Git's perspective. This might hide underlying repository state issues and cause unexpected behavior. It would be safer to propagate the error from c.List() directly to ensure the caller is aware of the problem.

if err != nil {
	return "", err
}

Comment on lines +212 to +215
if filepath.Base(commonDir) == ".bare" {
return filepath.Dir(commonDir), nil
}
return filepath.Dir(commonDir), nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This if condition is redundant. Since GetGitCommonDir returns an absolute path, filepath.Dir(commonDir) correctly returns the parent directory (the repository root) for both .bare and .git common directories. You can simplify the code by removing this if block.

return filepath.Dir(commonDir), nil

@samzong samzong merged commit d7aa54a into main Mar 14, 2026
1 check passed
@samzong samzong deleted the feat/worktree-discovery-share-common-dir branch March 14, 2026 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants