Github Family is a tiny CLI for coordinating related Git repositories as one logical project.
It is built for projects that need separate visibility domains, such as:
- public source
- private core code
- security fixes
- experiments
- client-specific work
- private notes or docs
The pitch:
Work like it is one project. Store it as multiple Git repos. Preserve separate permissions. Detect incomplete cross-repo states.
The core abstraction is a small topology:
family -> domains -> reposFor example, private.security can be a child domain of private, and commands can target either repo roles or whole domains.
The portable unit is a small control repo:
my-project-family/
.repo-family.toml
.repo-family.lock.toml
.gitignore
public/ # child repo working copy, ignored
private/ # child repo working copy, ignored
security/ # child repo working copy, ignoredCommit the manifest and lockfile in the control repo. Recreate the child working copies with rf bootstrap.
Repo Family does not make Git atomic across repositories.
It coordinates normal Git repositories and records bundle metadata so partial states can be detected, repaired, or reverted by normal Git operations.
No new VCS. No hosting platform. No magic distributed transaction layer.
From this checkout:
py -m pip install -e .Then:
rf --helpYou can also run it without installing:
py -m repo_family --helpCreate a new family control repo:
rf new my-project --template open-core
rf new my-project --template public-private-security --github owner/my-project-family --pushOr create a manifest in the current directory:
rf init my-project
rf init my-project --template public-private-securityAvailable templates:
public-privateopen-corepublic-private-security
Edit .repo-family.toml:
[family]
name = "my-project"
[domains.public]
description = "Open source surface"
[domains.private]
description = "Closed source work"
[repos.public]
url = "git@github.com:you/my-project.git"
path = "public"
domain = "public"
visibility = "public"
[repos.private]
url = "git@github.com:you/my-project-private.git"
path = "private"
domain = "private"
visibility = "private"
optional = trueClone the repos:
rf clone
rf bootstrapSee the whole family:
rf status
rf tree
rf pathsCreate a lockfile snapshot:
rf snapshotCheck whether the workspace still matches the manifest and snapshot:
rf doctor
rf check
rf diffCommit and push selected domains:
rf branch public private auth-refactor
rf commit public private --all -m "Update family bundle"
rf push public private --set-upstreamCreate PRs with GitHub CLI:
rf pr public private --draftCreates a new repo-family control repo in one step:
rf new my-project --template open-coreThis creates:
- a target directory
.repo-family.toml- a managed
.gitignore - a local Git repo on
main - an initial commit
Add a GitHub remote:
rf new my-project --github owner/my-project-familyIf the GitHub repo already exists and SSH auth is configured, push immediately:
rf new my-project --github owner/my-project-family --pushIf GitHub CLI is installed and authenticated, Repo Family can create the GitHub repo too:
rf new my-project --github owner/my-project-family --create-github --pushUseful options:
--template public-private
--template open-core
--template public-private-security
--repo-visibility private
--repo-visibility public
--no-commitCreates .repo-family.toml in the current directory.
By default, it also adds child repo paths to a managed block in .gitignore, so the family directory can be committed as a control repo without committing child working copies. Use --no-gitignore to skip that.
Use --template for common layouts:
rf init my-project --template public-private
rf init my-project --template open-core
rf init my-project --template public-private-securityAdds a repo table to the manifest.
rf add security git@github.com:you/my-project-security.git \
--domain private.security \
--visibility security \
--path security \
--optionalBy default, rf add updates the managed .gitignore block. Use --no-gitignore to skip that.
Clones each configured repo into its configured path. Optional repos may be unavailable, which supports degraded clone mode for users who only have access to part of the family.
Selectors can be repo roles or domains:
rf clone public
rf clone private.securityRecreates a family workspace from the control repo.
git clone git@github.com:org/my-project-family.git
cd my-project-family
rf bootstrapBootstrap does four small things:
- updates the managed
.gitignoreblock for child repo paths - clones missing repos
- fetches available repos with
git fetch --all --prune - verifies lockfile SHAs are available locally when
.repo-family.lock.tomlexists
Useful options:
rf bootstrap --json
rf bootstrap --degraded
rf bootstrap --skip-fetch
rf bootstrap --no-gitignore--degraded lets a workspace come up even when required repos are unavailable, which is useful for users who can only access part of the family.
Updates the managed .gitignore block for selected child repo paths:
rf ignore
rf ignore private --jsonThe command only edits the block between:
# >>> repo-family child repos >>>
# <<< repo-family child repos <<<Shows path, visibility, branch, head SHA, dirty state, and upstream drift for each repo.
Use --json for scripts or CI:
rf status --jsonShows the family topology as domains and repos:
my-project
|-- public - Open source surface
| `-- public [public] clean main abc123 path=public
`-- private - Closed source work
`-- security
`-- fixes [security] missing optional path=securityWrites .repo-family.lock.toml with the current commit for each repo.
By default, snapshot refuses dirty repos. Use --allow-dirty when you intentionally want to record that state.
Checks for missing repos, invalid repo paths, dirty repos, and changes since the lockfile.
Use --strict if warnings should fail the command.
Use --json for scripts or CI:
rf check --jsonChecks local setup:
gitavailabilityghavailability and auth status- manifest validity
- duplicate repo paths
- lockfile parseability
- workspace health
rf doctor
rf doctor --jsonMissing or unauthenticated gh is a warning, not a hard failure, because only rf pr needs it.
Compares the current repo heads to .repo-family.lock.toml.
rf diff
rf diff private.security --jsonrf diff exits nonzero when a selected repo has drifted from the lockfile.
Lists selected repo paths.
rf paths
rf paths private --jsonThis is useful when shell scripts need to map repo roles to local paths without parsing the manifest directly.
Creates and switches to a branch in selected repos:
rf branch auth-refactor
rf branch public private auth-refactorThe branch name is the last argument. Any earlier positional arguments are selectors.
Switches selected repos to an existing branch:
rf switch main
rf switch private.security embargo-fixThe branch name is the last argument. Any earlier positional arguments are selectors.
Fetches selected repos:
rf fetch
rf fetch private --prunePulls selected repos:
rf pull
rf pull public private --ff-only
rf pull private --rebase--ff-only and --rebase are mutually exclusive.
Commits changes across selected repos.
By default, this only commits already staged changes. Use --all to run git add -A first:
rf commit private --all -m "Implement auth bundle"Clean repos are skipped unless --allow-empty is passed.
Pushes selected repos.
rf push private
rf push private --set-upstream--set-upstream runs git push -u origin <branch> by default. Use --remote <name> to choose another remote.
Records the current fixed state in .repo-family.lock.toml and marks the bundle repaired.
rf repair
rf repair private.securityBy default, repair refuses dirty repos. Use --allow-dirty only when you intentionally want to record that state.
Reverts the locked bundle commit in each selected repo, then records a new lockfile state marked reverted.
rf revert
rf revert private.securityThis is coordinated Git revert, not atomic rollback. If one repo conflicts, normal Git conflict handling still applies.
Creates pull requests for selected repos using GitHub CLI:
rf pr public private --draft
rf pr public --title "Family bundle" --base mainThe default PR title is deliberately boring:
Family bundleThat keeps public/private bundle names from leaking unless you explicitly pass --title.
Runs a command in each selected repo:
rf exec private -- git status --shortWrites a named bundle snapshot:
rf bundle start auth-refactorShows both the recorded bundle status and the status computed from the current workspace.
Bundle: auth-refactor
Recorded status: complete
Computed status: completeUse --json for scripts or CI:
rf bundle status --jsonMarks the current bundle status in the lockfile.
rf bundle mark repaired
rf bundle mark revertedSupported statuses:
completeincompleterepairedreverted
The manifest lives at .repo-family.toml.
[family]
name = "example"
[domains.public]
description = "Open source surface"
[domains.private]
description = "Closed source work"
[domains.private.security]
description = "Embargoed security work"
[repos.public]
url = "git@github.com:org/example.git"
path = "public"
domain = "public"
visibility = "public"
[repos.private]
url = "git@github.com:org/example-private.git"
path = "private"
domain = "private"
visibility = "private"
optional = true
[repos.security]
url = "git@github.com:org/example-security.git"
path = "security"
domain = "private.security"
visibility = "security"
optional = trueEach repo needs:
url: Git clone URLpath: local path relative to the family rootdomain: logical domain, such aspublic,private, orprivate.securityvisibility: usuallypublic,private, orsecurityoptional: whether missing or inaccessible clones are allowed
If domain is omitted, Repo Family falls back to the repo's visibility so older manifests keep working.
The lockfile lives at .repo-family.lock.toml.
It records one bundle snapshot:
[snapshot]
id = "bundle-20260623-043000"
status = "complete"
family = "example"
created_at = "2026-06-23T04:30:00Z"
[repos.public]
path = "public"
domain = "public"
visibility = "public"
branch = "main"
head = "abc123..."
dirty = falseThis makes it obvious when a repo family is complete, incomplete, or changed since the last recorded bundle.
The deliberately small version is still only local coordination. Useful next features would be:
- policy checks for public visibility leaks
- dependency metadata between repo roles
- CI wait/check aggregation
- safer PR metadata templates per visibility domain
The core promise should stay boring:
Coordinate repo families. Do not pretend Git became atomic.
py -m unittest discover -s tests -v