Back to Journal

Git config and shell aliases

Today was infrastructure day. Not cloud infrastructure - the local kind. The kind that accumulates cruft over months of quick fixes and "I'll clean this up later." The development machine needed attention.

The goal: transform a functional-but-messy setup into something intentional. Git config that actually helps. Shell aliases that reduce keystrokes. Automation for the boring stuff.

The Starting Point

The existing .gitconfig was minimal - name, email, LFS, and a pull strategy. Everything else was defaults:

[user]
	name = Kenny Tran
	email = kenny@ketryon.com
[init]
	defaultBranch = main
[pull]
	rebase = false
[filter "lfs"]
	# ... LFS config

Functional. But Git has dozens of settings that can eliminate friction. We weren't using any of them.

BEFORE5 config linesNo aliasesDefault behaviorsAFTER70+ config lines15 aliasesIntentional defaultsOptimization

Push Configuration: The Silent Time-Saver

Every new branch required explicitly setting the upstream:

git push -u origin feature-branch  # Every. Single. Time.

The fix is two lines:

[push]
	autoSetupRemote = true
	default = current

autoSetupRemote = true means the first push of any branch automatically creates the upstream tracking. No more -u. default = current pushes to a branch of the same name on the remote - the behavior you want 99% of the time.

Branch Push FlowCreate branchgit checkout -b featMake commitsgit commit -m "..."Pushgit pushDoneWhat autoSetupRemote does automatically:1. Detects no upstream exists2. Creates origin/feat tracking branch3. Pushes commits to remoteNo more: git push -u origin featNo more: fatal: no upstream branchJust: git push

Fetch with Prune: Keeping References Clean

Remote branches get deleted. Locally, their references linger forever. After a few months, git branch -r becomes archaeology:

origin/feature-login-fix-v2-final
origin/hotfix-that-thing-from-march
origin/experiment-we-abandoned

The fix:

[fetch]
	prune = true

Every fetch now removes local references to deleted remote branches. The branch list stays honest.

Merge Conflict Resolution: diff3 and rerere

Merge conflicts show two versions - yours and theirs. But what was the original? Without context, you're guessing:

<<<<<<< HEAD
const timeout = 5000;
=======
const timeout = 10000;
>>>>>>> feature-branch

Was it 3000 before? 5000? Did both sides change it?

diff3 adds the original:

[merge]
	conflictstyle = diff3

Now conflicts show three versions:

<<<<<<< HEAD
const timeout = 5000;
||||||| merged common ancestors
const timeout = 3000;
=======
const timeout = 10000;
>>>>>>> feature-branch

You can see that main changed 3000→5000 and the feature branch changed 3000→10000. The merge decision becomes informed.

Merge Conflict StylesStandard (2-way)<<<<<<< HEADconst timeout = 5000;=======const timeout = 10000;>>>>>>> featureWhat was the original value?No way to know.diff3 (3-way)<<<<<<< HEADconst timeout = 5000;||||||| originalconst timeout = 3000;=======const timeout = 10000;>>>>>>> featureOriginal was 3000. Full context.

Then there's rerere - "reuse recorded resolution":

[rerere]
	enabled = true

Git remembers how you resolved conflicts. If the same conflict appears again (rebasing the same branch, cherry-picking), Git applies your previous resolution automatically. It's like merge conflict muscle memory.

Git Aliases: Reducing Keystrokes

Typing git status thousands of times adds up. Aliases compress common operations:

[alias]
	# Quick shortcuts
	s = status -sb
	co = checkout
	br = branch
	ci = commit
	cm = commit -m
 
	# Logging
	lg = log --oneline --graph --decorate -20
	lga = log --oneline --graph --decorate --all -20
	last = log -1 HEAD --stat
 
	# Undo helpers
	unstage = reset HEAD --
	undo = reset --soft HEAD~1
	amend = commit --amend --no-edit
 
	# Diff shortcuts
	d = diff
	ds = diff --staged
 
	# Stash shortcuts
	sl = stash list
	sp = stash pop
	ss = stash save
 
	# Maintenance
	cleanup = "!git branch --merged | grep -v '\\*\\|main\\|master\\|develop' | xargs -n 1 git branch -d"

The cleanup alias deserves explanation. It's a shell command (the ! prefix) that:

  1. Lists branches merged into current branch
  2. Filters out protected branches (main, master, develop)
  3. Deletes each one

After a release, one command removes all the merged feature branches.

Alias EfficiencyBefore: Full Commandsgit statusgit checkout -b featuregit log --oneline -10git commit --amend --no-editAfter: Aliasesgit sgit co -b featuregit lggit amend

The Log Graph: git lg

The lg alias produces a visual branch history:

git lg
* a1b2c3d (HEAD -> main) Merge feature-auth
|\
| * d4e5f6g Add login validation
| * g7h8i9j Create auth middleware
|/
* j1k2l3m Update dependencies
* m4n5o6p Fix header alignment

The --graph flag draws the branch structure. --decorate shows branch names. --oneline keeps it compact. -20 limits to recent history - rarely need more in terminal.

Global Gitignore: Stop Committing .DS_Store

Every Mac developer has accidentally committed .DS_Store. Every. Single. One.

The global gitignore catches files that should never be committed, regardless of project:

# ~/.gitignore_global
 
# macOS
.DS_Store
._*
.Spotlight-V100
.Trashes
 
# IDEs
.idea/
.vscode/
*.swp
*.swo
 
# Environment
.env
.env.local
.env.*.local
 
# Dependencies
node_modules/
 
