News & Views

Our Favourite Git Tips and Tricks (Beyond the Basics)

Engineering

A running list of git tips beyond the basics — precise staging with add -p, safer force-pushes, useful aliases, when to rebase, and a real git bisect example from one of our open-source dbt packages. For engineers who already know git.

Table of content

The commands you already run

If you work in a project that uses git, you know the basics:

  • git clone to “download” a repository
  • git fetch and git pull to update the repository
  • git checkout and git branch to manage commits and branches
  • git commit and git push to save and publish changes

Using these in their basic ways is enough to get by, but they really only scratch the surface.

Not only can we flex these commands more, there are loads of other helpful commands and third-party tools/frameworks to really get the most out of git — we’ll take you through a few of the Tasman favourites!

Flexing the basic commands further

Things you can use right now.

The “basic” git commands above have a whole bunch of options to customise them. Some of our quick-fire favourites:

  • git add --update (or -u) to add changes in files already tracked by git
  • git add --patch (or -p) to choose precisely which changes to stage
  • git commit --amend --no-edit to combine the latest changes with the most recent commit
  • git commit --allow-empty for making empty commits: good for root commits, and for re-triggering CI jobs without manual triggers
  • git push --force-with-lease for “safer” force pushes which rejects pushes that will overwrite other people’s commits
  • git log --graph --oneline to show a compact visual history of commits (although, you’ll probably use your IDE’s git graph instead)
  • git log --since='2 weeks ago' to filter the log by the specified interval (2 weeks ago can be replaced by other expressions)
  • git log --follow --patch -- <path> to see the history of changes for the file at <path>

…and some other simple commands that you might not have used:

  • git reset --hard HEAD to discard all changes (including non-staged ones!) in tracked files
    • ⚠️ This is a destructive command, so run with caution!
  • git reset --soft HEAD~1 to “undo” the previous commit, re-staging the commit’s changes and keeping any other staged and non-staged changes you may have made
  • git cherry-pick <sha> to apply the specified commit to the current branch, particularly useful when breaking a large feature branch into smaller ones
  • git switch [options] to manage branches the modern way
  • git switch - to switch to the previously checked-out branch: good for scripting!
  • git reflog to view your history of git operations (rather than commits): useful for undoing destructive changes like git reset!

Of course, there are so many more, these are just some of our favourites!

If you haven’t come across HEAD before, this just refers to the latest commit on your current branch. Check out the docs for the rev-parse command for additional names and ways you can adjust them:

Get your global config right

Set yourself up for success.

Chances are you ran the git config --global user.name and git config --global user.email commands the first time you set up git, and then forgot about git config — we all started that way 😄

In general, there are two levels of configuration to think about:

  • Project config, stored in your projects’ .git/config files
  • Global config, stored in your ~/.gitconfig (or ~/.config/git/config) file

Many git configurations are only appropriate at the project level — such as the corresponding remote(s) for the git repository. However, some options are best configured globally to improve your experience across all repos.

The core git developers themselves have a common set of global configuration which they (unofficially) agree on; the GitHub founders document them in the a blog.

We’d recommend copying this config as a starting point 😄 Their blog post explains each of the settings, too, which we won’t rehash here.

One config we will extend details for is the core.excludesFile setting: this makes it clear where git should look for a global ignore file. You should also add some paths to the specified global ignore file! A good starting point for macOS users is:

.idea/     # JetBrains configuration directory
.vscode/   # VS Code configuration directory
.DS_Store  # macOS system file

Just make sure not to be too liberal with your global ignores: keep this for paths more unique to you and your machine. Patterns that should be ignored for anyone working in a project are better placed in that project’s ignore file.

Save your fingers: add some aliases!

Aliases are simply names you give to git commands (or external commands, if you want!) so that you don’t need to write the entire command each time.

For example, writing git commit --amend --no-edit each time we want to amend a commit is a lot to write. We can create a global alias for it with:

  • git config --global alias.amend 'commit --amend --no-edit’

…and then we can run git amend for this instead!

You can alias pretty much whatever you want — check out the following article for more examples.

Master the rebase

A technique every git pro should know.

Rebasing can be a daunting concept when you first learn git, but it’s definitely worth getting the hang of it — it’s something that we use a lot to curate our git history (more on that below!).

What’s a rebase?

Simply put, rebasing a feature branch changes the starting point of the branch.

To illustrate this, we’ll borrow the example from git’s own rebase docs: imagine that you have been working on the feature branch in this history, and you want to “catch up” to the work done on the main branch:

      A---B---C  feature
     /
