The fastest way to rewrite Git history
Ever pushed commits and then realized you messed up?
- Wrong Git user/email.
- Bad commit messages.
- Accidentally committed secrets (API keys, passwords).
- A huge file is bloating the repo.
- ...and more.
Many developers try git rebase -i
, but it’s slow, manual, and limited. A better way?
Use git-filter-repo, it’s faster, more powerful, and works across the entire repo.
Examples of Git problems and fixes
Fix author name/email in all commits
git filter-repo --commit-callback ' commit.author_name = "Correct Name" commit.author_email = "[email protected]" '
Edit commit messages in bulk
git filter-repo --message-callback ' commit.message = commit.message.replace(b"fix typo", b"Fix: corrected typo") '
Remove sensitive files from history
git filter-repo --path secret-file.env --invert-paths
Delete large files from old commits
git filter-repo --strip-blobs-bigger-than 100M
Erase all commits from a specific author
git filter-repo --commit-callback ' if commit.author_email == b"[email protected]": commit.skip() '
For more use cases, check out the full docs.
You don’t need Husky
Do you use husky
to manage Git commit hooks? Husky is a popular tool (50M+ downloads per month!), but did you know that Git already has built-in support for hooks?
With Githooks, you don’t need to install extra dependencies. Git provides 28 different hooks that allow you to automate git hooks tasks.
How to use?
Create a
.git/hooks
or.githooks
directory (just like you’d configure.husky
)Configure Git to use your hooks scripts
Tell Git to use this folder for hooks by adding the following
postinstall
script in yourpackage.json
. This ensures that Git hooks are always active after runningnpm
install (oryarn
,pnpm
,bun
)."scripts": { "postinstall": "git config core.hooksPath ./.githooks || true" }
Hook scripts Inside the
.githooks
folder, create scripts named according to the Git Hooks documentation (e.g.,pre-commit
,prepare-commit-msg
).Example:
# .githooks/pre-commit #!/bin/bash npm run lint
# .githooks/prepare-commit-msg #!/bin/bash npx --no-install commitlint --edit "$1"
# .githooks/post-commit #!/bin/bash npm test
Display colors in Makefile
In a previous note, I shared how to create a help
command in a Makefile.
This time, let’s make it visually appealing by adding colors to the output.
Here’s how to do it:
# COLORS
YELLOW = \033[33m
GREEN = \033[32m
WHITE = \033[37m
RESET = \033[0m
help: ##@helper Display all commands and descriptions
@awk 'BEGIN {FS = ":.*##@"; printf "\n${WHITE}Usage:${RESET}\n make <target>\n"} \
/^[.a-zA-Z_-]+:.*?##@/ { \
split($$2, parts, " "); \
section = parts[1]; \
description = substr($$2, length(section) + 2); \
sections[section] = sections[section] sprintf(" ${YELLOW}%-15s${RESET} ${GREEN}%s${RESET}\n", $$1, description); \
} \
END { \
for (section in sections) { \
printf "\n${WHITE}%s${RESET}\n", section; \
printf "%s", sections[section]; \
} \
}' $(MAKEFILE_LIST)
YELLOW
,GREEN
,WHITE
, andRESET
are ANSI escape codes for terminal colors.\033[33m
sets the color to yellow,\033[32m
to green, and\033[37m
to white.\033[0m
resets the color to the terminal default.
It formats the output with colors:
- Yellow for target names.
- Green for descriptions.
- White for the rest
Display all Makefiles commands
Makefiles can be hard to navigate, especially as they grow. Adding a help command makes it easy to see all available targets and their purposes.
The magic help
target
Add this awk
snippet to your Makefile
:
.DEFAULT_GOAL := help
.PHONY: help
help: ##@helper Display all commands and descriptions
@awk 'BEGIN {FS = ":.*##@"; printf "\nUsage:\n make <target>\n"} \
/^[.a-zA-Z_-]+:.*?##@/ { \
split($$2, parts, " "); \
section = parts[1]; \
description = substr($$2, length(section) + 2); \
sections[section] = sections[section] sprintf(" \033[36m%-15s\033[0m %s\n", $$1, description); \
} \
END { \
for (section in sections) { \
printf "\n\033[1m%s\033[0m\n", section; \
printf "%s", sections[section]; \
} \
}' $(MAKEFILE_LIST)
How it works
- Use
##@
to group related targets - And place the description of each command after that group
Example Makefile
help: ##@helper Display all commands and their descriptions
@awk 'BEGIN {FS = ":.*##@"; printf "\nUsage:\n make <target>\n"} \
/^[.a-zA-Z_-]+:.*?##@/ { \
split($$2, parts, " "); \
section = parts[1]; \
description = substr($$2, length(section) + 2); \
sections[section] = sections[section] sprintf(" \033[36m%-15s\033[0m %s\n", $$1, description); \
} \
END { \
for (section in sections) { \
printf "\n\033[1m%s\033[0m\n", section; \
printf "%s", sections[section]; \
} \
}' $(MAKEFILE_LIST)
# You can split this in a separated file: validate.Makefile
install: ##@validate Install dependencies
@pnpm install
typecheck: ##@validate Check static types
@pnpm tsc --noEmit
lint: ##@validate Lint the codebase
@pnpm run lint
test: ##@validate Run tests
@pnpm run test
# You can split this in a separated file: deploy.Makefile
build: ##@deploy Build for production
@pnpm run build
deploy: ##@deploy Deploy to production
@pnpm run deploy
.PHONY: help install typecheck lint test build deploy
.DEFAULT_GOAL := help
Running make
or make help
shows:
❯ make
Usage:
make <target>
deploy
build Build for production
deploy Deploy to production
helper
help Display all commands and their descriptions
validate
install Install dependencies
typecheck Check static types
lint Lint the codebase
test Run tests
Supercharge Git with fzf
Working with Git branches can be annoying, especially with long names or when cleaning up old ones. Try fzf, a tool that makes managing branches easy.
Switch branches
Use fzf
to select and switch branches interactively:
git branch | fzf --preview 'git log -p main..{-1} --color=always {-1}' | cut -c 3- | xargs git switch
Delete branches
Delete branch interactively with fzf
:
git branch | fzf -m --preview 'git log -p main..{-1} --color=always {-1}' | cut -c 3- | xargs git branch -d
I setup them in my shell config to save time:
alias gs="git switch"
alias gsc="git witch -c"
alias gsi="git branch | fzf --preview 'git log -p main..{-1} --color=always {-1}' | cut -c 3- | xargs git switch"
alias gbd="git branch | fzf -m --preview 'git log -p main..{-1} --color=always {-1}' | cut -c 3- | xargs git branch -d"
Better Git log
The default git log
output can be hard to read. Here’s how I make it more visual and informative using aliases.
- Add these aliases to the global
.gitconfig
:
[alias]
# Tree-like log with relative dates
logs = log --graph --date=relative --pretty=tformat:'%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%an %ad)%Creset'
# Limit to 20 commits
log = logs -n 20
- Alternatively, set up aliases in your shell config:
alias glog="git log --graph --date=relative --pretty=tformat:'%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%an %ad)%Creset' -n 20"
alias glogs="git log --graph --date=relative --pretty=tformat:'%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%an %ad)%Creset'"
- Example output
* c0eb700 - (HEAD -> master, origin/master, origin/HEAD) chore(fish): update alias (woula 26 minutes ago)
* 960e14f - chore(nvim): update plugins, active flash, disable cappuchine (woula 16 hours ago)
* bc1129d - feat(nvim): add disabled plugins list (woula 3 days ago)
* 6e14bad - feat(nix): update config for homebrew (woula 5 days ago)
That is a little bit better than this: git log --graph --oneline --decorate --all
❯ git log --graph --oneline --decorate --all
* c0eb700 (HEAD -> master, origin/master, origin/HEAD) chore(fish): update alias
* 960e14f chore(nvim): update plugins, active flash, disable cappuchine
* bc1129d chore(nvim): recrete list of disabled plugins
* 6e14bad feat(nix): update config for homebrew
And a lot more readable than the default: git log
❯ git log
* c0eb700 (HEAD -> master, origin/master, origin/HEAD) chore(fish): update alias
commit c0eb700 (HEAD -> master, origin/master, origin/HEAD)
Author: woula <[email protected]>
Date: Sun Jan 26 10:02:37 2025 +0100
chore(fish): update alias
commit 960e14f
Author: woula <[email protected]>
Date: Sat Jan 25 18:32:42 2025 +0100
feat(nvim): update plugins, active flash, disable cappuchine
commit bc1129d
Author: woula <[email protected]>
Date: Thu Jan 23 07:00:58 2025 +0100
feat(nix): update config for homebrew
Managing multiple Git accounts
Working with multiple Git accounts (e.g., work, personal, open-source) can be tricky. Here’s how I manage them seamlessly using conditional includes in .gitconfig
.
Global .gitconfig
setup
Add conditional includes to the global .gitconfig
:
[includeIf "gitdir:~/projects/company1/"]
path = ~/projects/company1/.gitconfig
[includeIf "gitdir:~/projects/company2/"]
path = ~/projects/company2/.gitconfig
[includeIf "gitdir:~/projects/oss/"]
path = ~/projects/oss/.gitconfig
Local .gitconfig
In each project’s .gitconfig
, specify the user and SSH key.
Here is example for ~/projects/company1/.gitconfig
[user]
name = username1
email = [email protected]
[core]
sshCommand = ssh -i ~/.ssh/company1_rsa
Git will help us to automatically switch configurations based on the project directory.
Git aliases
Git aliases are a must have for every developer. They save time, reduce typing, and make your workflow more efficient.
You can set them up in two ways: terminal shell aliases or .gitconfig
.
Terminal shell aliases
Here’s a part how I set up my Git aliases in my shell config (e.g. .zshrc
or fish.config
...etc.):
# Git
alias g="git"
alias gc="git commit -m"
alias gca="git commit -a -m"
alias gp="git push origin HEAD"
alias gpu="git pull origin"
alias gpf="git push --force-with-lease"
alias gst="git status"
alias gs="git switch"
alias gsc="git switch -c"
alias gdiff="git diff"
alias gco="git checkout"
alias gcob="git checkout -b"
alias gb="git branch"
alias gba="git branch -a"
alias gadd="git add"
alias ga="git add -p"
alias gre="git reset"
.gitconfig
alias
Other way to manage git alias with global .gitconfig
[alias]
# List all aliases
aliases = !git config --get-regexp alias | sed -re 's/alias\\.(\\S*)\\s(.*)$/\\1 = \\2/g'
# Command shortcuts
st = status
cm = commit -m
co = checkout
stl = stash list
stp = stash pop stash@{0}
sts = stash save --include-untracked
sw = switch
a = !git add .
ca = !git commit --amend -C HEAD
fetch = !git fetch --all --prune
# Force-push safely (won’t overwrite others’ work)
pf = push --force-with-lease
# Update last commit with staged changes
oups = !(git add . && git commit --amend -C HEAD)
# Edit last commit message
reword = commit --amend
# Undo last commit but keep changes staged
uncommit = reset --soft HEAD~1
# Remove file(s) from Git but keep them on disk
untrack = rm --cached --
# Delete merged local branches
delete-local-merged = "!git fetch && git branch --merged | egrep -v 'master' | xargs git branch -d"
# Create an empty commit (useful for CI triggers)
empty = commit --allow-empty
Run a command if there are unstaged changes
A quick one-liner to run a command only if there are unstaged changes: the --quiet
flag of git diff
The flag does two things:
- Disables all output of the command
- Exits with
1
if there are differences, and0
if there are no differences.
That means you can combine it with boolean operators to only run another command if files have (or have not) changed:
# Run `command` if there are unstaged changes
git diff --quiet || command
# Run `command` if there are NO unstaged changes
git diff --quiet && command
Other tips
Check for untracked files
git ls-files --others --exclude-standard | grep -q . && command
Include staged changes
git diff --cached --quiet || command
Combine with
entr
for file watchinggit diff --quiet || entr -r command
Use in CI pipelines
git diff --quiet || echo "Changes detected, running tests..." && npm test
List all files tracked by Git
Sometimes you need a list of all files tracked by Git—for example, when using tools like entr
to watch files for changes. Instead of fiddling with find
, Git provides a clean and concise command git-ls-files:
git ls-files
This lists all files in the repository that are tracked by Git.
Other tips
Include ignored files:
git ls-files --others --ignored --exclude-standard
Filter by file type:
git ls-files '*.js' # List only JavaScript files
Use with
entr
to watch files:git ls-files | entr -r your-command
Git Checkout vs. Git Switch
git checkout
Switch branches:
git checkout <branch> # Switch to an existing branch
Create and switch to a new branch:
git checkout -b <new-branch> # Create and switch to a new branch
Restore files from a specific commit or branch:
git checkout <commit> -- <file> # Restore a file from a specific commit
git switch
(modern alternative)
Switch branches:
git switch <branch> # Switch to an existing branch
Create and switch to a new branch:
git switch -c <new-branch> # Create and switch to a new branch
Key differences
Command | Purpose | Notes |
---|---|---|
git checkout <branch> | Switch branches | Older, more versatile command. |
git checkout -b <branch> | Create and switch to a new branch | Combines branch creation and switch. |
git checkout <commit> -- <file> | Restore a file from a commit | Useful for recovering files. |
git switch <branch> | Switch branches | Modern, focused alternative. |
git switch -c <branch> | Create and switch to a new branch | Simpler and more intuitive. |
When to use?
git checkout
:- Use for restoring files from a specific commit or branch.
- Still works for switching branches, but
git switch
is preferred.
git switch
:- Use for switching branches or creating new branches.
- Cleaner and more focused than
git checkout
.
Pro tips
- Recover deleted branches:
Use git reflog
to find the branch’s last commit, then recreate it:
git switch -c <branch> <hash>
- Use git switch for branch operations. It’s designed specifically for branches, making it more intuitive.
Git Reset vs. Git Restore
git reset
Undo commits (keep changes staged):
git reset --soft HEAD~ # Move HEAD but keep changes staged git reset --soft <commit> # Move to specific commit, keep changes staged
Unstage changes (keep changes in working directory):
git reset HEAD~ # Reset --mixed (default), move HEAD and unstage changes git reset <commit> # Reset --mixed, move to specific commit and unstage changes git reset HEAD <file> # Unstage a specific file
Discard commits and changes (destructive):
git reset --hard HEAD~1 # Discard commits and changes permanently
git restore
Discard working directory changes:
git restore <file> # Revert file to its state in the last commit
Unstage changes:
git restore --staged <file> # Move changes from staging area to working directory
Restore a file from a specific commit:
git restore --source=<commit> <file> # Restore file from a specific commit
Key differences
Command | Branch pointer | Staging area | Working directory |
---|---|---|---|
git reset <commit> | Moves | Resets (unstages) | Unchanged |
git reset --soft <commit> | Moves | Unchanged | Unchanged |
git restore <file> | Unchanged | Unchanged | Resets file |
git restore --staged <file> | Unchanged | Resets (unstages) | Unchanged |
When to use?
git reset
:- Move branch pointers or undo commits.
- Unstage changes (though
git restore --staged
is more intuitive).
git restore
:- Discard changes in the working directory.
- Unstage changes (modern alternative to
git reset HEAD <file>
).
Pro tips
- Recover from a hard reset: Use
git reflog
to find the lost commit and reset back:
git reflog
git reset --hard <hash>
- Combine
git reset
andgit restore
: Reset to a specific commit but keep changes in the working directory:
git reset <commit>
git restore .
- Avoid
--hard
unless necessary: Use--soft
or--mixed
to preserve changes.
Ignore all .DS_Store
files globally
If you use Git on a Mac, you’ve probably accidentally committed a .DS_Store
file to a repo at least once. I used to add .DS_Store
to every .gitignore
file to avoid this, but there’s a better way!
You can create a global .gitignore
file that applies to all your repositories. Just run this command:
git config --global core.excludesFile '~/.gitignore'
Then, add .DS_Store
to your ~/.gitignore
file:
echo ".DS_Store" >> ~/.gitignore
This command adds the following to your ~/.gitconfig
:
[core]
excludesFile = ~/.gitignore
Now, .DS_Store
files will be ignored across all your projects, no more accidental commits!
You can directly edit the ~/.gitignore
file to globally ignore many other files.
Update all Git submodules to the latest commit
If you use Git submodules often, here's the one-liner to update them to the latest commit on origin (since Git 1.8.2):
git submodule update --remote --rebase
Prefer merging? Swap --rebase
for --merge
.
Run Github actions locally
Run GitHub actions locally with act to skip the long feedback loop! It uses Docker to pull images and run workflows right on your machine.
# Run the `push` event
act
# Run a specific event or job
act pull_request
act -j test_unit
Need a GitHub token? Use the -s
flag with the GitHub CLI:
act -s GITHUB_TOKEN="$(gh auth token)"
Quick, easy, and no more waiting for GitHub to run workflows!
First attempt at migrating from Homebrew to Nix with Nix Home Manager
It didn’t go exactly as planned. I dived into Nix scripts, flakes, and started installing packages with nixpkgs while keeping Homebrew on the side. But... nothing seemed to work correctly. 😵
Tools like fish
shell, fzf
, and ghostty
.etc... didn't work. I probably need to configure each program properly, manage environments, and link the ~/.config
files with Nix...
During the migration, I enabled autoCleanUp Homebrew with "zap" without paying close attention. Big mistake! It wiped out everything I’d installed through Homebrew. 😱 Aie aie aie.
Thankfully, I had saved all my tools in a Brewfile in my dotfiles. A quick brew bundle
restored everything (though it took time to install).
Lesson learned: I need to take it step by step with Nix, learning more about proper configurations before jumping in too deep.
For now, I’m sticking with Homebrew but I’ll give Nix another try someday.
Tiling window management on macOS with aerospace
In the past, I was a rectangle
user. I used it to move windows into corners or split the screen, basic functionality. Honestly, it never felt transformative. It was fine, but quite basic. I only used it during screen-sharing sessions on Teams calls. I always wanted something more controllable and better organized, with groupings of applications.
Recently, I explored raycast
, and at first, it seemed like the solution I had been looking for. It offered intuitive window organization, powerful workspace controls, and a lot of cool features.
But then I discovered that many of the features I wanted required the Pro version. So I knew I had to look elsewhere.
That’s when I came across aerospace, thanks to all the people sharing it on YouTube. And wow! This is the tool I was looking for! (powerful, free, open-source, workspace management simple, entirely keyboard shortcuts)
Using aerospace has completely transformed my workflow. I now have workspaces neatly organized by task coding (C), terminal (T), browser (B), music (M)... and switching between them is really simple. I can even set apps to open in specific workspaces by default. So cool! Everything feels smoother, faster, and more focused.
For me, there’s only one drawback: aerospace doesn’t natively support the AZERTY keyboard layout, it might a bit inconvenient. However, I can map the QWERTY shortcuts on same location with AZERTY layout and that solution is ok for me.
Manage better for my dotfiles
Up until now, I’ve been handling my dotfiles manually with a Makefile and shell scripts. I used commands like rsync
to mirror files from my ~/.config
directory to my ~/dotfiles
folder, and then saved everything on GitHub. While this approach worked, it was tedious and had a lot of limitations:
rsync
wouldn’t handle deletions properly, so if I deleted a file in~/.config
, it wouldn’t be removed from the dotfiles folder.- I had to run
make sync
every time I wanted to update my dotfiles - Restoring configurations wasn’t properly handled
- I also wanted to directly manage the
~/.config
folder with Git, but my current setup didn’t make that easy.
Then, I stumbled across a YouTube video about using GNU Stow to manage dotfiles. It’s such a simple yet powerful tool! Stow creates symlinks from your dotfiles folder to your ~/.config
directory, so there’s no need for manual mirroring. Everything stays organized and up-to-date automatically.
Now, I manage my ~/dotfiles
easily with Stow, and my ~/.config
folder is directly synced via symlinks.
I love how clean and efficient this setup feels. Highly recommend giving Stow a try if you’re looking for a better way to manage dotfiles!
CLI tools I love using
I’m a huge fan of Rust, and it’s no surprise that many of the tools I rely on in the terminal are written in rust.
Here’s a quick rundown of my favorites:
Fzf
: Fuzzy finder that makes searching files or commands a breeze.Eza
: A modern, colorful alternative tols
that adds more functionality.Zoxide
: A smartercd
command that remembers your most-used directories.Fish shell
: A user-friendly shell with auto-suggestions and syntax highlighting.Starship prompt
: Fast, customizable prompt with support for all major shells.Ripgrep
: Lightning-fast search tool, a must-have for large codebases.Sd
: Simpler, more intuitive replacement forsed
.Fd
: Faster, friendlier alternative tofind
with colorful output.Jq
: Power tool for processing JSON data in the terminal.Lazygit
: Terminal-based Git interface, perfect for lazy devs.Lazydocker
: Easy-to-use terminal tool for managing Docker containers.Bat
: Bettercat
with syntax highlighting and line numbers.Git-delta
: Enhanced git diff tool with better formatting.Fnm
: Fast, Rust-based Node.js version manager.
UPDATED LIST:
Neovim
: + LazyvimStow
: Manages dotfiles.Ghostty
: Terminal emulator.Aerospace
: Window manager.Hyperfine
: Benchmarking tool.
Returning to Neovim for coding
I’ve started using Neovim for coding again.
Early in my career, like many others, I tried to learn Vim. At first, it was difficult to get used to, but I found it fun and rewarding. For some, Vim can feel more "professional," and there's something satisfying about the cool things you can do with it.
I really liked Vim, but when it came to coding, I ran into many challenges. Configuring Vim with the right plugins, settings, and workflows took a lot of time. Back then, I was working with many different languages—Python, Ruby, JavaScript, TypeScript, HTML, CSS—and I could never quite get Vim to work smoothly across all of them. I faced too many issues, so eventually, I switched to VSCode and IntelliJ for most of my coding, using Vim only for occasional file edits.
Now, with modern terminals like Wezterm and Ghostty, I find myself enjoying the terminal environment more. I want to keep my hands on the keyboard as much as possible, so I decided to give Vim another try, but this time with Neovim. Neovim is a more modern and flexible alternative to Vim, with support for Lua-based scripting and plugins.
It’s taking some time to get fully comfortable with Neovim again, but I’m using LazyVim to manage my plugins, and it’s been fantastic. It supports many of the plugins I need for coding, especially the which-key
plugin, which shows all available keybindings in Neovim—something I missed when using Vim before.
LazyVim makes it easy to add, remove, or configure plugins, and the documentation is top-notch, so I can quickly reference the keymaps I need.
I also tried Helix, a text editor built from scratch with Rust. It’s designed to provide a similar experience to Vim but with better performance. It’s still young and doesn’t support plugins yet, and it lacks features like a file explorer, which I rely on. However, I think it has potential, and in the future, it could be a solid alternative to Vim.
For now, I feel more comfortable with Neovim, but it's still not perfect. I continue using VSCode for larger projects, but I’ve enabled the Neovim extension in VSCode, which lets me use Vim keybindings and workflows within VSCode. It’s not the full Vim experience, but it’s been a great way to continue learning Vim while working in a modern IDE. At least now, I don’t need to use the mouse when coding in VSCode.
For smaller projects, I stick to using Neovim in the terminal.
For browsing, I now use Vim as well. With the Vimium extension, I can use Vim-like keybindings and perform almost everything without touching the mouse. It’s really fun and efficient to use.
Ghostty?
Ghostty has just released its official production-ready version: 1.0, and it’s causing quite a hype among developers across all platforms. I’m really curious to try it out!
Ghostty is a terminal emulator built with Zig. Most terminal emulators I’ve used, like WezTerm, Warp, Alacritty, and Zellij, are written in Rust. So it’s exciting to see a terminal tool developed in Zig instead. (I really like both of 2 this languages: rust and zig)
I decided to give it a shot, even though I’ve been happily using WezTerm. And I have to say, I’m impressed.
It’s fast—faster than WezTerm in terms of startup time and when opening new tabs. It integrates smoothly with Fish shell, which I love.
The keybindings are easy to access, and the setup process is a really simple. Plus, it supports a variety of themes.
Some standout features include Quick Terminal and the ability to use Cmd + Triple-click for selection—simple yet powerful.
So, I’ve made the switch. My terminal is now Ghostty.
I’ve started implementing notes on my website
The goal is to create a space for sharing shorter updates, thoughts, and progress on ongoing projects. I plan to update this space regularly with useful insights and updates.
Stay tuned for more posts soon.