2021-05-09 Version control for beginners

I was talking to @pkotrcka who said that git is hard to understand, and I agree. But there is hope: if you just start with simple requirements, then git can be simple to use.

What are simple requirements? “I want to keep this directory under version control so that I can recover old revisions of what I wrote.”

Slightly harders is this: “I want to keep all my dot files under version control but they aren’t in a single directory.” The way to solve this is to put them all in a directory. What I do is I have a directory with all these files, and elsewhere I just have symlinks pointing there.

Here you can see that a bunch of dot files from my home directory all live in ~/src/home.

alex@melanobombus:~ $ ls -la | grep '>' | cut -c 66-
.XCompose -> /home/alex/src/home/.XCompose
.addresses -> /home/alex/src/home/.addresses
.bash_aliases -> /home/alex/src/home/.bash_aliases
.bash_logout -> /home/alex/src/home/.bash_logout
.bashrc -> /home/alex/src/home/.bashrc
.ecompleterc -> /home/alex/src/home/.ecompleterc
.gitconfig -> /home/alex/src/home/.gitconfig
.gitignore -> /home/alex/src/home/.gitignore
.pause -> /home/alex/src/home/.pause
.profile -> /home/alex/src/home/.profile
.rcirc-authinfo -> /home/alex/src/home/.rcirc-authinfo
.selected_editor -> /home/alex/src/home/.selected_editor
.signature -> /home/alex/src/home/.signature
.vf1-bookmarks.txt -> /home/alex/src/home/.vf1-bookmarks.txt
lynx_bookmarks.html -> /home/alex/src/home/lynx_bookmarks.html

Starting a local repository: init

OK, so now you have a directory full of files. Time to create a local repository.

alex@melanobombus:~/src/home $ git init

This creates a hidden “.git” directory and populates it with whatever git needs.

Adding files to your repository: commit

Now that you have a local repository, all you need are two commands:

git add .
git commit -m "new stuff got added"

“git add” just “stages” any new files for commit, and “.” means all of the files in this directory and all its subdirectories, i.e. all of them.

“git commit” does the actual commit, adding the files to your local version control directory (the “.git” subdirectory), and “-m” indicates that what follows is the commit message. Eventually, I hope you will write better messages. If you don’t use the “-m” option, git launches your favourite editor and you can type a longer message.

Longer commit messages should consist of a short summary (50 characters or less), followed by an empty line, followed by as much explanation you deem necessary, line wrapped.

One common mistake I used to make was to forget to run “git add”. Nothing got staged, and then the commit didn’t see any staged files so nothing happened. My files were still there, modified, unstaged, and uncommitted.

Another common mistake I used to make was that after I had used “git add” to stage some files, I would discovered a mistake I had made and I’d edited the file and use “git commit” without using “git add” again. The problem is that git commits what you staged and not the working directory. A staged file isn’t “marked” for commit, the changes are copied to a “stage” (called the “index”). Thus, if you edit the file again, the staged changes are not updated. You need to “git add” them again. Otherwise, you’ll experience what I used to experience a lot: you commit your staged changes, use “git status” and the file is still noted as having been modified, and your fix isn’t part of the commit!

To amend this mistake, git has an “--amend” option to “git commit”:

git add .
git commit --amend -m "new stuff got added"

You can also make no changes and amend the last commit to fix typos in your commit message!

git commit --amend -m "new things got added"

Finding out what’s going on: status

“git status” gives you a short message discribing what’s going on. I use it a lot.

alex@melanobombus:~/src/home $ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   .ecompleterc
	modified:   gpg.conf

no changes added to commit (use "git add" and/or "git commit -a")

Let’s see: “On branch …” tells me what the current branch is. As long as your requirements are as simple as what we started with, you don’t need to worry about it.

“Changes not staged…” means that you have modified or new files. As you can see, git helpfully lists some of the commands you might want to use. If you want to stage them all, use “git add .” as mentioned above. Alternatively, stage and commit them individually:

alex@melanobombus:~/src/home$ git add .ecompleterc && git commit -m "new recipients"
[master 1c9c3b7] new recipients
 1 file changed, 18 insertions(+), 7 deletions(-)

Finding out what changed: log

In the previous section, we saw that the commit we made created a commit numbered “1c9c3b7”. The command to show us all the commits is “git log”. As you can see, “1c9c3b7” is actually just an abbreviation for “1c9c3b7fa0b45b50be1449a78b86ee3cff636194”.

alex@melanobombus:~/src/home$ git log
commit 1c9c3b7fa0b45b50be1449a78b86ee3cff636194 (HEAD -> master)
Author: Alex Schroeder <alex@gnu.org>
Date:   Sun May 9 10:21:58 2021 +0200

    new recipients

commit af8e2cebf19e15f73402e73e3d956749f27b5be9
Author: Alex Schroeder <alex@gnu.org>
Date:   Sun May 9 09:44:44 2021 +0200

    New aliases for bc and cal

commit d89f0d0cab1e54640c6ab151c908dea28659b3aa
Author: Alex Schroeder <alex@gnu.org>
Date:   Tue Apr 13 19:49:07 2021 +0200

    Add tabletop alias