D---E---F---G  main

The feature branch’s starting point is commit E, so it doesn’t include F or G. While on the feature branch, we can rebase it onto the main branch with the git rebase main command to change the starting point to commit G:

              A'--B'--C'  feature
             /
D---E---F---G  main

This will pretend that we had started by branching off commit G all along!

Note that the feature branch commits have a mark to indicate that their content is the same, but that their actual git SHAs will be different: they are different commits.

Rebase interactively for more options

In addition to changing the starting point of your feature branch, you can also reorder commits, squash commits, split commits, discard commits, and many other operations.

These options become available when you initiate an interactive rebase with the --interactive (or -i) flag, e.g. git rebase --interactive main. This opens an editable text file with the list of commits it will keep (see below): this is where commits can be reordered by changing their order in this file, or modified in other ways using the commands listed at the bottom of the text file.

This is a very handy option for curating many aspects of the history in one go!

$ git rebase --interactive main

pick 3f49fec  # A
pick db3e166  # B
pick 3d9bd63  # C

# Rebase 0377411..3d9bd63 onto 0377411 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label

Where does a rebase go wrong?

Rebases are simple in theory, but they can easily go wrong when there’s a merge conflict. Using the example above, this would be where a change in commits F or G conflicted with a change in commits A, B, or C.

When you hit a merge conflict, git will be in a rebase mode: it’s important to handle this carefully. You can cancel the rebase with git rebase --abort, skip the commit with git rebase --skip, or you can handle the conflict, commit the changes, and continue the rebase with git rebase --continue until the rebase fully completes.

The rerere git config options that you’ll see in the the GitHub founders blog above makes this considerably less painful, so definitely check it out! The following blog post also includes some helpful recommendations when rebasing. Check it our here.

You might also find that rebasing (and bisecting, coming up next) doesn’t work as well if you don’t have atomic commits: a commit which makes a small, complete change. (A complete change means that the code remains in a working state, e.g. tests still pass.)

This highlights one of many reasons why curating your history is important — more on this below!

Keep bisection in your back pocket

…for when you need to identify when a behaviour changed.

Git’s bisect command is not that well-known, but it can be super helpful.

As the name implies, it executes a binary search on the git history to find a commit where something changed — it’s most commonly used to identify the commit in which a bug was introduced, and we’ll see that it’s actually designed to expect it to be used in this case.

Show me an example!

We’ll illustrate this in one of our open-source dbt packages, tasman-dbt-mta. We switched it to use uv a while ago, so for this example we’ll find the commit which introduced it.

To identify whether uv is being used or not, we’ll just check for the presence of the uv.lock file. The bisection needs two commits to start with: one which has the uv.lock file, and one which doesn’t. We’ll just use the initial commit as the one without the uv.lock file, and the latest commit as the one with the uv.lock file.

Bisection always starts with the same three commands:

  • git bisect start to move into “bisection mode”
  • git bisect new to set the head commit as a commit which does have uv.lock
  • git bisect old ff908ad to set the initial commit as a commit which does not have uv.lock

After running these, the repo will be immediately checked out to a commit between the two specified above. We can verify whether the uv.lock file is present any way we want, then we mark the current commit as new or old:

  • If uv.lock exists: git bisect new
  • If uv.lock does not exist: git bisect old

After running one of the git bisect commands, the next midpoint commit will be checked out — and we repeat until we find the commit. You’ll know when the bisection is finished because the Bisecting: X revisions left to test after this line will be gone, and the commit will be printed instead.

Finally, run git bisect reset to get out of bisection mode. In just a few steps, we found the commit!

Can we automate this?

Manually stepping through each commit and tagging them is fine for small, simple cases like this, but many real bisection use-cases will involve a lot more steps. We can automate this by using git bisect run and specifying a command to run (which may execute a script) which tags the commit depending on the command’s exit code:

  • If the return code is 0, tag the commit as old (AKA good)
  • If the return code is not 0, tag the commit as new (AKA bad)

Instead of old and new, we can alternatively use the terms good and bad — this is where the regression use case is made clear: old commit are “good” and new commits are “bad”.

One command we could run to check for the presence of the uv.lock file is simply cat uv.lock which returns with an exit code of 0 when the file exists, and an exit code of 1 when it doesn’t.

To make this example a little confusing, we want to flip the exit code: if the uv.lock file is missing (so cat has an exit code of 1), we want the commit to be tagged as old/good. So, a command we might actually want to use is cat uv.lock && exit 1 || exit 0.

