From 7a17a7e70c5ccef81d877d13f6c518e0ce8f074c Mon Sep 17 00:00:00 2001 From: Jad Bitar Date: Fri, 1 May 2026 22:46:26 -0400 Subject: [PATCH 1/3] fix(git-hop): set upstream tracking when creating new worktree branches When 'git hop clone' or 'git hop add' creates a new branch, configure it to track the remote default branch (e.g., origin/main). This fixes first push failures due to missing upstream configuration. Changes: - Add trackBranch parameter to git.CreateWorktree - Pass origin/ when creating branches in add, fork, clone, and doctor repair flows - Add regression tests for --track flag behavior --- LICENSE | 3 +- cmd/add.go | 2 +- cmd/doctor.go | 2 +- internal/git/wrapper.go | 8 ++- internal/git/wrapper_test.go | 57 +++++++++++++++++---- internal/hop/clone_worktree.go | 2 +- internal/hop/conversion.go | 2 +- internal/hop/fork.go | 2 +- internal/hop/worktree.go | 12 +++-- internal/hop/worktree_transactional_test.go | 5 ++ test/mocks/mock_git.go | 2 +- 11 files changed, 75 insertions(+), 22 deletions(-) diff --git a/LICENSE b/LICENSE index 18a733c..8d7b16f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) -Copyright © 2025 Jad Bitar +Copyright © 2026 Idea Crafters LLC +Copyright © 2026 Jad Bitar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/cmd/add.go b/cmd/add.go index 8a2b866..8f18b78 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -118,7 +118,7 @@ var addCmd = &cobra.Command{ // Create Worktree in the current hub wm := hop.NewWorktreeManager(fs, g) - worktreePath, err = wm.CreateWorktreeTransactional(hopspace, hubPath, branch, globalConfig.Defaults.WorktreeLocation, hub.Config.Repo.Org, hub.Config.Repo.Repo) + worktreePath, err = wm.CreateWorktreeTransactional(hopspace, hubPath, branch, globalConfig.Defaults.WorktreeLocation, hub.Config.Repo.Org, hub.Config.Repo.Repo, hub.Config.Repo.DefaultBranch) if err != nil { // Check if it's a state error if stateErr, ok := err.(*hop.StateError); ok { diff --git a/cmd/doctor.go b/cmd/doctor.go index 0bce88d..32473da 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -194,7 +194,7 @@ Use --fix to automatically repair issues.`, // Recreate the worktree using git g := git.New() - if err := g.CreateWorktree(hopspacePath, b.HopspaceBranch, linkPath, "", false); err != nil { + if err := g.CreateWorktree(hopspacePath, b.HopspaceBranch, linkPath, "", false, "origin/"+b.HopspaceBranch); err != nil { output.Error("Failed to recreate worktree: %v", err) continue } diff --git a/internal/git/wrapper.go b/internal/git/wrapper.go index c17ed51..f3fa6f0 100644 --- a/internal/git/wrapper.go +++ b/internal/git/wrapper.go @@ -13,7 +13,7 @@ import ( type GitInterface interface { Clone(uri, path, branch string) error CloneBare(uri, path string) error - CreateWorktree(hopspacePath, branch, path, base string, forceCreate bool) error + CreateWorktree(hopspacePath, branch, path, base string, forceCreate bool, trackBranch string) error WorktreeRemove(hopspacePath, path string, force bool) error WorktreePrune(hopspacePath string) error WorktreeRepair(basePath string) error @@ -145,7 +145,8 @@ func (g *Git) CloneBare(uri, path string) error { // CreateWorktree creates a new worktree. // If the branch exists and forceCreate is false, it links the existing branch. // If the branch doesn't exist or forceCreate is true, it creates a new branch from base (or HEAD if base is empty). -func (g *Git) CreateWorktree(hopspacePath, branch, path, base string, forceCreate bool) error { +// If trackBranch is specified, the new branch will track it (e.g., "origin/main"). +func (g *Git) CreateWorktree(hopspacePath, branch, path, base string, forceCreate bool, trackBranch string) error { args := []string{"worktree", "add"} if forceCreate { @@ -168,6 +169,9 @@ func (g *Git) CreateWorktree(hopspacePath, branch, path, base string, forceCreat if branchExists != nil { // Branch doesn't exist, create it with -b args = []string{"worktree", "add", "-b", branch, path} + if trackBranch != "" { + args = append(args, "--track", trackBranch) + } if base != "" { args = append(args, base) } diff --git a/internal/git/wrapper_test.go b/internal/git/wrapper_test.go index da1fe48..046e90f 100644 --- a/internal/git/wrapper_test.go +++ b/internal/git/wrapper_test.go @@ -17,7 +17,7 @@ func TestCreateWorktree_LinkExistingBranch(t *testing.T) { g := &Git{Runner: runner} // Success case - existing branch links successfully - err := g.CreateWorktree("/hub", "existing-branch", "/path/to/worktree", "", false) + err := g.CreateWorktree("/hub", "existing-branch", "/path/to/worktree", "", false, "") assert.NoError(t, err) assert.Equal(t, 1, runner.callCount, "Should call git once") assert.Contains(t, runner.lastCommand, "worktree add /path/to/worktree existing-branch") @@ -35,7 +35,7 @@ func TestCreateWorktree_CreateNewBranch(t *testing.T) { runner.errors["/hub:git worktree add /path/to/worktree new-branch"] = fmt.Errorf("branch not found") runner.errors["/hub:git rev-parse --verify refs/heads/new-branch"] = fmt.Errorf("not a valid ref") - err := g.CreateWorktree("/hub", "new-branch", "/path/to/worktree", "HEAD", false) + err := g.CreateWorktree("/hub", "new-branch", "/path/to/worktree", "HEAD", false, "") assert.NoError(t, err) assert.Equal(t, 3, runner.callCount, "Should call git three times - link attempt, rev-parse check, then create") assert.Contains(t, runner.lastCommand, "worktree add -b new-branch /path/to/worktree HEAD") @@ -53,7 +53,7 @@ func TestCreateWorktree_CreateNewBranchFromBase(t *testing.T) { runner.errors["/hub:git worktree add /path/to/worktree feature-branch"] = fmt.Errorf("branch not found") runner.errors["/hub:git rev-parse --verify refs/heads/feature-branch"] = fmt.Errorf("not a valid ref") - err := g.CreateWorktree("/hub", "feature-branch", "/path/to/worktree", "develop", false) + err := g.CreateWorktree("/hub", "feature-branch", "/path/to/worktree", "develop", false, "") assert.NoError(t, err) assert.Equal(t, 3, runner.callCount) assert.Contains(t, runner.lastCommand, "worktree add -b feature-branch /path/to/worktree develop") @@ -68,7 +68,7 @@ func TestCreateWorktree_ForceCreate(t *testing.T) { g := &Git{Runner: runner} // With forceCreate=true, should create directly without trying to link - err := g.CreateWorktree("/hub", "forced-branch", "/path/to/worktree", "HEAD", true) + err := g.CreateWorktree("/hub", "forced-branch", "/path/to/worktree", "HEAD", true, "") assert.NoError(t, err) assert.Equal(t, 1, runner.callCount, "Should call git once with -b flag") assert.Contains(t, runner.lastCommand, "worktree add -b forced-branch /path/to/worktree HEAD") @@ -82,7 +82,7 @@ func TestCreateWorktree_ForceCreateWithoutBase(t *testing.T) { } g := &Git{Runner: runner} - err := g.CreateWorktree("/hub", "forced-branch", "/path/to/worktree", "", true) + err := g.CreateWorktree("/hub", "forced-branch", "/path/to/worktree", "", true, "") assert.NoError(t, err) assert.Equal(t, 1, runner.callCount) assert.Contains(t, runner.lastCommand, "worktree add -b forced-branch /path/to/worktree") @@ -102,7 +102,7 @@ func TestCreateWorktree_BothCallsFail(t *testing.T) { runner.errors["/hub:git rev-parse --verify refs/heads/bad-branch"] = fmt.Errorf("not a valid ref") runner.errors["/hub:git worktree add -b bad-branch /path/to/worktree HEAD"] = fmt.Errorf("permission denied") - err := g.CreateWorktree("/hub", "bad-branch", "/path/to/worktree", "HEAD", false) + err := g.CreateWorktree("/hub", "bad-branch", "/path/to/worktree", "HEAD", false, "") assert.Error(t, err) assert.Contains(t, err.Error(), "permission denied") assert.Equal(t, 3, runner.callCount, "Should try link, rev-parse, then create") @@ -121,7 +121,7 @@ func TestCreateWorktree_BranchExistsButCheckedOut(t *testing.T) { // rev-parse succeeds — branch exists, so we should NOT try -b // (default mock returns no error) - err := g.CreateWorktree("/hub", "my-branch", "/path/to/worktree", "HEAD", false) + err := g.CreateWorktree("/hub", "my-branch", "/path/to/worktree", "HEAD", false, "") assert.Error(t, err) assert.Contains(t, err.Error(), "already checked out") assert.Equal(t, 2, runner.callCount, "Should try link and rev-parse, but not -b since branch exists") @@ -137,7 +137,7 @@ func TestCreateWorktree_ForceCreateFails(t *testing.T) { runner.errors["/hub:git worktree add -b fail-branch /path/to/worktree HEAD"] = fmt.Errorf("disk full") - err := g.CreateWorktree("/hub", "fail-branch", "/path/to/worktree", "HEAD", true) + err := g.CreateWorktree("/hub", "fail-branch", "/path/to/worktree", "HEAD", true, "") assert.Error(t, err) assert.Contains(t, err.Error(), "disk full") assert.Equal(t, 1, runner.callCount, "Should only try once with forceCreate") @@ -153,10 +153,49 @@ func TestCreateWorktree_EmptyParameters(t *testing.T) { // Should still attempt the command even with empty parameters // (git will handle validation) - err := g.CreateWorktree("", "", "", "", false) + err := g.CreateWorktree("", "", "", "", false, "") assert.NoError(t, err) // Mock doesn't validate parameters } +// TestCreateWorktree_SetsUpstreamTracking tests that --track flag is used when creating branches +func TestCreateWorktree_SetsUpstreamTracking(t *testing.T) { + runner := &MockRunner{ + responses: make(map[string]string), + errors: make(map[string]error), + } + g := &Git{Runner: runner} + + // First call fails (branch doesn't exist), rev-parse confirms it doesn't exist, then create with tracking + runner.errors["/hub:git worktree add /path/to/worktree main"] = fmt.Errorf("branch not found") + runner.errors["/hub:git rev-parse --verify refs/heads/main"] = fmt.Errorf("not a valid ref") + + err := g.CreateWorktree("/hub", "main", "/path/to/worktree", "HEAD", false, "origin/main") + assert.NoError(t, err) + assert.Equal(t, 3, runner.callCount, "Should call git three times") + // Verify the create command includes --track flag + assert.Contains(t, runner.lastCommand, "worktree add -b main /path/to/worktree --track origin/main HEAD") +} + +// TestCreateWorktree_NoTrackingWhenEmpty tests that --track is omitted when trackBranch is empty +func TestCreateWorktree_NoTrackingWhenEmpty(t *testing.T) { + runner := &MockRunner{ + responses: make(map[string]string), + errors: make(map[string]error), + } + g := &Git{Runner: runner} + + // First call fails (branch doesn't exist), rev-parse confirms it doesn't exist, then create without tracking + runner.errors["/hub:git worktree add /path/to/worktree feature"] = fmt.Errorf("branch not found") + runner.errors["/hub:git rev-parse --verify refs/heads/feature"] = fmt.Errorf("not a valid ref") + + err := g.CreateWorktree("/hub", "feature", "/path/to/worktree", "develop", false, "") + assert.NoError(t, err) + assert.Equal(t, 3, runner.callCount, "Should call git three times") + // Verify the create command does NOT include --track flag + assert.Contains(t, runner.lastCommand, "worktree add -b feature /path/to/worktree develop") + assert.NotContains(t, runner.lastCommand, "--track") +} + // TestDeleteLocalBranch tests force-deleting a local branch func TestDeleteLocalBranch(t *testing.T) { runner := &MockRunner{ diff --git a/internal/hop/clone_worktree.go b/internal/hop/clone_worktree.go index 23b8b84..d01a8fc 100644 --- a/internal/hop/clone_worktree.go +++ b/internal/hop/clone_worktree.go @@ -213,7 +213,7 @@ func cloneRegularRepo(fs afero.Fs, g git.GitInterface, uri, projectRoot, default // Create main worktree under hops/ mainPath := filepath.Join(hopsDir, defaultBranch) - if err := g.CreateWorktree(projectRoot, defaultBranch, mainPath, defaultBranch, false); err != nil { + if err := g.CreateWorktree(projectRoot, defaultBranch, mainPath, defaultBranch, false, "origin/"+defaultBranch); err != nil { return fmt.Errorf("failed to create main worktree: %w", err) } diff --git a/internal/hop/conversion.go b/internal/hop/conversion.go index 176f4e4..0c8bdb7 100644 --- a/internal/hop/conversion.go +++ b/internal/hop/conversion.go @@ -199,7 +199,7 @@ func (c *Converter) performConversion(repoPath string, useBare bool, result *con continue } worktreePath := c.getWorktreePathForBranch(branch, repoPath) - if err := c.git.CreateWorktree(repoPath, branch, worktreePath, "", false); err != nil { + if err := c.git.CreateWorktree(repoPath, branch, worktreePath, "", false, "origin/"+branch); err != nil { result.Warnings = append(result.Warnings, fmt.Sprintf("failed to create worktree for %s: %v", branch, err)) } } diff --git a/internal/hop/fork.go b/internal/hop/fork.go index c1e7d34..8d3f434 100644 --- a/internal/hop/fork.go +++ b/internal/hop/fork.go @@ -152,7 +152,7 @@ func ForkAttach(fs afero.Fs, g git.GitInterface, uri, branch, hubPath string) er wm := NewWorktreeManager(fs, g) // For forks, the hopspace path acts as the hub path (worktrees are stored in hopspace) locationPattern := "{hubPath}/hops/{branch}" - worktreePath, err := wm.CreateWorktree(forkHopspace, forkHopspacePath, branch, locationPattern, forkHopspace.Config.Repo.Org, forkHopspace.Config.Repo.Repo) + worktreePath, err := wm.CreateWorktree(forkHopspace, forkHopspacePath, branch, locationPattern, forkHopspace.Config.Repo.Org, forkHopspace.Config.Repo.Repo, forkHopspace.Config.Repo.DefaultBranch) if err != nil { return fmt.Errorf("failed to create worktree in fork: %v", err) } diff --git a/internal/hop/worktree.go b/internal/hop/worktree.go index 70546cf..12de14d 100644 --- a/internal/hop/worktree.go +++ b/internal/hop/worktree.go @@ -25,7 +25,7 @@ func NewWorktreeManager(fs afero.Fs, g git.GitInterface) *WorktreeManager { } // CreateWorktreeTransactional creates a git worktree with validation and auto-cleanup -func (m *WorktreeManager) CreateWorktreeTransactional(hopspace *Hopspace, hubPath string, branch string, locationPattern string, org string, repo string) (string, error) { +func (m *WorktreeManager) CreateWorktreeTransactional(hopspace *Hopspace, hubPath string, branch string, locationPattern string, org string, repo string, defaultBranch string) (string, error) { // Validate inputs early (before path computation) if hubPath == "" { return "", fmt.Errorf("hubPath cannot be empty") @@ -64,7 +64,7 @@ func (m *WorktreeManager) CreateWorktreeTransactional(hopspace *Hopspace, hubPat } // Step 4: Call existing CreateWorktree method to do the actual work - _, err = m.CreateWorktree(hopspace, hubPath, branch, locationPattern, org, repo) + _, err = m.CreateWorktree(hopspace, hubPath, branch, locationPattern, org, repo, defaultBranch) if err != nil { // Return our cleaned path on error return worktreePath, err @@ -74,7 +74,7 @@ func (m *WorktreeManager) CreateWorktreeTransactional(hopspace *Hopspace, hubPat } // CreateWorktree creates a git worktree at the configured location -func (m *WorktreeManager) CreateWorktree(hopspace *Hopspace, hubPath string, branch string, locationPattern string, org string, repo string) (string, error) { +func (m *WorktreeManager) CreateWorktree(hopspace *Hopspace, hubPath string, branch string, locationPattern string, org string, repo string, defaultBranch string) (string, error) { // Validate inputs if hubPath == "" { return "", fmt.Errorf("hubPath cannot be empty") @@ -144,7 +144,11 @@ func (m *WorktreeManager) CreateWorktree(hopspace *Hopspace, hubPath string, bra // baseWorktreePath is already absolute after resolution by ResolveWorktreePath // CreateWorktree will automatically try to link existing branch first, then create if needed - if err := m.git.CreateWorktree(baseWorktreePath, branch, worktreePath, "HEAD", false); err != nil { + trackBranch := "" + if defaultBranch != "" { + trackBranch = "origin/" + defaultBranch + } + if err := m.git.CreateWorktree(baseWorktreePath, branch, worktreePath, "HEAD", false, trackBranch); err != nil { return "", fmt.Errorf("failed to create worktree: %w", err) } diff --git a/internal/hop/worktree_transactional_test.go b/internal/hop/worktree_transactional_test.go index 89c2d97..1d17465 100644 --- a/internal/hop/worktree_transactional_test.go +++ b/internal/hop/worktree_transactional_test.go @@ -56,6 +56,7 @@ func TestCreateWorktreeTransactional_Clean(t *testing.T) { locationPattern, org, repo, + "", ) // Assert - expect git error but path should be computed correctly @@ -120,6 +121,7 @@ func TestCreateWorktreeTransactional_CleansUpOrphanedDirectory(t *testing.T) { locationPattern, org, repo, + "", ) // Assert @@ -187,6 +189,7 @@ func TestCreateWorktreeTransactional_AlreadyExists(t *testing.T) { locationPattern, org, repo, + "", ) // Assert - should return error from CreateWorktree about already existing @@ -218,6 +221,7 @@ func TestCreateWorktreeTransactional_EmptyHubPath(t *testing.T) { "{hubPath}/../hops/{branch}", "test-org", "test-repo", + "", ) // Assert @@ -252,6 +256,7 @@ func TestCreateWorktreeTransactional_EmptyBranch(t *testing.T) { "{hubPath}/../hops/{branch}", "test-org", "test-repo", + "", ) // Assert diff --git a/test/mocks/mock_git.go b/test/mocks/mock_git.go index 0d2e498..d801c99 100644 --- a/test/mocks/mock_git.go +++ b/test/mocks/mock_git.go @@ -85,7 +85,7 @@ func (m *MockGit) CloneBare(uri, path string) error { } // CreateWorktree mocks creating a worktree and tracks the operation -func (m *MockGit) CreateWorktree(hopspacePath, branch, path, base string, forceCreate bool) error { +func (m *MockGit) CreateWorktree(hopspacePath, branch, path, base string, forceCreate bool, trackBranch string) error { m.CreatedWorktrees = append(m.CreatedWorktrees, path) m.LastWorktreeBasePath = hopspacePath m.LastWorktreeBranch = branch From 84d1b091e69e26415556d0cb1079a7a6859d5189 Mon Sep 17 00:00:00 2001 From: Jad Bitar Date: Fri, 1 May 2026 23:55:20 -0400 Subject: [PATCH 2/3] docs: apply intent-driven progressive disclosure principles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure README and configuration guide to lead with reader intent and reveal information in layers: README changes: - Lead with pain point ("work on multiple branches in parallel...") instead of feature list - Move Architecture section to "Advanced: How It Works" at end - Add verification steps to all workflows - Reorder: Intent → Quickstart → Commands → Workflows → Config → Troubleshooting → Advanced Configuration.md changes: - Add Quick Start TL;DR upfront ("you only edit global.json") - Reorder settings table BEFORE full JSON schema (reference, not primary) - Add "Don't edit" warnings at top of auto-managed file sections - Reframe Configuration Hierarchy as task: "Override Settings for Specific Situations" with concrete examples - Move directory structure to reference section (still available, non-blocking) Follow principles from intent-driven progressive disclosure: - Start with reader's goal (Do, Learn, Fix, Decide, Reference) - Essential context before steps - Verification after actions - Advanced details below main path - One primary intent per section --- README.md | 229 +++++++++++++++++++----------------------- docs/configuration.md | 103 ++++++++++++------- 2 files changed, 166 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index 8efe7c1..d06dfd6 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,7 @@ > force-pushed and rewritten** multiple times. Do not fork, clone, or > depend on this repo in any capacity until we tag a stable release. -A Git subcommand that wraps `git worktree` with deterministic, isolated, and reproducible multi-branch development environments. - -## Overview - -git-hop eliminates the friction of managing multiple Git worktrees by automatically organizing worktrees, allocating resources (ports, volumes, networks), and providing lifecycle management for Docker environments. Work on multiple branches in parallel without port conflicts, manual configuration, or lost context. +Work on multiple branches in parallel without manual port setup, directory management, or lost context. Each branch gets its own isolated environment with deterministic ports and volumes. **Perfect for:** - Multi-branch development workflows @@ -27,15 +23,6 @@ git-hop eliminates the friction of managing multiple Git worktrees by automatica - Multi-tenant setups - Monorepo optimization (see Git's native monorepo support) -## Features - -- 🚀 **Automatic Navigation** - Optional shell integration for seamless worktree switching -- 🔗 **Smart Symlinks** - `current` symlink always points to your last worktree -- 🐳 **Docker Integration** - Isolated environments with deterministic port allocation -- 📦 **Dependency Management** - Automatic npm/yarn/pnpm installation per worktree -- 🔄 **State Tracking** - Track all worktrees across your system -- 🛠️ **Zero Config** - Sensible defaults, works out of the box - ## Quick Start ### Install @@ -82,6 +69,12 @@ git hop add feature-x cd feature-x ``` +**Verify:** Confirm setup succeeded: +```bash +git hop list # See all worktrees +pwd # Should show .../feature-x +``` + **Optional:** Install shell integration for automatic directory switching: ```bash @@ -144,114 +137,6 @@ git hop env start # Start Docker services for current worktree git hop env stop # Stop Docker services ``` -### Global Flags - -Available on all commands: - -```bash ---config # Path to config file ---json # Output in JSON format ---porcelain # Machine-readable output ---quiet, -q # Suppress output ---verbose, -v # Verbose output ---force # Bypass safety checks ---dry-run # Preview changes without applying ---global, -g # Use global hopspace instead of local ---git-domain # Git domain for shorthand notation (default: github.com) ---help, -h # Show command help ---version # Show version information -``` - -## Architecture - -### Hubs - -A **hub** is a directory that serves as your local working context. It contains: -- A `hop.json` configuration file tracking all worktrees -- A `.git` reference to the bare repository -- Direct access to worktrees via paths stored in config - -``` -my-repo/ # Hub directory (local context) - .git # Bare repository reference - hop.json # Hub configuration (tracks worktree paths) -``` - -The hub's `hop.json` maintains references to all worktrees with their full paths, allowing you to quickly switch between branches without manual path management. - -### Hopspaces - -A **hopspace** is the canonical storage location for all worktrees of a repository. Located at: - -``` -$GIT_HOP_DATA_HOME//// - hop.json # Hopspace configuration - ports.json # Port allocations - volumes.json # Volume allocations - feature-x/ # Actual worktree directory - feature-y/ - ... -``` - -All worktrees for a repository reference the same hopspace, ensuring consistency. - -### Deterministic Resource Allocation - -Ports, volumes, and networks are derived from stable hashing: - -- **Same branch = same ports** across worktrees (reproducible) -- **Different branches = different ports** (no conflicts) -- **Predictable allocation** (no manual configuration) - -Example: branch `feature-x` always gets ports 11500-11505 if not already assigned. - -## Configuration - -Configuration follows a hierarchy (higher priority overrides lower): - -1. Environment variables -2. Command-line flags -3. `$XDG_CONFIG_HOME/git-hop/config.json` (global) -4. Hub-level `hop.json` -5. Hopspace-level `hop.json` - -### Configuration File - -Create `$XDG_CONFIG_HOME/git-hop/config.json`: - -```json -{ - "auto_env_start": "detect", - "port_base": 10000, - "port_limit": 5000, - "defaults": { - "worktree_location": "hops" - } -} -``` - -Configuration options: - -- `auto_env_start` - Auto-start Docker services (`true`, `false`, or `"detect"` to start only if services exist) -- `port_base` - Starting port for allocation (default: 10000) -- `port_limit` - Maximum ports available (default: 5000) -- `defaults.worktree_location` - Directory for storing worktrees (default: `hops`) - -### Environment Variables - -```bash -GIT_HOP_DATA_HOME # Hopspace storage location (OS-specific default) -GIT_HOP_CONFIG_HOME # Config directory (default: $XDG_CONFIG_HOME/git-hop) -GIT_HOP_CACHE_DIR # Cache directory (default: $XDG_CACHE_HOME/git-hop) -GIT_HOP_LOG_LEVEL # Log level: debug, info, warn, error (default: info) -``` - -### Data Directory Defaults - -- **Linux/Unix**: `~/.local/share/git-hop` -- **macOS**: `~/Library/Application Support/git-hop` -- **Windows**: `%LOCALAPPDATA%\git-hop` - ## Common Workflows ### Add a New Feature Branch @@ -264,6 +149,12 @@ git hop add feature-new-ui cd feature-new-ui ``` +**Verify:** Confirm the worktree was created: +```bash +git hop list # See feature-new-ui in the list +pwd # Should show .../feature-new-ui +``` + ### Switch Between Branches ```bash @@ -276,6 +167,11 @@ cd ../feature-existing # Or use your shell navigation (e.g., cd /path/to/hop/feature-existing) ``` +**Verify:** Confirm you're in the right worktree: +```bash +git branch # Should show the feature branch checked out +``` + ### Start/Stop Services ```bash @@ -284,8 +180,16 @@ git hop env start # Stop services git hop env stop +``` -# Auto-start on worktree creation +**Verify:** Check service status: +```bash +git hop status # Shows running services +docker ps # Verify containers are running +``` + +To auto-start services on worktree creation: +```bash git hop config auto_env_start detect ``` @@ -320,6 +224,24 @@ git hop prune git hop doctor --fix ``` +## Configuration + +For detailed configuration, see [Configuration Guide](docs/configuration.md). + +Quick setup: + +```bash +git hop config --help # View all settings +git hop config port_base 20000 # Change port base +``` + +Configuration hierarchy (first found wins): +1. Environment variables +2. Hub-level `hop.json` +3. Hopspace-level `hop.json` +4. Global `~/.config/git-hop/config.json` +5. Built-in defaults + ## Troubleshooting ### Port Conflicts @@ -405,7 +327,60 @@ Place hooks in `$GIT_HOP_CONFIG_HOME/hooks/` or hopspace-specific directories to See [Hooks System](docs/hooks.md) for detailed examples. -## Output Formats +## Advanced: How It Works + +### How git-hop Organizes Your Work + +Git-hop keeps your branches isolated across two layers: + +**Hub** — the directory where you run `git hop` commands. Usually `~/my-repo/`. Tells git-hop which branches exist locally. Contains `hop.json` and a `.git` reference. + +**Hopspace** — centralized storage for all branches of a repository, shared by all hubs. Located at `$GIT_HOP_DATA_HOME///`. Contains actual worktree directories, port allocations, and volume mappings. + +Why two? Hubs are your local workspace; hopspace is shared storage. This lets you have multiple hubs (checkouts) of the same repository, all using the same branches without duplication. + +### Hubs + +A **hub** is a directory that serves as your local working context. It contains: +- A `hop.json` configuration file tracking all worktrees +- A `.git` reference to the bare repository +- Direct access to worktrees via paths stored in config + +``` +my-repo/ # Hub directory (local context) + .git # Bare repository reference + hop.json # Hub configuration (tracks worktree paths) +``` + +The hub's `hop.json` maintains references to all worktrees with their full paths, allowing you to quickly switch between branches without manual path management. + +### Hopspaces + +A **hopspace** is the canonical storage location for all worktrees of a repository. Located at: + +``` +$GIT_HOP_DATA_HOME//// + hop.json # Hopspace configuration + ports.json # Port allocations + volumes.json # Volume allocations + feature-x/ # Actual worktree directory + feature-y/ + ... +``` + +All worktrees for a repository reference the same hopspace, ensuring consistency. + +### Deterministic Resource Allocation + +Ports, volumes, and networks are derived from stable hashing: + +- **Same branch = same ports** across worktrees (reproducible) +- **Different branches = different ports** (no conflicts) +- **Predictable allocation** (no manual configuration) + +Example: branch `feature-x` always gets ports 11500-11505 if not already assigned. + +### Output Formats Control output with flags: @@ -423,9 +398,9 @@ git hop list --porcelain git hop status --quiet ``` -## Configuration Examples +### Configuration Examples -### Multi-Repository Setup +#### Multi-Repository Setup Manage multiple repositories with different configurations: @@ -444,7 +419,7 @@ git hop config port_base 20000 Each repository has its own hopspace with independent resource allocation. -### Docker with Custom Services +#### Docker with Custom Services Repositories with docker-compose.yml automatically detect and allocate resources: diff --git a/docs/configuration.md b/docs/configuration.md index 0e9f98e..c20b60d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,10 +1,25 @@ # Configuration -## Overview +Git-hop separates three types of data across your system. Here's where they live and what you need to know. -git-hop uses a layered configuration system that separates user preferences (configuration) from repository tracking (state). This follows the XDG Base Directory specification for portability and organization. +## Quick Start -## Directory Structure +**TL;DR:** You only edit `global.json`. Everything else is managed automatically. + +```bash +# View your settings +git hop config + +# Change a setting +git hop config port_base 20000 + +# See where settings come from +git hop config --verbose +``` + +--- + +## Where Does git-hop Store Things? git-hop uses different directories for different types of data: @@ -103,17 +118,31 @@ export GIT_HOP_LOG_LEVEL=debug git hop clone https://github.com/org/repo.git ``` -## Global Configuration +## Customize Your Settings -The global configuration file (`global.json`) stores user preferences that apply across all repositories. +The global configuration file (`global.json`) stores your preferences. This is the ONLY file you'll typically edit. -### Location +**Location:** +- Linux/Unix: `~/.config/git-hop/global.json` +- macOS: `~/Library/Preferences/git-hop/global.json` +- Custom: `$XDG_CONFIG_HOME/git-hop/global.json` -- **Linux/Unix:** `~/.config/git-hop/global.json` -- **macOS:** `~/Library/Preferences/git-hop/global.json` -- **Custom:** `$XDG_CONFIG_HOME/git-hop/global.json` +### Settings Reference -### Schema +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `autoEnvStart` | boolean | `false` | Automatically start environment services when switching to a branch | +| `showAllManagedRepos` | boolean | `false` | Show all managed repositories in list command | +| `unusedThresholdDays` | number | `30` | Days before a worktree is considered unused | +| `bareRepo` | boolean | `true` | Use bare repository structure for new clones | +| `enforceCleanForConversion` | boolean | `true` | Require clean working directory for repo conversion | +| `conventionWarning` | boolean | `true` | Warn when worktree doesn't follow naming conventions | +| `gitDomain` | string | `"github.com"` | Default Git hosting domain | +| `worktreeLocation` | string | `"hops"` | Directory name for worktrees | + +### Full Configuration Schema + +For reference, here's the complete JSON structure: ```json { @@ -151,19 +180,6 @@ The global configuration file (`global.json`) stores user preferences that apply } ``` -### Default Settings - -| Setting | Type | Default | Description | -|---------|------|---------|-------------| -| `autoEnvStart` | boolean | `false` | Automatically start environment services when switching to a branch | -| `showAllManagedRepos` | boolean | `false` | Show all managed repositories in list command | -| `unusedThresholdDays` | number | `30` | Days before a worktree is considered unused | -| `bareRepo` | boolean | `true` | Use bare repository structure for new clones | -| `enforceCleanForConversion` | boolean | `true` | Require clean working directory for repo conversion | -| `conventionWarning` | boolean | `true` | Warn when worktree doesn't follow naming conventions | -| `gitDomain` | string | `"github.com"` | Default Git hosting domain | -| `worktreeLocation` | string | `"hops"` | Directory name for worktrees | - ### Package Managers Custom package managers can be defined to extend or override built-in support. See [Dependency Sharing](dependency-sharing.md) for details. @@ -206,7 +222,9 @@ Control repository conversion behavior: ## Hopspace Configuration -Each repository has its own configuration stored in the hopspace directory. +**This file is managed automatically by git-hop. Do not edit it manually.** + +Each repository has its own hopspace configuration that tracks branches and metadata. You won't need to touch this; git-hop maintains it. ### Location @@ -306,7 +324,9 @@ Example: `~/projects/myrepo/hop.json` ## State Tracking -The state file tracks all repositories and their locations across the system. +**This file is managed automatically by git-hop. Do not edit it manually.** + +The state file tracks all repositories and their locations across your system so git-hop can find them quickly. ### Location @@ -360,8 +380,6 @@ The state file enables: - Detection of orphaned worktrees and hubs - Multi-hub support for the same repository -**Note:** This file is managed automatically by git-hop. Manual editing is not recommended. - ## Dependency Registry Tracks shared dependencies across worktrees. See [Dependency Sharing](dependency-sharing.md) for details. @@ -436,21 +454,28 @@ Port and volume configurations are stored per repository for deterministic alloc } ``` -## Configuration Hierarchy +## Override Settings for Specific Situations + +Settings follow a hierarchy — git-hop uses the first one it finds: -When git-hop needs a setting, it searches in this order (first found wins): +1. **Environment variables** — for one command +2. **Hub config** (`/hop.json`) — for one workspace +3. **Hopspace config** (`$GIT_HOP_DATA_HOME///hop.json`) — for one repository +4. **Global config** (`~/.config/git-hop/global.json`) — for all repositories +5. **Built-in defaults** — fallback -1. Environment variables -2. Hub-level config (`/hop.json`) -3. Hopspace-level config (`$GIT_HOP_DATA_HOME///hop.json`) -4. Global config (`$XDG_CONFIG_HOME/git-hop/global.json`) -5. Built-in defaults +**Example:** Change port base for one repo only (don't affect others): -This allows you to: -- Set global defaults for all repositories -- Override for specific repositories (hopspace) -- Override for specific hubs (workspace-specific) -- Override for individual commands (environment variables) +```bash +# Edit ~/.local/share/git-hop/github.com/org/repo/hop.json +# Add this to the JSON: "portBase": 20000 +``` + +**Example:** Use environment variable for a single command: + +```bash +GIT_HOP_PORT_BASE=20000 git hop add feature-x +``` ## Best Practices From 516f65d47e5142d4a728d84b99963b4d9c677072 Mon Sep 17 00:00:00 2001 From: Jad Bitar Date: Fri, 1 May 2026 23:58:19 -0400 Subject: [PATCH 3/3] docs: add git hop repair to core commands table --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d06dfd6..236b7e0 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ git hop env stop | `git hop status` | Show status of current worktree or hub | | `git hop remove ` | Remove a worktree, hopspace, or hub | | `git hop prune` | Clean up orphaned worktrees and hubs | +| `git hop repair` | Repair a corrupted or stale worktree | | `git hop doctor` | Check and repair environment issues | ### Shell Integration (Optional)