A few days ago, I was told to refactor the codebase of a project where the name of a package and its references were to be modified. At first, it seems like the task has a pretty simple solution i.e.
- Rename the files and directories.
git add --all
git commit -m "[refactor] Rename package XYZ to ABC"
git push origin master
- And you’re done! (you think)
But what happens when you head over to GitHub and look at the commit histories of the renamed files and folders?
The histories of the files have suddenly vanished and, to a person who hasn’t been working on the project since its inception, it looks like the renamed files were newly added in the last [refactor] commit.
Background
When you rename a file in a git repository, git log <filename>
only shows the changes done to the renamed files after the rename operation, if you use the --follow
argument, however, it continues history before a rename operation (that is, it searches for similar content using the heuristics and ignores the names of the files).
Unfortunately, there is no way to see the history of the files before the rename operation on GitHub, since GitHub shows logs similar to the output of the git log
command without — -follow
. In short, git (implicitly) doesn’t show commit histories of files after they have been renamed.
I’ll be using this sample project repository on GitHub throughout the rest of the article as the remote repository for my local setup.
1. Rename a file/folder and preserve its history
While looking for a solution to preserve and persist the commit histories of renamed files and folders to GitHub, I found this gist
The above script is a lifesaver when you have over 4000 commits with more than 200 files to refactor/move! The usage and working of the script is very well documented in the commented sections. Learn to use the script to rename files while persisting their histories (before renaming) to GitHub.
2. Branch off a set of recent commits
Let’s say you started working on a new feature for an existing project, you made some changes, pushed a few commits, and then you realized you should have made a new branch to work on the feature. So how do you move those changes from, let’s say, master
, to a new feature
branch?
You simply want to keep these changes (commits) in the feature
branch and roll back to the commit before the new feature’s code was added in the master
branch —
You need to be extra careful with 2 steps -
- Resetting to the commit you want: Make sure you correctly enter the commit hash (from where your feature branch branches off) to ensure none of your commits are lost in the process of migrating them to a new branch.
- Updating the remote forcefully: Other developers in your team might have already pushed to your remote (while you were working on the new feature) and if you want to keep those changes, doing a
git pull
before pushing would be the way to go.
[WARN] Force pushing commits to the remote may lead to code loss!
3. Stage all (only) deleted files for your next commit
If you have additions, modifications and deletions all at once, ready to be committed, it is generally a better idea to put all 3 operations in separate commits for ease of debugging, in case the changes break something in production.
So how do you select only the deleted files to stage them for a commit? Well, this simple one line works like magic for exactly that —
The magic words add (technically track removals of) all the deleted files to the stage. They can now be easily committed and pushed while keeping the additions and modifications in the next commits.
Unlike git add *
which adds all the additions to the stage, git rm *
doesn’t add all the deletions, instead, it deletes all the files in the repository and stages them for a commit. Use git rm
very cautiously, you have been warned!
4. Git Resets — Soft, Mixed, Hard
Ever wonder why there are three types of resets? The fundamental difference between the different kinds of resets is what you want to do with the changes done after the commit you are resetting to.
git reset --hard <commit-id>
resets the HEAD
to <commit-id>
and all the changes after<commit-id>
are discarded. If you are absolutely sure you don’t want any changes after <commit-id>
, you should do a hard reset.
git reset --mixed <commit-id>
resets the HEAD
to <commit-id>
and all the changes after<commit-id>
are added to your working tree i.e. under “Changes not staged for commit”. This preserves all the changes done after <commit-id>
and you can continue work on them.
git reset --soft <commit-id>
resets the HEAD
to <commit-id>
and all the changes after <commit-id>
are added to your staging area i.e. under “Changes to be committed” so that you can immediately commit them with an updated commit message.
A soft reset is essentially a mixed reset plus an additional step of adding the changes to the staging area i.e. doing a git add <files>
after resetting.
5. Forgot to add a few changes in the last commit?
If you’ve ever been in a position when you stage (add) some changes to be committed, modify them again and forget to add them again before committing, you will know what I’m talking about.
You just forget to add that one line change in the last commit and now you have to add it to the next commit which may not describe the context of that one line change.
Instead of creating two (or more) commits for a single context, you can update the changes in the last commit. Use the--amend
flag with git commit
, if you forget to add some files or code changes.
Note that if you had already pushed the latest commit to your remote, then you will need to do a forced update, since amending a commit changes its hash and will throw an error when you try to push the amended commit to your remote
Fix some stuff, add to stage, amend the latest commit and push to remote!
6. Delete a commit / undo a remote push!
The good old use case where you were too excited to commit your changes and pushed them to your remote without realizing you added a bug in the commit and want to roll back, so how do you delete the commit once it’s pushed to your remote?
The gist of the solution is that you have to roll back to an earlier commit in your local repository and then do a forced push to overwrite your remote.
The -f
flag pushes the commits forcefully if your local branch and remote branch have diverged and syncs up your remote with your local branch. Be extra careful as there is a chance that it will overwrite some of the remote commits.
Hopefully, I’ve made your life a little easier, if you are frequently looking for git commands/procedures to do something, do share your experiences, until then happy coding!
Leave a Reply
Your email address will not be published. Required fields are marked *