Skip to content

fix(init): back-fill missing hop.json on idempotent re-init#27

Merged
jadb merged 2 commits into
mainfrom
fix/init-backfill-missing-hop-json
May 19, 2026
Merged

fix(init): back-fill missing hop.json on idempotent re-init#27
jadb merged 2 commits into
mainfrom
fix/init-backfill-missing-hop-json

Conversation

@jadb
Copy link
Copy Markdown
Contributor

@jadb jadb commented May 19, 2026

Summary

  • git hop init was a no-op when a bare-worktree-shaped repo had the structure but was missing hop.json (legacy bare repos cloned outside git hop, or repos whose hop.json was lost). The command printed "Repository already initialized" and exited, leaving the directory un-registered as a hub.
  • handleAlreadyInitializedWithFlags now back-fills a missing hop.json from runtime git state before printing the idempotent summary, so re-running git hop init becomes a true recovery path.
  • Inputs gathered: origin URL via git remote get-url origin (best-effort, with <parent>/<dir> fallback matching registerAsIs); default branch via git symbolic-ref HEAD (falls back to main); branches by parsing git worktree list --porcelain (bare/detached entries skipped; short branch name used as the map key).
  • For the worktree-child case the hub root is derived from <cwd>/.git's gitdir: pointer rather than hop.FindProjectRoot, because the latter aborts when its DetectRepoStructure walk hits an intermediate hops/ directory that returns NotGit.

Companion PR

Test plan

  • go build ./... clean.
  • go test ./internal/... ./cmd/... — 865 pass, 0 fail (8 new test cases across cmd/init_backfill_test.go, all written test-first: parseWorktreeListPorcelain parser shape, backfillHubConfigIfMissing happy-path / no-op / no-remote fallback / no-default-branch fallback / error propagation, and resolveBackfillRoot for BareWorktreeRoot / WorktreeRoot / WorktreeChild / declined-on-StandardRepo).
  • go vet ./cmd/ clean.
  • Manual end-to-end on a legacy bare-worktree repo (10 worktrees, mixed branch paths including a path-with-suffix refactor/kit-12fcc-baseline-T-0119-20 whose branch name has no suffix): removed hop.json, ran git hop init from the bare-repo root → prints Created missing hop.json at <path>. and writes a valid config; subsequent git hop status shows the full hub table. Repeated from inside hops/main/ (worktree-child) → also resolves the correct hub root via the gitdir pointer and writes hop.json there.

Notes

  • envPatterns defaults to ["dev","staging","qa"] so the back-filled hub matches CreateHub's defaults.
  • Write failures emit a warning: line to stderr but do not abort init — hooks installation and the "Repository already initialized" summary still run.
  • Existing TestIdempotentRun_* tests previously passed g=nil; updated to pass mocks.NewMockGit() since the function now touches git state on first invocation.

When git hop init was run in a bare-worktree-shaped repo that already
had the structure but was missing hop.json (legacy bare repos cloned
outside git hop, or repos whose hop.json was lost), the command
printed "Repository already initialized" and exited without writing
hop.json. Downstream commands (status, list, add) then refused to
treat the directory as a registered hub.

handleAlreadyInitializedWithFlags now back-fills a missing hop.json
from runtime git state before printing the idempotent summary:

  - origin URL via 'git remote get-url origin' (best-effort; falls
    back to <parent>/<dir> naming, matching registerAsIs);
  - default branch via 'git symbolic-ref HEAD' (falls back to "main");
  - branches by parsing 'git worktree list --porcelain' — the bare
    entry and detached-HEAD worktrees are skipped; the short branch
    name (refs/heads/ stripped) is used as the map key.

When invoked from a worktree child (structure WorktreeChild), the hub
root is derived from <cwd>/.git's gitdir pointer, not hop.FindProjectRoot,
because the latter aborts when its DetectRepoStructure walk hits an
intermediate hops/ directory that returns NotGit.

envPatterns defaults to ["dev","staging","qa"] so a backfilled hub
behaves identically to one created by CreateHub. Write failures emit
a "warning:" line to stderr but do not abort init — hooks installation
and the "Repository already initialized" summary still run.

Existing TestIdempotentRun_* tests passed g=nil because the function
didn't touch git state; they now pass a mocks.NewMockGit() with the
symbolic-ref response wired up.
Copilot AI review requested due to automatic review settings May 19, 2026 15:30
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 makes git hop init a recovery path for legacy/partially-initialized bare-worktree repos by back-filling a missing hop.json from runtime git state before printing the usual idempotent “already initialized” summary.

