From c8d75a5cc7b70e4bc5feb008d058d37c7f52cae1 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Sun, 26 Apr 2026 09:21:28 +0300 Subject: [PATCH 1/4] docs: improve thin recipes in rebasing, stashing, and submodules Add context, explanations, and tips to 6 recipes that were bare commands without enough context for beginners. Closes #154 Co-Authored-By: Claude Opus 4.6 (1M context) --- chapters/recipes/rebasing.md | 14 +++++++++++++- chapters/recipes/stashing.md | 12 ++++++++++++ chapters/recipes/submodules.md | 16 ++++++++++------ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/chapters/recipes/rebasing.md b/chapters/recipes/rebasing.md index 79b69d5..c360dfb 100644 --- a/chapters/recipes/rebasing.md +++ b/chapters/recipes/rebasing.md @@ -16,17 +16,29 @@ $ git rebase main ### Squash commits with interactive rebase +Squashing combines multiple commits into one, keeping history clean +before merging a feature branch. + ```text $ git rebase -i HEAD~3 -# change "pick" to "squash" for commits to combine +# editor opens — change "pick" to "squash" (or "s") for commits to fold +# save and close — a second editor opens to combine the messages ``` +If conflicts arise during the squash, resolve them and run +`git rebase --continue`. + ### Abort a conflicted rebase +Restores the branch to its exact state before the rebase started. +Use this when conflicts are too tangled to resolve cleanly. + ```text $ git rebase --abort ``` +Any conflict resolutions you made during the rebase are discarded. + ### Continue after resolving a rebase conflict ```text diff --git a/chapters/recipes/stashing.md b/chapters/recipes/stashing.md index bef4635..8670ac9 100644 --- a/chapters/recipes/stashing.md +++ b/chapters/recipes/stashing.md @@ -9,10 +9,16 @@ order: 77 ### Save work in progress +Stashing saves your modified and staged files without creating a +commit — useful when you need to switch branches mid-task. + ```text $ git stash push -m "description" ``` +Without `-m`, Git generates a message from the current HEAD commit, +which makes it harder to find the right stash later. + ### Save including untracked files ```text @@ -28,8 +34,14 @@ $ git stash apply # restore but keep in stash ### List and drop stash entries +Entries are numbered starting at 0 (most recent). Use the +`stash@{N}` syntax to target a specific entry. + ```text $ git stash list # show all entries $ git stash drop stash@{0} # delete a specific entry $ git stash clear # delete all entries ``` + +`stash@{0}` is always the latest stash. After dropping an entry, +the remaining entries are renumbered. diff --git a/chapters/recipes/submodules.md b/chapters/recipes/submodules.md index d06f7bd..44a8cb5 100644 --- a/chapters/recipes/submodules.md +++ b/chapters/recipes/submodules.md @@ -9,11 +9,18 @@ order: 79 ### Add a submodule +A submodule embeds an external repository at a fixed commit inside +your project. Use submodules when you need to track an upstream +library or shared component while keeping its history separate. + ```text $ git submodule add $ git commit -m "Add submodule" ``` +This creates a `.gitmodules` file (or appends to it) and records the +pinned commit in the index. + ### Clone a repo with submodules ```text @@ -36,9 +43,6 @@ $ git commit -m "Update submodule" ### Remove a submodule -```text -$ git submodule deinit -$ git rm -$ rm -rf .git/modules/ -$ git commit -m "Remove submodule" -``` +See [Remove a Submodule](remove-submodule.md) for a full +walkthrough — the three cleanup steps, what each does, and common +gotchas. From 65144bab2a68cae009cfeeaa384fc5bc4b4090f4 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Sun, 26 Apr 2026 09:21:38 +0300 Subject: [PATCH 2/4] docs: split 5 substantial playbook recipes into standalone pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract git bisect, pre-commit hook, commit-msg hook, SSH setup, and remove submodule into their own pages. Update parent pages with cross-references and add entries to the playbook index. Borderline candidates (merge conflicts, sync fork, fetch+inspect, update submodule) evaluated and kept grouped — not enough substance to stand alone. Closes #153 Co-Authored-By: Claude Opus 4.6 (1M context) --- chapters/07-playbook.md | 5 ++ chapters/recipes/commit-msg-hook.md | 55 ++++++++++++++++++++++ chapters/recipes/debugging.md | 16 +------ chapters/recipes/git-bisect.md | 54 ++++++++++++++++++++++ chapters/recipes/hooks.md | 33 ++------------ chapters/recipes/pre-commit-hook.md | 58 +++++++++++++++++++++++ chapters/recipes/remote-management.md | 12 ++--- chapters/recipes/remove-submodule.md | 42 +++++++++++++++++ chapters/recipes/ssh-setup.md | 66 +++++++++++++++++++++++++++ 9 files changed, 289 insertions(+), 52 deletions(-) create mode 100644 chapters/recipes/commit-msg-hook.md create mode 100644 chapters/recipes/git-bisect.md create mode 100644 chapters/recipes/pre-commit-hook.md create mode 100644 chapters/recipes/remove-submodule.md create mode 100644 chapters/recipes/ssh-setup.md diff --git a/chapters/07-playbook.md b/chapters/07-playbook.md index 4d43133..758e197 100644 --- a/chapters/07-playbook.md +++ b/chapters/07-playbook.md @@ -38,6 +38,7 @@ For definitions, see [Glossary](09-glossary.md). |--------|-----------------| | [Remote Operations](playbook/remote-operations.md) | Push, pull, force push safely, sync forks | | [Remote Management](playbook/remote-management.md) | Add, rename, remove remotes, switch URL, SSH setup | +| [SSH Setup](playbook/ssh-setup.md) | Key generation, agent, GitHub registration, troubleshooting | ## Project Structure @@ -45,6 +46,7 @@ For definitions, see [Glossary](09-glossary.md). |--------|-----------------| | [Tagging](playbook/tagging.md) | Create, push, and delete annotated tags | | [Submodules](playbook/submodules.md) | Add, clone, update, and remove submodules | +| [Remove a Submodule](playbook/remove-submodule.md) | Step-by-step cleanup — deinit, git rm, cached data | | [Subtrees](playbook/subtrees.md) | Add, pull, push, and remove subtrees | ## Advanced @@ -53,5 +55,8 @@ For definitions, see [Glossary](09-glossary.md). |--------|-----------------| | [Selectors](playbook/selectors.md) | Tilde, caret, double-dot, triple-dot, reflog refs | | [Hooks](playbook/hooks.md) | Pre-commit, commit-msg, sharing hooks, bypassing | +| [Pre-commit Hook](playbook/pre-commit-hook.md) | Script creation, common checks, sharing and bypassing | +| [Commit-msg Hook](playbook/commit-msg-hook.md) | Message validation, Conventional Commits, examples | | [Debugging](playbook/debugging.md) | Bisect, blame, and search commit history | +| [Git Bisect](playbook/git-bisect.md) | Binary search for the commit that introduced a bug | | [Configuration](playbook/configuration.md) | Identity, defaults, aliases, and diagnostics | diff --git a/chapters/recipes/commit-msg-hook.md b/chapters/recipes/commit-msg-hook.md new file mode 100644 index 0000000..710bde6 --- /dev/null +++ b/chapters/recipes/commit-msg-hook.md @@ -0,0 +1,55 @@ +--- +title: "Commit-msg Hook" +description: "How to create a Git commit-msg hook that validates or enforces commit message conventions." +section: "playbook/commit-msg-hook" +order: 90 +--- + +## Commit-msg Hook + +A commit-msg hook runs after you write the commit message but before +the commit is finalized. It receives the path to a temporary file +containing the message as its first argument (`$1`). If the script +exits non-zero, the commit is aborted. + +### Create the hook + +```text +$ cat > .git/hooks/commit-msg << 'EOF' +#!/bin/sh +# Enforce minimum message length +if [ $(wc -c < "$1") -lt 10 ]; then + echo "Error: commit message too short" + exit 1 +fi +EOF +$ chmod +x .git/hooks/commit-msg +``` + +### Common validations + +- **Minimum length** — reject messages that are too terse to be + meaningful. +- **Conventional Commits format** — check that the message matches + `type(scope): description` using a regex. +- **Ticket reference** — require a Jira/Linear/GitHub issue key + (e.g. `PROJ-123`) in the message. + +### Example: enforce Conventional Commits + +```text +$ cat > .git/hooks/commit-msg << 'EOF' +#!/bin/sh +pattern="^(feat|fix|docs|chore|refactor|test|ci|style)(\(.+\))?: .{3,}" +if ! grep -qE "$pattern" "$1"; then + echo "Error: message must follow Conventional Commits format" + exit 1 +fi +EOF +$ chmod +x .git/hooks/commit-msg +``` + +### Sharing and bypassing + +Same as other hooks — see the [Hooks](hooks.md) recipe for +`core.hooksPath` and `--no-verify`. diff --git a/chapters/recipes/debugging.md b/chapters/recipes/debugging.md index 0ad3963..4f7af80 100644 --- a/chapters/recipes/debugging.md +++ b/chapters/recipes/debugging.md @@ -9,20 +9,8 @@ order: 80 ### Find which commit introduced a bug -```text -$ git bisect start -$ git bisect bad # current commit has the bug -$ git bisect good # this older commit was fine -# ... test each midpoint, mark good/bad ... -$ git bisect reset # done — return to original branch -``` - -### Automated bisect with a test script - -```text -$ git bisect start HEAD -$ git bisect run ./test.sh -``` +Use [Git Bisect](git-bisect.md) for a full walkthrough of manual and +automated binary search through commit history. ### See who last changed each line diff --git a/chapters/recipes/git-bisect.md b/chapters/recipes/git-bisect.md new file mode 100644 index 0000000..0b8d145 --- /dev/null +++ b/chapters/recipes/git-bisect.md @@ -0,0 +1,54 @@ +--- +title: "Git Bisect" +description: "How to use git bisect to find the exact commit that introduced a bug, with manual and automated workflows." +section: "playbook/git-bisect" +order: 88 +--- + +## Git Bisect + +Bisect performs a binary search through your commit history to find +which commit introduced a bug. Instead of checking each commit +one-by-one, it halves the search space at every step. + +### Manual bisect + +```text +$ git bisect start +$ git bisect bad # current commit has the bug +$ git bisect good # this older commit was fine +``` + +Git checks out a midpoint commit. Test it, then mark it: + +```text +$ git bisect good # midpoint is clean +$ git bisect bad # midpoint has the bug +``` + +Repeat until Git identifies the first bad commit, then clean up: + +```text +$ git bisect reset # return to original branch +``` + +### Automated bisect with a test script + +If you have a script that exits 0 for good and non-zero for bad, +bisect can run unattended: + +```text +$ git bisect start HEAD +$ git bisect run ./test.sh +``` + +Git runs the script at each midpoint and reports the first bad +commit when done. + +### Tips + +- The good and bad commits do not have to be on the same branch — + bisect works across the entire reachable history. +- If a midpoint commit cannot be tested (e.g. broken build), use + `git bisect skip` to move past it. +- Use `git bisect log` to replay or share a bisect session. diff --git a/chapters/recipes/hooks.md b/chapters/recipes/hooks.md index a74774a..61449e2 100644 --- a/chapters/recipes/hooks.md +++ b/chapters/recipes/hooks.md @@ -9,38 +9,13 @@ order: 83 ### Create a pre-commit hook -```text -$ cat > .git/hooks/pre-commit << 'EOF' -#!/bin/sh -# Reject commits that contain TODO -if git diff --cached --quiet -S "TODO"; then - exit 0 -fi -echo "Error: commit contains TODO" -exit 1 -EOF -$ chmod +x .git/hooks/pre-commit -``` - -The hook runs before every commit. A non-zero exit code aborts the -commit. +See [Pre-commit Hook](pre-commit-hook.md) for a full walkthrough — +script creation, common checks, and sharing with the team. ### Create a commit-msg hook -```text -$ cat > .git/hooks/commit-msg << 'EOF' -#!/bin/sh -# Enforce minimum message length -if [ $(wc -c < "$1") -lt 10 ]; then - echo "Error: commit message too short" - exit 1 -fi -EOF -$ chmod +x .git/hooks/commit-msg -``` - -Receives the commit message file as `$1`. Useful for enforcing -message conventions. +See [Commit-msg Hook](commit-msg-hook.md) for a full walkthrough — +message validation, Conventional Commits enforcement, and examples. ### Share hooks with the team diff --git a/chapters/recipes/pre-commit-hook.md b/chapters/recipes/pre-commit-hook.md new file mode 100644 index 0000000..0abee0a --- /dev/null +++ b/chapters/recipes/pre-commit-hook.md @@ -0,0 +1,58 @@ +--- +title: "Pre-commit Hook" +description: "How to create a Git pre-commit hook that validates staged changes before every commit." +section: "playbook/pre-commit-hook" +order: 89 +--- + +## Pre-commit Hook + +A pre-commit hook runs automatically before every commit. If the +script exits with a non-zero code, the commit is aborted. Use it to +catch problems early — linting errors, TODO markers, large files, +or secrets. + +### Create the hook + +```text +$ cat > .git/hooks/pre-commit << 'EOF' +#!/bin/sh +# Reject commits that contain TODO +if git diff --cached --quiet -S "TODO"; then + exit 0 +fi +echo "Error: commit contains TODO" +exit 1 +EOF +$ chmod +x .git/hooks/pre-commit +``` + +### How it works + +1. You run `git commit`. +2. Git executes `.git/hooks/pre-commit` before opening the message + editor. +3. If the script exits 0, the commit proceeds. Any other exit code + aborts it. + +### Common checks + +- **Lint staged files** — run your linter on `git diff --cached --name-only` + output. +- **Detect secrets** — search for API keys, tokens, or passwords in + the diff. +- **Enforce file size limits** — reject files above a threshold. + +### Sharing pre-commit hooks + +Hooks in `.git/hooks/` are local and not committed. To share them +with the team, see the [Hooks](hooks.md) recipe for the +`core.hooksPath` approach. + +### Bypassing + +```text +$ git commit --no-verify -m "WIP: skip hooks" +``` + +Use sparingly — hooks exist for a reason. diff --git a/chapters/recipes/remote-management.md b/chapters/recipes/remote-management.md index 0a8ef46..1b9c722 100644 --- a/chapters/recipes/remote-management.md +++ b/chapters/recipes/remote-management.md @@ -46,12 +46,6 @@ $ git remote set-url origin git@github.com:/.git ### Set up SSH authentication -```text -$ ssh-keygen -t ed25519 -C "you@example.com" -$ eval "$(ssh-agent -s)" -$ ssh-add ~/.ssh/id_ed25519 -$ ssh -T git@github.com # verify connection -``` - -Add the public key (`~/.ssh/id_ed25519.pub`) to your GitHub account -under Settings > SSH Keys. +See [SSH Setup](ssh-setup.md) for a full walkthrough — key +generation, agent configuration, GitHub registration, and +troubleshooting. diff --git a/chapters/recipes/remove-submodule.md b/chapters/recipes/remove-submodule.md new file mode 100644 index 0000000..b53543f --- /dev/null +++ b/chapters/recipes/remove-submodule.md @@ -0,0 +1,42 @@ +--- +title: "Remove a Submodule" +description: "Step-by-step guide to fully removing a Git submodule — deinit, git rm, and cleaning up cached module data." +section: "playbook/remove-submodule" +order: 92 +--- + +## Remove a Submodule + +Removing a submodule is not a single command — it requires three +cleanup steps to fully clear the submodule from your repository. + +### Steps + +```text +$ git submodule deinit # 1. unregister from .git/config +$ git rm # 2. remove from working tree and index +$ rm -rf .git/modules/ # 3. delete cached clone +$ git commit -m "Remove submodule" +``` + +### What each step does + +1. **`deinit`** — removes the submodule entry from `.git/config` and + clears the working directory at ``. The submodule's URL + remains in `.gitmodules` until the next step removes it. +2. **`git rm`** — removes the submodule entry from `.gitmodules` and + from the index (staging area). Also deletes the working directory + if `deinit` did not already. +3. **`rm -rf .git/modules/`** — deletes the cached bare clone + that Git keeps under `.git/modules/`. Without this step, re-adding + a submodule at the same path can use stale data. + +### Common gotchas + +- **Forgetting step 3** — the cached clone stays behind. If you later + add a submodule at the same path, Git may reuse the old checkout + and you get confusing state. +- **Uncommitted changes in the submodule** — `deinit` will refuse to + run. Use `--force` if you are certain the changes are not needed. +- **Nested submodules** — if the submodule itself contains submodules, + you need to deinit recursively or clean up `.git/modules/` manually. diff --git a/chapters/recipes/ssh-setup.md b/chapters/recipes/ssh-setup.md new file mode 100644 index 0000000..48035a8 --- /dev/null +++ b/chapters/recipes/ssh-setup.md @@ -0,0 +1,66 @@ +--- +title: "SSH Setup" +description: "How to generate an SSH key, add it to the SSH agent, register it with GitHub, and switch a remote from HTTPS to SSH." +section: "playbook/ssh-setup" +order: 91 +--- + +## SSH Setup + +SSH authentication lets you push and pull without entering your +password every time. It uses a key pair — a private key on your +machine and a public key registered with your Git host. + +### 1. Generate a key + +```text +$ ssh-keygen -t ed25519 -C "you@example.com" +``` + +Accept the default file location (`~/.ssh/id_ed25519`). Set a +passphrase for extra security, or press Enter to skip. + +### 2. Start the SSH agent and add the key + +```text +$ eval "$(ssh-agent -s)" +$ ssh-add ~/.ssh/id_ed25519 +``` + +On macOS, add `--apple-use-keychain` to persist the key across +reboots. + +### 3. Register the public key with GitHub + +Copy the public key to your clipboard: + +```text +$ cat ~/.ssh/id_ed25519.pub +``` + +Then go to **GitHub > Settings > SSH and GPG Keys > New SSH Key**, +paste the key, and save. + +### 4. Verify the connection + +```text +$ ssh -T git@github.com +``` + +You should see a message like "Hi username! You've successfully +authenticated." + +### 5. Switch an existing remote from HTTPS to SSH + +```text +$ git remote set-url origin git@github.com:/.git +``` + +### Troubleshooting + +- **Permission denied (publickey)** — the agent does not have the + key loaded. Run `ssh-add` again. +- **Wrong key used** — if you have multiple keys, create an + `~/.ssh/config` entry to map the host to the correct key file. +- **Firewall blocks port 22** — use SSH over HTTPS port: + `ssh -T -p 443 git@ssh.github.com`. From 3988c2e237adb730ec416032e2da05cf048c0272 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Sun, 26 Apr 2026 09:24:33 +0300 Subject: [PATCH 3/4] docs: add back-link from remove-submodule to parent recipe Co-Authored-By: Claude Opus 4.6 (1M context) --- chapters/recipes/remove-submodule.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chapters/recipes/remove-submodule.md b/chapters/recipes/remove-submodule.md index b53543f..365863a 100644 --- a/chapters/recipes/remove-submodule.md +++ b/chapters/recipes/remove-submodule.md @@ -40,3 +40,6 @@ $ git commit -m "Remove submodule" run. Use `--force` if you are certain the changes are not needed. - **Nested submodules** — if the submodule itself contains submodules, you need to deinit recursively or clean up `.git/modules/` manually. + +For other submodule operations, see the [Submodules](submodules.md) +recipe. From 51c012542b1baabcea7699e4df9ac70d4df3dd84 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Sun, 26 Apr 2026 09:25:09 +0300 Subject: [PATCH 4/4] docs: add back-link from git-bisect to parent recipe Co-Authored-By: Claude Opus 4.6 (1M context) --- chapters/recipes/git-bisect.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chapters/recipes/git-bisect.md b/chapters/recipes/git-bisect.md index 0b8d145..7c08b4b 100644 --- a/chapters/recipes/git-bisect.md +++ b/chapters/recipes/git-bisect.md @@ -52,3 +52,6 @@ commit when done. - If a midpoint commit cannot be tested (e.g. broken build), use `git bisect skip` to move past it. - Use `git bisect log` to replay or share a bisect session. + +For blame, log search, and other debugging tools, see the +[Debugging](debugging.md) recipe.