GitGroundworkGit
5 / 18 written chapters

G5 · Part 1 · Foundations

The everyday commands

The loop you run hundreds of times a week, and the history-reading tools that turn "who changed this?" into a one-liner.

8 min read2 diagrams7 sections
07On this page
  1. What each command touches
  2. Committing well
  3. Reading history like a pro
  4. Pretty formats
  5. git show: one object in full
  6. Searching: grep, pickaxe, and line history
  7. git shortlog: who did what
Fresher Mid Senior

This is the loop you'll run several hundred times a week. Nothing here is clever; fluency is the point. The second half of the chapter is about reading history, which is where experienced engineers get most of their value out of Git.

ONE TASK, START TO PULL REQUEST status where am I? switch -c a branch per task edit change files diff read your change add -p choose what goes in commit record a snapshot fetch + rebase catch up with main push share it, open a PR next task
The edit, diff, add, commit part repeats many times per branch. Fetch and rebase happen whenever main has moved, and always before you push for review.

a normal working day

git status
git switch -c feature/checkout

git diff
git diff --staged
git add -p
git commit -m "feat: apply coupon before tax"

git fetch origin
git rebase origin/main
git push -u origin feature/checkout

Run git status constantly; it is free and it tells you what state you are in. switch -c creates a branch and moves onto it. The two diffs show unstaged and staged changes. fetch downloads other people's work without touching yours, and rebase origin/main replays your commits on top of theirs. push -u sets the upstream the first time, so later a bare git push and git pull know where to go.

What each command touches

Most anxiety about Git commands comes from not knowing what they will change. This table answers that for the everyday set. "Safe" means it cannot lose work you have not committed.

