Skip to content

feat: status, sync and open across repositories - #14

Merged
simonostendorf merged 6 commits into
mainfrom
feat/5-status-sync
Nov 13, 2026
Merged

simonostendorf merged 6 commits into
mainfrom
feat/5-status-sync

Conversation

@simonostendorf

@simonostendorf simonostendorf commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

What

  • internal/status parses git status --porcelain=v2 --branch into a repository state.
  • internal/gitsync runs fetches in a bounded pool and hands results back serially.
  • internal/git gains Fetch and a porcelain status read. internal/remote gains WebURL.
  • cmd/gitcd gains status with --json and --strict, sync, and open with --print.

Why

  • Closes feat: status, sync and open across repositories #5.
  • A cloned empty repository carries a # branch.upstream line but no # branch.ab line, and so
    does a branch with no upstream at all. Both were probed against real git before the parser was
    written, because treating a missing comparison as "up to date" is exactly the silent
    misreport this command exists to prevent. Each renders as its own state.
  • --strict counts staged, unstaged and untracked. A forgotten new file is the thing a strict
    gate must not wave through.
  • sync fetches only. It never pulls, rebases or merges. Results are delivered on the run's own
    goroutine, so one repository's failure never interleaves into another's line.
  • An unknown host falls back to the generic https://{host}/{owner}/{repo} shape. Azure DevOps
    keeps its own _git path.

Verification

  • task check exit 0.
  • Against a generated tree holding a clean repository, one dirty with only an untracked file, one
    with no upstream and one with an unborn HEAD:
NAME                  BRANCH  STATE   SYNC
host/acme/clean       trunk   clean   no upstream
host/acme/dirty       trunk   dirty   no upstream
host/acme/noupstream  trunk   clean   no upstream
host/acme/unborn      trunk   unborn  -
  • status --strict exits 1 on the untracked file alone.
  • sync against a deliberately broken remote reports each failure, continues, prints
    3 of 4 repositories failed, exits 1, and writes nothing to stdout.
  • open --print resolves the web URL and exits 0.

Risk

  • cmd/gitcd/discover.go holds a walk over root that overlaps internal/scan, which feat: list and cd with fuzzy matching #3
    introduces. feat: status, sync and open across repositories #5 was deliberately told to build the minimum it needed rather than wait, so the
    duplication is known, is noted in AGENTS.md, and is to be reconciled once both land. It is
    not meant to survive.
  • sync runs plain git fetch rather than naming a remote, which is right while a gitcd clone
    carries one remote and wrong the moment one carries two.
  • The status table shifts columns rather than truncating when a name is long, so a narrow
    terminal wraps instead of hiding information. --json carries every field, including whether
    a sync comparison exists at all, so a consumer can tell the three cases apart.

Closes #5


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

@simonostendorf simonostendorf added feature New feature or capability stack: go Related to Go code labels Nov 13, 2026
@simonostendorf

Copy link
Copy Markdown
Member Author

Verdict: approve

Checked every acceptance criterion of #5 against a real fleet built under a throwaway HOME (clean, dirty-by-untracked-file-alone, no-upstream, ahead, behind, ahead-and-behind, a cloned empty repository with an unborn HEAD, and a detached HEAD). Every one of the eight states rendered as its own distinct line in both the table and --json, never as a blank or an "up to date" misreport. The unborn case was probed directly against real git: a clone of an empty remote prints # branch.upstream with no # branch.ab line, exactly as the PR claims, and status reports unborn with hasSyncComparison: false while still carrying the upstream name.

--strict exits 0 on a clean tree and 1 the moment a single untracked file appears, nothing else changed.

sync against a fetchable fleet exits 0, writes nothing to stdout, and reports one line per repository on stderr. With one remote broken, it still fetches every other repository, reports the failure without interleaving into another repository's line, exits 1, and stdout stays empty. go test -race -count=1 ./... (part of task check) passes, including the concurrency-bound and no-interleaving tests. A cancelled context never deadlocks: every fetch fails fast with "context canceled" and every repository still gets a result.

open --print resolves correctly for GitHub, a GitLab subgroup, an Azure DevOps remote and an unknown host, and no test anywhere calls the real launchBrowser.

task check passes end to end: fmt, vet, golangci-lint (0 issues), markdownlint, yamllint, actionlint, goreleaser check, gitleaks (no leaks), go test -race, build.

Spot-checked three of the mutation claims by reverting the guarding logic in a detached copy of the source tree: breaking the unborn-HEAD detection fails TestInspect_UnbornHEADRendersAsAState and the cloned-empty-repository test; removing the pool cap in gitsync.Run fails TestRun_BoundsConcurrency every time; dropping Untracked from State.Dirty() fails both TestInspect_UntrackedFileAloneIsDirty and TestRunStatus_StrictExitsNonZeroWhenAnyRepositoryIsDirty. All three claims hold.

cmd/gitcd/discover.go's walk matches internal/scan.Walk's boundary rule exactly: same .git-entry skip, same root skip, same git.IsRepository check, same SkipDir after a match. No behavioural divergence found. The duplication is contained to one function, reused by status, sync and the refactored findByName, and it is honestly flagged in AGENTS.md, the PR body and the code's own comment.

Nit: cmd/gitcd/discover.go:38 and :62 return the raw os.Stat and filepath.WalkDir errors unwrapped, with no component name, unlike internal/scan.Walk's equivalent fmt.Errorf("scan: ...: %w", ...). Wrap both as fmt.Errorf("discover: stat %s: %w", root, err) and fmt.Errorf("discover: walk %s: %w", root, err) to match the convention AGENTS.md states for the rest of the codebase.

Nit: cmd/gitcd/open_test.go exercises open --print directly only for GitHub and an unknown host; a GitLab and an Azure DevOps remote are covered only at the unit level through internal/remote/remote_test.go's TestRemote_WebURL. Low risk, since openRepo adds no logic between remote.Parse and WebURL(), but a CLI-level case for each would close the gap directly.

A cloned empty repository carries an upstream but no branch.ab line, and so
does a branch with no upstream at all, so a missing comparison is its own
state rather than up to date. Probed against git before the parser was
written.

Refs #5
Results are handed back on the run's own goroutine, so a failure never
interleaves another repository's line into nonsense.

Refs #5
status renders an unborn HEAD and a missing upstream as states of their own,
never as an empty column that reads as up to date. sync fetches only, reports
each failure and exits non zero when any failed. open resolves the remote's
web URL, or prints it with --print.

Refs #5
Including the walk in cmd/gitcd/discover.go, which overlaps internal/scan
from #3 and is flagged for reconciliation once both land.

Refs #5
Review nits: the walk returned bare errors unlike internal/scan, and the
forge URL shapes were only covered through the remote package, not from the
command itself.

Refs #5
@simonostendorf
simonostendorf marked this pull request as ready for review November 13, 2026 21:56
@simonostendorf
simonostendorf merged commit 2dfb99e into main Nov 13, 2026
10 checks passed
@simonostendorf
simonostendorf deleted the feat/5-status-sync branch November 13, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or capability stack: go Related to Go code

Development

Successfully merging this pull request may close these issues.

feat: status, sync and open across repositories

1 participant