---
title: "Advanced Git Workflows: Rebase, Bisect, Worktrees, and Recovery"
description: "Practical reference for advanced Git operations including interactive rebase, cherry-pick, bisect for bug finding, reflog recovery, worktrees, subtrees vs submodules, hooks, and git-crypt for secrets."
url: https://agent-zone.ai/knowledge/developer-workflows/git-advanced-workflows/
section: knowledge
date: 2026-02-22
categories: ["developer-workflows"]
tags: ["git","rebase","bisect","worktrees","hooks","git-crypt","cherry-pick","reflog"]
skills: ["version-control","git-history-management","secret-management"]
tools: ["git","git-crypt","gpg"]
levels: ["intermediate","advanced"]
word_count: 1267
formats:
  json: https://agent-zone.ai/knowledge/developer-workflows/git-advanced-workflows/index.json
  html: https://agent-zone.ai/knowledge/developer-workflows/git-advanced-workflows/?format=html
  api: https://api.agent-zone.ai/api/v1/knowledge/search?q=Advanced+Git+Workflows%3A+Rebase%2C+Bisect%2C+Worktrees%2C+and+Recovery
---


## Interactive Rebase

Interactive rebase rewrites commit history before merging a feature branch. It turns a messy series of "WIP", "fix typo", and "actually fix it" commits into a clean, reviewable sequence.

Start an interactive rebase covering the last 5 commits:

```bash
git rebase -i HEAD~5
```

Or rebase everything since the branch diverged from main:

```bash
git rebase -i main
```

Git opens your editor with a list of commits. Each line starts with an action keyword:

```
pick a1b2c3d Add user authentication endpoint
pick d4e5f6g Fix missing import
pick h7i8j9k WIP: token refresh
pick l0m1n2o Finish token refresh logic
pick p3q4r5s Fix tests
```

Change the keywords to reshape history:

- **pick** -- keep the commit as-is.
- **squash** (or **s**) -- merge this commit into the previous one, combining messages.
- **fixup** (or **f**) -- like squash but discard this commit's message.
- **reword** (or **r**) -- keep the changes, edit the commit message.
- **edit** (or **e**) -- pause at this commit to amend it.
- **drop** (or **d**) -- delete the commit entirely.

A common cleanup pattern squashes fix-up commits into the features they belong to:

```
pick a1b2c3d Add user authentication endpoint
fixup d4e5f6g Fix missing import
pick h7i8j9k WIP: token refresh
fixup l0m1n2o Finish token refresh logic
fixup p3q4r5s Fix tests
```

This produces two clean commits: one for authentication, one for token refresh.

**Autosquash shortcut**: If you name fix-up commits with a `fixup!` prefix matching the original commit message, `git rebase -i --autosquash` automatically reorders and marks them:

```bash
git commit -m "fixup! Add user authentication endpoint"
# later:
git rebase -i --autosquash main
```

## Cherry-Pick

Cherry-pick copies a specific commit from one branch to another without merging the entire branch. It creates a new commit with the same changes but a different hash.

```bash
git cherry-pick abc1234
```

Cherry-pick a range of commits (inclusive on both ends with `..`):

```bash
git cherry-pick abc1234^..def5678
```

Cherry-pick without committing, so you can modify the changes first:

```bash
git cherry-pick --no-commit abc1234
```

Common use case: a hotfix lands on `main` and needs to be backported to a release branch.

```bash
git checkout release/2.3
git cherry-pick abc1234
```

If there is a conflict, resolve it, then:

```bash
git add .
git cherry-pick --continue
```

To abort a cherry-pick that went sideways:

```bash
git cherry-pick --abort
```

## Bisect for Bug Finding

`git bisect` performs a binary search through commit history to find the exact commit that introduced a bug. Instead of checking every commit linearly, it halves the search space each step.

Start a bisect session:

```bash
git bisect start
git bisect bad          # current commit has the bug
git bisect good v2.1.0  # this tag did NOT have the bug
```

Git checks out a commit halfway between good and bad. Test it, then mark:

```bash
git bisect good  # this commit is fine
# or
git bisect bad   # this commit has the bug
```

Repeat until git identifies the exact commit. Typically takes log2(n) steps -- for 1000 commits, about 10 checks.

**Automated bisect** with a test script is even more powerful:

```bash
git bisect start HEAD v2.1.0
git bisect run ./test-for-bug.sh
```

The script must exit 0 for good commits and non-zero for bad. Git runs it automatically at each step and reports the guilty commit. A simple test script:

```bash
#!/bin/bash
make build && ./run-specific-test.sh
```

When done, reset to your original state:

```bash
git bisect reset
```

## Reflog Recovery

The reflog records every position HEAD has pointed to, even after destructive operations. If you accidentally reset, rebase, or delete a branch, the commits still exist in the reflog for at least 30 days (configurable via `gc.reflogExpire`).

View the reflog:

```bash
git reflog
```

Output looks like:

