Everything the adaptive question bank can teach and test in Git, from foundations through advanced practice. Work through it in order, or start practicing and let the questions find your level.
Naming by hash means corruption cannot hide: re-derive the name and compare. It also means identical content stored twice is one object, which is why a repository full of near-identical files is smaller than its checkout.
Everything else you meet — a branch, a stash entry, the index — is either a pointer to one of these or a file outside the object store entirely. Knowing the list is this short is what makes the whole model learnable.
That split is why a rename stores no new content: the blob is untouched and only the tree entry moves. It is also why two files with identical bytes anywhere in the repository share a single object.
Trees nest, so a root tree reaches every file through a chain of them. Changing one deep file therefore writes a fresh tree for each directory on the path upward, which is why a tiny edit produces several objects.
Holding the whole project is why checking out any point in history is cheap and complete rather than a replay of patches. What looks like a change is computed by comparing two of these when you ask for it.
Acyclic is what makes ancestry decidable: the questions terminate. The parent count also classifies a commit for every tool — first-parent walks, merge handling and bisect all branch on exactly that number.
Keeping the model separate from the storage is what lets packing change without history changing. It also explains why asking for a patch costs computation: the one you are reading did not exist until you asked.
Append-only is why nothing is ever edited in place — amend, rebase and filter all write new objects and move a pointer. The originals survive until collection, which is what makes every recovery trick possible.
Recording only the executable bit is a portability decision, so finer permissions will not survive a round trip and must be restored by the build. The submodule mode is the odd one: it stores an id rather than content.
B. Refs, HEAD and reachability
Costing one small file is why branching freely is the intended workflow rather than an indulgence. It also means deleting a branch deletes a pointer, and the commits persist until nothing reaches them any more.
The indirection is the whole mechanism: committing updates whatever this points AT. Replace the symbolic value with a raw id and commits still succeed while nothing follows them, which is the next entry.
Nothing is broken and nothing is lost yet — the commits exist and merely lack a name. They become unreachable the moment you switch away, so making a branch there is what turns the work into something findable.
Full paths exist because a tag and a branch may share a name and the ambiguity has to resolve somehow. When a short name behaves unexpectedly, spelling the path out is the fastest diagnostic available.
Being a cache means every count derived from it is as old as your last fetch. That single fact explains an ahead-behind figure that looks wrong, and why fetching corrects it without changing any of your work.
Two settings rather than one, which is why a half-configured branch produces confusing errors. Setting it is also what makes the short commands safe to type, since the destination stops depending on where you are.
Both locations are consulted and the loose file wins, so a stale packed entry is harmless. Knowing both exist is what explains a ref that is absent from the directory listing and still resolves perfectly.
This is the rule the whole storage model rests on: nothing is removed when you delete a branch, it merely stops being reachable. Collection is what finally frees it, which is why the grace window matters so much.
Sharing no history is the point — a documentation site, generated output or a clean initial state all want exactly that. Merging it back into the rest later then needs the unrelated-histories flag.
C. The index and the three trees
Naming the three is what makes every confusing command legible: ask which of them it moves. Reset, restore, checkout and stash differ almost entirely in that answer, and barely at all in anything else.
Staging copies the bytes at that instant, so what is queued is a snapshot rather than a pointer at the file. That is why a later edit leaves the earlier version waiting and the diff appears to disagree with the disk.
Splitting one file across commits is what turns a messy working session into a reviewable history. The risk is committing a half that does not build, which is why this pairs with testing the staged state rather than the disk.
Reading them as two independent comparisons removes most of the mystery: a path can appear in both at once, meaning there are queued changes plus newer ones on top that are not queued yet.
Invisible is the operative word — such a file is protected by nothing, so a checkout or a clean can remove it without warning. Adding it once is what brings it under every later safety check.
It is the repair for a file that should never have been committed: stop tracking it, add a rule, and the local copy survives. Note that history still contains it, which is a separate and larger problem.
Neither is a way to keep secrets out of a repository, which is the use they most often get put to. The real difference is robustness: one is a performance hint git may discard, the other an explicit instruction.
Three entries is why a conflicted path can be inspected from any side without re-running the merge, and why checking out one side is a lookup rather than a recomputation. The numbered path syntax reads them directly.
The consequence is practical rather than philosophical: a directory that must exist needs a placeholder file inside it. That is why the usual filename is a convention the tooling knows nothing about.
D. Configuration
Later wins, so a surprising setting is usually one left behind at a narrower scope. Asking git which file supplied a value answers it in one command, where guessing can cost an afternoon.
Copied at commit time is why correcting the setting never retroactively corrects anything — the existing commits already carry it. Fixing them means rewriting, which changes the id of everything downstream.
The shell escape is the part worth knowing, because it turns an alias into an arbitrary program and runs it from the repository root rather than your current directory. That relocation is what surprises people.
Matching on where the repository lives is what makes it reliable, since the right identity is selected by location rather than by remembering to switch. It is the usual cure for commits authored under the wrong address.
This rewrites CONTENT, so it changes object names, which is why two differently configured machines can show every line of a file as modified. Pinning it per path removes the question from each user's discretion.
The filesystem is the cause rather than git — two names differing only in case are one file there. That is why the rename needs forcing or splitting in two, and why it reads as git refusing to notice.
Leaving it unset means a divergence produces a merge nobody chose, which is how an unintended merge enters a history meant to stay linear. Setting it makes the policy a decision taken once rather than per command.
Matching on the stored URL is why a token can work for one remote and fail for another differing only in host or scheme. When authentication fails inexplicably, listing what the helper actually holds is the first check.
E. Making commits
Every tool reads that first line as the summary — the log, blame, the forge's interface, a merge's default text — so a subject that runs on is truncated everywhere at once. The blank line is what separates the two parts.
Two identities exist precisely so a replayed commit still credits whoever wrote it while recording who moved it. That is why a rebased history shows your name on commits you did not write, in the second field.
The shortcut covers modifications and deletions only, so a newly created file is quietly left out of the commit. That asymmetry is why it is safe for edits and unreliable as a habit while files are being added.
A new id means anything referring to the old one now points at something that leads nowhere. On a pushed branch that forces a push; locally it goes unnoticed, because the log of ref movements still holds the original.
The marker is only a message convention, so all the power sits in the rebase flag that reads it. Writing them as you go lets a long review end in one mechanical fold rather than hand-ordered surgery.
Being parsed is what lets tooling act on them — attribution, sign-off policies and generated release notes all read these. Formatting therefore matters: a line that looks right and parses wrong contributes nothing at all.
Verification is a separate step from signing, and the status characters report several distinct outcomes rather than a simple yes or no. A signature attests who created the object, never that its content is correct.
The payoff sits in the tools: reverting needs a change it can undo cleanly, bisect needs each point to build, and copying one change needs it to be separable. All three degrade together when a commit bundles unrelated work.
F. Ignoring files, and attributes
This is the single most common surprise in the whole area: adding a rule for a file already in the index changes nothing at all. Untracking it is a separate command, and the history still holds every past version.
Order decides, so a broad rule placed after a narrow one silently overrides it. The nesting rule compounds that: a file deep in the tree can be governed by a rule you are not looking at.
The reason is performance — git never descends into an excluded directory, so it cannot find the file to re-include. Excluding the contents rather than the directory itself is the way round it.
Where the slash sits changes the meaning entirely: leading pins the pattern to one directory, trailing restricts the match to directories. A pattern with neither floats and matches at any depth.
Crossing separators or stopping at one is the difference between matching a file anywhere and matching it only in the current directory. Most patterns that mysteriously fail to match wanted the longer form.
Naming the file AND the line number is what makes it decisive — the question is never whether a path is excluded but which of many rules did it. Guessing by reading the files rarely converges.
Being per-clone is the point: editor droppings and personal scratch files belong here rather than in a shared list. Nobody else ever sees it, which also means it is lost when the clone is.
Overriding each user's own configuration is the whole value, since a setting every contributor must remember is one some will not. Marking a path binary also stops git attempting a textual merge on it.
Changing how one path merges is the escape hatch for files that conflict constantly by their nature — a changelog, a lock file, a generated list. The union driver keeps both sides rather than asking.
Transforming content on the way in and out is a powerful and invisible mechanism: what is stored differs from what you edit. A missing filter on another machine then produces a file that looks corrupt.
G. Naming revisions
The two characters do genuinely different jobs, which is why they are so easily swapped: one walks backward along first parents, the other selects WHICH parent of a single merge. They coincide only outside merges.
Reachability rather than comparison is the distinction that matters, because the same syntax means something else to diff. Read it as what one side has and the other does not, and the asymmetry stops surprising.
Excluding shared ancestry is what makes it the right form for reviewing two divergent branches: everything common to both is dropped. Note that it says nothing about which side each commit came from unless you ask.
The same punctuation carrying two meanings is a genuine wart, and the one place where remembering a rule is unavoidable. In a comparison it answers what the branch added, ignoring what the base moved on to.
Reading a local log of movements means the answer depends on your own machine and your own history, so the same expression gives different results elsewhere — or fails outright in a fresh clone.
A position in a log of moves is not a step through ancestry, so these two numbering schemes diverge immediately after any switch or reset. Mixing them up is how an undo lands somewhere unexpected.
The two shorthands can resolve differently, which matters on a triangular setup where you fetch from one place and push to another. Both depend on configuration, so neither works on an unconfigured branch.
Naming content inside a commit is what lets you read or extract one file from any point without checking anything out. The stage-numbered form is the same idea applied to a conflict's three candidates.
Ambiguity grows with the repository, so a prefix recorded in a comment or a script can stop resolving years later. Anything durable should carry the full name, and anything typed can stay short.
The separator exists because one word can legitimately be both a branch and a file, and git would otherwise have to guess. Typing it defensively is free, and it forecloses an entire family of baffling failures.
H. Reading history: log and blame
Clock skew is real — commit timestamps come from whichever machine made them — so ordering by date can interleave branches that never ran concurrently. Ordering by topology instead reflects the actual structure.
Pruning is what makes the output readable and also what makes it incomplete: history simplification can drop a merge that genuinely touched the path. When a change seems to have vanished, that is usually why.
Working for one path only is the limitation to remember, since renames are inferred rather than recorded and the inference needs a single thread to follow. A directory rename cannot be traced this way.
One counts occurrences and the other matches the patch text, so they answer different questions: when a string was introduced or removed, against every commit whose diff mentions it. The second is far noisier.
Searching metadata only is the constraint that matters — these never look inside the files. Combining several filters defaults to matching any of them, which is why an unexpectedly wide result is usually this.
Collapsing each merged topic to one entry is what turns a busy history into a readable release narrative. It also hides the individual commits entirely, so it is a reading mode rather than a complete view.
Showing nothing by default is deliberate: a merge's content came from its parents, so there is no single obvious diff. The three alternatives differ in which parent they compare against and what they suppress.
Reporting the last commit to touch a line is not the same as attribution, which is the misreading the tool's name invites. A reformat, a move or a whitespace fix makes someone the apparent author of code they never wrote.
Following moved and copied lines is what rescues the previous entry's problem in the common cases, at the cost of a slower search. Turning it on is usually the difference between a useful answer and a misleading one.
Listing the bulk reformats in a committed file is what makes this durable rather than a flag someone has to remember. It is the standard companion to any repository-wide formatting change.
Narrowing to one function answers the question people actually have — how this logic evolved — where a whole-file history buries it. The function form depends on git recognizing the language's shape.
I. Diffs and patches
Showing nothing once everything is queued is the behavior that reads as a broken command. It is comparing the disk against the queue, so an empty result means those two agree, not that there are no changes.
Comparing the queue against the last commit is exactly the preview of what you are about to record, which makes it the one to read before committing rather than after wondering what went in.
The header's four numbers are a start line and a count on each side, which is what lets a patch be applied to a file that has drifted. Reading them turns a confusing hunk into an obvious one.
Nothing about a rename is stored, so it is a guess made at display time from content similarity — and a rename combined with heavy editing falls below the threshold and shows as a delete plus an add.
Hiding reformatting noise is what makes a genuine change findable inside a mechanical one. The checking flag is the opposite direction: it reports whitespace problems before they become someone else's diff noise.
Restricting by the kind of change is how a script asks a precise question — which files were deleted, which were added — instead of parsing prose. Capitals select those kinds and lowercase excludes them.
Plain application is textual and fails as soon as the surrounding lines have moved. Falling back to a real merge uses the recorded object ids instead, so it succeeds wherever an ordinary merge would.
Preserving author and message is what makes this a transfer of commits rather than of changes, which is why the mailing-list workflow produces proper history on the far side rather than a pile of anonymous edits.
J. Branching
Writing one small file is why the branch-per-change habit is effectively free, and why a repository with hundreds of them is unremarkable. Nothing is copied, so the expense people expect simply is not there.
The split exists because one command used to do both jobs and its flags were ambiguous. Choosing by intent — am I moving, or am I changing file contents — is what makes the newer pair unambiguous.
Carrying dirty files across is usually what you want and occasionally a trap, because the work now sits on a branch it was not written for. The refusal when files would be overwritten is the only guard.
Starting anywhere is what makes a hotfix from a release tag, or a branch from a colleague's fetched work, a single command. Omitting the start point is the special case rather than the rule.
The lowercase form is a safety check asking whether the work is contained somewhere else; the uppercase one skips that question. After a squash merge the check fails on work that IS merged, which is the usual false alarm.
Asking which branches are already contained is what makes pruning safe rather than a guess from names and dates. It is relative to wherever you are standing, so run it from the branch you merge into.
Repairing the link after the fact is the common case, because a branch created locally and pushed without the flag has none. Without it the short commands cannot know where this branch belongs.
The counts compare against a local snapshot, so they are exactly as current as your last fetch and no more. A figure that contradicts the forge's page is almost always this rather than a real divergence.
K. Merging
Creating no commit is the right default when nothing diverged — there is nothing to reconcile. The cost is that the branch leaves no trace in history, which is what the forcing flag exists to prevent.
The two flags encode opposite policies: always record that a branch existed, or refuse anything that would need reconciling. Choosing deliberately is what keeps a history either faithful or linear rather than accidentally mixed.
The common ancestor is what turns two unrelated-looking trees into a comparable pair — each side is measured against it rather than against the other. Almost every surprising merge result traces back to which commit it picked.
Non-overlapping changes are combined without asking, which is why most merges are silent. A conflict means the same region moved on both sides, so git declines to choose rather than guessing wrong.
The extra base section is worth turning on permanently: without it you see two final states and must infer what changed, and with it you can see what each side did to the original.
The labels are positional rather than fixed, and the position inverts during a rebase because your commits are the ones being replayed. That inversion is the single commonest cause of resolving a conflict backwards.
Nothing inspects the file for leftover markers — queuing the path is the declaration, so a file with markers still in it can be committed. A quick search for them before continuing is the only real guard.
One unwinds everything and the other walks away leaving the half-finished state in place. The second is for salvaging partial work deliberately, and is the wrong one to reach for when you simply want out.
Producing no second parent means git no longer knows the branch was merged, so later checks report it as unmerged and a re-merge offers the same changes again. That is the cost of the tidy single commit.
The refusal is a safety check, because combining two independent roots is usually an accident — a wrong remote, or a repository initialized twice. When it is deliberate the flag says so explicitly.
L. Merge strategies and rerere
The replacement was about correctness and speed on large trees rather than a change of behavior you would notice day to day. Knowing the name matters mostly when reading documentation written before the switch.
Several candidate ancestors is the case nobody expects, and it arises from criss-cross merges between branches. Combining them into one synthetic base is what lets the ordinary three-way comparison proceed.
Keeping your tree byte for byte while recording the merge is a bookkeeping move: it declares a branch integrated without taking any of its content. Confusing it with the option below is an expensive mistake.
These are not variants of one thing — one discards the other side entirely, the other takes it wherever there is no conflict. Choosing the wrong one silently drops work or silently accepts it.
Resolving toward the incoming side only where sides collide is what makes it usable: everything non-conflicting from both branches still lands. It is an auto-resolution preference, never a wholesale choice.
A whitespace-only reformat on one side turns every touched line into a conflict, which is a mechanical problem with no content behind it. Telling the merge to ignore that dimension removes the noise.
Aligning one project's root with a subdirectory of yours is what lets an upstream repository be merged in as a vendored component. The alignment is detected, so the shapes must actually correspond.
Replaying your earlier decision is what makes a long-lived branch rebased repeatedly bearable — the same conflict resolves itself after the first time. It records what you did, so a wrong resolution replays too.
M. Rebasing and rewriting history
New identities even for identical content is the fact every other entry in this chapter follows from. Nothing is moved; the commits are rewritten, and the originals linger unreferenced until collection.
The reason is mechanical rather than etiquette: anyone who built on the originals now holds commits whose parents no longer exist in your branch. Their next merge reintroduces both copies of everything.
Specifying a range explicitly is what lets an intermediate branch be cut out of the lineage — the hard case the two-argument form cannot express. It is worth learning precisely because the default guesses.
The list executes from the top down, so reordering lines reorders history. Deleting a line drops that commit entirely, which is why an accidental deletion is a real loss rather than a no-op.
One asks you to combine the messages and the other throws the second away silently. Picking the wrong one loses a commit message nobody notices is gone until they read the history later.
Reordering the marked commits under their targets BEFORE the list opens is what makes the whole convention pay — otherwise you would be dragging lines around by hand. It depends entirely on the message format.
Three outcomes rather than two is the part to hold onto: continue, drop this one commit, or unwind everything. Reaching for the last when you wanted the middle discards an afternoon of resolutions.
The inversion catches almost everyone, because your own work is what is being replayed and so counts as the incoming side. Checking which label means which before resolving is cheaper than discovering it afterwards.
Setting dirty work aside automatically removes the most common reason a rebase refuses to start. The restoration can itself conflict, which is the one case where the convenience costs something.
Running a command after each replayed commit is how you find which one broke the build rather than discovering it at the end. It is bisect's logic applied during the rewrite instead of after it.
The check is against the value you last fetched, so it refuses when someone else has pushed in the meantime. A blind force has no such check and silently discards their work, which is the whole difference.
The older tool is slow and has correctness traps that bite on exactly the repositories big enough to need it. Preferring the newer one is a plain recommendation rather than a matter of taste.
Every commit after the edited one gets a new identity, because each names its parent. That cascade is why rewriting old history is disruptive out of all proportion to the size of the edit.
N. Cherry-pick and revert
A new identity means the repository now holds two commits with the same effect and no recorded relationship. That duplication is harmless until the branches meet, which is what the later entry here is about.
The left endpoint is the starting point rather than the first item, which is the same convention the reachability ranges use. Expecting it to be included is how one commit silently fails to arrive.
A merge has no single diff, so the command cannot proceed until you say which parent to measure against. Choosing the wrong side gives a clean-looking result containing precisely the opposite change.
Recording where the copy came from is what keeps the duplicate traceable months later, when nobody remembers that two commits are the same change. It is a message line, so nothing enforces or reads it automatically.
The same change arriving twice through different lineages is what produces a conflict between a commit and itself. Git has no record that they are related, so it treats two identical edits as rival ones.
Appending rather than removing is what makes it the only safe undo on a shared branch: nobody's clone is invalidated, because nothing they hold has changed. The mistake stays visible, which is usually correct.
Undoing a merge has the same ambiguity as copying one, and it leaves a lasting consequence: the branch still counts as merged, so merging it again brings nothing. Re-landing it needs the next entry.
Undoing the undo is the documented route precisely because the original merge is still recorded, so the branch cannot simply be merged again. It looks like a trick and is the supported answer.
O. Undoing: reset, restore and clean
Moving only the pointer leaves everything staged exactly as it was, which is what makes it the tool for recombining recent commits. Nothing on disk changes, so it is the least destructive of the three.
Rewriting the queue while leaving files alone is why this is the default: the changes come back as unstaged edits, ready to be re-split. Your work is never at risk here, only its arrangement.
This is the one command in ordinary use that destroys uncommitted work with no copy anywhere, because the discarded content was never in an object. The log of ref movements can restore the commits and not the edits.
Supplying a path changes the operation entirely — no branch moves, and one file is copied into the queue from a commit. The shared command name hides that these are two different things.
Naming which of the trees to rewrite is exactly the clarity the older combined command lacked. Choosing both at once is the destructive combination, and it is the one that needs saying out loud.
Pulling one file out of any commit without moving anything is the operation people reach for reset to do and get wrong. It touches a path, never a branch, which is what makes it safe to run anywhere.
Requiring the flag is deliberate, since the files it removes exist nowhere else. The preview flag is the habit worth forming: run it first every time, because there is no undo afterwards.
One rewrites what the branch points at and the other adds a commit, so only the second survives contact with anyone who has fetched. Choosing between them is a question about who else holds the history.
Recording the previous position before a big operation gives a one-step way back without reading any log. It is overwritten by the next such operation, so it rescues the last move only.
P. Stashing and multiple working trees
Being real commits is why a stash survives crashes and can be recovered after a careless drop — the content is in the object store like anything else. It is not a patch file and never was.
The default leaves untracked files in place, which is why a stash sometimes seems not to have cleaned the tree. Two further levels exist, and the widest one sweeps in ignored files you probably want to keep.
Popping removes the entry only on a clean application, so a conflict leaves it intact. Applying never removes it, which makes it the safer verb when the same stash may be needed on a second branch.
The indices shift every time you stash again, so a number written down a moment ago can refer to something else. Referring to an entry by its message rather than its position avoids that entirely.
Leaving the entry in place during a conflict is the designed safety: nothing is lost while you work out the resolution. Dropping it manually afterwards is the step people forget, leaving a stale duplicate.
A dropped entry is merely unreferenced rather than gone, so it can be found and given a name again until collection runs. This is the same recovery route every unreachable commit has.
Several checkouts over one object store is what makes reviewing a colleague's branch cost no download and no second configuration. The shared store is also why the next two constraints exist.
The restriction follows from the shared refs: two directories advancing one branch would be two answers to where it points. The refusal is the system refusing to be inconsistent rather than an arbitrary limit.
Sharing refs, configuration and stashes is the advantage and the hazard in one — a branch deleted in one directory is deleted everywhere. A clone isolates completely, at the cost of a second copy of everything.
Q. Remotes, transports and clones
The default name carries no special meaning, which matters as soon as a fork workflow adds a second one. Naming them for their role rather than by convention is what keeps the commands readable.
The transport decides how you authenticate and whether writing is possible at all, which is why a clone can work and a push fail with nothing else changed. A read-only protocol is the usual cause.
Never touching your work is what makes fetching unconditionally safe, and why it is the right first move when anything looks out of date. It changes only the cache and the object store.
The second half is the part that can surprise you, because its behavior comes from configuration rather than from the command you typed. Splitting it into two commands is how you see what is about to happen.
The mapping is what every fetch and push uses underneath, usually from configuration rather than typed. The leading character permits an update that would otherwise be refused, which is where force lives.
The refusal means accepting your push would drop commits the remote has, so it is a protection rather than an obstacle. Fetching and integrating is the answer; forcing is choosing to discard them.
Comparing against what you last saw is what distinguishes it from a blind overwrite: it refuses when someone else pushed meanwhile. It is the form to make a habit, since the blind one has no such check.
Deleted branches linger in the cache forever otherwise, so a long-lived clone accumulates names for things that no longer exist. Pruning on fetch keeps the listing an accurate picture of the server.
Sending none by default is why a release tag so often exists locally and nowhere else. The two flags differ in reach, and the narrower one sends only what the pushed commits actually reach.
Having no working tree is precisely why pushing into one is safe — there is no checkout to be left inconsistent with the refs. That is the whole reason server-side repositories take this form.
Truncating history buys clone speed and costs the operations that need ancestry: finding a common ancestor, tracing a line's origin, rebasing onto anything old. For a build that never asks, the trade is free.
Deferring file contents until something needs them keeps ancestry intact, which is why it suits a working clone better than truncating history does. The cost is a fetch mid-command when you touch old files.
R. Tags, describe and notes
Only one of the two is an object with its own metadata, so only one can be signed, dated or carry a message. Releases want that; a throwaway marker does not, and the choice is made at creation.
Staying put is the entire value — a name that tracked the branch would be useless for identifying a release. It also means a tag made too early stays wrong until somebody deliberately replaces it.
Not traveling with an ordinary push is why a release exists on one laptop and nowhere else, and why nobody notices until a build asks for it. Making the push explicit is the only cure.
Existing clones never update a tag they already have, so a moved one means two machines disagree about what a release is while both believe they are current. Publishing a new version avoids the disagreement entirely.
Naming a commit by its distance from the last tag is where build version strings come from, which is why a repository with no tags produces a build that cannot describe itself. It needs annotated tags by default.
Signing and verifying are separate steps, so an unverified signature is indistinguishable from none unless something checks. In a release pipeline the checking step is the one that carries the value.
Attaching metadata without changing the commit is what makes it possible to annotate history that is already published. They live in their own namespace, so they are not fetched or pushed by default.
S. Hooks
Living outside the repository is the fact every attempt to share hooks runs into: cloning installs none of them, so a hook nobody has is a policy nobody follows. The next entry is the usual answer.
Pointing at a tracked directory is what makes hooks shareable, because the scripts then arrive with the clone. It still needs each person to set the configuration once, which a setup script usually does.
Running before the message is composed means a rejection costs you nothing written. A non-zero exit aborts, so a hook that is slow or flaky becomes a tax on every commit and gets bypassed.
Receiving the file path rather than the text is what lets it rewrite the message in place as well as reject it — appending a ticket reference, normalizing a prefix. Validation and mutation use the same hook.
Seeding the message before the editor opens is the difference between offering a template and rejecting what somebody typed. It runs earlier than the validating hook and does a complementary job.
Sitting last among the client-side gates is why it suits expensive checks that should not run on every commit. It is given the refs and ids on standard input, so it can inspect exactly what is leaving.
Only the first two run before the update, so only those can refuse it; the third runs after and is for notification. Putting a rejection in the wrong one produces a hook that complains about an accepted push.
Being advisory is the point to internalize: client hooks catch mistakes and cannot enforce policy, because anyone can skip them with a flag. Anything that genuinely must hold belongs on the server.
T. Bisecting a regression
The logarithm is what makes it practical — a thousand candidate commits is about ten tests. That ratio is why bisecting is almost always faster than reasoning about which change looks suspicious.
Ancestry is required because the search walks between the two points; unrelated endpoints give it no path to halve. Choosing a known-good point far enough back costs one extra test and avoids the undefined case.
Reading the exit code is what turns the whole search into one unattended command. The reserved value for untestable is the part worth knowing, since a script that cannot build should say so rather than report a failure.
Routing around an untestable commit is what keeps a search alive through a stretch of broken history. Too many skips leaves the result as a range rather than a single commit, which is still useful.
The detached state persists until you end the session explicitly, so an interrupted bisect leaves the repository somewhere unexpected. Ending it is the step that gets forgotten after the answer is found.
Renaming the endpoints is what makes the search usable when you are hunting the commit that FIXED something, where the good-and-bad vocabulary reads backwards and invites mistakes.
A history whose intermediate commits do not build makes every other test untestable, which is where the previous entry's skipping comes from. The discipline that pays here is the one from the commit chapter.
U. Submodules and vendoring
Storing one id rather than the files is why a superproject stays small and why the submodule's content is invisible to its diffs. The special mode is the tell when a tree listing looks wrong.
Two locations is why a submodule can point somewhere different from what the repository says: the committed mapping is the intent, your clone's configuration is the reality. They drift after a URL change.
Empty directories after a fresh clone is the single most common first encounter, and it reads as a broken repository. The recursive flag at clone time avoids the whole experience.
Checking out a recorded id leaves no branch, so work done inside is on nothing and is easy to lose on the next update. Creating a branch before editing is the habit that prevents it.
Updating the dependency is a commit in the parent repository, which is what makes the change reviewable and revertable. Forgetting that second commit is why a colleague's clone keeps the old version.
Following a branch rather than a pinned id trades reproducibility for convenience: a clone made tomorrow may get different content. It still requires the explicit update, so it is not automatic.
No single command removes one, and the leftover directory in the repository's internals is what makes a re-add behave strangely afterwards. Knowing there are three steps is most of the fix.
Vendoring the files into the tree means a clone needs nothing extra and nobody has to learn the commands. The cost is that the upstream relationship is a merge history rather than a pointer anyone can read.
V. Maintenance, packing and large repositories
Running automatically is why unreferenced objects disappear without anyone deciding, which is what ends every recovery window. Knowing it is already scheduled matters more than knowing how to run it.
Delta compression lives here rather than in the object model, which is the distinction the snapshots entry depends on. Objects are whole; what is on disk is encoded against its neighbors.
The default window is what makes almost every recovery story possible, and it is also why those stories stop working after a fortnight. It is a configurable default, not a guarantee.
Caching the expensive part of ancestry queries is what keeps a large history's log and merge-base lookups fast. It is derived data, so it can be rebuilt and never needs to be in the repository.
Verifying integrity is the rarely-needed half; listing unreachable objects is the half people actually use, because it is how a dropped commit is found once no log entry points at it.
Materializing part of the tree leaves the repository complete, so this is a working-copy optimization and not a truncation. Confusing it with a shallow or partial clone leads to the wrong tool for a slow checkout.
Every clone carries it until history itself is rewritten, which is disruptive for everyone. That asymmetry — cheap to avoid, expensive to undo — is why the guard belongs at the point of adding.
Moving upkeep into the background is what stops it interrupting a command at the worst moment on a large repository. It replaces the ad-hoc habit rather than adding to it.
W. Reflog and recovery
Logging every movement, with the command responsible, is what makes almost any local mistake reversible. It is the safety net under reset, rebase and a mistaken checkout alike.
Never traveling is the limitation that matters: a colleague cannot recover your lost commit, and a fresh clone has no history of moves at all. The net exists only on the machine that made the mistake.
The tip is still logged, so re-creating the branch at that id restores it exactly. This is why deleting a branch locally is a low-stakes action and deleting one on a server is not.
The previous position is one entry back, which makes undoing a mis-aimed reset a single command. Knowing this before you need it is what turns a frightening moment into an uneventful one.
The overwritten commits remain in your own object store even though the remote no longer names them, so the machine that pushed is the one that can put them back. That window closes at collection.
Two different windows apply depending on whether anything still reaches the entry, which is why some recoveries work months later and others fail within weeks. Both are defaults rather than fixed limits.
When no logged movement survives, scanning for unreferenced objects is the last route in. It returns raw ids with no context, so identifying the right one is a reading exercise rather than a lookup.
Every technique in this chapter depends on the objects still existing, so collection is the real deadline rather than any of the individual expiry settings. Recover first and investigate afterwards.
X. Collaboration workflows
The choice is about who needs write access rather than about history: one grants it, the other replaces it with a review step. That is why the fork model suits contributors you do not know.
Making the branch the unit is what lets review, merge and revert all operate on the same thing. A branch carrying two unrelated changes breaks all three at once, which is the practical argument for splitting.
The value is a place where work is combined before it reaches the stable line, and the rule for graduating between them is what makes it more than extra ceremony. Without that rule it is a branch nobody trusts.
The trade is faithfulness against readability, and both cost something at different moments: one preserves what actually happened, the other produces a history that is easier to search and bisect.
The compromise keeps the content linear while leaving one commit that records the topic existed, which is what the squash approach throws away. It is why this combination is so widely adopted.
The enforcement lives in the hosting platform rather than in git, so it is absent from a bare clone and from any other remote. Treating it as a property of the repository is how people are surprised locally.
Adding the original as a second remote is the step that makes the rest routine, because without it there is no local name for where the work actually belongs. Fetching it is then an ordinary operation.
The forward-port is the half that gets forgotten, and the symptom is a bug returning in the next release. Treating the fix as two commits from the outset is what prevents it.
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.