CommandReadsWritesSafe?
git statusFiles, index, HEADNothing (may refresh the index's stat cache)Yes
git diff, log, show, grepFiles, index, objectsNothingYes
git addFilesObjects (new blobs), indexYes
git commitIndexObjects (trees, commit), current branch refYes
git switchTarget commitHEAD, index, filesYes: refuses if your edits would be overwritten
git restore fileIndex (or --source)FilesNo: discards edits
git restore --staged fileHEADIndexYes: files untouched
git stashFiles, indexA stash commit; resets files and indexYes: recoverable
git fetchThe remoteObjects, refs/remotes/*Yes
git pullThe remoteEverything fetch writes, then branch, index, filesMostly: stops on conflicts
git pushYour branchThe remote's branchYes, unless forced

Committing well

git commit with no -m opens your editor, and git commit -v puts the full staged diff under the message so you can read the change while you describe it. Other habits that pay off:

  • git commit --amend replaces the last commit with a new one (new message, or extra staged changes). Fine before you push; after that it rewrites shared history. --amend --no-edit keeps the message.
  • git commit --fixup=<sha> records a small correction aimed at an older commit. git rebase -i --autosquash later folds it in.
  • git commit -a stages every tracked file's changes and commits. It never adds new files, and it skips the review step, so use it only when you just read the whole diff.

Reading history like a pro

git log, actually useful

git log --oneline --graph --decorate --all
git log -p src/cart.js
git log --follow -p src/cart.js
git log -S "calculateTax"
git log --since="2 weeks" --author="Aisha"
git show a3f9c1

In order: the whole graph in your terminal, every change to one file with its diffs, the same but following the file across renames, the commits where a string appeared or vanished, a filtered list, and one commit in full. The first one is worth an alias, because it answers "where is everything?":

$ git log --oneline --graph --decorate --all
* 1b06427 (HEAD -> main) docs: add README
* eeb36f5 fix: round tax to two decimals
| * f9ea75e (feature/coupons) feat: apply coupon before tax
|/
* 9ee1c69 (tag: v1.0) feat: include tax in cart total
* e750e96 feat: add GST calculation
* 37a4c57 feat: cart total

Read it bottom to top for time order. Each * is a commit; the lines to its left are the graph. The parentheses show which refs point at each commit, so here feature/coupons split from main at v1.0 and main has moved on by two commits since.

FilterShows
-n 10 or -10Only the newest ten.
--since="2 weeks", --until=2025-08-01A time window, by commit date.
--author=AishaAuthor matches the pattern (name or email).
--grep="coupon" -iCommit message matches, case-insensitive.
-- src/tax.jsOnly commits that touched this path.
--no-merges / --mergesHide merge commits / show only them.
--first-parentFollow only the first parent: main as a list of what landed.
main..featureCommits on feature that main does not have. See the mental model.
--stat, --name-statusWhich files each commit changed, with counts or with A/M/D/R letters.

Pretty formats

git log has built-in layouts, from terse to exhaustive: oneline, short, medium (the default), full, fuller and reference. fuller is the one to reach for when you need to see both the author and the committer, for example after a rebase:

$ git log -1 --format=fuller
commit 1b06427a8c7d8fe438acd53c39008641a3cff92e
Author:     Aisha Khan <aisha@corp.dev>
AuthorDate: Fri Aug 8 14:50:00 2025 +0530
Commit:     Aisha Khan <aisha@corp.dev>
CommitDate: Fri Aug 8 14:50:00 2025 +0530

    docs: add README

For anything else, build your own line with --format (also spelled --pretty=format:) and placeholders:

PlaceholderGivesPlaceholderGives
%H / %hFull / short commit hash%sSubject (first line)
%an / %aeAuthor name / email%bBody
%ad / %arAuthor date / relative ("3 days ago")%dRef names, like (HEAD -> main)
%cn / %crCommitter name / relative date%pShort parent hashes
%C(auto)Git's default colouring for what follows%nNewline
$ git log --format='%h %an %ad %s' --date=short -3
1b06427 Aisha Khan 2025-08-08 docs: add README
eeb36f5 Ravi Iyer 2025-08-07 fix: round tax to two decimals
9ee1c69 Aisha Khan 2025-08-06 feat: include tax in cart total

$ git config --global alias.lg "log --graph --format='%C(auto)%h%d %s %C(dim)%an, %ar'"
$ git lg -2
* 1b06427 (HEAD -> main) docs: add README Aisha Khan, 3 days ago
* eeb36f5 fix: round tax to two decimals Ravi Iyer, 4 days ago

--date controls how %ad and the default layouts print dates: short, iso, relative, or format:%Y-%m-%d %H:%M for strftime. Formats are also the right way to feed Git into a script: %H and a separator you control are far safer to parse than the human layouts, which can change.

For seniors: scripting against Git

Git splits its commands into porcelain, meant for people, and plumbing, meant for scripts. Porcelain output (plain git status, git branch, the default log layout) can change between versions and follows your colour and locale settings. For a script, ask for a stable format: git status --porcelain=v2 --branch prints the branch, upstream and ahead/behind counts as # branch.ab +1 -0 lines followed by one record per file; git for-each-ref --format lists refs; git log --format=%H lists commits; and -z on most of these separates records with NUL, so file names with spaces or newlines cannot break the parser. git rev-parse answers "what commit is this name?", and exits non-zero when the name does not exist.

git show: one object in full

git show prints whatever you point it at. For a commit, the header and the diff; for a tag, the tag message and then the commit; for a commit:path, the file as it was at that commit, with no checkout needed.

$ git show --stat HEAD~1
commit eeb36f5fdb53fc3f8dd9e874fe93c1f7fe7733d4
Author: Ravi Iyer <ravi@corp.dev>
Date:   Thu Aug 7 18:50:00 2025 +0530

    fix: round tax to two decimals

 src/tax.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

$ git show v1.0:src/cart.js > /tmp/cart-v1.js
$ git show HEAD --name-status
$ git show HEAD -- src/tax.js

Searching: grep, pickaxe, and line history

Different questions need different searches, and picking the right one is most of the skill.

Where does this text appear right now? Which commits added or removed this string? Which commits touched lines matching a regex? How did this one function evolve? Who last changed each of these lines? git grep -n text git log -S text git log -G regex git log -L :fn:file git blame -L 40,60
The first searches the present. The rest search history, and each answers a slightly different question.

git grep searches tracked files. It is fast because it takes the file list from the index instead of walking the disk, and it skips node_modules and build output for free because they are not tracked. It can also search any commit without checking it out.

$ git grep -n calculateTax
src/cart.js:1:import { calculateTax } from "./tax.js";
src/cart.js:4:  return sub + calculateTax(sub);
src/tax.js:1:export function calculateTax(amount, rate = 0.18) {

$ git grep -n "TODO" v1.0
$ git grep -c "console.log" -- '*.js'

git log -S, the "pickaxe", deserves a moment. It finds commits that changed the number of occurrences of a string, so "when did this function get deleted?" becomes a five-second question instead of an afternoon. git log -G takes a regular expression and finds commits whose diff adds or removes a line matching it. The difference shows up when code moves or is edited in place:

$ git log -S'0.18' --oneline
e750e96 feat: add GST calculation

$ git log -G'0\.18' --oneline
eeb36f5 fix: round tax to two decimals
e750e96 feat: add GST calculation

The fix commit rewrote the line containing 0.18, but the file had one 0.18 before and one after. The count did not change, so -S skips it; -G sees a matching line removed and added, so it reports it. Use -S to find where something was introduced or deleted, -G to find every commit that touched it. Add -p to either to see the diffs, and --all to search every branch.

git log -L tracks a range of lines through history. git log -L :calculateTax:src/tax.js finds the function by name and shows every commit that changed it, each with only the diff for that function. You can also give line numbers: -L 40,60:src/cart.js. git blame goes the other way: for each current line, the last commit that changed it.

$ git blame -L 1,2 src/tax.js
eeb36f5f (Ravi Iyer 2025-08-07 18:50:00 +0530 1) export function calculateTax(amount, rate = 0.18) {
eeb36f5f (Ravi Iyer 2025-08-07 18:50:00 +0530 2)   return Math.round(amount * rate * 100) / 100;

Blame names the last person to touch a line, which is often a formatter or a rename, not the author of the logic. -w ignores whitespace-only changes, -C follows lines moved or copied from other files, and a .git-blame-ignore-revs file listing bulk-reformat commits (enabled with git config blame.ignoreRevsFile .git-blame-ignore-revs) makes blame skip them. GitHub reads the same file.

git shortlog: who did what

shortlog groups commits by author. With -s it only counts, -n sorts by count, and -e adds emails, which is how you spot one person committing under two addresses.

$ git shortlog -sne --all
     3	Aisha Khan <aisha@corp.dev>
     2	Ravi Iyer <ravi@corp.dev>
     1	Meera Das <meera@corp.dev>

$ git shortlog v1.0..HEAD
Aisha Khan (1):
      docs: add README

Ravi Iyer (1):
      fix: round tax to two decimals

The second form is a first draft of release notes: everything since the last tag, grouped by person. Add --no-merges to drop merge commits. If one person shows up under several names or emails, a .mailmap file at the root of the repository maps them to one identity for shortlog, log and blame. One caution: in a script or pipe, give shortlog a revision such as HEAD. With no terminal attached and no revision, it waits to read a log from standard input.

switch and restore

Modern Git split the overloaded git checkout into two clearer commands: git switch for changing branches, git restore for throwing away file changes. checkout still works and you'll see it everywhere, but the newer pair is much harder to misfire. The three areas has the full mapping.