How do I ??? with Git?

The purpose of this post is to quickly go over some of the basic things most developers would require from source control software.

I highly recommend reading either Pro Git or Git Community Book.

Undoing a commit

If you committed some code by accident you have the following options:

Undo option #1: Amend

If you just forgot a file or wrote the wrong comment, stage your changes and run "git commit --amend". This will merge the new commit with the previous commit (overriding the old comment).

Undo option #2: Revert

Revert will create new commit which is the opposite of the commit you wish to remove, for example:

    $ git log

      dfdd917 added file2.txt
      fbe043e added file1.txt

    $ git revert dfdd917

    $ git log

      7bdd793 Revert "added file2.txt"
      dfdd917 added file2.txt
      fbe043e added file1.txt

If you already pushed the commit you wish to remove, this would be the best option.

Undo option #3: Reset

If you want to get rid of the last commit (or several last commits) you can use the "git reset" command which will make the HEAD and the current branch point on the given commit.

    $ git log

      fcd3f55 (HEAD, master) added file3.txt
      dfdd917 added file2.txt
      fbe043e added file1.txt

    $ git reset --hard dfdd917
    $ git log

      dfdd917 (HEAD, master) added file2.txt
      fbe043e added file1.txt

You might ask yourself, what happened to the third commit? is it gone forever? well... the answer is no, the commit is still there, it's just unreachable.

To see all of the unreachable commits use the "git fsck" command:

    $ git fsck --unreachable --no-reflogs

      unreachable blob a83ffd
      unreachable tree 984260
      unreachable commit fcd3f5

This shows us we have three unreachable objects:

  • file3.txt (blob a83ffd)
  • the state of the root directory including file3.txt (tree 984260)
  • the commit (fcd3f5)

Wait, what's --no-reflogs?

The reflog records every action you perform in a git repository, if we were to run "git reflog" after the third commit this would have been the output:

    $ git reflog

      fcd3f55 HEAD@{1}: commit: added file3.txt
      dfdd917 HEAD@{2}: commit: added file2.txt
      fbe043e HEAD@{3}: commit (initial): added file1.txt

After we run the "git reset" command the reflog will look like this:

    $ git reflog

      dfdd917 HEAD@{0}: dfdd917: updating HEAD
      fcd3f55 HEAD@{1}: commit: added file3.txt
      dfdd917 HEAD@{2}: commit: added file2.txt
      fbe043e HEAD@{3}: commit (initial): added file1.txt

As you can see the reset action did not remove the commit from the reflog, but added a new entry to the reflog saying that the HEAD was updated.

The "--no-reflogs" argument tells git to not consider commits that are referenced only by a reflog entry as reachable.

What do I do now?

Well, you can just leave that commit there, it won't be pushed to the remote repository and you can restore it whenever you like by running:

    $ git checkout fcd3f55

If you want to remove you can cleanup you repository as described here at the bottom (basically, you expire the reflog entries, and then remove all unreachable objects).

What if I already pushed the commit?

Then this is option is highly unrecommended, but if you need to remove the commit from the server then use the "-f" attribute:

    $ git push -f

Creating a remote repository

The two most common ways to create a remote repository are:

  1. Use the web interface of services such as github or bitbucket
  2. Create a bare repository by running the command "git init --bare" in an empty directory.

Using a remote repository

Option #1:

If you do not have an existing local repository, the you should just clone the remote repository:

    git clone path-to-repository

Notice that you get a warning, "You appear to have cloned an empty repository.".

Option #2:

If you have an existing local repository and the remote repository is empty you should push the local commits to the remote repository:

  1. Add a new remote to your local repository, name the new remote "origin":

    git remote add origin path-to-repository
    

This command will add the following lines to the .git/config file:

    [remote "origin"]
      url = path-to-repository
      fetch = +refs/heads/*:refs/remotes/origin/*

These lines let git know that the "origin" remote is mapped to the specified url and that it should fetch the heads and the branches from the remote repository (this is the default behavior).

Pushing changes to a remote repository

At this point, if you make some changes, commit them and try to push you will get the following error:

    No refs in common and none specified; doing nothing.
    Perhaps you should specify a branch such as 'master'.

Since the remote repository is empty there isn't a matching branch in origin, so the first time you push you must define which branch to push:

    git push origin master

This command makes git push all commits in the "master" branch to the "origin" repository.

Note: if we had cloned a non-empty repository we could just run "git push".

Getting changes from a remote repository

To get the latest changes from a remote repository run:

    git fetch

This command will download all the commits made to the remote repository since the latest fetch (or clone).

If there are changes in the remote repository we have the following options:

Option #1: Merge

    git merge origin/master

This command will take all of the commits from the given branch (in this case "origin/master") and incorporate them into the current branch.

Note #1: make sure you're in the correct branch before merging (run "git branch" to see which branch you're in).

Note #2: You can do fetch+merge automatically by running "git pull"

Option #2: Rebase

If you have made your own local commits before fetching new commits your history becomes split:

    * my local commit #2
    * my local commit #1
    | * new remote commit #2
    | * new remote commit #1
    |/
    * last commit before the split

One option is to do a merge and incorporate all of the remote commits into the current master branch. This will cause the history to look like this:

    * Merge remote-tracking branch 'origin/master'
    |\
    | * new remote commit #2
    | * new remote commit #1
    * | my local commit #2
    * | my local commit #1
    |/
    * last commit before the split

This is ugly...

The better way to do this is using Rebase which basically takes commits and modifies them (if needed) to match a different source commit.

So we can take the local master branch and make all of the commits from the split-off point match the current origin/master branch.

To perform a rebase we run:

    git rebase origin/master

This command will make all of the changes from 'master' to the previous 'origin/master' match the new origin/master so the history will look like this:

    * my local commit #2
    * my local commit #1
    * new remote commit #2
    * new remote commit #1
    * last commit before the split

For a more detailed example of rebase you can see my previous post

Thanks for reading, I hope you find this article useful.

Until the next time, David.

Comments

mine said…
thank you for the helpful explanations.

Popular posts from this blog

Restart the Windows File Sharing Service to fix weird problems

WPF, ImageSource and Embedded Resources

SharpDevelop dark color scheme