Git Tricks for When You Lose Track of Work

Even if you use Git every day, it’s easy to feel like you’re navigating a labyrinth. You don’t need a deep understanding of trees, blobs, or the internal object model to be productive. What you do need are practical, targeted commands that solve the common problems of daily work: losing a branch, forgetting where you were, or accidentally leaving work behind when you switch context.

Git has become the unchallenged standard for version control, and most of us run git init when starting any new project. Yet it often still feels like something magical — powerful but daunting. The reality is that a handful of lesser-known flags and commands can take most of the friction out of everyday use.

Finding the Branch You Meant to Commit To

The core value of Git is the ability to save work, switch context, and return to a different task later. The problem arises when you switch back and can’t find where you left off. It may have been committed to a branch, or perhaps you never committed at all. When that happens, a few commands narrow the search significantly.

If you need a quick overview, you can sort local branches by the date of their latest commit to see what you most recently touched.

# To sort branches by commit date
git branch --sort=-committerdate

If you didn’t commit before switching — or you ended up on a detached HEAD at a specific commit — you can simply move back to the place you were before.

# Checkout previous branch
git checkout -

The - character is shorthand for @{-1}, which refers to the previous checkout. This syntax extends further: after checking out feature/thing-a, then feature/thing-b, then bugfix/thing-c, you can use @{-2} to return directly to feature/thing-a.

# Checkout branch N number of checkouts ago
git checkout @{-N}

For a fuller picture of all branches, use git branch -v to see each branch with its last commit ID and message. Adding a second v (git branch -vv) also displays the upstream remote branch associated with each local one.

# List branches along with commit ID, commit message and remote
git branch -vv

Retrieving a Single File

It’s a familiar scenario: you realise a single file ended up in the wrong branch, and you don’t want to redo the work or manually copy and paste code between branches. Git has a straightforward answer, though the syntax is not immediately obvious. The -- separator, placed after a branch name on checkout, lets you target a specific file from that branch.

git checkout feature/my-other-branch -- thefile.txt

Simplifying Status and Viewing Full History

Git output is often verbose. When you only need a concise view of what’s changed, using git status -sb condenses the output to a single line per item, making it easier to digest at a glance. Most Git commands have similar flags worth investigating to streamline your workflow.

# Usually we would use git status to check what files have changed
git status

# Outputs:
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   README.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    another-file
    my-new-file

# Using the flags -sb we can shorten the output
git status -sb

# Outputs:
## master
 M README.md
?? another-file
?? my-new-file

For more serious mishaps — like discarding staged changes before committing — standard git log may not be enough. In those situations, git reflog is a lifesaver. It records every action that changes where HEAD@{} points, including push, pull, branch, checkout, and commit. Unlike git log, which shows the history of a specific branch, git reflog is a history of everything you have done across all branches.

Overview all in one place

With the commit ID from reflog, you can use git show to inspect the change. If it is the one you want, use git checkout to recover it, or select a specific file as shown earlier.

In the worst-case scenario, where reflog cannot help (for instance, after a hard reset that discarded staged files), there is one last option. Git stores every change in .git/objects, but deciphering those files manually is impractical. The git fsck command verifies repository integrity, and the --lost-found flag finds all objects not linked to any commit — these are known as “dangling blobs”. The command also identifies “dangling trees” and “dangling commits”.

# See the reference log of your activity
git reflog --all

# Look at the HEAD at given point from reflog
git show HEAD@{2}

# Checkout the HEAD, to get back to that point
git checkout HEAD@{2}

The --lost-found flag has the advantage of extracting all appropriate files into the .git/lost-found folder, far easier to inspect. Even on an active project, you may have many dangling objects; Git’s garbage-collection process cleans them up regularly. Once you find the files you need, note that each object is stored individually with an unrecognisable hash-based filename, so you will need to copy the files you want rather than check them out.

# This will find any change that was staged but is not attached to the git tree
git fsck --lost-found

# See the dates of the files
ls -lah .git/lost-found/other/

# Copy the relevant files to where you want them, for example:
cp .git/lost-found/other/73f60804ac20d5e417783a324517eba600976d30 index.html

Working With Git as a Team

