News & Views
Our Favourite Git Tips and Tricks (Beyond the Basics)
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 cloneto “download” a repositorygit fetchandgit pullto update the repositorygit checkoutandgit branchto manage commits and branchesgit commitandgit pushto 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 gitgit add --patch(or-p) to choose precisely which changes to stagegit commit --amend --no-editto combine the latest changes with the most recent commitgit commit --allow-emptyfor making empty commits: good for root commits, and for re-triggering CI jobs without manual triggersgit push --force-with-leasefor “safer” force pushes which rejects pushes that will overwrite other people’s commitsgit log --graph --onelineto 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 agocan 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 HEADto discard all changes (including non-staged ones!) in tracked files- ⚠️ This is a destructive command, so run with caution!
git reset --soft HEAD~1to “undo” the previous commit, re-staging the commit’s changes and keeping any other staged and non-staged changes you may have madegit cherry-pick <sha>to apply the specified commit to the current branch, particularly useful when breaking a large feature branch into smaller onesgit switch [options]to manage branches the modern waygit switch -to switch to the previously checked-out branch: good for scripting!git reflogto view your history of git operations (rather than commits): useful for undoing destructive changes likegit reset!- Check out the following YouTube video for a great overview: fixing a git mistake with reflog
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/configfiles - 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 startto move into “bisection mode”git bisect newto set the head commit as a commit which does haveuv.lockgit bisect old ff908adto set the initial commit as a commit which does not haveuv.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.lockexists:git bisect new - If
uv.lockdoes 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(AKAgood) - If the return code is not 0, tag the commit as
new(AKAbad)- …unless the exit code is 125, in which case the commit is skipped
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:
2e54d803feat: align module to project style guidea04e2220feat: add feature 1f14906dafeat: add feature 2c3afb87atest: add tests for feature 1abf5408ffix: correct edge case for feature 190df73b2test: add tests for feature 232a33b71refactor: simplify implementation for feature 22161db46chore: 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:
2e54d803refactor: align module to project style guide0ac0ff6efeat: add feature 15f7a20d0feat: add feature 224d50366chore: 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:
gcb→git checkout -bgpsup→git push --set-upstream origin $(git_current_branch)gcan!→git commit --verbose --all --no-edit --amendggf→git 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.