Git, GitHub & Version Control

Chapter 13
Git Troubleshooting, Performance and Advanced Practice

Bachelors Program in Full Stack Development
Git, GitHub & Version Control   •   Bachelor

Git Troubleshooting, Performance and Advanced Practice

Introduction

Large and long-lived Git repositories require more than routine add, commit, pull, and push commands. Developers must understand repository state, object storage, history rewriting, synchronization, and diagnostic tools. A mistaken reset, an interrupted rebase, an oversized binary asset, or an unsuitable branching strategy can affect an entire development team.

This chapter presents a systematic approach to advanced Git work. It focuses on diagnosing failures before changing history, recovering apparently lost work, reducing repository growth, and selecting appropriate tools for complex projects.

Learning Objectives

By the end of this chapter, you should be able to:

  • diagnose detached HEAD, merge, rebase, and synchronization problems;
  • recover commits and files using reflogs and object inspection;
  • use stash, bisect, blame, hooks, worktrees, sparse checkout, and garbage collection safely;
  • identify historical large blobs using size-aware inspection;
  • distinguish repository maintenance from history rewriting;
  • optimize repositories without compromising collaboration, traceability, or security.

1. A Diagnostic Model for Git Problems

A Git repository has several related but distinct states:

ComponentMeaningTypical inspection command
HEADThe currently checked-out reference or commitgit status, git symbolic-ref HEAD
IndexProposed contents for the next commitgit diff --cached
Working treeFiles currently present on diskgit diff
ReferencesNames such as branches and tags pointing to commitsgit show-ref
Object databaseCommits, trees, blobs, and annotated tagsgit count-objects -vH
ReflogsLocal records of reference movementgit reflog

A reliable troubleshooting sequence is:

  1. Preserve evidence: avoid destructive commands until the state is understood.
  2. Inspect: run git status, git log --oneline --graph --decorate --all, and relevant diffs.
  3. Identify the desired end state.
  4. Create a safety reference when necessary, for example git branch rescue-before-repair.
  5. Apply the least destructive repair.
  6. Verify tests, history, and synchronization status.

git status often reports the immediate problem, but the graph and reflog explain how it occurred.

2. Detached HEAD States

Normally, HEAD points symbolically to a branch, and that branch points to a commit. In a detached HEAD state, HEAD points directly to a commit, tag, or other object rather than to a local branch. This commonly occurs after git switch --detach <commit> or when checking out a tag.

A detached HEAD is not itself an error. It is useful for inspecting historical versions, reproducing bugs, and running experiments. The risk is creating commits that are not reachable from a named branch. Such commits may later become difficult to find.

Inspect the state with:

git status
git log --oneline --decorate -5

If experimental commits should be retained, create a branch immediately:

git switch -c experiment-preserved

If no commits were created, return to a branch:

git switch main

If commits were created and the user wants them on an existing branch, create a rescue branch first, then merge or cherry-pick:

git branch rescue HEAD
git switch main
git merge rescue

The branch name is the important recovery action; merely switching away does not delete the commit immediately, but it removes the normal named path to it.

3. Merge and Rebase Failures

Merge conflicts

A merge conflict occurs when Git cannot combine changes automatically. The correct procedure is:

git status
git diff
# edit conflicted files
git add <resolved-file>
git commit                 # or git merge --continue where supported

Conflict markers must be removed from the final file. git diff --check can identify whitespace errors. If the merge should be abandoned:

git merge --abort

Rebase conflicts

During a rebase, Git temporarily replays commits onto a new base. Resolve each conflict, stage the result, and continue:

git add <resolved-file>
git rebase --continue

To abandon the operation:

git rebase --abort

git rebase --skip discards the currently replayed commit and should be used only when its change is genuinely unnecessary or already present. Before rebasing published commits, establish team policy: rewriting shared history can invalidate others’ descendant commits.

Merge versus rebase

A merge preserves the existing branch topology and is usually safer for published history. A rebase creates a linear-looking sequence by rewriting commit identities. Neither changes the content automatically in a superior way; they express different historical models. A practical policy is to rebase private, unpublished work and merge or use carefully coordinated rebases for public branches.

