Advanced Git Workflows

git reset moves the current branch reference and can update the index and working tree:

What this module covers

Interactive rebase and autosquash Fixup commits and commit cleanup

Rewriting local versus published history Reset modes: soft, mixed, and hard Restore and checkout distinctions Revert versus reset Bisect for regression hunting Worktrees for parallel branches Stash, patches, and format-patch

Rename detection and whitespace handling Sparse checkout and partial clone concepts Large File Storage (LFS) Submodules and subtree workflows Hooks and automation Signing commits and tags

Git maintenance and repository performance

12.1 Reset modes

git reset moves the current branch reference and can update the index and working tree:

Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible) on a scratch branch with two commits, restoring with ORIG_HEAD between modes:

--soft: move HEAD/branch but keep index and working tree.

--mixed (default): move HEAD/branch and reset the index, while leaving working-tree files.

--hard: also overwrite the working tree; uncommitted work can be lost.

Safety rule: do not run --hard unless you understand exactly what will be discarded. Create a branch or backup first when uncertain.

git reset --soft HEAD~1
git reset --mixed HEAD~1
git reset --hard HEAD~1
$ git log --oneline -2
b3a5a82 notes: step 2
16d6c73 notes: step 1
$ git reset --soft HEAD~1 && git status --short && cat notes.md
M  notes.md
step 2
$ git reset -q --hard ORIG_HEAD
$ git reset --mixed HEAD~1 && git status --short && cat notes.md
Unstaged changes after reset:
M	notes.md
 M notes.md
step 2
$ git reset -q --hard ORIG_HEAD
$ git reset --hard HEAD~1 && git status --short && cat notes.md
HEAD is now at 16d6c73 notes: step 1
step 1

12.2 Revert versus reset

Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible):

revert creates a new commit that reverses a prior change. It is generally safer for changes already shared with others. reset moves a branch reference and is usually used for local history management. Reverting a merge requires selecting a mainline parent and understanding later merge behavior.

git revert <commit>
$ git revert 8b1fe66
[main 2f7f0f2] Revert "feat: switch bus to 250 kbit/s"
 Date: Thu Jan 1 00:29:00 2026 +0000
 1 file changed, 1 deletion(-)
 delete mode 100644 src/config.h
$ git log --oneline -2
2f7f0f2 Revert "feat: switch bus to 250 kbit/s"
8b1fe66 feat: switch bus to 250 kbit/s
$ git show --stat HEAD | tail -2
 src/config.h | 1 -
 1 file changed, 1 deletion(-)

12.3 Interactive rebase

Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible). GIT_SEQUENCE_EDITOR prints the todo list instead of opening an editor, so the rebase runs unchanged: Common todo actions include:

pick: keep commit.

reword: change message.

edit: stop to amend.

squash: combine with previous commit and edit message.

fixup: combine and discard the fixup message.

drop: remove commit from the rewritten sequence.

Use interactive rebase to clean up unpublished work. Do not casually rewrite commits other people have based work on.

git rebase -i HEAD~4
$ GIT_SEQUENCE_EDITOR='sed -n /^[^#]/p' git rebase -i HEAD~4
pick 8b1fe66 feat: switch bus to 250 kbit/s
pick 2f7f0f2 Revert "feat: switch bus to 250 kbit/s"
pick 76f94cd feat: add a
pick d63057b feat: add b

Successfully rebased and updated refs/heads/cleanup.

12.4 Fixup and autosquash

Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible):

A fixup commit marks a change intended to be folded into an earlier commit. Verify the final diff and tests after autosquashing.

git commit --fixup=<commit>
git rebase -i --autosquash <base>
$ echo 'a, fixed' > a.txt && git add a.txt
$ git commit --fixup=ccab415
[cleanup 14db233] fixup! feat: add a
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline -3
14db233 fixup! feat: add a
6d25051 feat: add b
ccab415 feat: add a
$ GIT_SEQUENCE_EDITOR='sed -n /^[^#]/p' git rebase -i --autosquash HEAD~3
pick ccab415 feat: add a
fixup 14db233 fixup! feat: add a
pick 6d25051 feat: add b
Rebasing (2/3)
Rebasing (3/3)