…

Remember how we said good commit messages start with a single, short line of 50 characters or less? Well, the reason is that you can use “git log --oneline” to get very compact output. There are also a gazillion other ways to print the log, but that’s the most useful alternative right now. 😄

Showing what changed: diff

Before talking about changes let us quickly review the working directory, the stage, and commits: The working directory is what you’re looking at when you run “ls” or when you edit them. The “staged files” are the files you added using “git add”.

By default, “git diff” shows the difference between the working directory and the stage.

alex@melanobombus:~/src/home$ git diff
diff --git a/gpg.conf b/gpg.conf
index ee169ee..65ad9e3 100644
--- a/gpg.conf
+++ b/gpg.conf
@@ -135,7 +135,7 @@ charset utf-8
 #keyserver ldap://keyserver.pgp.com
 
 # gpg --keyserver ... --search-key ...@...
-# keyserver hkp://keys.gnupg.net
+# keyserver hkps://keys.openpgp.org
 # keyserver hkps://api.protonmail.ch
 keyserver hkps://keys.openpgp.org
 

If you have staged but not commited changes, use “git diff --staged”. Right now, I have committed everything so the stage is empty:

alex@melanobombus:~/src/home$ git diff --staged

Notice what happens when I stage my change: “git diff“ shows no changes and “git diff --staged“ shows the change!

alex@melanobombus:~/src/home$ git add .
alex@melanobombus:~/src/home$ git diff
alex@melanobombus:~/src/home$ git diff --staged
diff --git a/gpg.conf b/gpg.conf
index ee169ee..65ad9e3 100644
--- a/gpg.conf
+++ b/gpg.conf
@@ -135,7 +135,7 @@ charset utf-8
 #keyserver ldap://keyserver.pgp.com
 
 # gpg --keyserver ... --search-key ...@...
-# keyserver hkp://keys.gnupg.net
+# keyserver hkps://keys.openpgp.org
 # keyserver hkps://api.protonmail.ch
 keyserver hkps://keys.openpgp.org
 

If you use “git status” it’ll tell you what to do in order to unstage the change:

alex@melanobombus:~/src/home$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	modified:   gpg.conf

Let’s try it:

alex@melanobombus:~/src/home$ git reset HEAD .
Unstaged changes after reset:
M	gpg.conf

But let’s go back to “git diff” for a moment. We saw how “git log” gives us a history of all the changes. We can refer to those commits as well. A useful shortcut is this: append “^” to a commit to get the previous one. So if you want to see what the diff “New aliases for bc and cal” is all about, take the first few characters of the commit, “af8e2c” instead of “af8e2cebf19e15f73402e73e3d956749f27b5be9”, and let’s look at the difference between it’s predecessor “af8e2c^” and “af8e2c” itself.

alex@melanobombus:~/src/home$ git diff af8e2c^..af8e2c
diff --git a/.bash_aliases b/.bash_aliases
index cbce85d..00f956c 100644
--- a/.bash_aliases
+++ b/.bash_aliases
@@ -24,3 +24,9 @@ alias serve="python3 -m http.server"
 
 # mastodon archives
 alias tabletop="cd ~/Documents/Mastodon/ && mastodon-archive text kensanata@tabletop.social"
+
+# load math library and set scale=20 for the calculator
+alias bc="bc -ql"
+
+# show more months, add week numbers, start on Mondays
+alias cal="ncal -A2 -B1 -w -M"

Going back in time: checkout

Say you made a bunch of changes to one of your files and you really just want to get a copy of the file at some point in time. Use “git checkout” to go back to a particular commit. Let’s take the above situation. We look at the difference between “af8e2c” and it’s predecessor, “af8e2c^”. Say we want to go back in time: we want all the files from before that change.

First, verify that there are no changes we need to make:

alex@melanobombus:~/src/home$ git status
On branch master
nothing to commit, working tree clean

Looking good, let’s go back in time:

alex@melanobombus:~/src/home$ git checkout af8e2c^
Note: checking out 'af8e2c^'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at d89f0d0 Add tabletop alias

OK, so we checked out the predecessor of the af8e2c change. The “detached HEAD” state means that we moved back in time. You can think of the commits like the branch of a tree: it starts somewhere, and it has a tip, the so-called “head”. That’s where a new commit to the same branch is added. But if we move back in time, we move back along the branch, away from the “head”. We’re somewhere in the middle of a branch and we shouldn’t make any changes – at least for now. Let’s keep it simple: just look, don’t touch! 😃

Look at the old files, make copies, load them into your programs, whatever. And when you’re done, it’s time to return to the head. Reattaching head, I guess? Let’s do this!

We’ll use “git checkout” again. Instead of specifying a commit, however, we simply specify the default branch – implying the tip of the branch, the “head”.

alex@melanobombus:~/src/home$ git checkout master
Previous HEAD position was d89f0d0 Add tabletop alias
Switched to branch 'master'

Make sure we’re back! Remember, “git status” is your friend.

alex@melanobombus:~/src/home$ git status
On branch master
nothing to commit, working tree clean

Troubleshooting: reset