Changes:

  • Add backfill logic to reconstruct and write hop.json using git remote get-url origin, git symbolic-ref HEAD, and git worktree list --porcelain.
  • Invoke the backfill step from handleAlreadyInitializedWithFlags (warning-only on failure).
  • Add unit tests for the porcelain parser, backfill behavior, and hub-root resolution; update existing init flag tests to use a non-nil mock git interface.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
cmd/init.go Calls the new backfill step during idempotent init runs to heal missing hop.json.
cmd/init_flags_test.go Updates idempotent-init tests to pass a mock git interface (now required).
cmd/init_backfill.go New helper implementation for parsing worktree porcelain, deriving hub root, and writing hop.json.
cmd/init_backfill_test.go New tests covering parser behavior, backfill behavior, and hub-root resolution.
Comments suppressed due to low confidence (2)

cmd/init_backfill_test.go:247

  • The comment above TestResolveBackfillRoot says the WorktreeChild case "walks up via FindProjectRoot", but resolveBackfillRoot no longer calls FindProjectRoot—it parses the worktree's .git gitdir pointer via hubFromWorktreeChild. Please update this comment to reflect the current behavior to avoid misleading future changes.
// resolveBackfillRoot picks the path to back-fill given the cwd and the
// repo structure detected there. BareWorktreeRoot → cwd; WorktreeChild
// → walk up via FindProjectRoot. Anything else → ("", false).
func TestResolveBackfillRoot(t *testing.T) {

cmd/init_backfill_test.go:283

  • The WorktreeChild test data uses a gitdir pointer of "/worktrees/", but elsewhere in the repo the worktree-child shape is typically "/.git/worktrees/" (e.g. cmd/init_idempotent_test.go). Add coverage for the ".git/worktrees" form (and ensure resolveBackfillRoot returns the hub root, not the .git directory) so the backfill logic matches real repos.
		// A worktree child under <tmp>/repo/hops/main.
		hopsMain := filepath.Join(repo, "hops", "main")
		if err := fs.MkdirAll(hopsMain, 0755); err != nil {
			t.Fatal(err)
		}
		if err := afero.WriteFile(fs, filepath.Join(hopsMain, ".git"), []byte("gitdir: "+filepath.Join(repo, "worktrees", "main")+"\n"), 0644); err != nil {
			t.Fatal(err)
		}

		got, ok := resolveBackfillRoot(fs, hopsMain, config.WorktreeChild)
		if !ok || got != repo {
			t.Errorf("got (%q, %v), want (%q, true)", got, ok, repo)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cmd/init_backfill.go Outdated
Comment on lines +199 to +204
// gitdir shape: <hub>/worktrees/<name>. Strip the last two segments.
dir := filepath.Dir(gitdir) // <hub>/worktrees
if filepath.Base(dir) != "worktrees" {
return "", false
}
return filepath.Dir(dir), true
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in bab9f56. hubFromWorktreeChild now strips an optional trailing ".git" segment after locating the "worktrees" parent, so both bare-hub ("/worktrees/") and regular-hub ("/.git/worktrees/") shapes resolve to the hub root. Added a test for the regular-hub shape plus a malformed-gitdir negative case.

Skipped the relative-gitdir-path suggestion: git emits absolute paths by default, and the codebase has no precedent for handling --relative here. Happy to add it if you can point to a repro.

Comment thread cmd/init_backfill_test.go Outdated
})
}

var _ = hop.ParseRepoFromURL
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in bab9f56. Removed the dead var _ = hop.ParseRepoFromURL line and the now-unused hop import.

The "also appears at line 244, 272" claim looks hallucinated — there was only the one occurrence in this file.

The initial impl assumed the worktree's gitdir pointer always had the
shape "<hub>/worktrees/<name>", which holds for bare hubs (git hop's
own layout) but not for non-bare hubs. `git worktree add` from a
regular repo writes "<hub>/.git/worktrees/<name>" — feeding that
through filepath.Dir twice returned "<hub>/.git", and the back-fill
would have tried to write hop.json into the .git directory instead
of the hub root.

Strip an optional trailing ".git" segment after locating the
"worktrees" parent so both shapes resolve to the actual hub.

Also drop the dead `var _ = hop.ParseRepoFromURL` (leftover from an
earlier draft that was going to assert on it) and the now-unused hop
import. Update the TestResolveBackfillRoot docstring which still
referenced the old FindProjectRoot-based implementation.

New test cases:
  - WorktreeChild with regular-hub shape "<hub>/.git/worktrees/<n>"
    (regression coverage for the bug above).
  - WorktreeChild with a gitdir whose parent isn't "worktrees" — must
    return ok=false rather than guessing a hub from arbitrary paths.
@jadb jadb merged commit ee260ba into main May 19, 2026
2 of 5 checks passed
@jadb jadb deleted the fix/init-backfill-missing-hop-json branch May 19, 2026 20:27
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.

2 participants