4. Lost Commits and Reflogs

A commit is often “lost” only because no branch or tag currently names it. Local reflogs record movements of references, including resets, rebases, and branch updates:

git reflog --all
git fsck --full --no-reflogs --unreachable

After locating the commit, preserve it:

git branch recovered-work <commit-id>

Reflogs are local and expire according to repository configuration. They are not a substitute for backups or remote history. Garbage collection may eventually remove unreachable objects, so recovery should occur before aggressive pruning.

A destructive reset can often be repaired as follows:

git reflog
# identify the pre-reset HEAD, for example HEAD@{1}
git branch before-reset HEAD@{1}

The exact reflog entry depends on the local sequence of operations; never assume that HEAD@{1} always represents the desired commit.

5. Stash as Temporary State Management

git stash stores selected working-tree and index changes in commits that are referenced by the stash reflog. Basic usage is:

git stash push -m "before experiment"
git stash list
git stash show --stat stash@{0}
git stash apply stash@{0}

apply retains the stash; pop applies it and normally removes it if application succeeds. Stashes can conflict just like ordinary changes. A safer practice is to create a temporary branch when work is substantial, because a branch communicates ownership more clearly and is easier to share.

Use git stash push --include-untracked when untracked files are required. Ignored files require --all; this should be used cautiously because build artifacts and secrets may be captured.

6. git bisect: Locating a Regression

Binary search identifies the first commit at which a test changes from good to bad. Begin with a known bad and known good revision:

git bisect start
git bisect bad HEAD
git bisect good <known-good-commit>

Git checks out a midpoint. Test it and report the result:

git bisect good
# or
git bisect bad

When finished:

git bisect reset

The automated form is powerful but its exit-status contract must be exact:

git bisect run <test-command>

For each checked-out revision, the command’s exit status means:

  • 0: the revision is good;
  • 1 through 127, except 125: the revision is bad;
  • 125: the revision is skipped because it cannot be tested reliably;
  • other statuses may terminate the bisect as an error rather than classify the revision.

A test script should therefore reserve status 125 for an untestable revision. For example:

#!/bin/sh
make test
status=$?
if [ "$status" -eq 125 ]; then
    exit 125
elif [ "$status" -eq 0 ]; then
    exit 0
else
    exit 1
fi

The method assumes that revisions can be classified consistently and that the property is sufficiently monotonic: once the defect appears, later tested commits should remain bad unless the history includes a genuine repair. Dependency availability, nondeterministic tests, and environment changes can violate this assumption.

7. Blame and Historical Responsibility

git blame shows the commit and author associated with each line in a selected revision:

git blame -L 40,80 src/service.js
git show <commit-id>

Blame is a historical tracing tool, not a measure of moral responsibility or code quality. Refactoring, generated files, mass formatting, and copied code can make line attribution misleading. Use git log -S<string> or git log -G<regular-expression> to locate when content was introduced or changed. The --follow option can help trace a file across some renames, although rename detection is heuristic rather than a permanent historical fact.

8. Hooks and Governance

Hooks are programs triggered by Git events. Client-side hooks can enforce formatting or tests; server-side hooks can enforce branch policies. Examples include pre-commit, commit-msg, pre-push, and server-side pre-receive.

Hooks are not automatically transferred by ordinary repository cloning, so teams should manage them through a reviewed tool or repository-controlled mechanism. A hook should fail clearly, run quickly enough for its stage, and avoid silently changing source. Security-sensitive hooks must be treated as executable code: review them, restrict permissions, and never execute untrusted hooks without inspection.

Hooks complement, but do not replace, continuous integration. A developer can bypass many client-side checks, whereas protected server-side branches and CI provide shared enforcement.

9. Worktrees and Sparse Checkout

A worktree permits multiple working directories connected to one repository object database:

git worktree add ../release release
 git worktree list

This is useful when a developer must maintain a release branch while implementing a feature. A branch generally cannot be checked out simultaneously in two worktrees, which prevents accidental concurrent edits to the same branch.

