DEV Community

Nir Lanka ニル
Nir Lanka ニル

Posted on • Edited on

This is why I love Git (and Vim)! (also, is there a better solution?)

Today I was pushing new commits to the remote pull-request branch which already had 5 commits. Then we realized it can't be tested with functionality in our main develop branch since mine was too behind. Well, without merging from develop to my-feature.

The problem was not how to do this easily, as a simple git checkout my-feature; git merge develop; and a bit of conflict resolution before git add .; git commit should do the trick; but how to have a cleaner history. i.e. We wanted a single commit that can be tested and merged back to develop.

And to top that, we wanted to squash all 6 commits in my-feature into one, for ease of code-review.

Disclaimer: I don't like over-relying on rebase, since it can be used to change history, which is (1) dishonest, and (2) dangerous. But in this case, we had to do it.

So I got to work of fixing this issue.

Now, I'm not a great Git hacker. I do know the concepts of git and bash and try to optimize the way I do things, but I am far from being able to hack out solutions like this without looking up stuff.

So I thought how I'd manually do this.

  1. Extract the changes
  2. Remove branch
  3. Add new branch
  4. Add changes
  5. Commit
  6. Force push

Fun with squash (in Vim)

First I squashed the changes (which was redundant, as we later realize) with git rebase -i <hash-last-commit-in-develop-branch>. At the rebase edit in Vim, I wanted to replace all but the first pick commands with s or squash. I looked up and found cgn which helps replace search items. I did these steps:

  1. Go to first line and search pick by typing /pick. Then press Enter.
  2. Type cgn and type in the new phrase to replace the old. Then press Esc.
  3. Press . until all the adjacent lines I need are modified.
  4. :wq

Of course, this was totally unnecessary, since I'm using git diff later, which I didn't plan at this stage.

This is when I realized I have to delete this new squashed commit and start over! (because I would still have to merge from develop and it would create a merge commit and would be messy).

Save the changes

I'm not a huge fan of git stash. I prefer something more tangible and reliable like git diff > ./diff. Of course I may be ill-informed, but I find git stash to be pretty limited and superficial, unlike git diff.

So, to save the changes (which is now only in the current commit), I ran git diff HEAD^ HEAD > ../diffs/my-feature.diff. This dumps all the changes (file updates, creates, deletes, everything) into a text file.

Re-apply changes

Let's remove the local branch copy: git branch -D my-feature. We can force push later to replace the remote branch copy.

Now I re-created the branch new from develop:

git checkout develop
git pull
git checkout -b my-feature

Then I applied the diff back: git apply ../diffs/my-feature.diff. That hit me in the face. Hard.

... lots of lines
error: patch failed: ...
... more lines

Of course, I should've expected this. This has conflicts! Hey, wouldn't it be great if applying a git diff could work like a branch merge? Well, I was in luck, as it turned out to not just be entirely possible, but also quite simple!

git apply ../diffs/my-feature.diff -3

So simple! Just add -3. Then it applied the diff with conflicts saved the same way git merge does. All I had to do then was to resolve conflicts, add, commit everything, and do git push origin my-feature --force.

That was all!

So why do I love Git and Vim, again?

Well, it's the crazy simplicity. Everything in Git can be represented as text. And everything, no matter how separate they seem, follow the same rules. Look at how git diff can work similar to git merge! They are completely different areas!

Also, Vim. I love Sublime Text. I use it for quick advanced text-edits. I love its multi-cursor functionality. It's crazy. But Vim doesn't need a multi-cursor functionality! Instead, it makes the whole process simpler and more extensible! No limitations of simple GUI apps. You can do so much more with very little knowledge and concepts!

Bonus: my git aliases

~/.gitconfig - [alias] -

co = checkouti
br = branch
ci = commit
cm = commit -m
a = add
st = status
unstage = reset HEAD --
last = log -1 HEAD
visual = !gitk
stage = add
df = diff
rb = rebase
undo = reset HEAD~1
redo = reset \"HEAD@{1}\"
amend = commit --amend

I got these aliases from somewhere that I can't find right now. Sorry about not crediting the original writer. Thanks!

Thanks for reading through this ramble! I hope it had some exciting new stuff that would help you. I may have been silly in how I did this, in which case I'd love to hear about how you'd solve it. :)

Top comments (0)