Successfully rebased and updated refs/heads/cleanup.
$ git log --oneline -2
d63057b feat: add b
76f94cd feat: add a
$ git show --stat --oneline HEAD~1 | tail -1 && cat a.txt
 1 file changed, 1 insertion(+)
a, fixed

12.5 Bisect

Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible) in a 10-commit repository where calc.sh gained a bug in change 7, using git bisect run with a test script:

Git performs a binary search through commit history to identify a regression boundary. The test must be reliable and runnable at candidate revisions. Automated mode can use git bisect run <command> when the command returns consistent good/bad exit codes.

Try it: binary search through history With 10 commits Git needed only 3 test runs in the session above; in general bisect needs about log₂(n) steps.

git bisect start
git bisect bad
git bisect good <known-good-commit>
# Test each checked-out revision:
git bisect good
# or:
git bisect bad
git bisect reset
$ git log --oneline | head -3; git log --oneline | wc -l
b242eda change 10
73c79f2 change 9
702f7c6 change 8
10
$ ./test.sh; echo test exit status: $?
test exit status: 1
$ git bisect start
$ git bisect bad
$ git bisect good 50e4839
Bisecting: 4 revisions left to test after this (roughly 2 steps)
[79f356c2968ae019b78015bc3e87b9b5d58f2f26] change 5
$ git bisect run ./test.sh
running  './test.sh'
Bisecting: 2 revisions left to test after this (roughly 1 step)
[50ebb8deb8dc5bdd7ebb7b0e8195149b4bbcc017] change 7
running  './test.sh'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[85fa001336dd5b84d246a77a663f579a15f11a96] change 6
running  './test.sh'
50ebb8deb8dc5bdd7ebb7b0e8195149b4bbcc017 is the first bad commit
commit 50ebb8deb8dc5bdd7ebb7b0e8195149b4bbcc017
Author: Example Developer <[email protected]>
Date:   Thu Jan 1 00:41:00 2026 +0000

    change 7

 calc.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
bisect found first bad commit
$ git bisect reset
Previous HEAD position was 85fa001 change 6
Switched to branch 'main'

12.6 Worktrees

Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible):

Worktrees allow multiple working directories linked to one repository, useful for reviewing or fixing a second branch without repeatedly switching the primary working tree. Respect branch/worktree constraints and remove only worktrees you intend to remove.

git worktree add ../project-hotfix hotfix/critical-fix
git worktree list
git worktree remove ../project-hotfix
$ git worktree add ../project-hotfix hotfix/critical-fix
Preparing worktree (checking out 'hotfix/critical-fix')
HEAD is now at 2f7f0f2 Revert "feat: switch bus to 250 kbit/s"
$ git worktree list
/tmp/bsvc/project         2f7f0f2 [main]
/tmp/bsvc/project-hotfix  2f7f0f2 [hotfix/critical-fix]
$ git worktree remove ../project-hotfix

12.7 Stash

Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible):

A stash temporarily stores local changes. It is not a durable backup or a substitute for a WIP branch. Conflicts may occur when applying it later.

git stash push -m "temporary diagnostics"
git stash list
git stash show -p stash@{0}
git stash pop
$ echo 'printf("diag");' >> src/parse.c && git status --short
 M src/parse.c
$ git stash push -m "temporary diagnostics"
Saved working directory and index state On main: temporary diagnostics
$ git stash list
stash@{0}: On main: temporary diagnostics
$ git stash show -p stash@{0}
diff --git a/src/parse.c b/src/parse.c
index 29c8be0..3c72352 100644
--- a/src/parse.c
+++ b/src/parse.c
@@ -4,3 +4,4 @@ int parse(const char *s)
         return -1;
     return s[0] - 0x30;
 }
+printf("diag");
$ git stash pop
On branch main
Your branch and 'origin/main' have diverged,
and have 3 and 1 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   src/parse.c

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (8f9a0740f33021e3b1798135bff4ea36f6ce4f6b)
$ git status --short && git restore src/parse.c
 M src/parse.c

12.8 Patches

Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible): the patch is applied with git am in a second clone that lacks the commit:

