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:

  1. Start Bisecting: git bisect start
  2. Mark Bad Commit: git bisect bad [<commit>] (If <commit> is omitted, HEAD is used).
  3. Mark Good Commit: git bisect good <commit> (e.g., git bisect good v1.0.0).
  4. 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
  5. Repeat: Git continues to check out commits, and you continue to test and report good or bad (or skip).
  6. Identify Culprit: Eventually, Git will report the first bad commit found.
  7. 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 then git cherry-pick --continue. You can also use git 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 before git rebase.
      • post-checkout: Runs after git checkout or git switch.
      • post-merge: Runs after a successful git merge.
      • pre-push: Runs before git push. Can be used to run tests before pushing.
  • 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 to pre-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.
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 configuring core.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 to HEAD).
  • 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 or git 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 on feature/my-branch not in main)
    • Example (bundle new commits since a tag): git bundle create updates.bundle v1.0..HEAD
  • 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:
    1. Treat the bundle as a remote: git remote add offline-bundle /path/to/your.bundle
    2. Fetch from it: git fetch offline-bundle
    3. Then you can merge or checkout branches fetched from the bundle.Alternatively, git pull /path/to/your.bundle <branch_name> can sometimes work directly.

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 bisect start
  • git bisect bad [<commit>]
  • git bisect good <commit>
  • git bisect skip
  • git bisect reset
  • git bisect run <script>
git cherry-pick Apply specific commits from one branch to another.
  • git cherry-pick <commit-hash>
  • git cherry-pick <start-hash>^..<end-hash>
  • --continue, --abort, --quit (for conflicts)
  • -n (no-commit), -e (edit message)
git worktree Manage multiple working trees linked to the same repository.
  • git worktree add <path> [<branch>]
  • git worktree list
  • git worktree remove [-f] <path>
  • git worktree prune
Git Hooks Scripts automatically run by Git at certain events (not a single command).
  • Client-side (e.g., pre-commit, commit-msg, pre-push) in .git/hooks/.
  • Server-side (e.g., pre-receive, post-receive) on remote.
  • Enable by renaming *.sample files and making them executable.
git notes Attach metadata to commits without altering their history.
  • git notes add -m "message" [<commit>]
  • git notes show [<commit>]
  • git notes list
  • git notes remove [<commit>]
  • Fetch/Push: refs/notes/*
git bundle Create and use bundle files for offline sharing of repository data.
  • git bundle create <file.bundle> <rev-list-args>
  • git bundle verify <file.bundle>
  • git bundle list-heads <file.bundle>
  • git clone <file.bundle> <dir> -b <branch>
  • git fetch <file.bundle> (after adding as remote)

Practical Examples

Setup:

Create a new repository for these examples.

Bash
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).

Bash
# 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):

Bash
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”.

Bash
grep "BUG" file.txt

If it outputs “BUG in version 3”, then this commit (C3) is bad.

Bash
git bisect bad

Git will check out another commit (likely C2).

Expected output:

Bash
Bisecting: X revisions left to test after this (roughly Y steps)
[<hash_C2>] C2: Add feature A

Test again:

Bash
grep "BUG" file.txt

This time, it should output nothing, so C2 is good.

Bash
git bisect good

Git will now report the first bad commit:

Expected output:

Bash
<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):

Bash
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.

Bash
# 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):

Bash
[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:

Bash
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, on main branch)
  • hotfix-v0.4-worktree/ (new worktree, on release-v0.4 branch)

You can cd ../hotfix-v0.4-worktree, make commits, etc., independently.

Bash
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 your advanced-git-lab repository:
Bash
cd .git/hooks
  • Create a file named pre-commit (no extension):
Bash
# 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:
Bash
#!/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:
Bash
chmod +x pre-commit
  • Go back to the repository root:
Bash
cd ../.. # Back to advanced-git-lab
  • Try to make a commit with a short message:
Bash
echo "New change" > newfile.txt
git add newfile.txt
git commit -m "Short"

Expected Output:

Bash
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:
Bash
git commit -m "This is a sufficiently long commit message"

This commit should succeed.

5. git notes Example

Bash
# 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

Bash
# 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 hook pre-commit.bat or pre-commit.ps1. Git for Windows is generally smart enough to find and execute these. Ensure your scripting language interpreter is in the system PATH.
    • 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.
  • 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:
  1. Resolve conflicts in affected files manually.
  2. Stage resolved files: git add <resolved-file>.
  3. Continue: git cherry-pick --continue.
  4. Or abort: git cherry-pick --abort.
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

  1. Bug Hunt with git bisect:
    1. 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.
    2. Use git bisect start, git bisect bad HEAD, and git bisect good <an_early_commit_hash_before_bug> to initiate the process.
    3. Manually “test” each commit Git checks out (e.g., by running the code or checking the file content). Use git bisect good or git bisect bad accordingly.
    4. Identify the commit that introduced the bug.
    5. Run git bisect reset.
  2. Selective Feature Application with git cherry-pick:
    1. Create a main branch with a few commits.
    2. Create a feature-X branch from main. Add two distinct commits to feature-X (e.g., “Commit A: Add core logic for X”, “Commit B: Add UI for X”).
    3. Create another branch feature-Y from main.
    4. Decide that “Commit A” from feature-X is also needed for feature-Y.
    5. On feature-Y, use git cherry-pick to apply “Commit A” from feature-X.
    6. Verify that the changes (and commit message) from “Commit A” are now on feature-Y as a new commit.
  3. Parallel Development with git worktree:
    1. In an existing repository, while on the main branch, create a new worktree for a branch named experiment in a directory called experiment-workarea (e.g., git worktree add ../experiment-workarea experiment).
    2. cd into ../experiment-workarea. Check git status and git branch. Make a new commit on the experiment branch in this worktree.
    3. cd back to your original repository directory. Check git status and git branch (you should still be on main).
    4. Use git worktree list to see both worktrees.
    5. (Optional) When done, remove the experiment-workarea worktree using git worktree remove ../experiment-workarea.
  4. (Bonus) Simple pre-commit Hook:
    1. 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.
    2. Make the hook executable.
    3. Create and git add a test.tmp file. Try to commit it. The hook should block the commit.
    4. Remove or unstage test.tmp and try committing again (e.g., with a non-.tmp file). The commit should now succeed.

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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top