In git log, what does a trailing -- <path> limit, and what does --follow add?
answer
- Everything after the separator is a path
- Ambiguity between a branch and a file name
- Path history is simplified, not merely filtered
- Git records snapshots, never rename events
- Rename following handles one file only
basics
~20 sA trailing pathspec limits git log to commits that changed matching files, and the -- separator disambiguates paths from revision names. --follow additionally continues the history of a single file across renames, using per-commit rename detection.
solid answer
~50 s`git log -- src/client.go` shows only commits that touched that path. The `--` matters because Git otherwise has to guess whether an argument is a revision or a path, and it errors out when a branch and a file share a name; `--` removes the ambiguity permanently, which is why scripts always include it. Two behaviours surprise people. First, path-limited log applies history simplification: commits whose version of the path is identical to a parent's are pruned, and merges are simplified, so a change that reached your branch through a merge can vanish — `--full-history` brings those back. Second, a rename ends the history, because the pathspec no longer matches the old name. `--follow` fixes that for exactly one file by re-writing the pathspec whenever rename detection fires, which is heuristic and cannot take multiple paths.
code
bash · 4 linesgit log --oneline -- src/client.go
git log --oneline --follow -- src/client.go
git log --oneline --full-history -- src/client.go
git log --oneline -- . ':!vendor/'go deeper
Know that a path after -- limits git log to that file or directory, and that --follow is what you add when the file has been renamed at some point.
Explain why -- is needed at all, and that path-limited history is simplified rather than filtered, which is why --full-history sometimes reveals commits the default view hides.
Show investigative rigour: verify a suspiciously short file history with --full-history, read renames directly with --name-status rather than trusting a heuristic, and know that rename detection is similarity-based.
Own the consequences for archaeology at scale — large refactors and mass moves degrade rename detection, so encourage separating pure moves from content changes to keep file history answerable years later.
## Pathspec limiting Every revision-walking command accepts a pathspec at the end: `git log -- <path>`. The effect is to limit output to commits that changed something matching the pathspec. Pathspecs are more expressive than plain filenames — a directory matches everything under it, globs work (`'*.go'`, quoted so the shell does not expand them first), and magic prefixes such as `:(exclude)` (short form `:!`) subtract paths, as in `git log -- . ':!vendor/'`. ## Why the `--` separator exists Git command lines mix revisions and paths. When an argument could be either, Git tries to resolve it as a revision first and falls back to treating it as a path — and if both interpretations are valid, it refuses with an ambiguity error. The `--` separator ends the revision list: everything before it is a revision, everything after it is a path. `git log main` is the history of a branch; `git log -- main` is the history of a file called `main`. Even when nothing is ambiguous today, including `--` is the habit that keeps a script working after someone adds a branch named `docs`. ## History simplification: the surprising part Path-limited log does not simply filter a full traversal. It runs *history simplification*, whose purpose is to show a simple, explanatory history of the path rather than every commit that technically touched it. The rules that bite: - A commit whose version of the path is identical to one of its parents' is not shown, and traversal continues through that parent only. - At a merge, if the merged result of the path matches one parent, Git follows that parent alone and prunes the other side. This is why a change made on a feature branch can be invisible in `git log -- <path>` on the mainline: the merge picked it up, but the simplified walk followed the parent where the file already had that content. When you need completeness rather than readability, `--full-history` disables the pruning of side branches, and `--full-history --simplify-merges` re-prunes only the commits that genuinely change nothing, giving a complete but still readable picture. `--sparse` and `--dense` are the related controls. In an investigation — "who introduced this line and through which merge" — reaching for `--full-history` is often the difference between a wrong answer and a right one. ## Renames and `--follow` Git stores snapshots, not rename records. A rename is inferred at diff time by comparing content similarity between a deleted path and an added path. Consequently, `git log -- new/name.go` stops at the commit that created that path, because earlier commits contain no such path. `--follow` works around this: while walking, when it detects that the file being followed came from a rename, it rewrites the pathspec to the old name and keeps going. The consequences of that design: - It works on exactly one path. Passing more than one is an error, because there is a single pathspec being rewritten. - Detection is heuristic and similarity-based, so a rename combined with a heavy rewrite in the same commit may not be recognised, and the trail ends there. - It interacts awkwardly with history simplification and with graph output, since the pathspec changes mid-walk. When `--follow` fails or when you want to see the renames themselves rather than pretend they did not happen, `git log --name-status` prints a status letter per file — including `R100 old/name.go new/name.go` for renames — and `-M` (`--find-renames`) tunes rename detection explicitly. Reading a rename chain manually with `--name-status` is more work but never lies to you about what happened. ## The shapes you actually type `git log --oneline -- src/client.go` for a quick file history; `git log --oneline --follow -- src/client.go` when the file has moved; `git log --oneline --full-history -- src/client.go` when something is missing; `git log --oneline --name-status -- src/` to see what changed in a directory and how. ## What is being assessed The flag list is trivia. The two ideas that matter are that path-limited history is *simplified* by default rather than filtered, and that rename following is an inference performed at diff time rather than recorded metadata. A candidate who knows both will not be fooled when a file's history looks shorter than it should.
- Why can git log -- <path> miss a commit that clearly changed the file?Because path-limited traversal applies history simplification. At a merge, if the merged content of the path matches one parent, Git follows that parent only and prunes the other side, so a change that arrived via the merged branch disappears. --full-history disables that pruning, optionally with --simplify-merges to keep the output readable.
- Why can --follow take only one path?It works by rewriting the pathspec to the old name whenever rename detection fires during the walk. There is a single pathspec being rewritten, so multiple paths have no coherent meaning and Git rejects them. If you need several files, run separate queries or read renames directly with --name-status.
- How does Git know a file was renamed at all?It does not record renames. At diff time it pairs deletions with additions and scores their content similarity, reporting a rename when the score passes a threshold you can tune with -M. That is why a rename combined with a substantial rewrite in one commit is often reported as a delete plus an add instead.
saying these in an interview costs you the question
- Believes Git stores rename operations in the commit
- Omits -- and blames Git when a branch and file share a name
- Assumes path-limited log lists every commit touching the path
- Expects --follow to accept several paths
- Thinks --full-history changes the output format rather than the traversal