Sparse checkout reduces the files populated in a working tree, while retaining repository history according to the chosen configuration:

git sparse-checkout init --cone
git sparse-checkout set services/api shared

Sparse checkout reduces working-tree clutter and some file-system costs; it does not automatically remove large objects from repository history. It is therefore different from shallow cloning, partial cloning, or history rewriting.

10. Binary-File Growth and Repository Size

Git is highly effective for text because similar versions can be represented efficiently through compression and delta storage. Frequently changing binary files—videos, archives, compiled packages, design assets, and datasets—often produce substantial historical growth because small logical changes may create largely different blobs.

Inspect current storage:

git count-objects -vH
git gc --auto

To identify large historical blobs, object names must be paired with object types and sizes. This is important because git rev-list --objects --all | sort -k2 sorts by pathname, not by object size; it does not identify the largest blobs.

A size-aware pipeline is:

git rev-list --objects --all |
while read object path; do
  size=$(git cat-file -s "$object") || continue
  type=$(git cat-file -t "$object") || continue
  printf '%s\t%s\t%s\n' "$size" "$type" "${path:-<unreferenced-name>}"
done |
sort -nr | head -20

This reports bytes, object type, and a path when available. For a large repository, repeatedly invoking git cat-file can be slow. A more scalable approach uses batch inspection:

git rev-list --objects --all | cut -d' ' -f1 |
git cat-file --batch-check='%(objectname) %(objecttype) %(objectsize)' |
sort -k3,3nr | head -20

The object IDs and paths can then be joined if path reporting is required. Results should be interpreted carefully: one object can have multiple names, and an object’s current path is not a complete record of all historical paths.

For future large binary assets, consider Git LFS, artifact repositories, or external dataset storage. .gitignore prevents new untracked files from being added; it does not remove an already committed object. Removing sensitive or oversized history requires a coordinated history rewrite, such as a supported filtering tool, followed by force-updated protected branches and communication to every clone.

11. Garbage Collection and Object Maintenance

Git stores loose objects and packfiles. Maintenance consolidates objects and removes unreachable data after appropriate expiration periods:

git gc
git maintenance run
git repack -Ad

Do not use aggressive pruning as a first response to a recovery problem. It can remove unreachable objects that reflogs would otherwise make recoverable. In shared repositories, server maintenance policies and hosting-provider behavior also matter.

git fsck verifies object connectivity and can report dangling objects. Dangling does not necessarily mean corrupt; it may represent a recently abandoned commit that is still recoverable.

12. Synchronization Problems

Before integrating remote work, inspect both local and remote-tracking branches:

git fetch origin
git log --oneline --graph --decorate HEAD..origin/main
git log --oneline --graph --decorate origin/main..HEAD

git fetch downloads and updates remote-tracking references without changing the current branch. git pull combines fetch with integration, commonly merge or rebase according to configuration. Explicit commands are often safer in complex situations.

A non-fast-forward rejection means the remote branch contains commits absent from the local branch, or the local history was rewritten. Fetch first, inspect the divergence, then merge or rebase according to policy. Use git push --force-with-lease rather than --force when a coordinated history rewrite is necessary. The lease protects against overwriting a remote update that the pusher has not observed, although it is not a complete substitute for review and branch protection.

Avoid treating a successful push as proof that the software is correct. Synchronization is a history operation; tests, review, and deployment validation remain separate concerns.

13. Performance Optimization for Large Repositories

Performance has several dimensions: command latency, working-tree scanning, network transfer, disk usage, and cognitive complexity. Useful practices include:

  • keep generated outputs and dependencies out of ordinary history;
  • use shallow or partial clones when full history is unnecessary;
  • use sparse checkout for monorepo subtrees;
  • use Git LFS or artifact storage for suitable large binaries;
  • run current Git maintenance features and monitor repository statistics;
  • avoid enormous commits that combine unrelated refactoring and behavior changes;
  • define ownership and branch policies for high-risk areas;
  • use commit-graph and multi-pack-index support where appropriate.

