Git — Engineering
Notes for Operators
A field-tested Git reference distilled from operating engineering teams across telecom, enterprise and infrastructure programs. Written as I use it day-to-day — terse on theory, dense on the commands and patterns that actually move work forward.
Navigate to the directory you want to place under version control and initialise an empty Git repository:
$ git init # Creates a hidden .git folder — the repo's plumbing $ git status # Review what Git will add before staging anything $ git add <file1> <file2> # Stage specific files $ git add . # Stage everything in current directory $ git commit -m "Initial commit" # Creates a snapshot of staged changes
$ git clone https://example.com/user/repo.git # Full clone with history $ git clone https://example.com/user/repo.git my-folder # Clone into a specific folder name $ git clone --depth 1 https://example.com/user/repo.git # Shallow clone — latest snapshot only, faster download $ git clone --branch develop https://example.com/user/repo.git # Clone and check out a specific branch
# Set identity for all repos (stored in ~/.gitconfig) $ git config --global user.name "Your Name" $ git config --global user.email "you@example.com" # Add a remote (e.g. after git init) $ git remote add origin https://example.com/owner/repo.git # Set up upstream fork tracking $ git remote -v # origin https://example.com/myusername/repo.git (fetch) $ git remote set-url upstream https://example.com/projectusername/repo.git # Push and track remote branch $ git push -u origin main
git config --list to view all your current configuration settings. Use --global for user-wide settings and omit it for repo-specific overrides.# Get full docs for any command $ git help diff $ git diff --help $ git checkout -h # Quick flag reference # SSH key setup $ ls -al ~/.ssh # Check for existing keys $ ssh-keygen -t ed25519 -C "you@example.com" $ cat ~/.ssh/id_ed25519.pub # Copy this output → your Git host's SSH key settings $ ssh -T git@example.com # Test connection
$ git log # Full history, most recent first $ git log -5 # Last 5 commits only $ git log --oneline # Compact one-line-per-commit view $ git log --author="Alice" # Filter by author $ git log --since="2024-01-01" # Since a date $ git log --grep="bugfix" # Search commit messages $ git log -p README.md # Show patches for a specific file $ git log --follow old-name.txt # Track file through renames
# Beautiful graph view — assign an alias $ git log --decorate --oneline --graph # Save as alias 'git lol' $ git config --global alias.lol "log --decorate --oneline --graph" $ git lol $ git lol HEAD develop origin/main # Compare multiple refs $ git lol --all # All branches # Colorized custom format $ git log --graph --pretty=format:'%C(red)%h%Creset -%C(yellow)%d%Creset %s %C(green)(%cr) %C(yellow)<%an>%Creset'
# Log between two branches $ git log main..feature-branch # Commits in feature not in main $ git log --left-right main...feature-branch # Show files changed in each commit $ git log --stat $ git log --name-only # Search for when a string was introduced $ git log -S "function myFunc" # Log for a range of lines in a file $ git log -L 10,25:src/main.js # Filter by file extension (--) $ git log -- '*.js'
$ git remote -v # List remotes with URLs $ git remote add upstream https://... # Add a new remote $ git remote rename origin newname # Rename a remote $ git remote remove upstream # Remove a remote $ git remote show origin # Detailed info about a remote $ git remote set-url origin git@example.com:user/repo.git # Fetch all remotes $ git fetch --all # Pull with rebase (cleaner history) $ git pull --rebase origin main # Set upstream tracking for current branch $ git push -u origin feature-branch
# Delete a remote branch $ git push origin --delete old-feature # List all remote branches $ git branch -r # Remove stale remote-tracking refs $ git remote prune origin $ git fetch --prune # Force push (after rebase) — use with caution! $ git push --force-with-lease origin main # Update from upstream fork $ git fetch upstream $ git merge upstream/main
--force-with-lease over --force. It will fail if someone else has pushed to the branch since your last fetch, preventing accidental overwrites.$ git add . # Stage all changes $ git add -A # Stage all including deletions $ git add -p # Interactive: choose hunks to stage $ git add -u # Stage only already-tracked files # Stage a file that contains changes $ git add src/app.js # Unstage a file (keep changes in working tree) $ git restore --staged src/app.js # Show staged changes (what will be committed) $ git diff --cached # Stage deleted files $ git rm old-file.txt
# Ignore a specific file secrets.env # Ignore all .log files *.log # Ignore the build/ directory build/ # Ignore node_modules everywhere **/node_modules/ # But don't ignore this specific file !important.log # Ignore files in subdirectory only docs/*.pdf
# Global ignore file (all repos) $ git config --global core.excludesfile ~/.gitignore_global # Untrack a file already committed (keep local copy) $ git rm --cached secrets.env # Check if a file is being ignored $ git check-ignore -v path/to/file # Ignore local changes temporarily $ git update-index --assume-unchanged config.local.js
.gitignore only apply to untracked files. To stop tracking a previously committed file, you must run git rm --cached first.# Diff a specific file only $ git diff -- src/index.js # Difference between current version and last release $ git diff v1.0..HEAD # Use external diff tool (meld, vimdiff, etc) $ git difftool -t meld
# Discard unstaged changes in a file $ git restore src/main.js $ git checkout -- src/main.js # Older syntax # Undo last commit, keep changes staged $ git reset --soft HEAD~1 # Undo last commit, keep changes unstaged $ git reset HEAD~1 # Undo last commit AND discard all changes (DESTRUCTIVE!) $ git reset --hard HEAD~1 # Safely revert a public commit (creates new commit) $ git revert abc1234 # Recover using reflog (Git's safety net) $ git reflog # Shows all recent HEAD movements $ git reset --hard HEAD@{2} # Go back 2 reflog entries
git reset --hard permanently discards uncommitted changes. Use git revert for commits already pushed to a shared branch — it creates a new "undo" commit instead of rewriting history.# Standard merge (creates a merge commit) $ git merge feature-branch # Merge without a commit (review before committing) $ git merge --no-commit feature-branch # Squash all feature commits into one $ git merge --squash feature-branch $ git commit -m "Add feature X" # Abort a merge in progress $ git merge --abort # Keep only our changes when conflicting $ git checkout --ours conflicted-file.js $ git checkout --theirs conflicted-file.js # Find branches with no merged changes $ git branch --no-merged main
# Clone a repo that has submodules $ git clone --recurse-submodules https://example.com/user/repo.git # If you forgot --recurse-submodules $ git submodule update --init --recursive # Add a new submodule $ git submodule add https://example.com/lib/helper.git lib/helper # Update all submodules to latest $ git submodule update --remote # Remove a submodule $ git submodule deinit lib/helper $ git rm lib/helper # Set submodule to follow a branch $ git config -f .gitmodules submodule.lib/helper.branch main
| Parameter | Details |
|---|---|
--message, -m | Commit message inline. Skips editor. |
--amend | Modify the most recent commit (message or files) |
--no-edit | Amend without changing the commit message |
--all, -a | Automatically stage all tracked modified files |
--patch, -p | Interactive: choose which hunks to commit |
--fixup | Create a fixup commit for a specific commit hash |
--squash | Create a squash commit for interactive rebase |
--allow-empty | Commit with no changes (useful for CI triggers) |
--no-verify | Skip pre-commit and commit-msg hooks |
# Stage and commit in one step (tracked files only) $ git commit -am "Fix typo in README" # Amend last commit message $ git commit --amend -m "Better message" # Add forgotten file to last commit $ git add forgotten.js $ git commit --amend --no-edit # Commit with a specific date $ git commit --date="Mon 20 Aug 2024 20:19:19" -m "Backdated commit"
# Common time-saving aliases $ git config --global alias.st status $ git config --global alias.co checkout $ git config --global alias.br branch $ git config --global alias.ci commit $ git config --global alias.lol "log --decorate --oneline --graph" $ git config --global alias.unstage "restore --staged" $ git config --global alias.last "log -1 HEAD --stat" # Shell command alias (prefix with !) $ git config --global alias.trim "!git branch --merged | grep -v '\*' | xargs -n 1 git branch -d" # List all aliases $ git config --get-regexp alias
# Rebase current branch on top of main $ git rebase main # Interactive rebase: edit last 3 commits $ git rebase -i HEAD~3 # Interactive rebase commands (in editor): # pick = keep commit as-is # reword = keep but edit message # edit = keep but stop to amend # squash = merge into previous commit # fixup = squash, discard message # drop = remove commit entirely # Continue/skip/abort a rebase $ git rebase --continue $ git rebase --skip $ git rebase --abort # Move branch to different base $ git rebase --onto newbase oldbase branch
# Set default editor $ git config --global core.editor "code --wait" # VS Code $ git config --global core.editor "vim" # Line ending handling $ git config --global core.autocrlf true # Windows $ git config --global core.autocrlf input # macOS/Linux # Configure merge tool $ git config --global merge.tool vimdiff # Coloring output $ git config --global color.ui auto # Set up a proxy $ git config --global http.proxy http://proxy:8080 # Configure for one command only $ git config --global alias.ignore.one "!git config core.fileMode false" # List + edit config $ git config --list $ git config --global -e # Open global config in editor
$ git branch # List local branches $ git branch -a # List all branches (local + remote) $ git branch -v # Show last commit on each branch # Create and switch $ git branch feature/auth # Create branch $ git switch feature/auth # Switch to it $ git switch -c feature/auth # Create AND switch (new syntax) $ git checkout -b feature/auth # Same (old syntax) # Delete branches $ git branch -d feature/auth # Delete (safe — merged only) $ git branch -D feature/auth # Force delete # Rename current branch $ git branch -m new-name # Search branches $ git branch -a | grep "feature" # Move HEAD to arbitrary commit $ git branch -f main abc1234
# Apply a single commit to current branch $ git cherry-pick abc1234 # Apply a range of commits $ git cherry-pick abc1234..def5678 # Cherry-pick without auto-committing $ git cherry-pick --no-commit abc1234 # Check if cherry-pick is needed $ git cherry -v main # + = not in main, - = is in main # Find commits to apply upstream $ git cherry -v upstream/main # Continue after conflict resolution $ git cherry-pick --continue $ git cherry-pick --abort
# Recover from accidental git reset --hard $ git reflog # Find the commit hash you lost $ git reset --hard HEAD@{1} # Go back # Recover a deleted branch $ git reflog | grep "deleted-branch" $ git checkout -b deleted-branch abc1234 # Restore a deleted file after commit $ git log --all -- deleted-file.js $ git checkout abc1234 -- deleted-file.js # Restore file to previous version $ git restore --source HEAD~2 -- src/api.js # Stash work in progress $ git stash # Stash tracked changes $ git stash -u # Include untracked files $ git stash pop # Apply and remove stash $ git stash list # Show all stashes $ git stash apply stash@{2} # Apply specific stash
# Squash last N commits interactively $ git rebase -i HEAD~4 # In editor: change 'pick' to 'squash' for commits to merge # Squash recent commits without rebasing $ git reset --soft HEAD~4 # Uncommit but keep staged $ git commit -m "Feature: auth module" # Squash during merge $ git merge --squash feature-branch $ git commit -m "Add feature X (squashed)" # Autosquash fixup commits $ git commit --fixup abc1234 # Create fixup commit $ git rebase -i --autosquash HEAD~5 # Auto-reorder and squash
# Total commits in repo $ git rev-list --count HEAD # Commits per author $ git shortlog -s -n HEAD # Lines of code per developer $ git log --author="Alice" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2 } END { printf "added: %s, removed: %s\n", add, subs }' # Commits per date $ git log --date=short --pretty=format:"%ad" | sort | uniq -c # Last commit on each branch $ git branch -v # Find all local Git repos on machine $ find ~ -name ".git" -type d 2>/dev/null | sed 's/\/.git//' # Total commits per author (pretty format) $ git log --format="%aN" | sort | uniq -c | sort -rn
Worktrees let you check out a different branch of the same repo into a separate directory. Indispensable when you have uncommitted work and need to quickly inspect another branch.
# Create a new worktree for an existing branch $ git worktree add ../repo-review review/pr-128 # Create a worktree AND a new branch in one shot $ git worktree add -b hotfix/auth ../repo-hotfix main # List active worktrees $ git worktree list # Remove a worktree (after the work is merged or discarded) $ git worktree remove ../repo-review # Prune entries for directories that no longer exist $ git worktree prune
.git/objects store, so creating one is cheap — there's no second clone, just a second checkout. Perfect for long-running review sessions or running tests on two branches simultaneously.You're deep in a feature branch, mid-refactor. Someone asks you to verify a bug fix on main right now. Without worktrees you'd stash, switch, build, switch back, unstash. With worktrees:
# From inside the repo, spin up a clean main checkout next to it $ git worktree add ../current-main main $ cd ../current-main $ npm install && npm test # Done — get rid of it, keep your feature branch untouched $ cd - $ git worktree remove ../current-main
Hooks are executable scripts in .git/hooks/. Each one is named after the event it fires on. Git ships sample files with a .sample suffix — rename to activate.
Block commits that contain obvious secrets, run a lint pass, and prevent committing to main directly.
#!/usr/bin/env bash # Fail fast — any error aborts the commit set -euo pipefail branch="$(git symbolic-ref --short HEAD)" if [[ "$branch" == "main" ]]; then echo "Refusing to commit directly to main. Open a branch." >&2 exit 1 fi # Block obvious secrets if git diff --cached | grep -Ei '(AWS_SECRET|BEGIN RSA PRIVATE KEY|password\\s*=)'; then echo "Possible secret in staged diff — aborting." >&2 exit 1 fi # Run a fast lint pass on staged files files=$(git diff --cached --name-only --diff-filter=ACM | grep '\\.\\(ts\\|tsx\\|js\\)$' || true) if [[ -n "$files" ]]; then npx eslint --max-warnings=0 $files fi
# Make it executable so Git will run it $ chmod +x .git/hooks/pre-commit
Hooks under .git/hooks/ are not version-controlled. Pin a shared directory with core.hooksPath so the whole team gets the same gates:
# Repo-local: commit a hooks/ directory and point Git at it $ git config core.hooksPath .githooks $ git add .githooks/pre-commit $ git commit -m "Add shared pre-commit hook" # Skip hooks for a one-off commit (use sparingly) $ git commit --no-verify -m "Bypass once"