git / workflow
For each change to my software, I add a card to a kanban board:

The card might refer to a feature, bug, or chore. Cards are sorted by priority in each column.
Start
I assign myself to the card, move it to "Doing", and create a worktree:
git-create-tree my-branch
This pulls the latest main,
creates a new branch and worktree directory,
and cds into it.
The main repo directory stays on main, untouched.
I can have multiple worktrees for different tasks at the same time,
which is useful when AI agents are working in parallel
or when I'm waiting on code review for one PR
while starting another.
I edit the code and commit the changes to version control:
git aa
git ci
Those are aliases in ~/.gitconfig:
[alias]
aa = add --all
ci = commit --verbose
I push to a remote branch:
git push
This only pushes my-branch to GitHub due to this setting in
~/.gitconfig:
[push]
default = current
Review
I open a pull request (PR):
git pr
This triggers webhooks that create:
I review the code again. I may push follow-up changes or edit the PR description.
While waiting for review,
I can switch to another worktree to work on something else
or create a new one with git-create-tree.
When CI passes, I open the Slack thread and ask a teammate to review:
@buddy PTAL
"PTAL" means "Please Take A Look".
When they are ready to review, they add an 👀 emoji to the thread and open the PR in a browser.
They comment in-line on the code, offer feedback, and approve it. I make follow-up changes and commit them. We may do these steps once, or multiple times.
Merge
My repo has these settings:
- Require pull request reviews before merging
- Require status checks to pass before merging
- Require branches to be up to date before merging
- Default commit message to pull request title and description
I press the "Squash and merge" button.
GitHub triggers a webhook to deploy the main branch
to my staging environment on
Render.
I acceptance test on staging.
Clean up
When everything looks good, I clean up from the worktree or main repo directory:
git-delete-tree
With no argument, it defaults to the current branch.
It cds to the main repo directory,
removes the worktree directory (or checks out main if the branch is in the main working tree),
deletes the local branch,
fast-forward merges origin/main,
and prunes stale remote refs.
It refuses to delete main.
I deploy to production with a deploy script:
go run ./cmd/deploy
I move the card on the kanban board to "Done".
Functions
git-create-tree and git-delete-tree are zsh functions
so they can cd in the current shell.
git-create-tree:
git-create-tree() {
if [ $# -eq 0 ]; then
echo 'usage: git-create-tree branch-name'
return 1
fi
if [ "$(git branch --show-current)" != "main" ]; then
echo 'error: must be on main'
return 1
fi
local username branch main_dir repo tree_dir
username=$(git config --get github.user || whoami)
branch="${username}/$1"
main_dir=$(git rev-parse --show-toplevel)
repo=$(basename "$main_dir")
tree_dir="$HOME/.worktrees/${repo}/${1}"
git pull
git worktree add -b "$branch" "$tree_dir" origin/main
# Worktrees don't share gitignored files with the main working tree
[ -e "$main_dir/.env" ] && ln -s "$main_dir/.env" "$tree_dir/.env"
# This `cd` is why this is a zsh function instead of a script on $PATH
cd "$tree_dir"
}
git-delete-tree:
git-delete-tree() {
local username name branch main_tree repo tree_dir
username=$(git config --get github.user || whoami)
if [ $# -eq 0 ]; then
name="$(git branch --show-current)"
name="${name#"${username}/"}"
else
name="$1"
fi
if [ "$name" = "main" ] || [ "${username}/${name}" = "main" ]; then
echo 'error: refusing to delete main'
return 1
fi
branch="${username}/${name}"
main_tree=$(git worktree list | head -1 | awk '{print $1}')
repo=$(basename "$main_tree")
tree_dir="$HOME/.worktrees/${repo}/${name}"
# This `cd` is why this is a zsh function instead of a script on $PATH
cd "$main_tree"
if git worktree list | grep -q "$tree_dir"; then
git worktree remove "$tree_dir"
else
git checkout main
fi
git branch -D "$branch"
git fetch origin
git merge --ff-only origin/main
git remote prune origin
}
git-pr is a script on $PATH:
#!/bin/sh
set -e
branch=$(git symbolic-ref --short HEAD)
if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then
echo "Error: Cannot push from $branch branch"
exit 1
fi
git push
gh pr create --fill
gh pr view --web