Oops I Git It Again
Git is one of those things that beginners (and non beginners) tend to struggle with. Part of that I find is that people are worried about messing things up, and then it being permanent, or they worry when they try to fix things they will make things worse.
So, I'm going to make a repo and then mess things up repeatedly, and then I'm going to fix them.
Some of these fixes will be clean, some of them will be hacky, some of them will be NSFW. As such I'd like to preface this with the golden rule - you only get to revert stuff if it's just you.
Local repo, not pushed changes yet? Go nuts.
Remote repo but it's set private and just for you? Sure not a problem, just make sure you remember when you access it from another machine that you rewrote the history.
Force pushing to a shared repo? NO, BAD, step away from the keyboard and think about what you've done.
Now in theory anyone competent will have disabled force pushing and generally made sure it's not possible for you to screw up their remote, but lets not rely on that, practice good git hygiene.
Oopsie #1
Ok with all that out of the way let's set up a repo. I've made a file main.txt containing the text "First commit" and I've committed it. You'll note here I'm using Git graph plugin with vscode so it's easy to see where everything lies, but I'll also be using command line so don't worry.
I'm going to change the text in main.txt now and commit again.
Oh no, I've made a misspelling, everyone's going to know I'm a complete failure.
But it's fine I can fix it, I'll just revert my latest commit.
AGGGHH, oh no now I've got another commit, and now I've just fixed my spelling and committed again making a fourth commit. AND my spelling mistake is still in the history.
Ok so everything is now how it should be at least, I've just got two extra commits that I don't need.
Can I get rid of them somehow?
Yes, yes I can.
What I want to do is use reset - I'm going to right click the first commit, choose "reset to the Initial commit" and undo all this mess.
and popup
So three options. Option one "soft" lets me go back to my initial commit, but all the changes (commits) I've made since
- changing the text but spelling it wrong
- changing the text back to the original value
- changing the text and spelling it correct
Are combined into one change which is already staged, and just needs committing.
And now I've a tidy little single commit and all my past spelling related issues have vanished.
Alternatively if I'd picked mixed I'd have had my changes saved, but not staged. So I could decide what I wanted to stage or discard and then commit.
Finally hard gets rid of everything since the point you're reverting. Yes everything, every change you made since, well I hope you memorized them because git hasn't.
In general I almost always will go for mixed. I want to commit everything after all well there's a stage all button, that took me all of a second. I decide I want to get rid of everything after all, well lucky me there's also a discard all button. If in doubt go softer, you can always get rid of stuff if you don't need it.
And now with the command line - it's painless I promise.
I've reverted back to just my initial commit, and I'm going to make three quick commits back to back A, B, and C.
Now we're going to pretend that the nice vscode extension doesn't exist. So how do we find out where we are?
git log
which will give us something like this
commit 8aa92e86b1e014e5a0f3b824e4f3f4483c0ab5b7 (HEAD -> main)
Author: averybiteydinosaur
Date: Wed Aug 21 18:35:15 2024 +0100
c
commit 404fab569573d7ac6c104988b8ecdb812906db96
Author: averybiteydinosaur
Date: Wed Aug 21 18:34:57 2024 +0100
b
commit dd4435ba2e0d211a974a33202310f4c9ad40a37d
Author: averybiteydinosaur
Date: Wed Aug 21 18:34:47 2024 +0100
a
commit 88cf97ed9af221f74c94f9f91009064034ac45bc
Author: averybiteydinosaur
Date: Wed Aug 21 17:35:23 2024 +0100
Initial
Here we can see our four commits, Initial and then A, B, and C.
Each one has a hash to identify them, and we're going to use the hash of the initial commit to reset.
git reset 88cf97ed9af221f74c94f9f91009064034ac45bc
Now we check git log
git log
commit 88cf97ed9af221f74c94f9f91009064034ac45bc (HEAD -> main)
Author: averybiteydinosaur
Date: Wed Aug 21 17:35:23 2024 +0100
Initial
Then follow up with git status
git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: main.txt
no changes added to commit (use "git add" and/or "git commit -a")
We can see that all three commits have been removed, and we have one unstaged change which combines them.
As we didn't specify we did a mixed reset, but could have specified with --soft, --mixed, or --hard.
Now to get these changes tidied away
git add main.txt
git commit
And then add a suitable commit message.
Finally lets check in vscode and everything is as expected.
Oopsie #2
Now lets add a remote repo in play. I've cloned from my remote which has one initial commit
I'm going to add a new file
Now I commit it locally
Now I'm going to push it to remote to synchronize things
Oh no!
Someone else has been working on this remote branch and has made a change, vs code won't let me push MY changes.
This is a super common occurrence when multiple people are working on code, and it's pretty easy to fix
Making sure I have my branch checked out I rebase on top of the new changes
we're going to ignore this for now
And like magic
Just like that I can push my changes to remote, as I've re-written history so that my changes (adding the new file) were made on top of the other changes. Before I was adding a new file, but would effectively have also been deleting their file (as it didn't exist in my commit history). Now however according to the history their file always existed before I started making my changes. This is also why it's important to do this only on your own versioning, as soon as you're sharing with other people and rewriting history it gets messy.
Ok and now the same thing with the terminal
I've gone ahead and added some quick code, and also removed the junk file from before. I've made my commit and I'm ready to push to remote.
git push
Oh yay, another issue
To https://github.com/averybiteydinosaur/git-it-again.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'https://github.com/averybiteydinosaur/git-it-again.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
So it looks like there has been another commit made before I could get there, lets see the issue.
git fetch
git log --graph --all
* commit e17e5bb0b84b552f4a7a88c746e1944260b6d367 (origin/main, origin/HEAD)
| Author: averybiteydinosaur
| Date: Sat Aug 24 20:58:49 2024 +0100
|
| Broken
|
| * commit 7eeeef71de05e549eb0adae8edf312d15cf4a60e (HEAD -> main)
|/ Author: averybiteydinosaur
| Date: Sat Aug 24 20:58:39 2024 +0100
|
| Fixed
|
* commit 2749b6db6598e8721c373ec1f635139674725f48
| Author: averybiteydinosaur
| Date: Wed Aug 21 19:31:50 2024 +0100
|
| Amazing code
|
* commit 4a1b6d377d7261499d1e6c027cfc1aae55f623db
| Author: averybiteydinosaur
| Date: Wed Aug 21 19:19:59 2024 +0100
|
| haha
|
* commit 88cf97ed9af221f74c94f9f91009064034ac45bc
Author: averybiteydinosaur
Date: Wed Aug 21 17:35:23 2024 +0100
Initial
Ok ok, fine here's the graph in vscode
Checking we can see that main.txt has been deleted, and code has been added to code.rs
We're going to check the hashes from our git log command, and run a rebase and we're using interactive this time around. Checking we see that the remote origin/HEAD has hash/ID e17e5bb0b84b552f4a7a88c746e1944260b6d367, and we have our local branch main checked out.
git rebase -i e17e5bb0b84b552f4a7a88c746e1944260b6d367
we're using -i here as we want to do an interactive rebase, and the hash is what we want to rebase on top of.
pick 7eeeef7 Fixed
# Rebase e17e5bb..7eeeef7 onto e17e5bb (1 command)
#
# 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 [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# create a merge commit using the original merge commit's
# message (or the oneline, if no original merge commit was
# specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
# to this position in the new commits. The <ref> is
# updated at the end of the rebase
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
Ok so this is the interactive part of rebasing, rather than automatically applying the commits in question we can modify this before setting it going. Here if we were to save and exit, we would use our commit as is, but instead if I change the first line we can reword the commit message for example.
reword 7eeeef7 Fixed
...
...
Then I save and quit the file.... and get another error message.
Auto-merging code.rs
CONFLICT (content): Merge conflict in code.rs
error: could not apply 7eeeef7... Fixed
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 7eeeef7... Fixed
Thankfully there is the option to abort with git rebase --abort, so lets run that, give up with command line and run via the GUI.
Okay, rebase again
Choose interactive
Remember to change it to reword, then save the file and exit
AWH COME ON. We get the same exact issue.
So what's the actual issue here? According to the error message we have a conflict with code.rs
Makes sense right, remote made one change and we made a different one. So to resolve this lets open the file.
vi code.rs
fn main() {
<<<<<<< HEAD
println!("Goodbye World");
=======
println!("Hello World!");
>>>>>>> 7eeeef7 (Fixed)
}
Huh. So our file contains both options the remote HEAD version and our version, so let's quickly modify this.
fn main() {
println!("Hello World!");
}
Checking git stats
interactive rebase in progress; onto e17e5bb
Last command done (1 command done):
reword 7eeeef7 Fixed
No commands remaining.
You are currently rebasing branch 'main' on 'e17e5bb'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
deleted: haha you can't merge now
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: code.rs
So we need to use git add code.rs to tell git we've resolved the issue.
git add code.rs
Then let's try and continue
git rebase --continue
As we asked to reword we get the option to change our commit
Fixed
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto e17e5bb
# Last command done (1 command done):
# reword 7eeeef7 Fixed
# No commands remaining.
# You are currently rebasing branch 'main' on 'e17e5bb'.
#
# Changes to be committed:
# modified: code.rs
# deleted: haha you can't merge now
#
Modify the commit message, save and exit.
Phew, what a ride, but we got there, everything neat and tidy. Oh but we're missing our main.txt that got deleted, so for extra credit another quick fire round
git checkout 88cf97ed9af221f74c94f9f91009064034ac45bc main.txt
git reset e17e5bb0b84b552f4a7a88c746e1944260b6d367
git add .
git commit -m "Tidying up"
Few new bits in there, but it's mainly an extension of what we did in the first example. In short
- Rather than manually adding back main.txt restore it from our first commit with hash 88cf97ed9af221f74c94f9f91009064034ac45bc
- Reset to before our latest commit, but don't delete our changes
- Stage both our restoration of main.txt, and the changes made in our last commit
- Commit with message "Tidying up"
And now our main.txt has been restored but we've squashed that into the previous commit. We could also have used git commit --amend to do the squashing, but I find reset more powerful so only remember the one command rather than both. Maybe a little bit using a shotgun as a flyswatter but hey.
Oopsie #3
What happens when you completely delete your commits? Remember when we did reset --hard and everything just sort of vanished into thin air?
So let's set the scene. We've got a repo, made three quick commits, one, two and three. From here on out we're using a mix of command line and vscode, whichever is beset for a given task.
For some reason we decide to revert, and we do a hard reset.
And everything is gone.
Ok and now we want our old commits back actually. Well as it happens we're not out of luck yet, otherwise this would be a very poor example, but we have to dig in a bit.
We're going to use the reflog option which is a log of where the HEAD/end of branches has been updated to.
git reflog
3c1f265 (HEAD -> main) HEAD@{0}: reset: moving to 3c1f26559a5692054b0920fcb2e3695b36ea75f4
9270d4a HEAD@{1}: commit: third
fa24292 HEAD@{2}: commit: second
3c1f265 (HEAD -> main) HEAD@{3}: commit (initial): first
Ok fantastic, while our graph (both in vscode and via the command line) only show our first commit, here we can see our entire sordid history of changes. The oldest commit is at the bottom, where we created commit "first". Then above we created commit "second" then "third", and then finally above that we stupidly reset everything.
We want to get back to where we had all three commits which is 9270d4a or HEAD@{1}.
To do this we're going to reset again (properly this time)
git reset HEAD@{1}
Unstaged changes after reset:
D 2
D 3
And so we have our commits back as if my magic.
You'll also notice we have some uncommitted changes. As we didn't specify our reset has been done as mixed, that is the deletions from before still exist as changes, but have not been staged. So from our history we have created file 1, then created file 2, then created file 3, and now we've just deleted file 2 and 3, but not staged or committed those changes
Lets quickly remove those deletions so we're back exactly where we started.
Finally lets check with reflog again
git reflog
9270d4a (HEAD -> main) HEAD@{0}: reset: moving to HEAD@{1}
3c1f265 HEAD@{1}: reset: moving to 3c1f26559a5692054b0920fcb2e3695b36ea75f4
9270d4a (HEAD -> main) HEAD@{2}: commit: third
fa24292 HEAD@{3}: commit: second
3c1f265 HEAD@{4}: commit (initial): first
Yup that's right reflog still knows what you did, but it's only saved locally - it's no snitch!
And that's it, short and sweet, but a real lifesaver for when you accidentally hit --hard.
Now go grab a drink of choice and put your feet up, you've earned it making it this far.