Using Git solo is one experience; working on a team with mixed backgrounds presents additional challenges. It can be powerful for sharing a codebase and enabling reviews, but it requires a shared understanding of how the team intends to use it. Good communication about naming conventions, commit message structure, and what gets committed is essential. The smoother the onboarding process, the quicker new developers can contribute without accidentally violating agreed principles.

Beyond social convention, several technical setups help the repository itself reinforce team-wide standards.

Handling Line Endings Consistently

Windows defaults to CRLF line endings (\r\n), while Mac and Linux both use LF (\n). Older Mac systems used CR (\r). As teams grow, these mismatches create noisy commits and pull requests full of irrelevant changes. They likely won’t break code, but they muddy the history.

Individual developers can set their config to handle this automatically:

# This will let you configure line-endings on an individual basis
git config core.eol lf
git config core.autocrlf input

That requires each individual, including new starters, to remember the setting. Broadly, Git checks configuration files in order: first the repository’s .git/config, then the user’s system-wide file at ~/.git/config, then the global config at /etc/gitconfig. However, repository-specific configs cannot be committed to the shared repository and carry over to other team members.

The solution is the .gitattributes file, which is committed to the repository. You won’t have one by default; create a file named .gitattributes. This file sets attributes per file path — for example, telling Git to avoid trying to diff binary image files. Using a wildcard, you can apply the line-ending rule to all files, creating a team-wide config that travels with the repo.

# Adding this to your .gitattributes file will make it so all files
# are checked in using UNIX line endings while letting anyone on the team
# edit files using their local operating system’s default line endings. 
* text=auto

Hiding Generated Files in Pull Requests

Adding dependency directories like node_modules/ to .gitignore is a well-known practice for keeping compiled files local. But what about files you do want to version, yet don’t need to inspect in detail on every pull request? You want them tracked without the visual noise.

On GitHub, paths annotated with linguist-generated in the .gitattributes file (checked in at the repository root) are collapsed by default in pull requests. The files still register as changed, but the full content isn’t displayed, reducing the cognitive load of review.

Anything to reduce stress and cognitive load of code reviewing is going to help improve the quality of the code reviews and reduce the time it takes.

For instance, with a Unity project where asset files are checked in but rarely need deep inspection at review time, the attributes file handles that elegantly:

*.asset linguist-generated

Using Blame as a Communication Tool

A simple but effective tip from Harry Roberts is to alias git blame to git praise. While purely semantics, the renamed command changes the team’s emotional response. The natural reaction to “blame” is defensive, but knowing who last touched the code is powerful for asking the right questions and avoiding wasted time hunting for the relevant person.

This is less about finding fault and more about where to direct questions. Some IDEs already frame this as neutral/positive — Visual Studio, for instance, shows the last modifier for each function as a helpful annotation. Approach blame as a communication tool that helps the whole team reduce confusion.

For a different problem — discovering who removed a file, and why — blame is useless because it works on lines currently present in a file. The answer lies in git log. While the default log examines the current branch, you can also filter it to show history for a specific file path.

# By using -- for a specific file,
# git log can find logs for files that were deleted in past commits
git log -- missing_file.txt

Within a team, commit message quality eventually becomes a topic of discussion. Perhaps you want each message to reference an issue ID in a project management tool, or you want to encourage more descriptive text than a single blank line. You can support this by committing a shared template file into the repository. Each developer must run one command to point their local Git at that file, since config files themselves are not committed:

# This sets the commit template to the file given,
# this needs to be run for each contributor to the repository.
git config commit.template ./template-file

Automating Git Workflows

Git's awareness of every past action in a repository makes it a surprisingly strong foundation for automation. Teams often need identical, repeated checks as they work — running linters or tests before a push, for example, can be enforced with a pre-push hook, while a pre-commit hook can enforce branch naming conventions. Smashing Magazine has previously covered team workflows built around Git hooks in depth.

Finding Breaking Changes With git bisect

Beyond project hooks, Git offers one standout automation feature: git bisect. Many developers know it exists but rarely use it. Its job is to walk through commit history to identify exactly where a bug was introduced. The manual flow starts with git bisect start, then you supply a known-good and a known-bad commit ID, marking each subsequent checkout with git bisect good or git bisect bad.