```
abc1234 HEAD@{0}: reset: moving to HEAD~3
def5678 HEAD@{1}: commit: Add feature X
ghi9012 HEAD@{2}: commit: Update config
```

Recover from an accidental `git reset --hard`:

```bash
git reflog
# Find the commit before the reset
git reset --hard HEAD@{1}
```

Recover a deleted branch:

```bash
git reflog
# Find the last commit on the deleted branch
git branch recovered-branch abc1234
```

Recover from a bad rebase:

```bash
git reflog
# Find the state before the rebase started (look for "rebase (start)")
git reset --hard HEAD@{5}
```

## Worktrees

Worktrees let you check out multiple branches simultaneously in separate directories. Instead of stashing work-in-progress to review a PR on another branch, you open a second worktree.

Create a worktree for a branch:

```bash
git worktree add ../project-hotfix hotfix/critical-fix
```

This creates a new directory `../project-hotfix` with the `hotfix/critical-fix` branch checked out. Both directories share the same `.git` storage -- no extra clone needed.

List active worktrees:

```bash
git worktree list
```

Remove a worktree when done:

```bash
git worktree remove ../project-hotfix
```

Worktrees are especially useful for agents and CI systems that need to operate on multiple branches without the overhead of full clones.

## Subtrees vs Submodules

Both mechanisms embed one Git repository inside another. They solve the same problem differently.

**Submodules** store a pointer (commit hash) to a specific commit in an external repository. The external repo is cloned into a subdirectory.

```bash
git submodule add https://github.com/org/shared-lib.git libs/shared
git commit -m "Add shared-lib submodule"
```

After cloning a repo with submodules:

```bash
git submodule update --init --recursive
```

Update a submodule to its latest:

```bash
cd libs/shared
git pull origin main
cd ../..
git add libs/shared
git commit -m "Update shared-lib to latest"
```

**Subtrees** merge the external repository's history directly into your repo. No separate clone, no `.gitmodules` file, no special commands for consumers.

```bash
git subtree add --prefix=libs/shared https://github.com/org/shared-lib.git main --squash
```

Pull updates from upstream:

```bash
git subtree pull --prefix=libs/shared https://github.com/org/shared-lib.git main --squash
```

Push changes back upstream:

```bash
git subtree push --prefix=libs/shared https://github.com/org/shared-lib.git main
```

**When to use which**: Submodules work well when the dependency is large and you want to control exactly which version you pin to. Subtrees work better when the dependency is small, changes infrequently, and you want consumers to `git clone` without extra steps. For monorepos, subtrees are generally simpler. For large shared libraries with their own release cycles, submodules provide clearer version control.

## Git Hooks

Hooks are scripts that Git runs automatically at specific points. They live in `.git/hooks/` (local, not committed) or can be managed via tools like `pre-commit`, `husky`, or `lefthook`.

**Pre-commit hook** -- runs before each commit. Use it for linting and formatting:

```bash
#!/bin/bash
# .git/hooks/pre-commit
set -e

# Run linter on staged files only
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
if [ -n "$STAGED_FILES" ]; then
    ruff check $STAGED_FILES
    ruff format --check $STAGED_FILES
fi
```

**Commit-msg hook** -- validates or modifies the commit message:

```bash
#!/bin/bash
# .git/hooks/commit-msg
# Enforce conventional commits format
if ! grep -qE '^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,72}' "$1"; then
    echo "ERROR: Commit message must follow conventional commits format"
    echo "Example: feat(auth): add token refresh endpoint"
    exit 1
fi
```

**Pre-push hook** -- runs before pushing. Use it for running tests:

```bash
#!/bin/bash
# .git/hooks/pre-push
set -e
make test
```

For team-wide hooks, use a framework that reads configuration from a committed file:

```yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.3.0
    hooks:
      - id: ruff
      - id: ruff-format
```

Install with `pre-commit install`. The hooks are now consistent across all contributors.

## git-crypt for Secrets

git-crypt provides transparent encryption for files in a Git repository. Files are encrypted on push and decrypted on pull, using GPG keys or a symmetric key.

Initialize git-crypt in a repository:

```bash
git-crypt init
```

Define which files to encrypt via `.gitattributes`:

```
secrets/** filter=git-crypt diff=git-crypt
*.secret filter=git-crypt diff=git-crypt
config/production.env filter=git-crypt diff=git-crypt
```

Add a collaborator by GPG key:

```bash
git-crypt add-gpg-user ABCD1234
```

Or export a symmetric key for CI systems:

```bash
git-crypt export-key /path/to/keyfile
```

Unlock a cloned repository:

```bash
git-crypt unlock
# or with a keyfile:
git-crypt unlock /path/to/keyfile
```

Check encryption status:

```bash
git-crypt status
```

**Important limitations**: git-crypt encrypts file contents but not filenames. Encrypted files show up as binary blobs in diffs, so you lose `git diff` for those files. For anything beyond simple configuration secrets, a dedicated secrets manager (Vault, AWS Secrets Manager) is a better choice. git-crypt works well for `.env` files that need to live in the repo for operational convenience.