To make the final commit clearer in the output, we can also run git bisect visualize after the run has completed to clearly print the found commit SHA in yellow.

Putting this together, the commands we could run to “automate” this example would be:

git bisect start
git bisect bad
git bisect good ff908ad
git bisect run zsh -c 'cat uv.lock && exit 1 || exit 0'
git bisect visualize
git bisect reset

The output of git bisect visualize is the important part and is:

commit 3e655f3222e239b75cc2edd97262c790f232aaed
Author: Bill <bill@tasman.ai>
Date:   Thu Nov 6 11:07:40 2025 +0000

    refactor: switch to uv (plus some minor housekeeping) (#45)

…which is precisely the commit we wanted to see!

Curate your history

Clean history is friendly and auditable.

The way you use git during development is totally up to you: we’re not going to tell you that there’s “one best approach”. However, there are some best practices to follow when it comes to the final commits you share with the world.

It’s well documented that you should keep your pull requests small (refs: Microsoft, GitHub, Atlassian), but the same applies to commits. Once you’ve finished implementing your feature, it’s a good idea to arrange your changes so that each commit is atomic (which we defined above).

What’s the point?

Your git history isn’t just a log of changes: it’s a ledger over your software.

Smaller, curated commits have several benefits:

  • Easier to understand the content of the commit and the scope of change
  • Easier to review PRs by reviewing commits individually, especially since most git platforms let you filter the PR by commit!
  • Individual changes are not as “dangerous”, as they have a smaller impact
  • Easier to revert only a bad change (rather than reverting all bundled changes)
  • Helps identify and mitigate scope creep
  • Easier to split a large feature branch into smaller, more manageable PRs
  • Bisection is more effective since the found commit will be smaller/more precise!

Show me an example!

To illustrate an example, suppose you’re adding a couple of small features. If you’re like me and commit little and often, your working commits might look something like:

  • 2e54d803 feat: align module to project style guide
  • a04e2220 feat: add feature 1
  • f14906da feat: add feature 2
  • c3afb87a test: add tests for feature 1
  • abf5408f fix: correct edge case for feature 1
  • 90df73b2 test: add tests for feature 2
  • 32a33b71 refactor: simplify implementation for feature 2
  • 2161db46 chore: fix an invalid configuration in CI

The first and last commits are related changes, but they’re not solely on implementing the feature — the middle commits, however, can be grouped into two changes: one for each feature. Before pushing this to the remote, I’d squash the middle commits (with git rebase -i main described earlier) so that my feature branch looks like:

  • 2e54d803 refactor: align module to project style guide
  • 0ac0ff6e feat: add feature 1
  • 5f7a20d0 feat: add feature 2
  • 24d50366 chore: fix an invalid configuration in CI

Now each commit is a small, complete change with clear separation of concerns. This keeps the git history clean, and makes it easier for other folks to review the changes commit-by-commit!

Since the two features can be implemented independently, this curated branch could also be split into separate PRs for each feature — that’s a judgement call you’d make depending on the size and scope of the changes.

For a more thorough explanation/example, check out the following YouTube video: stop making giant changesets! | YouTube.

Check out third-party tools and frameworks

Ooo a shiny new thing!

Git has been around for a long time, so there is a huge amount of third-party tools and frameworks to extend it and shape how we use it.

Some of our favourites:

  • The GitLens VS Code extension — a must-have for VS Code users!
  • Similarly, the GitToolBox plugin for JetBrains tools
  • For those that prefer TUIs, gitui or lazygit (whatever your preference is)
  • The git plugin for Oh My Zsh, which provides loads of aliases and functions, such as:
    • gcbgit checkout -b
    • gpsupgit push --set-upstream origin $(git_current_branch)
    • gcan!git commit --verbose --all --no-edit --amend
    • ggfgit push --force origin $(current_branch)
  • The git-filter-repo tool for rewriting history (better alternative to the built-in git filter-branch)
  • The pre-commit tool for managing and maintaining multi-language pre-commit hooks
  • The Conventional Commits framework for writing better commit messages
  • The Semantic Versioning framework for specifying better version tags

Choose Tasman for your data software needs

We’re not just good with data: we’re great at building software which works with data.

Written by Bill WallisPublish date: 12.08.2026Updated on: 12.08.2026

More news & views

Keep reading about the latest from us

8 ways to flex DuckDB
Engineering
8 MIN READ
Modern SQL: The Latest and Greatest SQL Features (part 1)
Engineering
10 MIN READ
Modern Python Development with uv
Engineering
5 MIN READ