Skip to content

DevOps Day 30: Rewriting History with git reset --hard

Today's task was a deep dive into one of the most powerful and "dangerous" commands in Git: git reset. The scenario was a common one: a test repository had accumulated some unwanted commits, and the team wanted to completely erase them, effectively turning back time to an earlier, clean state. This was not a job for the safe git revert; this required rewriting history.

This was a fantastic lesson in understanding how Git manages its history and the critical role of the HEAD pointer. I learned how to move the state of a repository back to a specific commit and then use a "force push" to update the remote server. This document is my detailed, first-person guide to that entire process, with a special focus on explaining the concepts for someone new to them.

Table of Contents


The Task

My objective was to clean up a Git repository on the Storage Server by resetting its history. The specific requirements were: 1. Work within the /usr/src/kodekloudrepos/beta Git repository. 2. Reset the repository's history to the commit with the message add data.txt file. 3. This action should remove all subsequent commits from the history. 4. The cleaned-up history must be pushed to the remote server.


My Step-by-Step Solution

The process required careful investigation to find the correct commit to reset to, followed by the reset itself and a force push. As the repository was in a root-owned directory, all commands required sudo.

Phase 1: Investigation

  1. I connected to the Storage Server (ssh natasha@ststor01) and navigated to the repository (cd /usr/src/kodekloudrepos/beta).
  2. This was the most critical first step. I needed the unique ID (hash) of the commit I wanted to return to. I used git log to find it.
    sudo git log --oneline
    
    The output listed all the commits. I found the line a1b2c3d add data.txt file and copied the hash a1b2c3d.

Phase 2: The Reset and Force Push

  1. With the target hash identified, I executed the reset command. The --hard flag was necessary to clean both the commit history and the working files.

    # I used the actual hash from my 'git log' command
    sudo git reset --hard a1b2c3d
    
    The output was immediate and clear: HEAD is now at a1b2c3d add data.txt file.

  2. My local repository was now clean, but it was out of sync with the remote server. A normal git push would be rejected. I had to use a force push to overwrite the remote history.

    sudo git push origin master --force
    

Phase 3: Verification

The final step was to confirm that the history was correctly rewritten on both my local and the remote branch. 1. I ran git log again.

sudo git log --oneline
2. The output now showed only two commits, with add data.txt file at the top. All the unwanted test commits were gone, as if they had never existed. This was the definitive proof of success.


Why Did I Do This? (The "What & Why")

- git reset: This is the key command for this task. It's a powerful tool used to rewrite history. Unlike git revert (which creates a new commit to undo a previous one), git reset moves the branch pointer back in time, effectively deleting commits from the history. - The --hard flag: The reset command has different modes. The --hard flag is the most impactful. It does three things: 1. Moves the branch pointer (e.g., master) to the specified commit. 2. Resets the Staging Area to be empty. 3. Resets the Working Directory to match the files from the specified commit, deleting any uncommitted changes and removing files from later commits. For a cleanup task, this is exactly what's needed. - Force Push (--force or -f): After I reset my local master branch, its history was different from the master branch on the remote origin server. The remote server, by default, will reject any push that doesn't "fast-forward" (i.e., just add new commits). Since I had rewritten history, a normal push would fail. A force push is a command that tells the remote server, "Ignore your own history; the history I am sending you is the new source of truth." This is a dangerous command and should only be used when you are absolutely certain you want to overwrite the shared history, like in this coordinated cleanup task.


Deep Dive: What Exactly is HEAD in Git?

Understanding HEAD is fundamental to understanding how Git works. It's not as complicated as it sounds.

[Image of Git HEAD, branch, and commit pointers]

  • Analogy: The "You Are Here" Marker: The best way I found to think about HEAD is as the "You Are Here" marker on the map of your project's commit history. It's a pointer that tells Git what commit you are currently looking at and what commit will be the parent of your next commit.

  • HEAD Points to a Branch: In 99% of normal operations, HEAD doesn't point directly to a commit. Instead, it points to a branch pointer (like master). The branch pointer, in turn, points to the latest commit on that branch.

    • So the chain is: HEAD -> master -> [Latest Commit Hash]
    • When I make a new commit, the master pointer moves forward to the new commit, and HEAD simply follows along for the ride.
  • How git reset Affects HEAD: The git reset command directly manipulates these pointers. When I ran git reset --hard a1b2c3d:

    1. Git first moved the master branch pointer from the latest commit all the way back to the commit with the hash a1b2c3d.
    2. Since HEAD was pointing to master, it automatically came along.
    3. The result was that my "You Are Here" marker was now at the older commit, and all the commits that came after it were effectively "forgotten" by that branch.
  • Detached HEAD (A side note): If I were to git checkout a1b2c3d directly, HEAD would then point directly to that commit hash, not to a branch name. This is called a "detached HEAD" state. It's useful for inspecting old code, but any new commits I make will be temporary unless I create a new branch from them.


The Dangers: reset vs. revert and the Force Push

- The Golden Rule: The most important lesson is that git reset should never be used on a shared, collaborative branch unless the entire team has agreed to a history rewrite (like this cleanup task). If other developers have already pulled the commits that I am about to delete, my force push will create a divergent history and cause major problems for them. - revert is for Collaboration: For day-to-day "undo" operations, git revert is the safe choice because it preserves history and works smoothly with the pull workflow. - reset is for Private Cleanup: git reset is perfectly safe to use on my own local, private feature branches before I have pushed them and shared them with anyone. It's a great tool for cleaning up my own messy commit history before I create a pull request.


Common Pitfalls

- Forgetting --hard: If I had used a "soft" or "mixed" reset, the branch pointer would have moved, but the unwanted changes would have remained in my working directory or staging area, failing the "cleanup" requirement. - Forgetting to Force Push: A regular git push after a reset will be rejected by the remote server. The --force flag is mandatory to complete this task. - Resetting to the Wrong Commit: It's very easy to copy the wrong hash. Always double-check the commit message in the git log output before running the reset command.


Exploring the Commands I Used

- sudo git log --oneline: A command to view the commit history in a condensed, one-line-per-commit format, which is perfect for finding commit messages and hashes. - sudo git reset --hard a1b2c3d: The main command of the task. - reset: The subcommand to rewrite history. - --hard: The mode that resets the branch pointer, staging area, AND the working directory. - a1b2c3d: The specific commit hash I wanted to return to. - sudo git push origin master --force: The command to update the remote server. - push origin master: The standard command to push the master branch to the origin remote. - --force: The crucial flag that tells the remote server to accept my rewritten history, even though it's not a fast-forward change.