A Very Bitey Dinosaur

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.

A single file and a single commit in vscode

I'm going to change the text in main.txt now and commit again.

Two commits in vscode showing the file text changed

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.

vscode menu showing hovering over the revert option

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.

vscode graph showing four commits, changes, revert changes and then changes (I can spell honest)

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.

vscode showing the option to reset to initial commit

and popup

vscode showing the option to do soft/mixed/hard reset

So three options. Option one "soft" lets me go back to my initial commit, but all the changes (commits) I've made since

  1. changing the text but spelling it wrong
  2. changing the text back to the original value
  3. changing the text and spelling it correct

Are combined into one change which is already staged, and just needs committing.

vscode showing all three commits above as one set of uncommitted changes

And now I've a tidy little single commit and all my past spelling related issues have vanished.

vscode showing two commits, with all previous commits squashed down

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.

vscode showing a single initial commit

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.

vscode showing four commits initial, 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.

vscode showing a single commit C

Oopsie #2

Now lets add a remote repo in play. I've cloned from my remote which has one initial commit

vscode showing a single commit locally and remote

I'm going to add a new file

vscode showing a new file code.rs staged and ready to commit

Now I commit it locally

vscode showing the code.rs file in a new commit

Now I'm going to push it to remote to synchronize things

vscode showing a merge conflict not allowing me to push my code

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

vscode showing a merge conflict not allowing me to push my code

we're going to ignore this for now

vscode asking if I want to use interactive rebase

And like magic

vscode showing my changes added right on top of the remote changes

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.

vscode showing a new commit adding hello world code, and deleting the file named haha you can't merge now
  
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

vscode showing a new remote commit called broken

Checking we can see that main.txt has been deleted, and code has been added to code.rs

vscode showing main.txt deleted and a new function printing Goodbye World

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.

vscode showing menu with rebase current branch selected

Okay, rebase again

vscode showing choosing interactive rebase option

Choose interactive

vscode interactive options again, here we've set it to reword the commit

Remember to change it to reword, then save the file and exit

vscode showing the exact same error message as before when we tried to rebase

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.

vscode showing our rebase finally finished with commit message Stop being a pain with remote please

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"
vscode showing one tidy commit which has also added back in main.txt

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.

vscode showing our three commits

For some reason we decide to revert, and we do a hard reset.

vscode showing a reset to commit one

And everything is gone.

vscode showing a single commit

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
  
vscode showing all three commits again, along with two deletions of files in pending changes

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

vscode in the middle of clicking discard all changes on deletions of files 2 and 3

Lets quickly remove those deletions so we're back exactly where we started.

vscode showing all three commits again with no pending changes

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.