A partial clone may use a promisor remote and download objects on demand. This reduces initial transfer but introduces dependence on remote availability and can make unexpected commands trigger network requests. Shallow clones reduce visible history and may interfere with blame, bisect, merge-base calculations, and release tooling. Optimization must therefore be evaluated against the repository’s actual workflows.

14. Applied Case: Recovering a Release Branch

Background. An illustrative web-platform team maintains a monorepo. A developer rebases a release branch, encounters conflicts, resets incorrectly, and later discovers that a 600 MB test archive was committed six months earlier.

Problem. The release branch is missing two commits locally; the remote rejects a normal push; repository cloning has become slow.

Analysis. The team first creates a backup reference and records the current state. git reflog identifies the pre-reset commit. git fetch origin reveals that the remote contains a different release tip. Historical inspection uses git rev-list --objects --all combined with git cat-file --batch-check and numeric sorting, rather than pathname sorting. The archive is confirmed as a historical blob.

Approach. The developer creates recovery-release at the reflog commit, compares it with the remote, and resolves the intended divergence through review. The archive is removed using a coordinated history-filtering procedure; all collaborators are warned that old clones must be recloned or carefully repaired. Future archives are stored in artifact storage, while source directories use sparse checkout for service-focused work. A pre-push hook provides fast checks, and CI remains the authoritative shared validation system.

Outcome. The missing commits are preserved, the release history is deliberately reconciled, and new clones are smaller. The team documents that reflogs are local recovery aids, that history rewriting requires coordination, and that repository size must be monitored continuously.

Learning points. Recovery begins with preservation; object size must be measured rather than inferred from pathnames; and technical optimization is inseparable from governance.

15. Limitations, Ethics, and Professional Practice

History rewriting can destroy evidence, invalidate audit trails, and interfere with incident investigation. Before rewriting, obtain authorization, preserve an archive where policy requires it, identify protected references, and document the old and new tips. Never use Git cleanup to conceal unauthorized changes or security incidents.

Repositories may contain credentials, personal data, licensed materials, or regulated information. Removing a secret from the latest commit does not guarantee that it has disappeared from every clone, cache, fork, or backup. Revoke exposed credentials first, then coordinate removal and notification.

AI-assisted coding and commit generation can increase throughput, but generated changes still require review, tests, license scrutiny, and authorship transparency. Automated tools should not be granted unrestricted permission to rewrite history or execute repository hooks.

Chapter Summary

Advanced Git practice depends on distinguishing working-tree, index, reference, and object-database states. Detached HEAD commits should be named before they are abandoned. Merge and rebase operations require deliberate conflict resolution and clear policies about public history. Reflogs and fsck can recover unreachable work, but recovery is time-sensitive.

Stash manages temporary state; bisect locates regressions under a strict exit-status contract; blame and search commands support historical analysis. Hooks provide automation but require security governance. Worktrees support parallel branch contexts, while sparse checkout reduces populated files rather than deleting history. Large historical blobs must be identified with object-size inspection, not pathname sorting. Garbage collection, partial or shallow cloning, maintenance, and binary-storage strategies improve performance, but each introduces trade-offs. Safe synchronization combines fetch, graph inspection, tested integration, and coordinated force updates when required.

Key Terms

Detached HEAD: A state in which HEAD points directly to a commit rather than a local branch.

Reflog: A local record of updates to references such as HEAD and branches.

Dangling object: An object not currently reachable from a reference but potentially recoverable.

Merge conflict: A condition in which Git cannot combine competing changes automatically.

Rebase: Reapplication of commits onto a new base, creating rewritten commit identities.

Stash: A Git-managed set of temporarily saved working-tree and index changes.

Bisect: A binary-search procedure for locating a regression commit.

Skip status: Exit status 125 in git bisect run, meaning the revision cannot be classified.

Hook: A program triggered by a Git lifecycle event.

Worktree: An additional working directory connected to one repository.

Sparse checkout: A configuration that populates only selected paths in a working tree.

Blob: A Git object containing file content.

Packfile: A compressed collection of Git objects.

Garbage collection: Maintenance that repacks objects and eventually prunes eligible unreachable data.

