Git
Installs Git and pre-commit, configures global Git settings, and sets up 20+ shell aliases.
Demo

What it does
- Installs
gitandpre-commitvia apt (Homebrew on macOS) - Applies global Git config (
~/.gitconfig) - Copies helper scripts to
~/scripts worktreeCLI: one.worktrees/convention for linked git worktrees, provisioned on create and shared with Claude Code, see Worktrees- Shell aliases for common Git workflows
- Tab completion for the Git aliases (bash) and the
worktreesubcommands and names (bash and zsh) - Clones configured private repositories
Variables
| Variable | Default | Description |
|---|---|---|
git_repositories_path | ~/ | Base path for cloned repositories |
git_config | See defaults | Key/value pairs applied via git config --global |
git_repositories | the machine's projects inventory | Repositories to clone — a flat mapping of name → repository (a URL string, or {repository, path, clone}) |
git_configs | {} | Conditional configs, each rendered to an include file and wired with includeIf.<match> |
git_configs_path | ~/.config/git | Directory the include files are written to |
Config (defaults)
This is the default application config the role ships for Git, written to ~/.gitconfig via git config --global. Set it as nested sections or flat dotted keys (the two can be mixed). With hash_behaviour = merge, your git_config overrides deep-merge onto these defaults, so you change only the keys you set.
user.name and user.email ship as placeholders to override. The full set of keys is documented in the git config reference:
| Setting | Value | Effect |
|---|---|---|
fetch.prune | true | Remove stale remote-tracking branches on fetch |
pull.rebase | true | Rebase instead of merge on pull |
rebase.autoStash | true | Auto-stash dirty working tree before rebase |
push.autoSetupRemote | true | Auto-set upstream on first push |
push.default | simple | Only push current branch |
rerere.enabled | true | Remember and replay resolved merge conflicts |
core.pager | less -FX | Stop git from paging output that fits on one screen |
diff.colorMoved | zebra | Distinguish moved lines from added/removed |
alias.undo | reset HEAD~1 --mixed | Roll back the last commit, keep changes staged |
Rendered, the defaults produce this ~/.gitconfig (user.name and user.email are the placeholders you override):
[user]
name = Example
email = git@example.com
[fetch]
prune = true
[pull]
rebase = true
[rebase]
autoStash = true
[push]
autoSetupRemote = true
default = simple
[rerere]
enabled = true
[core]
pager = less -FX
[diff]
colorMoved = zebra
[alias]
undo = reset HEAD~1 --mixedExamples
Set your identity and clone your repos. With hash_behaviour = merge, git_config adds to the shipped defaults rather than replacing them, and git_repositories entries merge across group_vars/host_vars (shared repos in group_vars/all, per-host repos in host_vars; a key set to false drops a merged-in entry):
git_config: # nested sections (or dotted keys) merged onto the shipped defaults
user:
name: Ada Lovelace
email: ada@example.com
git_repositories_path: ~/projects/ # default: ~/
git_repositories: # default: the `projects` inventory (below)
work/api: git@gitlab.com:org/api.git # → ~/projects/work/api
work/web: git@gitlab.com:org/web.git # → ~/projects/work/web
dotfiles:
repository: git@gitlab.com:ada/dotfiles.git
path: ~/.dotfiles # → ~/.dotfiles (absolute and ~ paths skip the base)An entry's key is its clone path under git_repositories_path (slashes nest directories) and its folder name. The value is a repository URL string, or a mapping: repository — the URL, or {url, version} to pin a branch or tag — plus path to clone somewhere other than the key and clone: false to skip the clone without dropping the entry.
The projects inventory
git_repositories defaults to the machine-level projects variable: the same entries plus project metadata this role ignores (title, description, website, docs — rendered into the agents role's ~/.agents/PROJECTS.md), and with repository optional (a project without one is listed but never cloned). A key with a trailing slash (work/) describes the group itself for PROJECTS.md and clones nothing. One inventory feeds both the clone list and the project map; set git_repositories directly to manage the clone list separately.
projects:
work/api:
repository: git@gitlab.com:org/api.git
title: Customer API
description: the REST API behind the customer portal
website: https://api.example.com
docs: https://docs.example.com/api
archive:
repository: git@gitlab.com:org/archive.git
clone: false # listed in PROJECTS.md, not cloned hereConditional configs
git_config sets one global config. To scope extra keys to a directory tree, a single repository, or a branch, declare them in git_configs. Each entry renders an include file config.<name> under git_configs_path (default ~/.config/git, next to git's own config) and wires an includeIf.<match> into your global config, so git applies the keys only when match holds. config takes the same nested-or-dotted form as git_config. The common case is a per-directory identity:
git_configs:
work:
match: gitdir:~/work/ # trailing slash matches everything under it
config:
user:
email: ada@work.example.com
signingkey: ABCD1234That writes ~/.config/git/config.work with the [user] keys and adds this to your global config:
[includeIf "gitdir:~/work/"]
path = ~/.config/git/config.workmatch accepts any git includeIf condition, so the same mechanism scopes config to a single repository (point gitdir: at the repo's own path) or to a branch, not just a directory or an identity:
git_configs:
api:
match: gitdir:~/work/api/ # a single repository, not a parent directory
config:
user:
email: ada@api.example.com
release-signing:
match: onbranch:release/ # extra config only on release/* branches
config:
commit:
gpgsign: "true"Only the keys you list are overridden; everything else falls through to the global git_config. For a one-off override in a single repo, run git config user.email ada@work.example.com inside it to write to its local .git/config instead.
Scripts
The role deploys helper scripts to ~/scripts, version-stamped at deploy time with the collection version that shipped them (the git_scripts_version default). Each reports it via --version and is registered in ~/.versions.d/, so a change in script behavior surfaces in the versions table.
Git statuses
Scans every git repo under a directory and reports working tree, stash, and per-branch push status, then prints a colored summary. Defaults to the current directory and takes an optional path. Because ~/scripts is on $PATH, git picks it up as a subcommand, so git-statuses, git statuses, and the gss alias are equivalent.
Linked worktrees are worktree-aware, not double-counted: a worktree's .git file is skipped by the scan (its branches and stash belong to its repo), and each repo instead embeds its worktree list overview plus a summary total:
$ git statuses # current directory
$ git statuses ~/ # everything under home
$ git statuses ~/projects
Scanning: /home/ada/projects
━━━ dotfiles /home/ada/projects/dotfiles
✔ Working tree clean
✔ main (current) — up to date with origin/main
━━━ api /home/ada/projects/api
✘ Uncommitted changes:
M src/server.py
?? notes.md
⚑ 1 stashed change(s)
✘ feature/auth (current) — 2 commit(s) ahead of origin/feature/auth (not pushed)
✔ main — up to date with origin/main
⌂ 2 linked worktree(s):
* main /home/ada/projects/api
feature-x +1/-0 merged .worktrees/feature-x
review merged .worktrees/review
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Repositories found : 2
Linked worktrees : 2
Dirty working trees : 1
Repos with unpushed : 1
Repos with stashes : 1Worktrees
One .worktrees/ convention for humans and agents: linked worktrees live under <repo>/.worktrees/<name> on a branch named after them (a / in the branch becomes - in the directory), kept out of git status via .git/info/exclude (local, nothing to commit). worktree is the CLI and, in your shell, a selector in the mold of the kubernetes role's kube-namespace: worktree <name> scopes the terminal to that worktree, creating and provisioning it first when missing (bare worktree shows the overview); the claude role points Claude Code's WorktreeCreate and WorktreeRemove hooks at worktree hook, so agent worktrees are created, provisioned, and cleaned up the same way as human ones.
One limitation is Claude Code's, not the hook's: a session launched in a .worktrees/ worktree (worktree <name> then claude, or claude --worktree) works normally, but you cannot switch a running session into an existing one. The in-session EnterWorktree tool (and Agent(isolation: "worktree")) bypasses the WorktreeCreate hook and only accepts worktrees that physically live under <repo>/.claude/worktrees/, so it never reaches one the hook placed under .worktrees/, and a symlink cannot bridge the two because the check resolves it back to the real path. This is tracked upstream in anthropics/claude-code#36205 (the tool ignores the hooks) and #48967 (it should honor .worktrees/) — both still open as of 2026-07-17; once the switch respects the hook, it will reach .worktrees/ worktrees too and this caveat goes away. Until then, start a fresh session in the target worktree instead of switching into it.
$ worktree feature-x # select: create .worktrees/feature-x (branch feature-x) and enter it
$ worktree # overview: branch state per worktree, colors on a TTY
* feature-x +45/-12 ?2 .worktrees/feature-x
main ↑2 /home/ada/projects/api
done-a merged .worktrees/done-a
feat-sq gone .worktrees/feat-sq
$ worktree add api main # scriptable: create from a base ref, prints the path
$ worktree exec api git status # run a command inside a worktree
$ worktree rm -d feature-x # remove the worktree, delete the branch when merged
$ worktree clean # sweep every merged (or squash-merged), clean worktree
$ worktree pick # fzf picker, prints the chosen worktree's path
$ worktree prune # drop stale worktree recordsWithout an explicit base ref, add branches from the repository's default branch resolved from origin/HEAD (origin/<default> when that remote-tracking ref exists), not from your current HEAD or a possibly stale local main, so a worktree created from inside another worktree never silently stacks on whatever branch you are standing in. Pass a base ref (worktree add api main) only when the work builds on unmerged changes.
The overview marks the worktree you are standing in (*), the working-tree delta (+added/-deleted lines and ?untracked files, the same vocabulary as the claude role's status line), ahead/behind arrows, and whether a branch is merged or its upstream is gone (a squash-merged MR), i.e. what worktree clean will sweep.
A new worktree starts without the gitignored state git won't carry over, so add copies the files matching the repo's wt.copy globs (default .env and .env.local) from the primary checkout and then runs its wt.setup command inside the new worktree, with WORKTREE_PATH, WORKTREE_BRANCH, WORKTREE_NAME, and MAIN_WORKTREE_PATH exported. Both are plain local git config, so nothing executable is committed to the repo:
$ git config wt.setup 'pnpm install'
$ git config --add wt.copy 'config/*.local.yaml'add is idempotent (an existing name just prints its path) and refuses a branch already checked out elsewhere, pointing at where. It prints only the worktree's absolute path on stdout (git's chatter goes to stderr), so scripts can consume the path directly; the selector is exactly that under the hood, a shell function running cd "$(worktree add <name>)", because only a function can cd your shell (the same reason the kubernetes role's kubectl wrapper is one). worktree tab-completes the subcommands and worktree names, and a name completes in the selector position too.
Aliases
| Alias | Command | Description |
|---|---|---|
| Navigation | ||
main | git checkout main && git pull | Switch to main and pull |
master | git checkout master && git pull | Switch to master and pull |
develop | git checkout develop && git pull | Switch to develop and pull |
| Status & Info | ||
status | git status | Show working tree status |
branches | git branch && git branch -rv | List local and remote branches |
gbs | branches | Short alias for branches |
log | git log --oneline --graph --decorate | Compact decorated graph log |
remote | git remote -v | List remotes |
origin | git remote show origin | Show origin remote details |
gitrepo | git remote get-url origin | Print origin URL |
gss | git-statuses | Status across multiple repos |
| Branching | ||
branch | git switch | Switch branches |
branch-delete | git branch --delete | Delete a branch |
gbd | branch-delete | Short alias |
branches-removed | git fetch --prune then list branches whose upstream is [gone] | List local branches whose remote was deleted |
branches-delete | Same listing piped to git branch -D | Delete local branches whose remote was deleted |
branches-cleanup | Lists gone branches, then prompts before running branches-delete and switching to main | Interactive cleanup of branches whose remote was deleted |
checkout | git checkout | Checkout a commit or file |
| Changes | ||
add | git add | Stage files |
gaa | add --all | Stage all files |
| Commits | ||
commit | git commit -m | Commit with message |
gcm | commit | Short alias |
| Sync | ||
pull | git pull | Pull changes |
push | git push | Push changes |
gpp | push | Short alias |
| Code reading | ||
who | git shortlog -sn --no-merges | Commit counts by author |
what | Files changed in the past year, sorted by frequency | Most-changed files |
when | Commits grouped by year | Commit history by year |
bugs | Files mentioned in fix/bug commits | Bug-prone files |
hotfixes | Recent revert/hotfix/emergency/rollback commits | Emergency changes |
git-wtf | Runs who, what, when, bugs, and hotfixes | Full codebase overview |
| Pre-commit | ||
precommit | pre-commit | Run pre-commit |
precommit-all | pre-commit run --all-files | Run pre-commit on every file |
Troubleshooting
A configured repository wasn't cloned
git_repositories entries clone over SSH, so the clone needs a loaded SSH key at apply time. If the ssh role hasn't run or Keychain hasn't loaded your key yet, the clone fails authentication. Order the ssh role before git, confirm ssh -T git@gitlab.com (or your host) authenticates, then re-apply.
Clones use update: false: a repo that already exists at the destination is left untouched and never pulled or reset, so an existing directory at the target path is skipped rather than updated. Remove or move that directory if you intended a fresh clone.
Clones run in parallel as async jobs with a 300 second per-repository timeout, so a very large or slow clone can report as failed when it runs long. Clone that repository manually, or re-apply once it can finish within the window.
git_configs keys aren't taking effect
The match value is passed to git's includeIf verbatim, so it must be a valid condition. A gitdir: match needs a trailing slash to cover a whole tree (gitdir:~/work/, not gitdir:~/work), and git evaluates the path against the repository's real location.
Check what git actually resolves from inside the repo with git config --show-origin --get user.email: if the include file isn't listed as the origin, the condition didn't match. Confirm the repo lives under the matched path and that the rendered include file exists at git_configs_path (default ~/.config/git/config.<name>).