Table of Contents
- 2.1 Introduction
- 2.2 Git version compatibility
- 2.3 Demo — Updating commit messages
- 2.4 Demo — Reorganization of commits
- 2.5 Demo — Squash of commits
- 2.6 Demo — Commit Splitting
- 2.7 Demo — Deleting commits
1. Course Overview
Welcome to the Rewriting Git History training presented by Michael Van Sickle on Pluralsight.
Git is a powerful tool for managing the source code of an application throughout its development and evolution. This course teaches how to use Git’s history editing tools to ensure that the repository maintains a clean, clear record of a project’s development.
Topics covered in this training
- Explore the history of a Git project
- Edit history by reordering, deleting and combining commits
- Rewrite the history of an entire repository quickly and easily
Prerequisites
Before starting this training, it is recommended to be comfortable with Git in its daily use and to know the basic terminology, including:
- commits (code state snapshots)
- branches (independent development lines)
- Basic Git commands (
git add,git commit,git log,git status)
2. Handling commits
Module duration: 24m 34s
2.1 Introduction
Editing the history of a source code repository may seem counterintuitive. After all, the purpose of a repository is precisely to record the evolution of a codebase over time. However, sometimes manipulating the history of a project is useful to improve the clarity of its development.
This training covers rewriting Git history — something you don’t do every day, but for which Git offers a very powerful set of tools.
Module plan
Module 2 is structured as follows:
- Infrastructure: Git version compatibility
- Reorder commits in the repository
- Squashing commits: merge several commits into one
- Split commits: the opposite of squash
- Delete commits from history
This training assumes good mastery of Git on a daily basis. It therefore does not go over basic theoretical concepts, but focuses on the practical use of Git commands to manipulate commits.
Module 3 will then address changes that affect the entire repository, via a specialized third-party tool — because Git alone does not provide a simple mechanism for these types of operations.
2.2 Git version compatibility
Before starting, it is important to know the versions of Git with which this training was designed.
- The training was created with a specific version of Git. To exactly reproduce the experience shown in the training, it is advisable to use this same version.
- The training is 100% compatible with all versions of Git that meet the compatibility criteria indicated.
- Versions outside of these criteria may have incompatibilities due to changes to the Git API over time.
Recommendation: Use a recent and stable version of Git (2.x and higher) to benefit from all the features discussed.
2.3 Demo — Updating commit messages
The demos in this course use Visual Studio Code as the working environment — not for its Git-specific features, but because it’s convenient to have the file tree on the left and the terminal integrated into the main body of the interface. It is entirely possible to follow the demonstrations using only the command line.
Setting up the demo repository
To focus on the manipulation of history and not on the construction of a particular project, we start with a very simple repository composed of empty files named one.txt, two.txt, three.txt and four.txt.
Creation of files and initialization of the repository:
# Créer des fichiers vides
touch one.txt two.txt three.txt four.txt
# Initialiser le dépôt Git
git init
# Basculer sur la branche principale
git checkout -b main
History construction (one file per commit):
Normally, if you have several changes at once, you group them into a single commit. Here, we intentionally add one file at a time to build a history of several commits — so that we can then manipulate it.
# Premier commit
git add one.txt
git status # Vérifie que one.txt est staged
git commit -m "initial commit"
# Deuxième commit
git add two.txt
git commit -m "second commit"
# Troisième commit
git add three.txt
git commit -m "third commit"
# Quatrième commit
git add four.txt
git commit -m "fourth commit"
# Vérifier l'historique
git log --oneline
Result of git log --oneline:
a1b2c3d (HEAD -> main) fourth commit
e4f5g6h third commit
i7j8k9l second commit
m0n1o2p initial commit
Updated last commit message with --amend
The git commit --amend command allows you to modify the message of the last commit (HEAD) without having to go through the interactive rebase.
# Modifier le message du dernier commit
git commit --amend -m "nouveau message pour le dernier commit"
It is also possible to use git commit --amend without the -m option to open the text editor configured in Git (e.g. Vim, Nano, VS Code) and modify the message there.
git commit --amend # Ouvre l'éditeur configuré
Warning:
--amendcreates a new commit which replaces the old one. The commit hash changes.
Updating an older commit message with git rebase -i
To modify the message of a commit that is not the last one, you must use interactive rebase (git rebase -i).
# Lancer le rebase interactif sur les 3 derniers commits
git rebase -i HEAD~3
Git then opens a file in the text editor. This file lists the affected commits, from oldest to most recent, with an action keyword as a prefix:
pick i7j8k9l second commit
pick e4f5g6h third commit
pick a1b2c3d fourth commit
# Rebase m0n1o2p..a1b2c3d onto m0n1o2p (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 <commit> = like squash, but discard this commit's log message
# d, drop <commit> = remove commit
To modify the message of a commit, simply change pick to reword (or simply r) on the corresponding line, then save and close the file:
pick i7j8k9l second commit
reword e4f5g6h third commit # <-- message à modifier
pick a1b2c3d fourth commit
Git then stops on this commit and opens a second editor to allow you to enter the new commit message. Once saved, the rebase continues automatically.
Final check:
git log --oneline
2.4 Demo — Reordering commits
It is sometimes necessary to reorder commits in the history — for example, if the second and third commits are in an order that could be confusing when reading the project history.
Point of attention before reordering
If you reorder commits that modify the same file, you will have to resolve conflicts in these files, because Git does not know in which order to apply changes that influence each other. In the demonstrations of this training, we work at the level of separate files to avoid precisely this type of conflict.
The git rebase command — fundamental concept
The secret to reordering commits (as with all other operations in this module) is the git rebase command. Rebasing consists of reapplying commits on a base chain — this is the origin of the term rebase: we reapply commits on a pre-existing base.
The interactive rebase (-i or --interactive) allows you to see and modify the operations to be performed before they are applied to the repository.
Interactive rebase syntax
# Rebase interactif sur les N derniers commits
git rebase -i HEAD~N
# Exemple : travailler sur les 3 derniers commits
git rebase -i HEAD~3
HEAD: reference to the last commit of the current branch~N: go back N commits--interactivecan be abbreviated to-i
Reordering commits in the editor
After running the command, the editor opens with the list of commits in chronological order (oldest to newest).
Editor initial state:
pick i7j8k9l second commit
pick e4f5g6h third commit
pick a1b2c3d fourth commit
To reorder, simply move the lines into the desired order. For example, to swap the second and third commit:
After modification:
pick i7j8k9l second commit
pick a1b2c3d fourth commit # <-- était en 3e position, maintenant en 2e
pick e4f5g6h third commit # <-- était en 2e position, maintenant en 3e
After saving and closing the file, Git replays the commits in the new order.
Checking result:
git log --oneline
Reminder: Reordering commits modifies the SHA-1 hashes of the commits concerned. If these commits have already been pushed to a shared remote repository, this operation should be avoided.
2.5 Demo — Squash of commits
Squash consists of merging several commits into one. This is commonly done before creating a pull request to merge a local branch into the main repository.
Why squash?
When developing locally, it is natural to have many commits that reflect the evolution of thinking as a feature develops. These commits can be:
- Wordy or redundant
- Work in progress (WIP) in nature
- Confused for outside reader browsing main project history
Squash therefore allows you to have a detailed commit history locally, but to condense it into clean and clear commits before integrating them into the main branch.
Procedure
As for the other operations, we use the interactive rebase:
git rebase -i HEAD~3
In the editor, we replace pick with squash (or its abbreviation s) for the commits to be merged with the previous commit (the commit just above in the list).
Example — initial state:
pick i7j8k9l second commit
pick e4f5g6h third commit
pick a1b2c3d fourth commit
After modification to squash the 3 commits into 1:
pick i7j8k9l second commit # commit de base (conservé)
squash e4f5g6h third commit # fusionné avec le précédent
s a1b2c3d fourth commit # fusionné avec le précédent (abréviation 's')
Note:
sandsquashare strictly equivalent.
After saving, Git merges the three commits into one. It then opens a second editor to allow writing the message of the merged commit. The original commit messages are posted in comments for reference.
Expected result:
Before squash:
a1b2c3d fourth commit
e4f5g6h third commit
i7j8k9l second commit
m0n1o2p initial commit
After squash:
x9y8z7w second + third + fourth commit (nouveau hash)
m0n1o2p initial commit
2.6 Demo — Splitting Commits
Commit splitting is the inverse of squash: it involves dividing an existing commit into several distinct commits.
At the end of the previous demonstration (squash), the repository has two commits: the initial commit and the merged commit which groups the second, third and fourth commits.
Method 1 — The commit to split is the last commit (HEAD)
If the commit to split is the last commit of the branch, we can use git reset HEAD~ to cancel it while keeping the modifications in the working directory (non-staged):
# Annuler le dernier commit sans perdre les fichiers
git reset HEAD~
# Vérifier l'état — les fichiers apparaissent comme non-stagés
git status
At this point, it’s as if the commit never happened. It is then possible to redo separate commits:
git add two.txt
git commit -m "second commit"
git add three.txt
git commit -m "third commit"
git add four.txt
git commit -m "fourth commit"
Method 2 — The commit to split is older in history
If the commit to split is not the last commit, you must use interactive rebase with the edit (or e) action.
Step 1 — Prepare interactive rebase
We start from a history of 5 commits (after recreating the history):
git log --oneline
# fifth commit
# fourth commit
# third commit
# second commit
# initial commit
We want to split the second commit (which is now in the middle of the history). We launch the interactive rebase:
git rebase -i HEAD~4 # Pour travailler sur les 4 derniers commits
Step 2 — Mark the commit to edit with edit
In the editor, we change pick to edit (or e) for the commit to split:
edit i7j8k9l second commit # <-- sera mis en pause ici
pick e4f5g6h third commit
pick a1b2c3d fourth commit
pick q5r6s7t fifth commit
Step 3 — Git stops on commit marked edit
After saving, Git replays commits up to the commit marked edit and stops there. The terminal displays a message indicating that rebase is paused.
# On annule le commit sans perdre les modifications
git reset HEAD~
# Vérifier l'état
git status
Step 4 — Redo separate commits
git add two.txt
git commit -m "premier commit fractionné"
git add three.txt
git commit -m "deuxième commit fractionné"
Step 5 — Continue rebase
git rebase --continue
Git resumes the rebase and replays the following commits.
Using reflog to retrieve a previous state
The reflog (reference log) is a log that records all HEAD movements in the repository. If we need to undo a git reset or return to a previous state, the reflog is valuable:
# Afficher le journal des références
git reflog
# Exemple de sortie :
# a1b2c3d HEAD@{0}: reset: moving to HEAD~
# x9y8z7w HEAD@{1}: commit: second commit (squashed)
# m0n1o2p HEAD@{2}: commit (initial): initial commit
# Revenir à un état précédent
git reset <hash-du-commit>
2.7 Demo — Removing Commits
Sometimes a commit in the history is no longer relevant: the changes it contains have become obsolete, useless, or simply sources of confusion. In this case, we can delete this commit from the history.
Starting point
At this stage of the demonstration, the repository has 5 commits:
git log --oneline
# q5r6s7t fifth commit
# a1b2c3d fourth commit
# e4f5g6h third commit
# i7j8k9l second commit
# m0n1o2p initial commit
Removing commits via git rebase -i
Interactive rebase is still used here. We run the command to work on the 4 commits placed above the initial commit:
git rebase -i HEAD~4
Two methods to delete a commit
Method 1 (recommended) — Use the drop (or d) command:
pick i7j8k9l second commit
pick e4f5g6h third commit # à supprimer
pick a1b2c3d fourth commit
drop q5r6s7t fifth commit # supprimé avec 'drop'
Method 2 — Delete the entire row:
pick i7j8k9l second commit
# ligne du third commit supprimée (commit perdu)
pick a1b2c3d fourth commit
pick q5r6s7t fifth commit
The
dropmethod is preferred because it is explicit: we clearly see the intention to delete a commit. Deleting a line can appear accidental and is less readable.
Example — removal of third and fifth commit:
pick i7j8k9l second commit
# ligne supprimée (third commit supprimé)
pick a1b2c3d fourth commit
d q5r6s7t fifth commit # 'd' = drop
Expected result:
After closing the editor and executing the rebase, the history only contains 3 commits:
a1b2c3d fourth commit
i7j8k9l second commit
m0n1o2p initial commit
And in the working directory only one.txt, two.txt and four.txt are present (the files associated with deleted commits are gone).
3. Repository wide changes
Module duration: 10m 9s
3.1 Introduction
Module 2 explored powerful features that Git offers for updating and reviewing the history of a source code repository. But what if you need even more power? What if you need to potentially modify everything saved in a repository?
The tools seen so far (interactive rebase) are excellent for modifying one or two commits at a time. But for modifications that extend to an entire repository, they are insufficient.
This module presents a tool that allows completely rewriting the history of an entire repository.
3.2 Overview of git-filter-repo
Reviewing the history of a project is not something you do every day. Rewriting the history of an entire repository is even rarer. However, there are situations where this ability can be extremely valuable and save a lot of time and energy.
The tool: git-filter-repo
The project this module focuses on is git-filter-repo, developed by Newren. This is a versatile tool for rewriting Git history.
“
git-filter-repois a versatile tool for history rewriting. » — README of the project
Its only role is to rewrite history — but it can rewrite many types of history in a Git repository.
Why use git-filter-repo?
Here are some use cases where git-filter-repo is particularly useful:
1. Deleting secrets throughout history
Imagine that at some point in the project lifecycle, we realize that a production secret — a password or key to access a cloud service — has been mistakenly committed to the repository. This secret is perhaps present several times, replicated in different places in history. How :
- Find all occurrences of this secret in entire project history?
- delete them completely?
This is exactly the use case for which git-filter-repo is ideal.
2. Extraction of a submodule (distillation)
If the repository is a monorepo containing several modules, it is possible to distill the history of a single module or a set of modules to create an independent repository.
3. Rewriting bulk commit messages
Modify patterns in commit messages throughout history.
4. Deleting large files
Remove large binary files (images, archives) that were committed by mistake and are making the repository heavier, even in old commits.
Installing git-filter-repo
git-filter-repo is not integrated with Git by default — it is a third-party tool that must be installed separately.
Via pip (Python Package Index) — recommended:
pip install git-filter-repo
Via Homebrew (macOS):
brew install git-filter-repo
Via the Linux package manager:
# Ubuntu / Debian
sudo apt install git-filter-repo
# Fedora
sudo dnf install git-filter-repo
Note:
git-filter-repois a Python script. It requires Python 3.x.
Comparison: git-filter-repo vs git filter-branch
Git has a similar native command: git filter-branch. However, git-filter-repo is significantly superior and is now the recommended solution:
| Criterion | git-filter-repo | git filter-branch |
|---|---|---|
| Performance | Extremely fast | Very slow |
| Status | Recommended | Deprecated/deprecated |
| Availability | Third-party tool (pip/brew) | Integrated with Git |
| API | Modern and clear | Complex and tricky |
| Security | Good | Possible unexpected behaviors |
3.3 Demo — Using git-filter-repo
Documentation and User Manual
The documentation for git-filter-repo is written in a format similar to the official Git documentation. After installation, the two main options of the command are:
git filter-repo --analyze # Analyser le dépôt
git filter-repo [OPTIONS...] # Appliquer des filtres
The --analyze option — Pre-analysis of the repository
Before applying filters, it is often useful to analyze the repository to:
- Determine how to apply a filter
- Check that a filter was executed correctly
git filter-repo --analyze
This command generates a directory .git/filter-repo/analysis/ containing detailed reports:
.git/filter-repo/analysis/
├── blob-shas-and-paths.txt # SHA et chemins des blobs
├── directories-all-time.txt # Tous les répertoires de l'historique
├── directories-deleted-files.txt
├── extensions-all-time.txt # Extensions de fichiers présentes
├── extensions-deleted-files.txt
├── filenames-all-time.txt # Noms de fichiers
└── path-all-time.txt # Chemins complets
Available filter categories
git-filter-repo options are grouped by category:
| Category | Description |
|---|---|
Filters on paths (--path) | Include or exclude files/directories |
Content filters (--replace-text) | Edit or delete content in files |
Filters on commit messages (--replace-message) | Edit bulk commit messages |
Author filters (--mailmap) | Correct author names and emails |
Demo: Rewriting commit messages with --replace-message
This demo illustrates how to modify bulk commit messages across the entire history.
Step 1 — Create an expressions file
The expressions file defines the replacement rules. Each line follows the format:
literal:texte_à_remplacer==>texte_de_remplacement
regex:motif_regex==>texte_de_remplacement
Concrete examples:
# expressions.txt
literal:initial commit==>Initial Setup
literal:second commit==>Add file two
regex:^(third|fourth) commit$==>Add file
Step 2 — Apply filter
git filter-repo --replace-message expressions.txt
The command goes through all commits in history and applies replacement rules to commit messages.
Step 3 — Check the result
git log --oneline
Commit messages will have been updated according to the defined rules.
Deleting a secret in the entire history
To delete a secret (API key, password, token) from the entire history, use --replace-text:
# 1. Créer le fichier d'expressions
echo "literal:MA_CLE_API_SECRETE==>REMOVED" > expressions.txt
# 2. Appliquer le filtre sur le contenu des fichiers
git filter-repo --replace-text expressions.txt
Warning: This operation rewrites all commits that contain the corresponding text. The commit hashes change.
Extracting a subdirectory
# Extraire uniquement l'historique du sous-répertoire 'mon-module'
git filter-repo --subdirectory-filter mon-module
Deleting a file from all history
# Supprimer le fichier 'secret.env' de tout l'historique
git filter-repo --path secret.env --invert-paths
Impact on shared repositories
Important: After using
git filter-repo, if the repository is shared with other collaborators, all will have to do a freshgit cloneof the repository because the history will have been rewritten (the SHA-1 hashes of the commits change). It is impossible to do a simplegit pullafter a history rewrite.
4. Summary of key commands
Actions available in interactive rebase
| Action | Abbreviation | Description |
|---|---|---|
pick | p | Keep commit as is (default behavior) |
reword | r | Keep the commit but modify its message |
edit | e | Pause the rebase to allow modification of the commit (amend, reset, etc.) |
squash | s | Merge this commit with previous commit (opens editor for post) |
fixup | f | Like squash, but ignores the merged commit message |
drop | d | Remove this commit from history |
exec | x | Execute a shell command after commit |
label | l | Label current HEAD |
reset | t | Reset HEAD to a label |
merge | m | Create a merge commit |
Essential Git commands for the course
# ── Consultation de l'historique ───────────────────────────────────
git log --oneline # Historique compact (1 ligne par commit)
git log --oneline --graph # Avec représentation graphique des branches
git reflog # Journal de référence (tous les mouvements de HEAD)
# ── Modification du dernier commit ─────────────────────────────────
git commit --amend -m "msg" # Modifier le message du dernier commit
git commit --amend # Modifier le dernier commit (ouvre l'éditeur)
# ── Rebase interactif ───────────────────────────────────────────────
git rebase -i HEAD~N # Rebase interactif sur les N derniers commits
git rebase -i HEAD~3 # Exemple : sur les 3 derniers commits
git rebase --continue # Continuer après résolution de conflits
git rebase --abort # Annuler le rebase en cours
git rebase --skip # Ignorer le commit courant et continuer
# ── Annulation de commit ────────────────────────────────────────────
git reset HEAD~ # Annuler le dernier commit (garde les modifs non-stagées)
git reset HEAD~N # Annuler les N derniers commits
git reset <hash> # Revenir à un commit spécifique
# ── git-filter-repo ─────────────────────────────────────────────────
git filter-repo --analyze # Analyser le dépôt
git filter-repo --replace-message expr.txt # Réécrire messages de commit
git filter-repo --replace-text expr.txt # Réécrire contenu de fichiers
git filter-repo --subdirectory-filter <dir> # Extraire un sous-répertoire
git filter-repo --path <fichier> --invert-paths # Supprimer un fichier de l'historique
5. Best practices and warnings
When is it appropriate to modify Git history?
Editing history is a powerful operation but must be used with discretion. Here are the situations where it is appropriate:
| Location | Appropriate ? | Recommended tool |
|---|---|---|
| Fix a local commit message | ✅ Yes | git commit --amend |
| Clean up history before a pull request | ✅ Yes | git rebase -i |
| Reorder commits not yet pushed | ✅ Yes | git rebase -i |
| Delete a secret committed by mistake | ✅ Necessary | git-filter-repo |
| Edit already shared commits (without urgency) | ❌ Avoid | — |
| Rewrite the history of a shared master branch | ❌ Very risky | — |
The golden rule
Never rewrite the history of commits that have already been pushed to a shared repository, unless absolutely necessary (eg: deletion of a security secret).
History rewrite modifies SHA-1 hashes of commits. This creates a divergence between the local copy and the remote copy, which prevents other collaborators from doing a normal git pull. They must:
- Save their local changes not pushed
- Make a
git clonefresh from the rewritten repository - Reapply their changes
Recommended work strategies
Use git rebase -i only on local non-shared branches
The typical workflow is:
- Develop on a local branch with as many commits as needed
- Before pushing, squash/clean local history with
git rebase -i - Push clean branch and create a pull request
Never use git push --force without coordination
If a history rewrite is unavoidable on a shared branch:
- Coordinate with the whole team
- Inform everyone to do a fresh
git cloneafter the operation - Document reason for rewrite
Use git-filter-repo for security emergencies
If a secret has been committed to the main branch:
- Immediately revoke the secret from the provider (it is compromised upon push)
- Use
git-filter-repo --replace-textto remove it from history - Force-push the main branch
- Have the repository recloned to all collaborators
Structure of the training exercises
The training exercises use a deliberately simple submission structure:
demo-repo/
├── one.txt # Committé en premier (commit initial)
├── two.txt # Committé en deuxième
├── three.txt # Committé en troisième
├── four.txt # Committé en quatrième
└── fifth.txt # Committé en cinquième (certaines démonstrations)
All of these files are empty — the goal is only to create a manageable commit history without worrying about the actual contents of the files.
Search Terms
rewriting · git · history · ci/cd · devops · commit · commits · rebase · git-filter-repo · interactive · reordering · repository · available · commands · deleting · edit · filter · last · message · messages · method · older · point · removing