# Build outputs
dist/
build/
.next/

Configured in git:

[core]
	excludesfile = ~/.gitignore_global

Project-level .gitignore handles project-specific files. Global handles developer-environment files. Separation of concerns.

Gitignore LayersGlobal (~/.gitignore_global).DS_Store.idea/, .vscode/.env, .env.localnode_modules/Developer environment filesApply to ALL repositoriesProject (.gitignore)/public/pdf.worker.min.mjs/coverage/*.generated.ts/temp/Project-specific filesCommitted with the repo

Shell Aliases: Beyond Git

Git aliases help, but shell aliases complete the picture. These live in ~/.oh-my-zsh/custom/aliases.zsh:

# Navigation
alias prov="cd ~/Documents/provsvaret"
alias dok="cd ~/Documents/doktera"
alias work="cd ~/Documents/worknode"
 
# Git shortcuts
alias gs="git status -sb"
alias glog="git log --oneline --graph --decorate -15"
alias gpull="git pull origin \$(git branch --show-current)"
alias gpush="git push origin \$(git branch --show-current)"
 
# Development
alias p="pnpm"
alias pi="pnpm install"
alias pd="pnpm dev"
alias pb="pnpm build"
 
# Docker
alias dc="docker compose"
alias dcu="docker compose up -d"
alias dcd="docker compose down"
 
# Utilities
alias reinstall="rm -rf node_modules && pnpm install"

The $(git branch --show-current) substitution means gpush always pushes the current branch. No typos, no wrong branch.

Functions for Complex Operations

Aliases can't handle arguments well. Shell functions fill that gap:

# Quick commit: gc "message"
gc() {
  git add -A && git commit -m "$1"
}
 
# Fresh branch from updated main
fresh() {
  git checkout main && git pull && git checkout -b "$1"
}
 
# Kill process on port
killport() {
  lsof -ti:$1 | xargs kill -9
}
 
# Open project in editor
proj() {
  cd ~/Documents/$1 && code .
}

fresh feature-auth is now a single command that:

  1. Switches to main
  2. Pulls latest changes
  3. Creates and switches to the new branch
fresh() Function Flowgit checkout mainSwitch to maingit pullGet latestgit checkout -b $1Create branchReady to codeClean statefresh feature-auth → All three steps, one command

GitHub CLI: The Missing Piece

Git handles local operations. GitHub operations still required the browser - creating PRs, checking CI status, reviewing issues.

The GitHub CLI (gh) bridges this gap:

brew install gh
gh auth login

Now from the terminal:

# Create pull request
gh pr create --title "Add auth" --body "Implements login flow"
 
# List open PRs
gh pr list
 
# Check PR status (CI, reviews)
gh pr status
 
# Checkout a PR locally
gh pr checkout 123
 
# View issues
gh issue list
gh issue view 45

The workflow becomes:

fresh feature-auth     # Create branch
# ... make changes
gc "Implement auth"    # Commit
gpush                  # Push
gh pr create           # Open PR

No browser until code review.

SSH Configuration

GitHub authentication was working, but SSH config was missing optimizations:

# ~/.ssh/config
 
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_rsa
  AddKeysToAgent yes
  UseKeychain yes
 
Host *
  ServerAliveInterval 60
  ServerAliveCountMax 3

AddKeysToAgent and UseKeychain mean no passphrase prompts after the first one - macOS keychain handles it. ServerAliveInterval prevents dropped connections during long operations.

Automated Maintenance

The final piece: a cleanup script that runs weekly via launchd:

#!/bin/bash
# ~/.local/bin/cleanup-dev.sh
 
# Package managers
yarn cache clean --silent
npm cache clean --force --silent
pnpm store prune
 
# Homebrew
brew cleanup --prune=7 -q
brew autoremove -q
 
# Old logs
find "$HOME/Library/Logs/JetBrains" -type f -mtime +7 -delete
 
# Xcode derived data (30+ days old)
find "$HOME/Library/Developer/Xcode/DerivedData" -mindepth 1 -maxdepth 1 -mtime +30 -exec rm -rf {} \;

Scheduled via launchd to run Sundays at 3 AM:

<key>StartCalendarInterval</key>
<dict>
  <key>Weekday</key>
  <integer>0</integer>
  <key>Hour</key>
  <integer>3</integer>
</dict>

Caches grow indefinitely if unchecked. Weekly cleanup keeps the machine lean.

The Complete Picture

Developer Environment ArchitectureGit Configuration~/.gitconfig~/.gitignore_globalPush/fetch/merge settings15 aliasesShell Environment~/.zshrc~/.oh-my-zsh/custom/aliases.zshNavigation shortcutsDev command aliasesToolsGitHub CLI (gh)SSH configVS Code integrationHomebrew packagesAutomationWeekly cleanup via launchdCache pruning, log rotation, dep cleanupProject Organization~/Documents/provsvaret/~/Documents/doktera/, tools/, clients/Result: Consistent, maintainable, fast development environment

Key Takeaways

  1. Git configuration is documentation - Your .gitconfig declares how you work. Make it intentional.

  2. Aliases compound - Each saved keystroke is small. Thousands of them matter.

  3. Automate maintenance - Caches grow silently. Scheduled cleanup prevents "disk full" surprises.

  4. Global gitignore is essential - .DS_Store commits are embarrassing. Prevent them once, globally.

  5. Shell functions beat aliases for complexity - When you need arguments or conditionals, write a function.

  6. GitHub CLI closes the loop - Git without browser context-switching is faster Git.

The investment was a few hours. The return is every day after.