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 configFunctional. But Git has dozens of settings that can eliminate friction. We weren't using any of them.
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 = currentautoSetupRemote = 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.
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-abandonedThe fix:
[fetch]
prune = trueEvery 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-branchWas it 3000 before? 5000? Did both sides change it?
diff3 adds the original:
[merge]
conflictstyle = diff3Now conflicts show three versions:
<<<<<<< HEAD
const timeout = 5000;
||||||| merged common ancestors
const timeout = 3000;
=======
const timeout = 10000;
>>>>>>> feature-branchYou can see that main changed 3000→5000 and the feature branch changed 3000→10000. The merge decision becomes informed.
Then there's rerere - "reuse recorded resolution":
[rerere]
enabled = trueGit 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:
- Lists branches merged into current branch
- Filters out protected branches (main, master, develop)
- Deletes each one
After a release, one command removes all the merged feature branches.
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_globalProject-level .gitignore handles project-specific files. Global handles developer-environment files. Separation of concerns.
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:
- Switches to main
- Pulls latest changes
- Creates and switches to the new branch
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 loginNow 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 45The workflow becomes:
fresh feature-auth # Create branch
# ... make changes
gc "Implement auth" # Commit
gpush # Push
gh pr create # Open PRNo 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 3AddKeysToAgent 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
Key Takeaways
-
Git configuration is documentation - Your
.gitconfigdeclares how you work. Make it intentional. -
Aliases compound - Each saved keystroke is small. Thousands of them matter.
-
Automate maintenance - Caches grow silently. Scheduled cleanup prevents "disk full" surprises.
-
Global gitignore is essential -
.DS_Storecommits are embarrassing. Prevent them once, globally. -
Shell functions beat aliases for complexity - When you need arguments or conditionals, write a function.
-
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.