The real power is that git bisect does not crawl linearly through the log. Instead, it applies a binary search, cutting the range in half with each test. That makes it the fastest possible route to the offending commit, even with thousands of revisions.

# Begin the bisect
git bisect start

# Tell git which commit does not have the bug
git bisect good c5ba734

# Tell git which commit does have the bug
git bisect bad 6c093f4

# Here, do your test for the bug.
# This could be running a script, doing a journey on a website, unit test etc.

# If the current commit has bug:
git bisect bad

# If the current commit does not have the bug
git bisect good

# This will repeat until it finds the first commit with the bug
# To exit the bisect, either:

# Go back to original branch:
git bisect reset

# Or stick with current HEAD
git bisect reset HEAD

# Or you can exit the bisect at a specific commit
git bisect reset <commit ID>

Turning Debugging Into a Repeatable Test

In his talk "Debugging With The Scientific Method," Stuart Halloway reframes git bisect as partial automation of the scientific method. Even though he demonstrates in Clojure, the insight is language-agnostic:

"Git bisect is actually partial automation of the scientific method. You write a little program that will test something and git will bounce back and fourth cutting the world in half each time until it finds the boundary at which your test changes."

— Stuart Halloway

Manual debugging usually involves guessing and inspecting code. Halloway argues for relying on empirical evidence instead: write a test that reproduces the problem and let the bisect run it against every commit since the last known good state. This eliminates the guesswork and narrows the investigation to the exact revision where behavior changed.

You can automate this entirely. Instead of marking each step yourself, pass a command to git bisect that it runs automatically at every checkpoint. That command can be a purpose-built script for one issue, or any existing unit, functional, or integration test. You could even write a test that guards against the regression and let the bisect verify it across history.

# Begin the bisect
git bisect start

# Tell git which commit does not have the bug
git bisect good c5ba734

# Tell git which commit does have the bug
git bisect bad 6c093f4

# Tell git to run a specific script on each commit
# For example you could run a specific script:
git bisect run ./test-bug

# Or use a test runner
git bisect run jest

Running Scripts Across Every Commit

Bisect works because it skips around history efficiently. But sometimes you genuinely need to visit every single commit in sequence. Writing a script to loop over git log output and execute commands is possible, though an underused existing command already does this: git rebase.

Developer Kamran Ahmed demonstrated how to run a test suite on each commit and stop at the first failure:

Find the commit that broke the tests

$ git rebase -i --exec "yarn test" d294ae9

This will run "yarn test" on all the commits between d294ae9 and HEAD and stop on the commit where the tests fail

— Kamran Ahmed (@kamranahmedse) February 2, 2020

For pinpointing one regression, git bisect is usually the better choice. Yet the --exec trick shows a broader possibility: running any script across a defined window of commits. This could generate historical reports, chart how a metric changed over time, or inspect past test outcomes. It may be the least immediately practical example here, but it highlights how flexible Git's plumbing can be when you think creatively.

# This will run for every commit between current and the given commit ID
git rebase -i --exec ./my-script 

Digging Deeper

These tricks just begin to scratch the surface of Git. For those wanting to explore further — from foundations to scripting, configuration, and terminal integration — the following resources offer solid starting points:

  • Git Explorer: An interactive site that helps you find the right command for what you're trying to do.
  • Dang it Git!: Practical fixes for common situations where developers get stuck.
  • Pro Git: The full book, freely available online, covers Git thoroughly.
  • Git Docs: Both the website and man git pages (e.g. man git-commit) detail Git's internals.
  • Thoughtbot: Their Git category collects useful tips for everyday work.
  • Git Hooks: Resources and ideas for every available hook.
  • Demystifying Git Internals: An explanation of trees, blobs, and other fundamentals that help you leverage Git fully.
  • Git From Beginner To Advanced: Mike Riethmuller's article is a good entry point.
  • Little Things I Like To Do With Git: Harry Roberts' piece on advanced daily tricks.
  • Atlassian's Advanced Git Tutorials: Detailed guides on many topics referenced above.
  • Github Git Cheatsheet: A handy reference for common commands.
  • Git Shortcuts: A look at flags and recommended aliases.

Further reading at Tech Report includes pieces on recovering deleted files from your working tree, retrieval augmented generation for language models, and overflow issues in CSS.