format-patch creates email-style commit patches; git am applies them as commits. git diff and git apply are useful for sharing uncommitted changes. Verify patch origin and content before applying.

git format-patch -1 HEAD
git am 0001-fix-input-validation.patch
git diff > changes.patch
git apply --check changes.patch
git apply changes.patch
$ sed -i 's/return -1;/return -2;   \/* distinct error code *\//' src/parse.c && git commit -qam 'fix: input validation'
$ git format-patch -1 HEAD
0001-fix-input-validation.patch
$ git clone -q . ../reviewer && cd ../reviewer && git reset -q --hard HEAD~1
$ git am ../project/0001-fix-input-validation.patch && git log --oneline -1
Applying: fix: input validation
5685a6d fix: input validation
$ echo '// reviewed' >> src/parse.c
$ git diff > changes.patch
$ git restore src/parse.c
$ git apply --check changes.patch
$ git apply changes.patch && git status --short
 M src/parse.c
?? changes.patch
$ cd ../project

12.9 Hooks

Hooks are scripts triggered by Git events, such as pre-commit or commit-msg. They can run formatting, linting, message validation, or other checks. Local hooks are not a security boundary because a user can bypass or modify them; enforce critical checks in trusted CI and server-side controls.

12.10 Signed commits and tags

Git can sign commits and annotated tags using supported signing mechanisms. Signing can help verify identity/integrity when keys and trust are managed correctly. Signing alone does not guarantee that code is safe or that CI used a trustworthy build environment.

12.11 LFS, submodules, and subtree

Git LFS: stores pointer files in Git while large file content is managed by the LFS service. Team infrastructure must support fetching LFS objects.

Submodules: record a specific commit from another repository. They require explicit initialization/update and can be confusing when the submodule pointer is not updated.

Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible): a fresh clone has an empty submodule directory until the command runs:

Subtree: integrates another repository's content into a directory and can simplify consumer workflows at the cost of different update/merge semantics.

Choose based on repository ownership, access control, versioning, and developer experience.

git submodule update --init --recursive
$ git init -q /tmp/bsvc/drivers && git -C /tmp/bsvc/drivers commit -q --allow-empty -m 'drivers: initial'
$ git submodule add -q /tmp/bsvc/drivers libs/drivers && git commit -qm 'build: add drivers submodule'
$ cd /tmp/bsvc && rm -rf fresh && git clone -q project fresh && cd fresh
$ ls -A libs/drivers | wc -l
0
$ git submodule update --init --recursive
Submodule 'libs/drivers' (/tmp/bsvc/drivers) registered for path 'libs/drivers'
Cloning into '/tmp/bsvc/fresh/libs/drivers'...
done.
Submodule path 'libs/drivers': checked out '8264d12c141467e598234821c23425f7ce735cc9'
$ git submodule status
 8264d12c141467e598234821c23425f7ce735cc9 libs/drivers (heads/main)

12.12 Sparse checkout and partial clone

Sparse checkout limits which paths appear in the working tree. Partial clone can defer fetching some objects until needed. These features can reduce initial work in large repositories but add operational complexity and require compatible Git/server support.

Quiz

? After `git reset --soft HEAD~1`, what did `git status --short` show?
+ `M  notes.md` — the change is still staged :: Soft moves only the branch.
- ` M notes.md` — modified but unstaged :: That is `--mixed`.
- Nothing, and the file reverted :: That is `--hard`.
- An error :: The command succeeded.

? You need to undo a commit that is already on the shared main branch. What is safest?
+ `git revert <commit>` :: It adds a new commit and does not rewrite shared history.
- `git reset --hard HEAD~1` and force-push :: Rewrites history others have.
- Delete the branch :: Destructive and unnecessary.
- `git stash` :: Stash is for uncommitted changes.

? What did `git commit --fixup` and `git rebase -i --autosquash` produce in the real run?
+ The todo list placed `fixup` directly after the target commit, and the result had two clean commits. :: Autosquash reorders and marks the fixup.
- Three commits including `fixup!` :: The fixup was folded in.
- A merge commit :: Rebase does not create merges here.
- An error :: The rebase succeeded.