git / workflow

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

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:

  1. a CI build
  2. a Slack message in my team's channel

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:

  1. Require pull request reviews before merging
  2. Require status checks to pass before merging
  3. Require branches to be up to date before merging
  4. 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

← All articles