Chapter 19: Advanced Git Techniques
Chapter Objectives
By the end of this chapter, you will be able to:
- Use
git bisect
to efficiently find the commit that introduced a bug. - Apply specific commits from one branch to another using
git cherry-pick
. - Manage multiple working trees linked to the same repository with
git worktree
. - Understand the concept of Git Hooks and implement simple client-side hooks to customize workflows.
- Attach metadata to commits without altering their history using
git notes
. - Create and use
git bundle
files for offline sharing of repository data. - Appreciate the power and appropriate use cases for these advanced Git tools.
Introduction
You’ve now journeyed through the essentials of Git, from basic commands to branching strategies and collaboration on remote platforms. With this solid foundation, you’re ready to explore some of Git’s more advanced capabilities. These tools can dramatically improve your efficiency in specific scenarios, such as pinpointing bugs, selectively applying changes, managing complex workflows, and automating tasks.
This chapter delves into several powerful, yet sometimes less frequently used, Git commands and concepts. We’ll start with git bisect
, a remarkable tool that helps you quickly find the exact commit that introduced a regression in your codebase using a binary search algorithm. Then, we’ll look at git cherry-pick
, which allows you to apply individual commits from one branch to another. You’ll learn about git worktree
, a feature that lets you check out multiple branches simultaneously in different directories linked to the same repository, avoiding the need to constantly switch or stash changes.
We will also introduce Git Hooks, scripts that Git can execute at various points in its lifecycle, enabling you to automate tasks and enforce policies. Finally, we’ll touch upon git notes
for adding annotations to commits without altering them, and git bundle
for packaging and sharing repository data offline. Mastering these advanced techniques will further enhance your Git prowess, allowing you to tackle complex situations with confidence and finesse.
Theory
Finding Bugs with git bisect
Imagine you discover a bug in your project, but you’re unsure when it was introduced. It worked fine a few weeks ago, but now it’s broken. Sifting through hundreds of commits manually to find the culprit can be a nightmare. git bisect
is designed to solve exactly this problem by automating the search process.
How git bisect Works:
git bisect performs a binary search through your commit history to find the specific commit that introduced a change (typically a bug). You start by telling Git a “bad” commit where the bug is present (usually HEAD) and a “good” commit where the bug is known to be absent (e.g., an older tag or commit hash).
Git then checks out a commit roughly in the middle of this range. You test your code at this commit and tell Git whether it’s “good” or “bad.” Based on your feedback, Git narrows down the range of commits where the bug could have been introduced by half. This process repeats—Git checks out a commit in the middle of the new range, you test and report—until Git pinpoints the exact commit where the code transitioned from “good” to “bad.” This first “bad” commit is the one that likely introduced the bug.
The git bisect
Process:
- Start Bisecting:
git bisect start
- Mark Bad Commit:
git bisect bad [<commit>]
(If<commit>
is omitted,HEAD
is used). - Mark Good Commit:
git bisect good <commit>
(e.g.,git bisect good v1.0.0
). - Test and Report: Git checks out a commit. You test your project.
- If the bug is present:
git bisect bad
- If the bug is absent:
git bisect good
- If you can’t test (e.g., commit doesn’t compile):
git bisect skip
- If the bug is present:
- Repeat: Git continues to check out commits, and you continue to test and report
good
orbad
(orskip
). - Identify Culprit: Eventually, Git will report the first bad commit found.
- End Bisecting:
git bisect reset
(This returns you to the branch you were on before starting the bisect).
%%{ init: { "theme": "base", "themeVariables": { "primaryColor": "#EDE9FE", "primaryTextColor": "#5B21B6", "primaryBorderColor": "#5B21B6", /* Start/End points */ "lineColor": "#5B21B6", "textColor": "#1F2937", "mainBkg": "#DBEAFE", "nodeBorder": "#2563EB", "nodeTextColor": "#1E40AF", /* Process Nodes */ "secondaryColor": "#D1FAE5", "secondaryBorderColor": "#059669", "secondaryTextColor": "#065F46", /* Success/Result Node */ "tertiaryColor": "#FEF3C7", "tertiaryBorderColor": "#D97706", "tertiaryTextColor": "#92400E", /* Decision/Test Nodes */ "errorColor": "#FEE2E2", "errorBorderColor": "#DC2626", "errorTextColor": "#991B1B" /* Bad/Problem Nodes */ } } }%% graph TD StartBisect["<b>1. Start Bisecting</b><br><code style='font-family:monospace;background-color:#E5E7EB;padding:1px 3px;border-radius:3px;'>git bisect start</code>"] --> DefineBad["<b>2. Mark Bad Commit</b><br>Known bug present (e.g., HEAD)<br><code style='font-family:monospace;background-color:#E5E7EB;padding:1px 3px;border-radius:3px;'>git bisect bad HEAD</code>"] style StartBisect fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 style DefineBad fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B DefineBad --> DefineGood["<b>3. Mark Good Commit</b><br>Known bug absent (e.g., v1.0)<br><code style='font-family:monospace;background-color:#E5E7EB;padding:1px 3px;border-radius:3px;'>git bisect good v1.0.0</code>"] style DefineGood fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46 DefineGood --> GitCheckout["<b>4. Git Checks Out Middle Commit</b><br>Git selects a commit halfway<br>between 'good' and 'bad' range"] style GitCheckout fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF GitCheckout --> UserTest{"<b>5. User Tests Commit</b><br>Is the bug present at this commit?"} style UserTest fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E UserTest -- "Bug Present" --> MarkBad["<b>6a. Report Bad</b><br><code style='font-family:monospace;background-color:#E5E7EB;padding:1px 3px;border-radius:3px;'>git bisect bad</code><br>Range narrows (current to older good)"] style MarkBad fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B MarkBad --> CheckDoneBad{More commits to test?} style CheckDoneBad fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E UserTest -- "Bug Absent" --> MarkGood["<b>6b. Report Good</b><br><code style='font-family:monospace;background-color:#E5E7EB;padding:1px 3px;border-radius:3px;'>git bisect good</code><br>Range narrows (newer bad to current)"] style MarkGood fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46 MarkGood --> CheckDoneGood{More commits to test?} style CheckDoneGood fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E UserTest -- "Cannot Test (e.g. build fail)" --> SkipCommit["<b>6c. Report Skip</b><br><code style='font-family:monospace;background-color:#E5E7EB;padding:1px 3px;border-radius:3px;'>git bisect skip</code><br>Git tries a nearby commit"] style SkipCommit fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF SkipCommit --> GitCheckout CheckDoneBad -- "Yes" --> GitCheckout CheckDoneGood -- "Yes" --> GitCheckout CheckDoneBad -- "No" --> CulpritFound["<b>7. Culprit Identified!</b><br>Git reports the first bad commit"] style CulpritFound fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 CheckDoneGood -- "No" --> CulpritFound CulpritFound --> EndBisect["<b>8. End Bisecting</b><br><code style='font-family:monospace;background-color:#E5E7EB;padding:1px 3px;border-radius:3px;'>git bisect reset</code><br>Return to original HEAD"] style EndBisect fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 linkStyle default interpolate basis
Automating git bisect:
If you have a script that can automatically test for the bug (e.g., a unit test that now fails), you can automate the entire bisect process:
git bisect run <your_test_script.sh> [args...]
The script should exit with code 0 if the commit is “good,” and a non-zero code (between 1 and 127, excluding 125) if the commit is “bad.” Exit code 125 tells git bisect to skip the commit.
Applying Specific Commits with git cherry-pick
Sometimes, you have a commit on one branch that you want to apply to another branch, but you don’t want to merge the entire source branch. git cherry-pick <commit-hash>
allows you to select a specific commit and apply its changes (as a new commit) onto your current branch.
%%{ init: { "theme": "base", "themeVariables": { "primaryColor": "#EDE9FE", "primaryTextColor": "#5B21B6", "primaryBorderColor": "#5B21B6", "lineColor": "#5B21B6", "textColor": "#1F2937", "mainBkg": "#DBEAFE", "nodeBorder": "#2563EB", "nodeTextColor": "#1E40AF", "secondaryColor": "#D1FAE5", "secondaryBorderColor": "#059669", "secondaryTextColor": "#065F46", "tertiaryColor": "#FEF3C7", "tertiaryBorderColor": "#D97706", "tertiaryTextColor": "#92400E" } } }%% gitGraph commit id: "C1" commit id: "C2" branch feature-A commit id: "A1" commit id: "A2" tag: "Pick Me!" commit id: "A3" checkout main commit id: "C3" cherry-pick id: "A2" tag: "A2'" commit id: "C4"
How git cherry-pick Works:
When you cherry-pick a commit, Git takes the diff (the changes) introduced by that commit and tries to apply it to your current HEAD. If successful, Git creates a new commit on your current branch with the same commit message and author information as the original commit (though the committer will be you, and the commit date will be now). The new commit will have a different SHA-1 hash because its parent and potentially its application context are different.
Use Cases:
- Backporting a bug fix: You fix a bug on a development branch and want to apply just that fix to a stable release branch without merging all other development changes.
- Applying a feature from a stalled branch: A feature was developed on a branch that won’t be merged, but one or two commits from it are still valuable.
- Reordering commits (less common, interactive rebase is often better): Though
git rebase -i
is usually preferred for this.
Potential Issues:
- Conflicts: If the changes in the cherry-picked commit conflict with changes on your current branch, Git will pause and ask you to resolve the conflicts, similar to a merge conflict. After resolving, you
git add
the files and thengit cherry-pick --continue
. You can also usegit cherry-pick --abort
to cancel. - Context Dependency: A commit often depends on previous commits. Cherry-picking a commit in isolation might lead to broken code if its dependencies are not present on the target branch.
- Duplicate Commits (if misused): If you later merge the branch from which you cherry-picked, you might end up with what appears to be duplicate changes, though Git is often smart enough to handle this during the merge if the changes are identical.
Cherry-picking a Range of Commits:
You can cherry-pick a range of commits:
git cherry-pick <oldest-commit-hash>^..<newest-commit-hash>
(picks commits from after oldest-commit-hash up to and including newest-commit-hash).
Managing Multiple Working Trees with git worktree
Traditionally, if you wanted to work on two different branches of the same repository simultaneously (e.g., fixing a bug on a release branch while continuing feature development on another), you’d have to stash changes and switch branches frequently, or clone the repository multiple times. git worktree
provides a more elegant solution.
%%{ init: { "theme": "base", "themeVariables": { "primaryColor": "#EDE9FE", "primaryTextColor": "#5B21B6", "primaryBorderColor": "#5B21B6", /* Main Repo .git */ "lineColor": "#5B21B6", "textColor": "#1F2937", "mainBkg": "#DBEAFE", "nodeBorder": "#2563EB", "nodeTextColor": "#1E40AF", /* Worktree Dirs */ "secondaryColor": "#D1FAE5", "secondaryBorderColor": "#059669", "secondaryTextColor": "#065F46", /* Branches */ "tertiaryColor": "#FEF3C7", "tertiaryBorderColor": "#D97706", "tertiaryTextColor": "#92400E" /* Object DB */ } } }%% graph TD subgraph "Project Root Filesystem" MainRepoDir["<b>my-project/</b> (Main Worktree)"] style MainRepoDir fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF GitDir["<b>my-project/.git/</b><br>(Shared Object Database, Refs, Config)"] style GitDir fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 WorktreeMetaDir["<b>my-project/.git/worktrees/</b><br>(Metadata for linked worktrees)"] style WorktreeMetaDir fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E Worktree1Dir["<b>../hotfix-branch/</b> (Linked Worktree 1)"] style Worktree1Dir fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF Worktree2Dir["<b>my-project-feature-X/</b> (Linked Worktree 2)"] style Worktree2Dir fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF MainRepoDir --- GitDir GitDir --- WorktreeMetaDir WorktreeMetaDir -- "links to" --> Worktree1Dir WorktreeMetaDir -- "links to" --> Worktree2Dir GitDir -. "Provides objects to" .-> MainRepoDir GitDir -. "Provides objects to" .-> Worktree1Dir GitDir -. "Provides objects to" .-> Worktree2Dir end subgraph "Branches Checked Out" MainBranch["<b>main</b> branch<br>(in my-project/)"] style MainBranch fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46 HotfixBranch["<b>release-v1.0</b> branch<br>(in ../hotfix-branch/)"] style HotfixBranch fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46 FeatureXBranch["<b>feature/X</b> branch<br>(in my-project-feature-X/)"] style FeatureXBranch fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46 MainRepoDir --> MainBranch Worktree1Dir --> HotfixBranch Worktree2Dir --> FeatureXBranch end linkStyle default interpolate basis
git worktree
allows you to have multiple working trees (directories) linked to a single Git repository. Each worktree can have a different branch checked out, allowing you to work on them in parallel without interfering with each other.
How git worktree Works:
The main repository (e.g., my-project/.git/) stores all the Git objects (commits, trees, blobs). A worktree is simply another directory on your filesystem that uses the object database from the main repository but has its own working copy of files for a specific branch and its own HEAD, index, and other per-worktree Git files (stored in my-project/.git/worktrees/<worktree-name>/).
Key git worktree
Commands:
- git worktree add <path> [<branch>]:Creates a new worktree at <path> and checks out <branch> in it. If <branch> is omitted, a new branch with the same name as the final directory component of <path> is created from HEAD.Example: git worktree add ../hotfix-branch release-v1.0This would create a directory hotfix-branch one level up from your current repository, check out the release-v1.0 branch into it, and link it to your main repository.
- git worktree list:Shows all connected worktrees, their paths, current HEAD commit, and branch.
- git worktree remove [-f] <path>:Removes a worktree. The actual worktree directory on the filesystem is not deleted by default unless it’s clean (use -f or –force to remove even if dirty, but be careful). The metadata in .git/worktrees is removed. You cannot remove the main worktree.
- git worktree prune:Removes worktree administrative files for worktrees that are no longer valid (e.g., if you manually deleted a worktree directory).
Benefits:
- Parallel Branch Work: Work on different branches simultaneously without stashing or context switching within a single directory.
- Testing: Run long-running tests on one branch in one worktree while continuing development on another.
- Resource Efficiency: Shares the Git object database, so it’s more disk-space efficient than multiple full clones.
- Clean Separation: Each worktree has its own
HEAD
and index.
Customizing Workflows with Git Hooks
Git Hooks are scripts that Git executes automatically before or after certain events occur, such as committing, pushing, or receiving commits. They allow you to customize Git’s internal behavior and enforce policies or automate tasks in your workflow.
Types of Hooks:
- Client-Side Hooks: Run on your local machine. They are not propagated when you clone a repository. Each developer sets up their own client-side hooks.
- Location:
.git/hooks/
directory within your repository. - Examples:
pre-commit
: Runs before a commit is created. Used for linting, running quick tests, checking commit message format. If it exits non-zero, the commit is aborted.prepare-commit-msg
: Runs before the commit message editor is opened. Can be used to auto-generate parts of the commit message.commit-msg
: Runs after you provide a commit message. Used to validate the message against project standards. If it exits non-zero, the commit is aborted.post-commit
: Runs after a commit is successfully created. Used for notifications or further processing.pre-rebase
: Runs beforegit rebase
.post-checkout
: Runs aftergit checkout
orgit switch
.post-merge
: Runs after a successfulgit merge
.pre-push
: Runs beforegit push
. Can be used to run tests before pushing.
- Location:
- Server-Side Hooks: Run on the remote server where the repository is hosted. Used to enforce project policies, send notifications, or trigger CI/CD pipelines.
- Location: In the
hooks/
directory of the bare repository on the server. - Examples:
pre-receive
: Runs when the server receives a push. Can be used to check if pushed commits meet certain criteria (e.g., message format, author, passing tests via a CI query). If it exits non-zero, the push is rejected.update
: Similar topre-receive
, but runs once for each branch being updated.post-receive
: Runs after a push has been successfully accepted. Used for notifications (email, chat), triggering CI/CD builds, updating other services.
- Location: In the
Hook Type | Common Hook Name | Trigger Point | Typical Use Cases |
---|---|---|---|
Client-Side Hooks (Run locally in .git/hooks/ , not versioned by default) |
pre-commit |
Before a commit is created. | Lint code, run quick tests, check commit message format (partially), check for secrets. Aborts commit on non-zero exit. |
prepare-commit-msg |
Before commit message editor opens, after default message is created. | Auto-generate or modify commit message template (e.g., add issue ID). | |
commit-msg |
After commit message is entered, before commit object is created. | Validate commit message against project standards. Aborts commit on non-zero exit. | |
post-commit |
After a commit is successfully created. | Notifications, triggering local build/test processes. | |
post-checkout |
After git checkout or git switch successfully runs. |
Environment setup, dependency installation based on branch, clearing generated files. | |
pre-push |
Before git push runs, after remote refs have been updated locally but before any objects are sent. |
Run extensive tests, ensure branch is up-to-date. Aborts push on non-zero exit. | |
Server-Side Hooks (Run on remote server, enforce policies for all users) |
pre-receive |
When server receives a push, before any refs are updated. Runs once per push. | Enforce project policies (e.g., commit message format, author, prevent non-fast-forward pushes, check for private keys), integrate with CI for pre-merge checks. Rejects push on non-zero exit. |
update |
Similar to pre-receive , but runs once for each branch being updated. |
Finer-grained control per ref being updated. Rejects specific ref update on non-zero exit. | |
post-receive |
After a push has been successfully accepted and refs are updated. | Notifications (email, chat), trigger CI/CD builds, update issue trackers, deploy applications. |
Implementing Hooks:
- Git provides sample hook scripts in
.git/hooks/
(e.g.,pre-commit.sample
). To enable a hook, rename the sample file (remove.sample
) and make it executable. - Hooks can be written in any scripting language your system supports (e.g., Bash, Python, Ruby, Perl). The script just needs to be executable and use an appropriate shebang (e.g.,
#!/bin/sh
or#!/usr/bin/env python3
). - Client-side hooks are not versioned with the repository (because
.git
is not tracked). To share hooks with a team, you typically commit them to a separate directory in the repository (e.g.,scripts/hooks/
) and provide instructions or a script for team members to copy or symlink them into their local.git/hooks/
directory. Some tools or Git extensions help manage shared hooks. (Git 2.9+ allows configuringcore.hooksPath
to point to a shared directory of hooks).
%%{ init: { "theme": "base", "themeVariables": { "primaryColor": "#EDE9FE", "primaryTextColor": "#5B21B6", "primaryBorderColor": "#5B21B6", /* Server */ "lineColor": "#5B21B6", "textColor": "#1F2937", "mainBkg": "#DBEAFE", "nodeBorder": "#2563EB", "nodeTextColor": "#1E40AF", /* Local Actions */ "secondaryColor": "#D1FAE5", "secondaryBorderColor": "#059669", "secondaryTextColor": "#065F46", /* Hooks */ "tertiaryColor": "#FEF3C7", "tertiaryBorderColor": "#D97706", "tertiaryTextColor": "#92400E", /* Remote Actions */ "errorColor": "#FEE2E2", "errorBorderColor": "#DC2626", "errorTextColor": "#991B1B" /* Abortable Hooks */ } } }%% sequenceDiagram actor Developer participant LocalRepo as Local Repository participant RemoteRepo as Remote Repository (Server) Developer->>LocalRepo: Edit files Developer->>LocalRepo: git add Developer->>LocalRepo: git commit -m "Message" activate LocalRepo LocalRepo->>LocalRepo: <b>pre-commit</b> hook (client-side) alt Hook Fails (non-zero exit) LocalRepo-->>Developer: Commit Aborted else Hook Succeeds LocalRepo->>LocalRepo: <b>prepare-commit-msg</b> hook (client-side) LocalRepo->>Developer: Open commit message editor Developer->>LocalRepo: Save commit message LocalRepo->>LocalRepo: <b>commit-msg</b> hook (client-side) alt Hook Fails LocalRepo-->>Developer: Commit Aborted else Hook Succeeds LocalRepo-->>Developer: Commit successful LocalRepo->>LocalRepo: <b>post-commit</b> hook (client-side) end end deactivate LocalRepo Developer->>LocalRepo: git checkout new-branch activate LocalRepo LocalRepo->>LocalRepo: <b>post-checkout</b> hook (client-side) deactivate LocalRepo Developer->>LocalRepo: git merge feature-branch activate LocalRepo LocalRepo->>LocalRepo: (Merge happens) LocalRepo->>LocalRepo: <b>post-merge</b> hook (client-side) deactivate LocalRepo Developer->>LocalRepo: git push origin main activate LocalRepo LocalRepo->>LocalRepo: <b>pre-push</b> hook (client-side) alt Hook Fails LocalRepo-->>Developer: Push Aborted else Hook Succeeds LocalRepo->>RemoteRepo: Transmit objects activate RemoteRepo RemoteRepo->>RemoteRepo: <b>pre-receive</b> hook (server-side) alt Hook Fails RemoteRepo-->>LocalRepo: Push Rejected LocalRepo-->>Developer: Push Failed else Hook Succeeds (for each ref) RemoteRepo->>RemoteRepo: <b>update</b> hook (server-side, per ref) alt Hook Fails for a ref RemoteRepo-->>LocalRepo: Ref Update Rejected LocalRepo-->>Developer: Push Partially Failed else Hook Succeeds for ref RemoteRepo->>RemoteRepo: (Ref updated) end end Note over RemoteRepo: If all refs accepted... RemoteRepo->>RemoteRepo: <b>post-receive</b> hook (server-side) RemoteRepo-->>LocalRepo: Push Successful LocalRepo-->>Developer: Push Successful deactivate RemoteRepo end deactivate LocalRepo
Annotating Commits with git notes
Sometimes you want to attach extra information or metadata to existing commits without altering their SHA-1 hashes (which would happen if you amended them). git notes
allows you to do this.
Notes are stored in a separate namespace (refs/notes/) and are not part of the commit object itself. This means they don’t affect the commit’s identity.
Common git notes
Commands:
git notes add -m "Your note message" [<commit-ish>]
: Adds a note to the specified commit (defaults toHEAD
).git notes append -m "Additional note" [<commit-ish>]
: Appends to an existing note.git notes show [<commit-ish>]
: Displays the notes for a commit.git log --show-notes
:git log
can be configured to display notes alongside commit messages.git notes list
: Lists all commits that have notes.git notes remove [<commit-ish>]
: Removes notes from a commit.- Fetching and Pushing Notes: Notes are not transferred by default with
git fetch
orgit push
. You need to specify the notes ref:git fetch origin refs/notes/*:refs/notes/*
- git push origin refs/notes/*You can configure Git to always push/fetch notes by modifying the fetch and push lines for your remote in .git/config.
Use Cases:
- Adding information from a code review system or bug tracker that doesn’t fit in the commit message.
- Marking commits with QA status or build results.
- Temporary annotations for personal use.
Offline Repository Sharing with git bundle
git bundle
allows you to package Git objects and references into a single archive file (a “bundle”). This bundle can then be easily transferred (e.g., via USB drive, email if small enough) to another developer or another machine that might not have network access to the original repository. The recipient can then unbundle it to get the commits.
How git bundle Works:
A bundle file contains all the necessary Git objects (blobs, trees, commits) and references (branches, tags) to reconstruct a part of the repository’s history.
Key git bundle
Commands:
- git bundle create
:Creates a bundle file. specifies which refs or commits to include. - Example (bundle entire repo):
git bundle create repo.bundle --all
- Example (bundle a specific branch):
git bundle create my-feature.bundle main..feature/my-branch
(commits onfeature/my-branch
not inmain
) - Example (bundle new commits since a tag):
git bundle create updates.bundle v1.0..HEAD
- Example (bundle entire repo):
- git bundle verify <file.bundle>:Checks if a bundle file is valid and lists what it contains (needed refs, provided refs).
- git clone <file.bundle> <repo-dir> -b <branch_name>:You can clone directly from a bundle file as if it were a remote repository. You typically need to specify a branch to check out.
- Fetching from a bundle into an existing repository:
- Treat the bundle as a remote:
git remote add offline-bundle /path/to/your.bundle
- Fetch from it:
git fetch offline-bundle
- Then you can merge or checkout branches fetched from the bundle.Alternatively, git pull /path/to/your.bundle <branch_name> can sometimes work directly.
- Treat the bundle as a remote:
Use Cases:
- Sharing code when network access is restricted or unavailable.
- Archiving specific states of a repository.
- Transferring Git history between disconnected systems.
A Summary Table for Advanced Git Commands:
Command | Primary Purpose | Key Operations / Options |
---|---|---|
git bisect |
Find the commit that introduced a bug using binary search. |
|
git cherry-pick |
Apply specific commits from one branch to another. |
|
git worktree |
Manage multiple working trees linked to the same repository. |
|
Git Hooks | Scripts automatically run by Git at certain events (not a single command). |
|
git notes |
Attach metadata to commits without altering their history. |
|
git bundle |
Create and use bundle files for offline sharing of repository data. |
|
Practical Examples
Setup:
Create a new repository for these examples.
mkdir advanced-git-lab
cd advanced-git-lab
git init
# Create some initial commit history
echo "File version 1" > file.txt
git add file.txt
git commit -m "C1: Initial commit"
echo "File version 2" >> file.txt
git add file.txt
git commit -m "C2: Add feature A"
echo "File version 3 (introduces a bug - let's say 'BUG' keyword)" >> file.txt
echo "BUG in version 3" >> file.txt
git add file.txt
git commit -m "C3: Add feature B (with a bug)"
echo "File version 4" >> file.txt
git add file.txt
git commit -m "C4: Add feature C"
echo "File version 5" >> file.txt
git add file.txt
git commit -m "C5: Refactor and more changes"
git log --oneline --graph
# Expected:
# * <hash_C5> (HEAD -> main) C5: Refactor and more changes
# * <hash_C4> C4: Add feature C
# * <hash_C3> C3: Add feature B (with a bug)
# * <hash_C2> C2: Add feature A
# * <hash_C1> C1: Initial commit
1. git bisect
Example
We know C5 is “bad” (has the bug “BUG”) and C1 was “good” (no “BUG” keyword).
# Start bisecting
git bisect start
# Mark current HEAD (C5) as bad
git bisect bad HEAD
# Mark C1 as good (replace <hash_C1> with actual hash from your log)
git bisect good <hash_C1>
Git will check out a commit in the middle (likely C3).
Expected output (something like):
Bisecting: X revisions left to test after this (roughly Y steps)
[<hash_C3>] C3: Add feature B (with a bug)
Now, test for the bug. Let’s check if file.txt
contains “BUG”.
grep "BUG" file.txt
If it outputs “BUG in version 3”, then this commit (C3) is bad.
git bisect bad
Git will check out another commit (likely C2).
Expected output:
Bisecting: X revisions left to test after this (roughly Y steps)
[<hash_C2>] C2: Add feature A
Test again:
grep "BUG" file.txt
This time, it should output nothing, so C2 is good.
git bisect good
Git will now report the first bad commit:
Expected output:
<hash_C3> is the first bad commit
commit <hash_C3>
Author: Your Name <youremail@example.com>
Date: ...
C3: Add feature B (with a bug)
file.txt | 1 +
1 file changed, 1 insertion(+)
We found it! C3 introduced the bug.
Now, reset to get back to your original branch (main at C5):
git bisect reset
git status # Should be on branch main, clean.
2. git cherry-pick
Example
Let’s create a feature branch and then cherry-pick a commit from it to main
.
# Create a feature branch from C2
git checkout -b feature/experimental <hash_C2> # Replace <hash_C2>
# Add a commit to the feature branch
echo "Useful utility function" > utility.py
git add utility.py
git commit -m "F1: Add useful utility"
# Let's note the hash of F1: <hash_F1>
echo "More experimental stuff" >> utility.py
git add utility.py
git commit -m "F2: More experimental changes"
# Now, switch back to main (which is at C5)
git checkout main
# We want to pick only F1 (the useful utility) onto main
git cherry-pick <hash_F1> # Use the actual hash of F1
Expected Output (if no conflicts):
[main <new_hash_F1_on_main>] F1: Add useful utility
Date: ...
1 file changed, 1 insertion(+)
create mode 100644 utility.py
```git log --oneline main` will now show a new commit on `main` with the message "F1: Add useful utility", and `utility.py` will be present.
### 3. `git worktree` Example
```bash
# We are in advanced-git-lab, on main branch
# Create a new worktree for a hotfix on an older state (e.g., based on C4)
# First, create a branch for the hotfix base if it doesn't exist
git branch release-v0.4 <hash_C4> # Replace <hash_C4>
# Add a worktree in a sibling directory named 'hotfix-v0.4-worktree'
# checking out the 'release-v0.4' branch
git worktree add ../hotfix-v0.4-worktree release-v0.4
Expected Output:
Preparing worktree (checking out 'release-v0.4')
HEAD is now at <hash_C4> C4: Add feature C
Now you have:
advanced-git-lab/
(main worktree, onmain
branch)hotfix-v0.4-worktree/
(new worktree, onrelease-v0.4
branch)
You can cd ../hotfix-v0.4-worktree
, make commits, etc., independently.
git worktree list
# Expected output:
# /path/to/advanced-git-lab <hash_main_HEAD> [main]
# /path/to/hotfix-v0.4-worktree <hash_C4> [release-v0.4]
# To remove the worktree when done:
# First, ensure you're not inside it.
# cd ../advanced-git-lab
# git worktree remove ../hotfix-v0.4-worktree
4. Git Hooks Example (Client-Side pre-commit
)
Let’s create a simple pre-commit
hook that prevents committing if the commit message is too short.
- Navigate to
.git/hooks/
in youradvanced-git-lab
repository:
cd .git/hooks
- Create a file named
pre-commit
(no extension):
# Using a text editor, create .git/hooks/pre-commit with this content:
# (Or use 'touch pre-commit' then edit)
- Content for
.git/hooks/pre-commit
:
#!/bin/sh
#
# A simple pre-commit hook to check commit message length.
# The commit message is passed in a file specified by $1.
COMMIT_MSG_FILE=$1
MIN_MSG_LENGTH=10
MSG_CONTENT=$(cat "$COMMIT_MSG_FILE")
ACTUAL_LENGTH=$(echo -n "$MSG_CONTENT" | wc -c) # wc -c counts bytes, good enough for demo
if [ "$ACTUAL_LENGTH" -lt "$MIN_MSG_LENGTH" ]; then
echo "Error: Commit message is too short."
echo "Message must be at least $MIN_MSG_LENGTH characters."
echo "Your message: '$MSG_CONTENT'"
exit 1 # Abort the commit
fi
exit 0 # Commit is OK
- Make the hook executable:
chmod +x pre-commit
- Go back to the repository root:
cd ../.. # Back to advanced-git-lab
- Try to make a commit with a short message:
echo "New change" > newfile.txt
git add newfile.txt
git commit -m "Short"
Expected Output:
Error: Commit message is too short.
Message must be at least 10 characters.
Your message: 'Short'
The commit will be aborted.
- Try again with a longer message:
git commit -m "This is a sufficiently long commit message"
This commit should succeed.
5. git notes
Example
# Add a note to the latest commit (C5 or the one after the hook test)
git notes add -m "Reviewed and approved by QA team." HEAD
# View notes for the last commit
git notes show HEAD
# Expected: Reviewed and approved by QA team.
# View log with notes
git log -n 1 --show-notes
# Expected output will include:
# commit <hash>
# Author: ...
# Date: ...
#
# Commit message...
#
# Notes:
# Reviewed and approved by QA team.
6. git bundle
Example
# Create a bundle file containing all branches and tags
git bundle create ../my-repo.bundle --all
# Verify the bundle (optional)
# git bundle verify ../my-repo.bundle
# To simulate cloning from the bundle:
cd .. # Go to parent directory
mkdir cloned-from-bundle
cd cloned-from-bundle
# Note: You need to specify a branch to checkout when cloning from a bundle
# First, list heads in bundle to know which branch to checkout
# git bundle list-heads ../my-repo.bundle
# Example output might show 'refs/heads/main'
git clone ../my-repo.bundle . -b main # Clone into current dir, checkout main
Now cloned-from-bundle
is a full Git repository initialized from the bundle.
OS-Specific Notes
- Git Hooks Scripting:
- Shebang (
#!
): The first line of a hook script (e.g.,#!/bin/sh
,#!/usr/bin/env python3
) tells the system how to execute the script. This is crucial on Linux and macOS. - Windows: If you’re using Git Bash, Unix-style shell scripts (Bash, Perl, Python if in Git Bash’s PATH) usually work. If you’re using Git from CMD or PowerShell and want to write hooks in native Windows scripting (e.g.,
.bat
,.ps1
), you’d name your hookpre-commit.bat
orpre-commit.ps1
. Git for Windows is generally smart enough to find and execute these. Ensure your scripting language interpreter is in the systemPATH
. - Executable Permissions: On Linux and macOS, hook scripts must have execute permissions (
chmod +x .git/hooks/my-hook
). This is not typically an issue on Windows for.bat
or.ps1
files.
- Shebang (
git worktree
Paths: Paths for new worktrees can be relative or absolute. Standard path handling for your OS applies. Using relative paths like../another-worktree
is common.git bundle
File Paths: Standard OS path conventions apply when specifying the bundle file location.
Generally, these advanced commands themselves behave consistently across platforms. The main OS-specific considerations arise when writing custom scripts for Git Hooks.
Common Mistakes & Troubleshooting Tips
Git Issue / Error (Advanced) | Symptom(s) | Troubleshooting / Solution |
---|---|---|
git bisect : Forgetting reset |
Mistake: Finishing bisect but not running git bisect reset .Symptom: Repo in detached HEAD state. git status shows this. |
Solution: Run git bisect reset to return to the original branch/HEAD. |
git bisect : “Good” and “Bad” are the same or inverted |
Mistake: Incorrectly identifying initial good/bad commits, or good/bad logic is flawed during testing. Symptom: Bisect gives confusing results or identifies the wrong commit. |
Solution: Restart bisect (git bisect reset , then git bisect start ). Carefully verify the known good and bad commits. Double-check testing logic for each step. Use git bisect log to review steps taken. |
git cherry-pick : Conflicts |
Mistake: Cherry-picked commit’s changes conflict with target branch. Symptom: Cherry-pick pauses, Git reports conflicts. |
Solution:
|
git cherry-pick : Creates new commit |
Mistake: Expecting cherry-pick to apply the *exact same* commit (same SHA-1). Symptom: Confusion about history; a new commit appears on target branch. |
Solution: Understand that git cherry-pick applies the *changes* as a new commit on the target branch. This new commit will have a different SHA-1, parent, and committer date. |
git worktree : Deleting directory manually |
Mistake: Manually deleting worktree directory without git worktree remove .Symptom: .git/worktrees/ metadata remains; git worktree list may show broken entries. |
Solution: Always use git worktree remove <path> . If already deleted, run git worktree prune to clean up stale administrative files. |
Git Hooks: Not Executable / Wrong Shebang | Mistake: Hook script lacks execute permissions (Linux/macOS) or has incorrect shebang (e.g., #!/bin/bash when script is Python).Symptom: Hook doesn’t run or errors out. |
Solution: Set execute permissions: chmod +x .git/hooks/<hookname> . Use portable shebang: #!/usr/bin/env python3 . Test script independently. |
Git Hooks: Client-side hooks not shared | Mistake: Expecting client-side hooks in .git/hooks/ to be cloned by team members.Symptom: Hooks work locally but not for others. |
Solution: Client-side hooks are not versioned. Store hook scripts in the repository (e.g., a /scripts/hooks dir) and have team members manually copy/symlink them, or use git config core.hooksPath ./scripts/hooks (Git 2.9+). |
git notes : Not appearing remotely |
Mistake: Adding notes locally and expecting them to auto-transfer with git push/fetch .Symptom: Notes visible locally, but not to collaborators. |
Solution: Push notes: git push origin refs/notes/* . Fetch notes: git fetch origin refs/notes/*:refs/notes/* . Configure remote’s fetch/push lines in .git/config for automatic transfer. |
git bundle : Cannot clone “empty” bundle |
Mistake: Creating a bundle with arguments that result in no new commits relative to what the recipient might already have, or not specifying a branch when cloning. Symptom: git clone from bundle fails or results in an empty repository. |
Solution: Ensure bundle includes necessary history (e.g., git bundle create my.bundle --all or git bundle create my.bundle main ). When cloning, specify a branch: git clone my.bundle repo-dir -b main . Use git bundle list-heads my.bundle to see available refs. |
Exercises
- Bug Hunt with
git bisect
:- In a practice repository, create a history of 5-10 commits. In one of the middle commits, intentionally introduce a simple bug (e.g., a function that returns a wrong value, or a specific text string appearing in a file). Ensure later commits don’t fix this bug.
- Use
git bisect start
,git bisect bad HEAD
, andgit bisect good <an_early_commit_hash_before_bug>
to initiate the process. - Manually “test” each commit Git checks out (e.g., by running the code or checking the file content). Use
git bisect good
orgit bisect bad
accordingly. - Identify the commit that introduced the bug.
- Run
git bisect reset
.
- Selective Feature Application with
git cherry-pick
:- Create a
main
branch with a few commits. - Create a
feature-X
branch frommain
. Add two distinct commits tofeature-X
(e.g., “Commit A: Add core logic for X”, “Commit B: Add UI for X”). - Create another branch
feature-Y
frommain
. - Decide that “Commit A” from
feature-X
is also needed forfeature-Y
. - On
feature-Y
, usegit cherry-pick
to apply “Commit A” fromfeature-X
. - Verify that the changes (and commit message) from “Commit A” are now on
feature-Y
as a new commit.
- Create a
- Parallel Development with
git worktree
:- In an existing repository, while on the
main
branch, create a new worktree for a branch namedexperiment
in a directory calledexperiment-workarea
(e.g.,git worktree add ../experiment-workarea experiment
). cd
into../experiment-workarea
. Checkgit status
andgit branch
. Make a new commit on theexperiment
branch in this worktree.cd
back to your original repository directory. Checkgit status
andgit branch
(you should still be onmain
).- Use
git worktree list
to see both worktrees. - (Optional) When done, remove the
experiment-workarea
worktree usinggit worktree remove ../experiment-workarea
.
- In an existing repository, while on the
- (Bonus) Simple
pre-commit
Hook:- In a test repository, create a
pre-commit
hook (in.git/hooks/pre-commit
) that checks if any files with a.tmp
extension are being staged for commit. If so, it should print an error message and exit with a non-zero status to prevent the commit. - Make the hook executable.
- Create and
git add
atest.tmp
file. Try to commit it. The hook should block the commit. - Remove or unstage
test.tmp
and try committing again (e.g., with a non-.tmp
file). The commit should now succeed.
- In a test repository, create a
Summary
git bisect
: Efficiently finds the commit that introduced a bug using a binary search (bisect start
,good
,bad
,reset
).git cherry-pick <commit>
: Applies the changes from a specific commit onto the current branch, creating a new commit.git worktree add <path> <branch>
: Creates a new working directory linked to the main repository, allowing simultaneous work on different branches.- Git Hooks: Scripts triggered by Git events (e.g.,
pre-commit
,commit-msg
,pre-push
,pre-receive
) to customize workflows and enforce policies. They reside in.git/hooks/
. git notes add -m "note" <commit>
: Attaches metadata to commits without altering their history. Notes are fetched/pushed separately.git bundle create <file> <refs>
: Packages repository objects and refs into a single file for offline transfer.
These advanced techniques provide powerful ways to debug, manage complex changes, streamline workflows, and handle repository data in specialized situations, further extending Git’s versatility.
Further Reading
- Pro Git Book:
- Chapter 7.3 Git Tools – Stashing and Cleaning (Mentions
git clean
which is sometimes related to worktree cleanup) - Chapter 7.6 Git Tools – Rewriting History (Context for why
cherry-pick
creates new commits) - Chapter 7.7 Git Tools – Debugging with Git (Covers
bisect
andblame
) - Chapter 7.10 Git Tools – Submodules (Related concept of managing separate repositories, contrasts with worktree’s single repo approach)
- Chapter 8.3 Customizing Git – Git Hooks: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
- Chapter 7.3 Git Tools – Stashing and Cleaning (Mentions
- Official Git Documentation:
git-bisect(1)
: https://git-scm.com/docs/git-bisectgit-cherry-pick(1)
: https://git-scm.com/docs/git-cherry-pickgit-worktree(1)
: https://git-scm.com/docs/git-worktreegithooks(5)
: https://git-scm.com/docs/githooksgit-notes(1)
: https://git-scm.com/docs/git-notesgit-bundle(1)
: https://git-scm.com/docs/git-bundle
- Atlassian Git Tutorials:
- Git Bisect: https://www.atlassian.com/git/tutorials/git-bisect
- Git Cherry-pick: https://www.atlassian.com/git/tutorials/cherry-pick