Remote-tracking branch: A local reference recording the last observed state of a remote branch.

Fast-forward: An update in which the new tip descends directly from the old tip.

Partial clone: A clone that defers retrieval of some objects.

History rewrite: An operation that creates replacement commit ancestry.

Git LFS: A system for storing large files outside ordinary Git object history while tracking pointers in Git.

Review Questions

Short Answer Questions

  1. What distinguishes a detached HEAD from an ordinary branch checkout?
  2. Why is git reflog useful after an accidental reset?
  3. What does exit status 125 mean in git bisect run?
  4. Why does pathname sorting not identify the largest Git blobs?
  5. State one important difference between sparse checkout and a partial clone.

Long Answer Questions

  1. Explain a safe procedure for resolving and either completing or abandoning a rebase conflict.
  2. Compare merge and rebase in terms of commit identity, topology, and collaboration risk.
  3. Describe how hooks, protected branches, and CI can work together in repository governance.
  4. Explain why large binary files cause repository growth and evaluate two mitigation strategies.
  5. Discuss the risks and safeguards associated with rewriting published history.

Analytical / Application Questions

  1. A developer discovers three commits after switching away from a detached HEAD. Design commands that preserve the commits and integrate them into main after review.
  2. A repository’s clone time is increasing, but the current working tree contains only source code. Propose a diagnostic and optimization plan that distinguishes current files, historical blobs, generated artifacts, and network causes.
  3. An automated bisect test returns 125 whenever an optional dependency is unavailable. Explain how this affects the search and design a classification policy that avoids falsely labeling untestable revisions as good or bad.

Multiple Choice Questions with Answers

Question 1

Which command creates a named branch at the current detached HEAD commit?

A. git switch -c preserved-work

B. git gc preserved-work

C. git fetch preserved-work

D. git clean -c preserved-work

Correct Answer: A. git switch -c preserved-work

Question 2

In git bisect run, what does exit status 125 mean?

A. The revision is definitely good

B. The revision is definitely bad

C. The revision should be skipped

D. The repository must be garbage-collected

Correct Answer: C. The revision should be skipped

Question 3

Why is git rev-list --objects --all | sort -k2 inadequate for finding the largest historical blobs?

A. It examines only remote branches

B. It sorts by pathname rather than object size

C. It ignores all commit objects

D. It deletes objects before sorting

Correct Answer: B. It sorts by pathname rather than object size

Question 4

Which command preserves a stash while applying it?

A. git stash drop

B. git stash clear

C. git stash apply

D. git stash destroy

Correct Answer: C. git stash apply

Question 5

What is the primary purpose of git reflog?

A. To record local reference movements

B. To compress binary files

C. To authenticate remote users

D. To list only staged files

Correct Answer: A. To record local reference movements

Question 6

What does git merge --abort generally do during an unresolved merge?

A. Publishes the conflict to the remote

B. Attempts to restore the pre-merge state

C. Deletes every local branch

D. Converts the merge into a rebase

Correct Answer: B. Attempts to restore the pre-merge state

Question 7

Which feature allows multiple working directories connected to one repository?

A. Git worktree

B. Git blame

C. Git bisect

D. Git archive

Correct Answer: A. Git worktree

Question 8

What is a principal limitation of .gitignore?

A. It cannot affect untracked files

B. It does not remove files already committed to history

C. It disables branch protection

D. It prevents all binary files from existing

Correct Answer: B. It does not remove files already committed to history

Question 9

Which push option is generally safer than --force for a coordinated rewrite?

A. --force-with-lease

B. --ignore-with-lease

C. --overwrite-safe

D. --rebase-only

Correct Answer: A. --force-with-lease

Question 10

What does git fetch normally do?

A. Updates remote-tracking references without integrating into the current branch

B. Deletes all local branches absent from the remote

C. Automatically resolves every merge conflict

D. Rewrites the current branch onto the remote branch

Correct Answer: A. Updates remote-tracking references without integrating into the current branch

Git, GitHub & Version Control
Chapter 13 of 14