If you’re like me, however, things won’t be as smooth as this. There will be mistakes. Every body makes them, no worries. As long as you committed your changes, you should be able to get them back somehow.

Here’s what happened to me as I was preparing the example:

alex@melanobombus:~/src/home$ git checkout af8e2c^
error: Your local changes to the following files would be overwritten by checkout:
	.ecompleterc
Please commit your changes or stash them before you switch branches.
Aborting

Whaat? Apparently a file had been modified. If we go back in time, this modification will be overwritten by the older copy, and since we didn’t commit our change, the change will be lost.

In this case, I figured I might want to keep the change. I verified this by using “git diff”. Remember? It shows you the difference between the working directory and whatever you have staged (the “index”). Do I want to keep these changes, or toss them?

alex@melanobombus:~/src/home$ git diff
diff --git a/.ecompleterc b/.ecompleterc
index 4208244..19dcabc 100644
--- a/.ecompleterc
+++ b/.ecompleterc
…
(lots of stuff)

Let’s keep it by adding and committing the changes:

alex@melanobombus:~/src/home$ git add .
alex@melanobombus:~/src/home$ git commit -m "Updates to the mail addresses"
[master ff8b3f3] Updates to the mail addresses
 1 file changed, 5 insertions(+), 5 deletions(-)

Cool!

What happens if we want to toss changes changes? We want git to make a hard reset! Here’s me accidentally deleting a file using “rm” (should have used “trash” instead!), finding out about using “git status”, and then undoing it using “git reset --hard”.

alex@melanobombus:~/src/home$ rm .ecompleterc 
alex@melanobombus:~/src/home$ git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	deleted:    .ecompleterc

no changes added to commit (use "git add" and/or "git commit -a")
alex@melanobombus:~/src/home$ git reset --hard
HEAD is now at ff8b3f3 Updates to the mail addresses
alex@melanobombus:~/src/home$ git status
On branch master
nothing to commit, working tree clean

Phew! We got our file back.

There are other options to “git reset”, but you already guessed that. For now, I don’t think you’ll need them.

How to get back on track: a summary

If you don’t know what you’re doing, use “git status” and read it carefully.

If there are modified files and you forgot what changed, use “git diff” to look at the changes.

If there are new and modified files and you want to add them to your repo, use “git add .” (for the whole directory and all its subdirectories), and then use “git commit -m “some message”” to make a commit.

If you want to know about all your past commits, use “git log”. Hopefully you wrote good and long commit messages! Use “git log --oneline” if you only write short commit messages. 😅

If you want to know about a particular change in the past, usig “git diff 123456^ 123456” where 123456 are the first few digits of the commit “hash”, that weird code you see in the log for every commit.

If you want to go back in time, use “git checkout 123456^” to return to a time before change 123456 was made. Use “git checkout master” to come back to the present (the “head” of the default “branch”).

If there are changes you don’t want, that you can’t explain, if you just want to return to whatever you checked out, use “git reset --hard”.

Good luck!

That’s it. Hopefully that helps you get started.

Comments

An alternative to changing relevant files to symlinks in your home directory is to simply make ~/ a git repository.

The problem is that git status by default shows all files it doesn’t know about as “untracked” - but that can be remedied by adding:

[status]
    showuntrackedfiles = no

to your ~/.git/config.

The upside is that you don’t have to move the file, make a symlink, and go somewhere else to commit.

I use this “trick” in / on all my machines to keep track of configuration files I change.

Adam 2021-05-09 09:45 UTC


Interesting, thanks. I did not know about not showing untracked files. Does that work well if some of your subdirectories are again git repositories?

– Alex 2021-05-09 10:45 UTC


Yes, it works fine - I have plenty of git repositories in subfolders.

git will look for .git/ in the current directory, then the parent, then the grandparent etc. until it finds one, and that defines what repository you are “in”.

The only catch is that if you think you are in a git repo in a subfolder, and isn’t actually a repo, then you’re in a subfolder of the top/parent repository. In practice this very seldom confuses me; I might be weird that way, though.

Adam 2021-05-11 21:00 UTC


To avoid the potential confusion with Adam’s trick you can use a different name for the .git folder at your top level directory. I have the following alias I use to manage all my user level configuration files, etc.:

home='git --git-dir="${HOME}/.home-git" --work-tree="${HOME}"'

To set it up, go to your home directory and git init && rename .git .home-git (or maybe you could just do home init?). From then on use home for you config files, and git for everything else.

(I’m not sure why I added that --work-tree option is/was important… it may have been related to when I didn’t have the same username on all computers?)

– Björn Buckwalter 2021-05-14 17:05 UTC


Ending up with two different commands for the two kinds of repositories is a great idea.

Definitely a level tinkering that goes beyond “for beginners”, though! 😀

– Alex 2021-05-14 18:18 UTC


Please make sure you contribute only your own work, or work licensed under the GNU Free Documentation License. Note: in order to facilitate peer review and fight vandalism, we will store your IP number for a number of days. See Privacy Policy for more information. See Info for text formatting rules. You can edit the comment page if you need to fix typos. You can subscribe to new comments by email without leaving a comment.

To save this page you must answer this question:

Just say HELLO