git.md (36237B)
1 --- 2 title: "Git" 3 description: "Everyday Git: staging, commits, remotes, log/diff, stash, merge/rebase and recovery." 4 category: git-workflow 5 tags: [git, version-control, workflow] 6 tools: [git] 7 difficulty: beginner 8 updated: "2026-08-09" 9 source: "vault:Git/git-cheatsheet.md" 10 --- 11 12 # Git 13 14 > **Scope —** Covers the full GitByBit core curriculum taught as *workflows* rather than a bare command list, plus a Real-World Workflows section (history wipe + re-publish, branch create/merge, conflict resolution). For deeper dives see Git Branching and Git Reset & Undo. 15 16 The mental model: Git tracks snapshots across three areas. Every command moves changes between them. 17 18 <figure class="flow plate corners"> 19 <figcaption class="flow__cap"><span class="flow__kind">The git object model</span><span class="flow__dir">LR</span></figcaption> 20 <div class="flow__body"> 21 <svg class="flow-svg" viewBox="0 0 900 200" role="img" aria-label="Working tree to staging to repository to remote, with checkout, restore, pull and fetch flowing back"> 22 <!-- forward chain --> 23 <path class="fedge" d="M185,82 L263,82" marker-end="url(#flow-arrow)" /> 24 <path class="fedge" d="M415,82 L493,82" marker-end="url(#flow-arrow)" /> 25 <path class="fedge" d="M645,82 L723,82" marker-end="url(#flow-arrow)" /> 26 <!-- back edges --> 27 <path class="fedge is-back" d="M540,106 L540,172 L110,172 L110,108" marker-end="url(#flow-arrow)" /> 28 <path class="fedge is-back" d="M800,106 L800,148 L600,148 L600,108" marker-end="url(#flow-arrow)" /> 29 <!-- nodes --> 30 <g class="fnode"><rect class="fnode__box" x="35" y="58" width="150" height="48" /><text class="fnode__label" x="110" y="79" text-anchor="middle">Working tree<tspan class="sub" x="110" dy="15">your edits</tspan></text></g> 31 <g class="fnode"><rect class="fnode__box" x="265" y="58" width="150" height="48" /><text class="fnode__label" x="340" y="79" text-anchor="middle">Staging area<tspan class="sub" x="340" dy="15">index</tspan></text></g> 32 <g class="fnode"><rect class="fnode__box" x="495" y="58" width="150" height="48" /><text class="fnode__label" x="570" y="79" text-anchor="middle">Repository<tspan class="sub" x="570" dy="15">.git history</tspan></text></g> 33 <g class="fnode is-goal"><rect class="fnode__box" x="725" y="58" width="150" height="48" /><text class="fnode__label" x="800" y="79" text-anchor="middle">Remote<tspan class="sub" x="800" dy="15">GitHub</tspan></text></g> 34 <!-- edge labels --> 35 <g class="felabel"><rect class="felabel__box" x="199" y="74" width="50" height="16" /><text class="felabel__text" x="224" y="85" text-anchor="middle">git add</text></g> 36 <g class="felabel"><rect class="felabel__box" x="422" y="74" width="64" height="16" /><text class="felabel__text" x="454" y="85" text-anchor="middle">git commit</text></g> 37 <g class="felabel"><rect class="felabel__box" x="655" y="74" width="58" height="16" /><text class="felabel__text" x="684" y="85" text-anchor="middle">git push</text></g> 38 <g class="felabel"><rect class="felabel__box" x="248" y="164" width="164" height="16" /><text class="felabel__text" x="330" y="175" text-anchor="middle">git checkout / restore</text></g> 39 <g class="felabel"><rect class="felabel__box" x="650" y="140" width="100" height="16" /><text class="felabel__text" x="700" y="151" text-anchor="middle">git pull / fetch</text></g> 40 </svg> 41 </div> 42 </figure> 43 44 ## 1. Intro & Setup 45 46 Git is a distributed version control system: every clone is a full repository with complete history. Before your first commit, tell Git who you are and set sane defaults. 47 48 ```bash 49 # Identity (used in every commit) — --global writes to ~/.gitconfig 50 git config --global user.name "Netrunner" 51 git config --global user.email "you@example.com" 52 53 # Make new repos use 'main' instead of 'master' 54 git config --global init.defaultBranch main 55 56 # Line endings: 'input' on Linux/macOS, 'true' on Windows 57 git config --global core.autocrlf input 58 59 # Handy quality-of-life 60 git config --global pull.rebase false # merge on pull (default, explicit) 61 git config --global core.editor "nvim" # editor for commit messages 62 63 git config --list # verify everything 64 ``` 65 66 > **Tip — Per-repo overrides:** Drop `--global` to set a value for the current repo only (e.g. a work email on a work project). Local config lives in `.git/config` and wins over global. 67 68 ## 2. Getting Files Into the Repo 69 70 The core loop you'll run hundreds of times a day: **edit → status → add → commit**. 71 72 ```bash 73 git init # turn a folder into a repo (creates .git/) 74 git init -b main # ...and name the initial branch 'main' 75 76 git status # what's changed, staged, untracked — run this constantly 77 git status -s # short format 78 79 git add file.py # stage one file 80 git add src/ # stage a directory 81 git add -A # stage everything (new, modified, deleted) 82 git add -p # interactively stage hunks (great for clean commits) 83 84 git commit -m "Add login handler" # commit staged changes with a message 85 git commit -am "Fix typo" # add (tracked files) + commit in one step 86 git commit # opens editor for a multi-line message 87 ``` 88 89 <figure class="flow plate corners"> 90 <figcaption class="flow__cap"><span class="flow__kind">The edit-commit loop</span><span class="flow__dir">LR</span></figcaption> 91 <div class="flow__body"> 92 <svg class="flow-svg" viewBox="0 0 900 190" role="img" aria-label="Edit files to git status to git add to git commit, then back to edit files"> 93 <path class="fedge" d="M185,82 L263,82" marker-end="url(#flow-arrow)" /> 94 <path class="fedge" d="M415,82 L493,82" marker-end="url(#flow-arrow)" /> 95 <path class="fedge" d="M645,82 L723,82" marker-end="url(#flow-arrow)" /> 96 <path class="fedge is-back" d="M800,106 L800,158 L110,158 L110,108" marker-end="url(#flow-arrow)" /> 97 <g class="fnode is-entry"><rect class="fnode__box" x="35" y="58" width="150" height="48" /><text class="fnode__label" x="110" y="86" text-anchor="middle">Edit files</text></g> 98 <g class="fnode"><rect class="fnode__box" x="265" y="58" width="150" height="48" /><text class="fnode__label" x="340" y="86" text-anchor="middle">git status</text></g> 99 <g class="fnode"><rect class="fnode__box" x="495" y="58" width="150" height="48" /><text class="fnode__label" x="570" y="86" text-anchor="middle">git add</text></g> 100 <g class="fnode"><rect class="fnode__box" x="725" y="58" width="150" height="48" /><text class="fnode__label" x="800" y="86" text-anchor="middle">git commit -m</text></g> 101 </svg> 102 </div> 103 </figure> 104 105 > **Note — A good commit:** Stage related changes together and write a message in the imperative mood ("Add", "Fix", "Refactor") describing *why*, not just *what*. Use `git add -p` to split unrelated edits into separate commits. 106 107 **Ignoring files** — create a `.gitignore` before your first commit: 108 109 ```bash 110 cat > .gitignore <<'EOF' 111 __pycache__/ 112 *.pyc 113 .env 114 loot/ 115 *.ccache 116 EOF 117 ``` 118 119 ## 3. Resetting Unwanted Changes 120 121 Undo operations, from safest (working tree) to most destructive (history). Know which area you're touching. Full treatment in Git Reset & Undo. 122 123 ```bash 124 # See exactly what changed before undoing anything 125 git diff # working tree vs staging (unstaged changes) 126 git diff --staged # staging vs last commit (what a commit would include) 127 128 # Discard UNSTAGED changes in the working tree (irreversible) 129 git restore file.py # modern 130 git checkout -- file.py # older syntax, same effect 131 132 # Unstage a file (keep the edit, just remove from index) 133 git restore --staged file.py 134 git reset HEAD file.py # older syntax 135 136 # Throw away ALL local uncommitted changes 137 git restore . 138 git reset --hard # nukes working tree + index to last commit 139 140 # Remove untracked files/dirs (careful!) 141 git clean -n # dry-run: show what would be deleted 142 git clean -fd # force-delete untracked files and directories 143 144 # Fix the LAST commit (message or forgotten file) 145 git add forgotten.py 146 git commit --amend # opens editor to edit message too 147 git commit --amend --no-edit # keep message, just add the file 148 ``` 149 150 > **Warning — `--amend` and `reset --hard` rewrite/discard:** `--amend` creates a *new* commit replacing the last one — never amend a commit you've already pushed to a shared branch. `reset --hard` permanently drops uncommitted work. 151 152 ## 4. Tagging & Branching 153 154 A branch is just a movable pointer to a commit. Branching lets you develop a feature in isolation, then merge it back. 155 156 ### Referencing commits & tags 157 158 ```bash 159 git tag v1.0 # lightweight tag on current commit 160 git tag -a v1.0 -m "First release" # annotated tag (has message/author) 161 git tag # list tags 162 git push origin v1.0 # push a tag (tags aren't pushed by default) 163 git push origin --tags # push all tags 164 165 # Refer to commits: HEAD (current), HEAD~1 (one back), HEAD~3, or a hash 166 git show HEAD~2 167 ``` 168 169 ### Creating & switching branches 170 171 ```bash 172 git branch # list local branches (* = current) 173 git branch feature-login # create a branch (doesn't switch) 174 git switch feature-login # switch to it (modern) 175 git switch -c feature-login # create AND switch in one step 176 git checkout -b feature-login # older syntax, same as above 177 178 git switch main # go back to main 179 git branch -d feature-login # delete a merged branch (safe) 180 git branch -D feature-login # force-delete (unmerged — careful) 181 git branch -m old new # rename a branch 182 ``` 183 184 ### Merging branches 185 186 <figure class="flow plate corners"> 187 <figcaption class="flow__cap"><span class="flow__kind">Feature branch merge</span><span class="flow__dir">TD</span></figcaption> 188 <div class="flow__body"> 189 <div class="flow__diagram" data-dir="td"> 190 <div class="flow-rank"><div class="flow-node is-entry">init</div></div> 191 <div class="flow-edge"></div> 192 <div class="flow-rank"><div class="flow-node">base</div></div> 193 <div class="flow-branches"> 194 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">branch feature-login</span></div><div class="flow-node">form</div><div class="flow-edge"></div><div class="flow-node">validate</div></div> 195 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">checkout main</span></div><div class="flow-node">hotfix</div></div> 196 </div> 197 <div class="flow-join"></div> 198 <div class="flow-rank"><div class="flow-node">merge feature-login</div></div> 199 <div class="flow-edge"></div> 200 <div class="flow-rank"><div class="flow-node is-goal">release</div></div> 201 </div> 202 </div> 203 </figure> 204 205 ```bash 206 # 1. Finish work on the feature branch, commit it 207 git switch feature-login 208 git add -A && git commit -m "Add login form + validation" 209 210 # 2. Switch to the target branch and merge 211 git switch main 212 git merge feature-login # brings the feature commits into main 213 214 # 3. Clean up 215 git branch -d feature-login 216 ``` 217 218 > **Tip — Fast-forward vs merge commit:** If `main` hasn't moved since you branched, Git just moves the pointer forward (fast-forward, no extra commit). If both diverged, Git creates a *merge commit*. Force a merge commit for a clear history with `git merge --no-ff feature-login`. 219 220 See Git Branching and Git Reset & Undo for advanced branch management. 221 222 ## 5. History 223 224 Read the past, compare states, and undo old commits safely. 225 226 ```bash 227 git log # full history 228 git log --oneline # compact, one line per commit 229 git log --oneline --graph --all # visual branch/merge graph 230 git log --stat # files changed per commit 231 git log -p # show the actual diffs 232 git log --author="Netrunner" --since="2 weeks ago" 233 234 # Compare things 235 git diff main..feature-login # difference between two branches 236 git diff HEAD~3 HEAD # last 3 commits' combined change 237 git show <hash> # one commit in full 238 239 # Who changed this line, and when 240 git blame file.py 241 ``` 242 243 ### Undoing old commits — `revert` vs `reset` 244 245 ```bash 246 # REVERT: safe, creates a NEW commit that undoes an old one. Use on shared branches. 247 git revert <hash> # undo one commit, keep history intact 248 git revert HEAD # undo the latest commit safely 249 250 # RESET: rewrites history by moving the branch pointer. Local branches only. 251 git reset --soft HEAD~1 # undo last commit, KEEP changes staged 252 git reset --mixed HEAD~1 # undo last commit, keep changes unstaged (default) 253 git reset --hard HEAD~1 # undo last commit AND discard its changes 254 ``` 255 256 > **Warning — revert = public, reset = private:** On a branch others have pulled, **always `revert`** — it adds history rather than rewriting it. Save `reset` for cleaning up local commits you haven't pushed. 257 258 ## 6. Remotes & GitHub 259 260 A remote is a copy of the repo hosted elsewhere (usually GitHub). You sync with `push` and `pull`. 261 262 ```bash 263 git clone https://github.com/USER/REPO.git # copy a remote repo locally 264 git clone https://github.com/USER/REPO.git dir # into a named folder 265 266 git remote -v # list remotes 267 git remote add origin https://github.com/YOU/REPO.git # link a remote named 'origin' 268 git remote set-url origin git@github.com:YOU/REPO.git # switch HTTPS -> SSH 269 270 git push -u origin main # first push: -u sets upstream tracking 271 git push # subsequent pushes (tracking already set) 272 273 git fetch # download remote changes WITHOUT merging 274 git pull # fetch + merge into current branch 275 git pull --rebase # fetch + replay your commits on top (linear history) 276 ``` 277 278 > **Tip — SSH vs HTTPS + credentials:** HTTPS needs a Personal Access Token (not your password) since 2021. SSH (`git@github.com:...`) uses your key pair and avoids the prompt entirely. Set up a key with `ssh-keygen -t ed25519` and add the public key to GitHub. 279 280 > **Warning — Dangers of rewriting public history:** `git push --force` overwrites the remote branch and can destroy teammates' commits. Prefer `git push --force-with-lease`, which refuses if the remote moved since you last fetched. Never force-push shared branches like `main`. 281 282 ## 7. Real-World Workflows 283 284 ### 7a. Set up a brand-new repo (local + GitHub) 285 286 Two ways to start a fresh project. Pick based on whether the code already exists on your machine. 287 288 <figure class="flow plate corners"> 289 <figcaption class="flow__cap"><span class="flow__kind">New repo path choice</span><span class="flow__dir">TD</span></figcaption> 290 <div class="flow__body"> 291 <div class="flow__diagram" data-dir="td"> 292 <div class="flow-rank"><div class="flow-node is-decision">Code already<span class="sub">on disk?</span></div></div> 293 <div class="flow-branches"> 294 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">Yes</span></div><div class="flow-node">Local-first:<span class="sub">git init here,</span><span class="sub">then link empty GitHub repo</span></div></div> 295 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">No</span></div><div class="flow-node">GitHub-first:<span class="sub">create repo on site,</span><span class="sub">git clone it down</span></div></div> 296 </div> 297 </div> 298 </div> 299 </figure> 300 301 **Step 1 — create the repo on the GitHub website** 302 303 1. Go to **github.com → New** (the `+` menu, top-right) → **New repository**. 304 2. Name it (e.g. `voidwalker`), add a description, choose **Public** or **Private**. 305 3. **If you already have local code:** leave "Add a README / .gitignore / license" **unchecked** — an empty repo avoids a first-push conflict. 306 **If you're starting fresh on GitHub:** tick README/.gitignore so there's something to clone. 307 4. Click **Create repository**. GitHub shows you the repo URL (HTTPS or SSH) — copy it. 308 309 **Path A — local-first (you already have files)** 310 311 ```bash 312 cd ~/projects/voidwalker # your existing project folder 313 git init -b main # start a repo, initial branch = main 314 git add -A # stage everything 315 git commit -m "Initial commit" # first commit 316 317 # Link the empty GitHub repo you just made: 318 git remote add origin https://github.com/YOU/voidwalker.git 319 git remote -v # verify: origin -> your URL (fetch + push) 320 321 git push -u origin main # -u sets upstream so later 'git push' just works 322 ``` 323 324 **Path B — GitHub-first (repo created with a README)** 325 326 ```bash 327 git clone https://github.com/YOU/voidwalker.git # brings repo down + wires origin 328 cd voidwalker 329 # ...add your files... 330 git add -A 331 git commit -m "Add project files" 332 git push # origin/main already tracked by clone 333 ``` 334 335 > **Tip — `git init` vs `git clone`:** `git init` makes a repo from a folder you already have and you wire the remote yourself with `git remote add`. `git clone` does init **plus** `remote add origin` **plus** the first fetch in one step. Use init when the code is local first, clone when it lives on GitHub first. 336 337 > **Warning — "Updates were rejected" on first push:** This means the GitHub repo already has commits (a README you added at creation) that your local repo doesn't. Either recreate the repo empty, or reconcile once: 338 > ```bash 339 > git pull --rebase origin main # replay your commit on top of GitHub's README 340 > git push -u origin main 341 > ``` 342 343 > **Note — Authentication reminder:** HTTPS pushes need a **Personal Access Token** (Settings → Developer settings → PAT), not your account password. Or switch to SSH: 344 > ```bash 345 > ssh-keygen -t ed25519 -C "you@example.com" # then add ~/.ssh/id_ed25519.pub to GitHub 346 > git remote set-url origin git@github.com:YOU/voidwalker.git 347 > ``` 348 349 ### 7b. Clone a project, wipe its history, publish as your own 350 351 Strip all prior history (and any commit trailers) and start a clean repo under your account. 352 353 ```bash 354 git clone https://gitlab.com/DAEMON-404/voidwalker.py.git fresh 355 cd fresh 356 rm -rf .git # wipes all history + trailers 357 git init -b main # -b main so it isn't "master" 358 git add -A 359 git commit -m "Initial commit" 360 git remote add origin https://github.com/YOU/NEWREPO.git 361 git push -u origin main 362 ``` 363 364 <figure class="flow plate corners"> 365 <figcaption class="flow__cap"><span class="flow__kind">Wipe history & re-publish</span><span class="flow__dir">LR</span></figcaption> 366 <div class="flow__body"> 367 <div class="flow__diagram" data-dir="lr"> 368 <div class="flow-rank"><div class="flow-node is-entry">git clone ...</div></div> 369 <div class="flow-edge"></div> 370 <div class="flow-rank"><div class="flow-node is-danger">rm -rf .git<span class="sub">history gone</span></div></div> 371 <div class="flow-edge"></div> 372 <div class="flow-rank"><div class="flow-node">git init -b main</div></div> 373 <div class="flow-edge"></div> 374 <div class="flow-rank"><div class="flow-node">git add -A<span class="sub">git commit</span></div></div> 375 <div class="flow-edge"></div> 376 <div class="flow-rank"><div class="flow-node">remote add origin</div></div> 377 <div class="flow-edge"></div> 378 <div class="flow-rank"><div class="flow-node is-goal">git push -u origin main</div></div> 379 </div> 380 </div> 381 </figure> 382 383 > **Warning — This is destructive and one-way:** `rm -rf .git` permanently deletes every commit, branch, and tag locally. Only do this when you deliberately want a clean slate. Make sure `origin` points at *your* new empty repo before pushing, or you'll try to overwrite the original. 384 385 > **Note — Alternative: keep files but squash to one commit:** If you'd rather keep the remote link but collapse history: 386 > ```bash 387 > git checkout --orphan clean # new branch with no history 388 > git add -A && git commit -m "Initial commit" 389 > git branch -D main # drop old branch 390 > git branch -m main # rename clean -> main 391 > git push -f origin main # force-replace remote history 392 > ``` 393 394 ### 7c. Feature branch, from creation to merged PR 395 396 ```bash 397 git switch main && git pull # start from an up-to-date main 398 git switch -c feature/xyz # branch off 399 400 # ...work... 401 git add -A && git commit -m "Implement xyz" 402 git push -u origin feature/xyz # publish the branch 403 404 # Open a Pull Request on GitHub (web UI), get it reviewed, then: 405 git switch main && git pull # bring the merged change down locally 406 git branch -d feature/xyz # delete local branch 407 git push origin --delete feature/xyz # delete remote branch 408 ``` 409 410 ### 7d. Resolving a merge conflict 411 412 ```bash 413 git switch main 414 git merge feature/xyz 415 # CONFLICT (content): Merge conflict in app.py 416 ``` 417 418 Git marks conflicts inside the file: 419 420 ```text 421 <<<<<<< HEAD 422 current_code_on_main() 423 ======= 424 incoming_code_from_feature() 425 >>>>>>> feature/xyz 426 ``` 427 428 ```bash 429 # 1. Edit each conflicted file: keep the right code, delete the <<<< ==== >>>> markers 430 # 2. Stage the resolved files 431 git add app.py 432 # 3. Complete the merge 433 git commit # (message pre-filled) — or: git merge --continue 434 435 git merge --abort # ...or bail out entirely and undo the merge 436 ``` 437 438 > **Tip — Let tools help:** `git mergetool` opens a three-way visual diff. In VS Code, the conflict UI gives "Accept Current / Incoming / Both" buttons over each block. 439 440 ### 7e. Oops — undo safely 441 442 ```bash 443 git reflog # your safety net: every HEAD move, even "lost" commits 444 git reset --hard HEAD@{2} # jump back to a state from reflog 445 git revert HEAD # undo the last commit on a shared branch (safe) 446 git restore file.py # discard local edits to one file 447 ``` 448 449 > **Note — `reflog` is the undo button for Git itself:** Even after a bad `reset --hard`, the old commit usually still exists and `git reflog` shows its hash for ~90 days. You can almost always recover. 450 451 ### 7f. Stash — park work without committing 452 453 You're mid-edit on `feature/x` and need to jump to `main` to fix something. Stashing shelves your changes cleanly. 454 455 ```bash 456 git stash # shelve tracked changes, clean working tree 457 git stash -u # also include untracked files 458 git stash push -m "wip: parser" # named stash 459 git stash list # see all stashes (stash@{0}, stash@{1}...) 460 461 # ...do the urgent thing on main, then come back... 462 git switch feature/x 463 git stash pop # re-apply latest stash AND drop it 464 git stash apply stash@{1} # re-apply a specific stash, KEEP it in the list 465 git stash drop stash@{0} # delete one stash 466 git stash clear # delete all stashes 467 ``` 468 469 > **Tip — Stash conflicts:** If files changed underneath you, `pop` can conflict. Resolve the markers exactly like a merge, then `git add` the files. The stash isn't dropped automatically on conflict, so `git stash drop` once you're happy. 470 471 ### 7g. Rebase — replay commits for a linear history 472 473 Rebasing moves your branch's commits on top of the latest `main`, producing a straight line instead of a merge commit. 474 475 <figure class="flow plate corners"> 476 <figcaption class="flow__cap"><span class="flow__kind">Rebase: before & after</span><span class="flow__dir">LR</span></figcaption> 477 <div class="flow__body"> 478 <div class="flow__diagram" data-dir="lr"> 479 <div class="flow-branches"> 480 <div class="flow-lane"><div class="flow-node is-note">Before</div><div class="flow-edge"></div><div class="flow-node">main: A-B-C</div><div class="flow-edge"></div><div class="flow-node">feat: B-D-E</div></div> 481 <div class="flow-lane"><div class="flow-node is-note">After</div><div class="flow-edge"></div><div class="flow-node">main: A-B-C</div><div class="flow-edge"></div><div class="flow-node">feat: C-D'-E'</div></div> 482 </div> 483 </div> 484 </div> 485 </figure> 486 487 ```bash 488 git switch feature/x 489 git fetch origin 490 git rebase origin/main # replay feat commits on top of latest main 491 492 # If a commit conflicts: 493 # fix the files, then: 494 git add <files> 495 git rebase --continue 496 git rebase --skip # skip the current commit 497 git rebase --abort # bail out, back to pre-rebase state 498 499 git push --force-with-lease # rebased history differs — safe force needed 500 ``` 501 502 > **Warning — Golden rule of rebasing:** Never rebase commits that others have already pulled. Rebase only your *own* un-pushed (or personal-branch) work. On shared branches, merge instead. 503 504 ### 7h. Interactive rebase — clean up before a PR 505 506 Squash, reorder, reword, or drop your last few commits into a tidy set. 507 508 ```bash 509 git rebase -i HEAD~4 # edit the last 4 commits 510 ``` 511 512 In the editor, change the verb before each commit: 513 514 ```text 515 pick a1b2c3 Add parser 516 squash b4c5d6 fix typo # fold into previous commit 517 reword e7f8g9 Add tests # change this commit's message 518 drop h1i2j3 debug print # remove this commit entirely 519 ``` 520 521 > **Tip — Squash a whole branch into one commit:** `git rebase -i main` then mark every commit after the first as `squash` (or `fixup` to discard their messages). Great for turning 12 messy WIP commits into one clean commit before opening a PR. 522 523 ### 7i. Cherry-pick — grab one commit from another branch 524 525 You need a single bug-fix commit from `dev` on your `release` branch, without merging everything. 526 527 ```bash 528 git switch release 529 git cherry-pick a1b2c3d # apply that one commit here 530 git cherry-pick a1b2c3d..e4f5g6h # a range (exclusive of the first) 531 git cherry-pick --continue # after resolving a conflict 532 git cherry-pick --abort # bail out 533 ``` 534 535 ### 7j. Undo a commit you already pushed 536 537 ```bash 538 # SAFE (shared branch): add an inverse commit 539 git revert <hash> 540 git push 541 542 # Undo a MERGE commit specifically (pick the mainline parent, usually -m 1) 543 git revert -m 1 <merge-hash> 544 git push 545 546 # NUCLEAR (only if you're sure nobody has pulled): rewrite remote 547 git reset --hard HEAD~1 548 git push --force-with-lease 549 ``` 550 551 ### 7k. Sync a fork with upstream 552 553 Keep your fork of someone else's repo current. 554 555 ```bash 556 git remote add upstream https://github.com/ORIGINAL/REPO.git # one-time 557 git remote -v # origin=you, upstream=source 558 559 git fetch upstream 560 git switch main 561 git merge upstream/main # or: git rebase upstream/main 562 git push origin main # update your fork on GitHub 563 ``` 564 565 ### 7l. Find the commit that broke something — `git bisect` 566 567 Binary-search through history to pin down a regression. 568 569 ```bash 570 git bisect start 571 git bisect bad # current commit is broken 572 git bisect good v1.0 # this old tag was fine 573 # Git checks out a midpoint commit — test it, then tell Git: 574 git bisect good # ...or 'git bisect bad' 575 # repeat until Git prints "<hash> is the first bad commit" 576 git bisect reset # return to where you started 577 578 # Automate it with a test script (exit 0 = good, non-zero = bad): 579 git bisect run ./test.sh 580 ``` 581 582 ### 7m. Work on two branches at once — `git worktree` 583 584 Check out a second branch into a separate folder without stashing or re-cloning. 585 586 ```bash 587 git worktree add ../repo-hotfix hotfix # hotfix branch in a sibling dir 588 git worktree add -b experiment ../exp # create a new branch in a new worktree 589 git worktree list 590 git worktree remove ../repo-hotfix # clean up when done 591 ``` 592 593 ### 7n. Tag a release and publish it 594 595 ```bash 596 git switch main && git pull 597 git tag -a v2.1.0 -m "Release 2.1.0: adds PMKID cracking" 598 git push origin v2.1.0 # push this tag 599 # On GitHub: Releases -> Draft a new release -> pick the tag -> add notes 600 601 git tag # list 602 git tag -d v2.1.0 # delete locally 603 git push origin --delete v2.1.0 # delete on remote 604 git checkout v2.0.0 # inspect the code at a tag (detached HEAD) 605 ``` 606 607 ### 7o. Recover a deleted branch or commit 608 609 ```bash 610 git reflog # find the last commit hash the branch pointed to 611 git switch -c recovered <hash> # recreate the branch at that commit 612 613 # Deleted a branch you hadn't merged? 614 git branch feature/x <hash-from-reflog> 615 616 # Find dangling commits reflog forgot 617 git fsck --lost-found 618 ``` 619 620 ### 7p. Selectively unstage / partial commits 621 622 ```bash 623 git add -p # stage hunks interactively (y/n/s to split) 624 git reset -p # unstage hunks interactively 625 git restore --staged --worktree file.py # unstage AND discard edits to a file 626 git checkout <hash> -- path/file # restore ONE file from an old commit 627 ``` 628 629 ### 7q. `warning: adding embedded git repository` — nested clones 630 631 Hit this on any tooling folder built by cloning other people's repos into it (kali arsenal, dotfiles with vendored plugins, a `tools/` dir full of `git clone`). 632 633 ```text 634 warning: adding embedded git repository: arsenal/Lateral/impacket 635 hint: You've added another git repository inside your current repository. 636 hint: Clones of the outer repository will not contain the contents of 637 hint: the embedded repository and will not know how to obtain it. 638 ``` 639 640 **What Git actually did:** it saw `arsenal/Lateral/impacket/.git/`, refused to recurse, and stored a bare **gitlink** — a pointer to a commit hash with no recorded URL. Push it and anyone who clones gets an **empty directory**. Not fatal, just silently useless. 641 642 **Diagnose — list every nested repo and its upstream:** 643 644 ```bash 645 # every embedded repo under the current dir 646 find . -mindepth 2 -name .git -not -path "./.git/*" -exec dirname {} \; 647 648 # same, with the URL each one came from (this is your rebuild manifest) 649 for d in $(find . -mindepth 2 -name .git -not -path "./.git/*" -exec dirname {} \;); do 650 printf '%s\t%s\n' "$d" "$(git -C "$d" remote get-url origin 2>/dev/null)" 651 done 652 653 # how big is this actually 654 du -sh . # >1 GB or any single file >100 MB => GitHub will reject it 655 find . -type f -size +100M -not -path "./.git/*" 656 ``` 657 658 **Pick one of three fixes:** 659 660 | Fix | When | Cost | 661 | :-- | :-- | :-- | 662 | **Ignore + rebuild script** | Third-party tools you only consume. **Default choice.** | Repo stays tiny; one script to maintain | 663 | **Submodule** | You need an exact pinned upstream commit reproducible for others | Everyone must `clone --recurse-submodules`; detached HEADs | 664 | **Flatten** (delete inner `.git`) | You forked the code and will edit it yourself | Lose upstream history and the ability to pull updates | 665 666 **Fix A — ignore it, script the rebuild (recommended):** 667 668 ```bash 669 git rm -r --cached arsenal # only if already staged/tracked 670 printf 'arsenal/\n' >> .gitignore # trailing slash = the dir and all under it 671 # then commit a build/fetch-arsenal.sh that loops the URL manifest from above: 672 # git clone --depth 1 "$url" "$dest" || git -C "$dest" pull --ff-only 673 git add -A && git status --short # verify: no warnings, no arsenal/ entries 674 ``` 675 676 **Fix B — make them real submodules:** 677 678 ```bash 679 git rm -r --cached arsenal/Lateral/impacket # drop the bare gitlink 680 rm -rf arsenal/Lateral/impacket # remove the loose clone 681 git submodule add https://github.com/fortra/impacket.git arsenal/Lateral/impacket 682 git commit -m "Add impacket as a submodule" 683 # consumers: git clone --recurse-submodules <url> 684 # git submodule update --init --recursive (after a plain clone) 685 # git submodule update --remote (bump to upstream tip) 686 ``` 687 688 **Fix C — flatten into your own history:** 689 690 ```bash 691 find arsenal -mindepth 2 -name .git -exec rm -rf {} + # destroys upstream history 692 git add -A # now the files are really tracked 693 ``` 694 695 > **Warning — GitHub hard limits:** 100 MB per file (hard reject), 2 GB per push, ~5 GB per repo (soft). Compiled offensive binaries (`sliver-server.exe`, `PingCastle.exe`, `winPEAS`) blow past these **and** get flagged by AV / secret scanners. Fetch them from upstream releases at build time; never commit them. 696 697 > **Tip — Already committed a huge blob?** Deleting the file in a new commit does **not** shrink the repo — the blob lives in history forever. With no commits worth keeping, `rm -rf .git && git init -b main` is the fastest reset (working tree untouched — verify with `git rev-list --all --count` first). Otherwise rewrite history with `git filter-repo --path arsenal --invert-paths`. 698 699 ### 7r. Pre-flight before the first push — don't leak secrets 700 701 New repos get scraped by credential bots within seconds of going public, and a force-push does **not** un-leak anything already fetched. 702 703 ```bash 704 # 1. What is actually about to ship, and how big? 705 git status --short 706 git ls-files | xargs du -ch | tail -1 707 708 # 2. Grep the STAGED tree (not the working dir) for secrets 709 git grep -nEi "password|passwd|api[_-]?key|secret|token|BEGIN.*PRIVATE KEY" HEAD 710 711 # 3. Confirm the env file is genuinely excluded 712 git check-ignore -v .env # prints the .gitignore rule that catches it 713 git ls-files | grep -c '^\.env$' # must be 0 714 ``` 715 716 Ship a `.env.example` with placeholder values, gitignore the real `.env`: 717 718 ```gitignore 719 .env 720 *.pem 721 *.key 722 723 # Ignore CONTENTS but keep the dir, so bind-mounts exist on a fresh clone. 724 # (Git tracks files, not directories — an empty dir disappears on clone.) 725 loot/* 726 !loot/.gitkeep 727 ``` 728 729 > **Danger — If a credential was already pushed:** **Rotate the credential first** — that is the only step that actually helps. Then clean history (`git filter-repo --invert-paths --path .env`) and force-push. Assume the old value is compromised regardless. 730 731 ## 8. Quick Reference 732 733 | Task | Command | 734 | :-- | :-- | 735 | New repo | `git init -b main` | 736 | Clone | `git clone <url>` | 737 | Stage all | `git add -A` | 738 | Commit | `git commit -m "msg"` | 739 | Status | `git status -s` | 740 | Diff (unstaged) | `git diff` | 741 | Discard file edits | `git restore <file>` | 742 | Unstage | `git restore --staged <file>` | 743 | New branch + switch | `git switch -c <name>` | 744 | Merge branch | `git merge <name>` | 745 | Delete branch | `git branch -d <name>` | 746 | Compact log | `git log --oneline --graph --all` | 747 | Undo last commit (keep work) | `git reset --soft HEAD~1` | 748 | Undo old commit (safe) | `git revert <hash>` | 749 | Add remote | `git remote add origin <url>` | 750 | First push | `git push -u origin main` | 751 | Fetch only | `git fetch` | 752 | Pull | `git pull` | 753 | Safe force push | `git push --force-with-lease` | 754 | Recover lost commit | `git reflog` | 755 | Shelve work | `git stash` / `git stash pop` | 756 | Rebase onto main | `git rebase origin/main` | 757 | Clean up commits | `git rebase -i HEAD~N` | 758 | Copy one commit | `git cherry-pick <hash>` | 759 | Undo pushed commit (safe) | `git revert <hash>` | 760 | Sync a fork | `git fetch upstream && git merge upstream/main` | 761 | Find a regression | `git bisect start` | 762 | Second working dir | `git worktree add ../dir <branch>` | 763 | Tag a release | `git tag -a v1.0 -m "..."` | 764 | Restore one old file | `git checkout <hash> -- <file>` | 765 | Find nested repos | `find . -mindepth 2 -name .git -not -path "./.git/*"` | 766 | Untrack an added dir | `git rm -r --cached <dir>` | 767 | Why is this ignored? | `git check-ignore -v <path>` | 768 | Add a submodule | `git submodule add <url> <path>` | 769 | Clone with submodules | `git clone --recurse-submodules <url>` | 770 | Find oversized files | `find . -type f -size +100M -not -path "./.git/*"` | 771 | Scan staged tree for secrets | `git grep -nEi "password\|api_key\|secret" HEAD` | 772 | Purge a file from history | `git filter-repo --path <f> --invert-paths` | 773 774 ## See Also 775 776 - Git Branching — branching deep dive 777 - Git Reset & Undo — restore / reset / clean in detail 778 - [GitByBit course](https://gitbybit.com/)