Install Homebrew

On MacOS we will use Homebrew to manage the installation of most development related packages. Homebrew (or simply brew) is a package manager that lets you install a wide range of open source and binary packages in a consistent way.

Get brew installed with a curl|bash. Open your terminal and use the command below to run the installer.

> /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

You can check out the detailed installation instructions on the Homebrew website. That's also where you'll want to go to discover packages and get help.

For now we'll really only be using the install command.

Install Git

Now that Homebrew is installed, use that to get Git installed.

> brew install git

You can check that git is working properly by executing it with the --version flag.

> git --version

Next: LAB-02: Github setup

Set up your Github account

Head to github.com and sign up for a new account with your work email address. Select a username that you wouldn't mind a client seeing (though, that's pretty unlikely). Use a secure password generated by your password manager of choice.

If you have an active Github presence then you should feel free to add a link to your personal account in the descirption for your new work account.

Setting up a new account for work is a great example of how we want to keep your personal and work accounts separate.

Add MFA to your account

It's good practice to use MFA for all sites that have it available. This is critically important when our confidential information is at play.

Head to your account settings and set up your mobile device or hardware token as a second factor. You should already have Microsoft Authenticator installed to use as a second factor for your work email account.

If you already have Authy, Google Authenticator or another TOTP application installed then feel free to use that instead.

Generate and add an SSH key

Follow Github's detailed instructions for generating and adding a new SSH key to your account.

Get added to our Github organisation

Send a quick message to one of today's hosts to be added to the Github organisation. Once you're there you'll be able to see some of our internal projects (including the respository for this workshop!)

Next: LAB-03: Configure Git

Set up your global Git configuration

The .gitconfig file in your home directory lets you control global configuration for git. You can edit the file manually, or use git config on the command line to modify it.

Start by setting your name and email address:

> git config user.email <your-work-email>
> git config user.name <your-full-name>

You can use cat to display the output of your .gitconfig file:

> cat ~/.gitconfig

Next: LAB-04: Create a Project

Create a "Hello, World" program

Let's start by creating an empty directory named “hello”.

> mkdir hello
> cd hello

Then add the following code to a new file called hello.rb:

puts "Hello, World"

It's optional, but at this point you can execute this file with the ruby interpreter to see the output.

> ruby hello.rb

Congratulations, you're a ruby development now :D.

Create the Repository

You now have a directory with a single file. To create a git repository from that directory, run the git init command.

> git init
> git init
Initialized empty Git repository in /Users/jim/Downloads/git_tutorial/work/hello/.git/

Add the program to the repository

Now let’s add the “Hello, World” program to the repository.

> git add hello.rb
> git commit -m "First Commit"
> git add hello.rb
> git commit -m "First Commit"
[main (root-commit) 19f3881] First Commit
1 file changed, 1 insertion(+)
create mode 100644 hello.rb

Next: LAB-05: Checking Status

Check the status of the repository

Use the git status command to check the current status of the repository.

> git status
> git status
On branch main
nothing to commit, working tree clean

The status command reports that there is nothing to commit. This means that the repository has all the current state of the working directory. There are no outstanding changes to record.

We will use the git status command to continue to monitor the state between the repository and the working directory.

Next: LAB-06: Making Changes

Change the "Hello, World" program

It’s time to change our hello program to take an argument from the command line. Change the file to be:

puts "Hello, #{ARGV.first}!"

Check the status

Now check the status of the working directory.

> 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:   hello.rb

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

The first thing to notice is that git knows that the hello.rb file has been modified, but git has not yet been notified of these changes.

Also notice that the status message gives you hints about what you need to do next. If you want to add these changes to the repository, then use the git add command. Otherwise the git restore command can be used to discard the changes.

Next: LAB-07: Staging Changes

Add changes

Now tell git to stage the changes. Then, check the status.

> git add hello.rb
> git status

You should see:

> git add hello.rb
> git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
    modified:   hello.rb

The change to the hello.rb file has been staged. This means that git now knows about the change, but the change hasn’t been permanently recorded in the repository yet. The next commit operation will include the staged changes.

If you decide you don’t want to commit that change after all, the status command reminds you that the git restore command can be used to unstage that change.

Next: LAB-08: Staging and Committing

Staging and Committing

A separate staging step in git is in line with the philosophy of getting out of the way until you need to deal with source control. You can continue to make changes to your working directory, and then at the point you want to interact with source control, git allows you to record your changes in small commits that record exactly what you did.

For example, suppose you edited three files (a.rb, b.rb, and c.rb). Now you want to commit all the changes, but you want the changes in a.rb and b.rb to be a single commit, while the changes to c.rb are not logically related to the first two files and should be a separate commit.

You could do the following:

> git add a.rb
> git add b.rb
> git commit -m "Changes for a and b"
git add c.rb
git commit -m "Unrelated change to c"

By separating staging and committing, you have the ability to easily fine tune what goes into each commit.

Next: LAB-09: Committing Changes

Commit the change

Ok, enough about staging. Let’s commit what we have staged to the repository.

When you used git commit previously to commit the initial version of the hello.rb file to the repository, you included the -m flag that gave a comment on the command line. The commit command will allow you to interactively edit a comment for the commit. Let’s try that now.

If you omit the -m flag from the command line, git will pop you into the editor of your choice. The editor is chosen from the following list (in priority order):

  • GIT_EDITOR environment variable
  • core.editor configuration setting
  • VISUAL environment variable
  • EDITOR environment variable

I have the EDITOR variable set to nano.

So commit now and check the status.

> git commit

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch main
# Changes to be committed:
#    modified:   hello.rb
#

On the first line, enter the comment: “Using ARGV”. Save the file and exit the editor. You should see output similar to the following.

> git commit
[main 2eac0b2] Using ARGV
1 file changed, 1 insertion(+), 1 deletion(-)

Check the status

Finally, let's check the status again.

> git status
> git status
On branch main
nothing to commit, working tree clean

The working directory is clean and ready for you to continue.

Next: LAB-10: Changes, not Files

Changes, not Files

Most source control systems work with files. You add a file to source control and the system will track changes to the file from that point on.

Git focuses on the changes to a file rather than the file itself. When you say git add file, you are not telling git to add the file to the repository. Rather you are saying that git should make note of the current state of that file to be committed later.

We will attempt to explore that difference in this lab.

First Change: Allow a default name

Change the “Hello, World” program to have a default value if a command line argument is not supplied.

name = ARGV.first || "World"

puts "Hello, #{name}!"

Add this change

Now add this change to git's staging area.

> git add hello.rb

Second change: Add a comment

Now add a comment to the “Hello, World” program.

# Default is "World"
name = ARGV.first || "World"

puts "Hello, #{name}!"

Check the current status

> git status

You should see:

git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
    modified:   hello.rb

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:   hello.rb

Notice how hello.rb is listed twice in the status. The first change (adding a default) is staged and is ready to be committed. The second change (adding a comment) is unstaged. If you were to commit right now, the comment would not be saved in the repository.

Let’s try that.

Committing

Commit the staged change (the default value), and then recheck the status.

> git commit -m "Added a default value"
> git status

You should see:

> git commit -m "Added a default value"
[main 58498b1] Added a default value
1 file changed, 3 insertions(+), 1 deletion(-)
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:   hello.rb

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

The status command is telling you that hello.rb has unrecorded changes, but is no longer in the staging area.

Add the second change

Now add the second change to the staging area, then run git status.

> git add .
> git status

We just used the current directory (‘.’) as the file to add. This is a really convenient shortcut for adding in all the changes to the files in the current directory and below. But since it adds everything, it is a really good idea to check the status before doing an add ., just to make sure you don’t add any file that is not intended.

You should see:

> git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
    modified:   hello.rb

Now the second change has been staged and is ready to commit.

Commit the second change

> git commit -m "Added a comment"

Next: LAB-11: History

History

Getting a listing of what changes have been made is the function of the git log command.

> git log

You should see:

> git log
commit c51cd709b284ba82ae2c7d1578e242cd11dc8a0e
Author: Git Committer <me@example.org>
Date:   Mon Aug 15 14:54:42 2022 +1000

    Added a comment

commit 58498b15c685b8332eb2031764cb2fd80e18a886
Author: Git Committer <me@example.org>
Date:   Mon Aug 15 14:54:42 2022 +1000

    Added a default value

commit a03ff84b97f40549ea1e21c4680a2fd3f30eeec4
Author: Git Committer <me@example.org>
Date:   Mon Aug 15 14:54:42 2022 +1000

    Using ARGV

commit 19f3881f40ceb5fa20328bf94a749b2c2ba650d7
Author: Git Committer <me@example.org>
Date:   Mon Aug 15 14:54:42 2022 +1000

    First Commit

One Line Histories

You have a great deal of control over exactly what the log command displays. I like the one line format:

> git log --pretty=oneline
> git log --pretty=oneline
c51cd709b284ba82ae2c7d1578e242cd11dc8a0e Added a comment
58498b15c685b8332eb2031764cb2fd80e18a886 Added a default value
a03ff84b97f40549ea1e21c4680a2fd3f30eeec4 Using ARGV
19f3881f40ceb5fa20328bf94a749b2c2ba650d7 First Commit

Controlling Which Entries are Displayed

There are a lot of options for selecting which entries are displayed in the log. Play around with the following options:

> git log --pretty=oneline --max-count=2
> git log --pretty=oneline --since='5 minutes ago'
> git log --pretty=oneline --until='5 minutes ago'
> git log --pretty=oneline --author=<your name>
> git log --pretty=oneline --all

See man git-log for all the details.

Getting Fancy

Here’s what I use to review the changes made in the last week. I’ll add --author=<my-name> if I only want to see changes I made.

> git log --all --pretty=format:'%h %cd %s (%an)' --since='7 days ago'

The Ultimate Log Format

Over time, I’ve decided that I like the following log format for most of my work.

git log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short

It looks like this:

> git log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short
* c51cd70 2022-08-15 | Added a comment (HEAD -> main) [Git Committer]
* 58498b1 2022-08-15 | Added a default value [Git Committer]
* a03ff84 2022-08-15 | Using ARGV [Git Committer]
* 19f3881 2022-08-15 | First Commit [Git Committer]

Let’s look at it in detail:

  • `--pretty="..."`` defines the format of the output.
  • %h is the abbreviated hash of the commit
  • %d are any decorations on that commit (e.g. branch heads or tags)
  • %ad is the author date
  • %s is the comment
  • %an is the author name
  • --graph informs git to display the commit tree in an ASCII graph layout
  • --date=short keeps the date format nice and short

This is a lot to type every time you want to see the log. Fortunately we will learn about git aliases in the next lab.

Other Tools

Both gitx (for Macs) and gitk (any platform) are useful in exploring log history.

Next: LAB-12: Aliases

Common Aliases

git status, git add, git commit, git switch and git checkout are such common commands that it is useful to have abbreviations for them.

Add the following to the .gitconfig file in your $HOME directory:

\[alias]  # remove this comment, and the leading slash (it breaks the layout!)
    co = checkout
    ci = commit
    st = status
    br = branch
    sw = switch
    hist = log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short
    type = cat-file -t
    dump = cat-file -p

We’ve covered the commit and status commands already. And we just covered the log command in the previous lab. The checkout command will be coming up soon.

With these aliases defined in the .gitconfig file you can type git co wherever you used to have to type git checkout. Likewise with git st for git status and git ci for git commit. And best of all, git hist will allow you to avoid the really long log command.

Go ahead and give the new commands a try.

Define the hist alias in your .gitconfig file

For the most part, I will continue to type out the full command in these instructions. The only exception is that I will use the hist alias defined above anytime we need to see the git log output. Make sure you have a hist alias setup in your .gitconfig file before continuing if you wish to follow along.

Type and Dump

We’ve added a few aliases for commands we haven’t covered yet. The git branch command will be coming up soon. And the git cat-file command is useful for exploring git, which we will see in a little while.

Next: LAB-13: Getting Old Versions

Getting Old Versions

Going back in history is very easy. The checkout command will copy any snapshot from the repository to the working directory.

Get the hashes for previous versions

git hist

Note: You did remember to define hist in your .gitconfig file, right? If not, review the lab on aliases.

> git hist
* db1f4d1 2022-08-16 | Added a comment (HEAD -> main) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value  [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

Examine the log output and find the hash for the first commit. It should be the last line of the git hist output. Use that hash code (the first 7 characters are enough) in the command below. Then check the contents of the hello.rb file.

> git checkout <hash>
> cat hello.rb

You should see something similar to:

> git checkout 19f3881
Note: switching to '19f3881'.

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 switching back to a branch.

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

git switch -c <new-branch-name>

Or undo this operation with:

git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 19f3881 First Commit
> cat hello.rb
puts "Hello, World"

The output of the checkout command explains the situation pretty well. Older versions of git will complain about not being on a local branch. In any case, don’t worry about that for now.

Notice the contents of the hello.rb file are the original contents.

Return to the latest version in the main branch

> git switch main
> cat hello.rb
> git checkout main
Previous HEAD position was 19f3881 First Commit
Switched to branch 'main'
> cat hello.rb
# Default is "World"
name = ARGV.first || "World"

puts "Hello, #{name}!"

main is the name of the default branch. By checking out a branch by name, you go to the latest version of that branch.

Next: LAB-14: Tagging versions

Tagging our first version

Let’s call the current version of the hello program version 1 (v1).

> git tag v1

Now you can refer to the current version of the program as v1.

Tagging previous versions

Let’s tag the version immediately prior to the current version v1-beta. First we need to checkout the previous version. Rather than look up the hash, we will use the `^`` notation to indicate “the parent of v1”.

If the v1^`` notation gives you any trouble, you can also tryv1~1`, which will reference the same version. This notation means “the first ancestor of v1”.

> git checkout v1^
> cat hello.rb
> git checkout v1^
Note: switching to 'v1^'.

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 switching back to a branch.

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

git switch -c <new-branch-name>

Or undo this operation with:

git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 58498b1 Added a default value
> cat hello.rb
name = ARGV.first || "World"

puts "Hello, #{name}!"

See, this is the version with the default value before we added the comment. Let’s make this v1-beta.

> git tag v1-beta

Checking Out by Tag Name

Now try going back and forth between the two tagged versions.

> git checkout v1
> git checkout v1-beta
> git checkout v1
Previous HEAD position was 58498b1 Added a default value
HEAD is now at c51cd70 Added a comment
> git checkout v1-beta
Previous HEAD position was c51cd70 Added a comment
HEAD is now at 58498b1 Added a default value

Viewing Tags using the tag command

You can see what tags are available using the git tag command.

git tag
> git tag
v1
v1-beta

Viewing Tags in the Logs

You can also check for tags in the log.

> git hist main --all
> git hist main --all
* db1f4d1 2022-08-16 | Added a comment (HEAD -> main, tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

You can see both tags (v1 and v1-beta) listed in the log output, along with the branch name (main). Also HEAD shows you the currently checked out commit (which is v1-beta at the moment).

Next: LAB-15: Undoing Local Changes Before Staging

Undoing Local Changes Before Staging

Let's learn how to roll back a change we've made to the code.

Checkout main

Make sure you are back on the latest commit in main before continuing.

> git checkout main

Change hello.rb

Sometimes you have modified a file in your local working directory and you wish to just revert to what has already been committed. The checkout command will handle that.

Change hello.rb to have a bad comment.

# This is a bad comment.  We want to revert it.
name = ARGV.first || "World"

puts "Hello, #{name}!"

Check the status

First, check the status of the working directory.

> 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:   hello.rb

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

We see that the hello.rb file has been modified, but hasn’t been staged yet.

Restore the changes to the working directory

Use the restore command to go back to the repository's version of the hello.rb file.

> git restore hello.rb
> git status
> cat hello.rb
> git restore hello.rb
> git status
On branch main
nothing to commit, working tree clean
> cat hello.rb
# Default is "World"
name = ARGV.first || "World"

puts "Hello, #{name}!"

The status command shows us that there are no outstanding changes in the working directory. And the “bad comment” is no longer part of the file contents.

Next: LAB-16: Undoing Local Changes Before Committing

Change the file and stage the change

Modify the hello.rb again file to have a bad comment.

# This is an unwanted but staged comment
name = ARGV.first || "World"

puts "Hello, #{name}!"

And then go ahead and stage it.

> git add hello.rb

Check the status

> git status
git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
    modified:   hello.rb

The status output shows that the change has been staged and is ready to be committed. Like a lot of git commands, it also gives us a hint that we can use to go back.

Restore the staging area

Fortunately the status output tells us exactly what we need to do to unstage the change.

> git restore --staged hello.rb

The restore --staged command takes the staging area back to be whatever is in HEAD. This clears the staging area of the change we just staged.

Using restore --staged doesn’t change the working directory. So the working directory still has the unwanted comment in it. We can use the restore command of the previous lab to remove the unwanted change from the working directory.

Restor the committed version

> git restore hello.rb
> git status
> git status
On branch main
nothing to commit, working tree clean

And our working directory is clean once again.

Next: LAB-17: Undoing Committed Changes

Undoing Commits

Sometimes you realise that a change that you have already committed was not correct and you wish to undo that commit. There are several ways of handling that issue, and the way we are going to use in this lab is always safe.

Essentially we will undo the commit by creating a new commit that reverses the unwanted changes.

Change the file and commit it

Change the hello.rb file to the following.

# This is an unwanted but committed change
name = ARGV.first || "World"

puts "Hello, #{name}!"

Then commit the change.

> git add hello.rb
> git commit -m "Oops, we didn't want this commit"

Create a Reverting Commit

To undo a committed change, we need to generate a commit that removes the changes introduced by our unwanted commit.

git revert HEAD

This will pop you into the editor. You can edit the default commit message or leave it as is. Save and close the file. You should see the following.

> git revert HEAD
[main 0110f9a] Revert "Oops, we didn't want this commit"
Date: Mon Aug 15 14:54:43 2022 +1000
1 file changed, 1 insertion(+), 1 deletion(-)

Since we were undoing the very last commit we made, we were able to use HEAD as the argument to revert. We can revert any arbitrary commit earlier in history by simply specifying its hash value.

Check the log

Checking the log shows both the unwanted and the reverting commits in our repository.

> git hist
> git hist
* 567e59e 2022-08-16 | Revert "Oops, we didn't want this commit" (HEAD -> main) [Brenton Cleeland]
* 148b282 2022-08-16 | Oops, we didn't want this commit [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

This technique will work with any commit (although you may have to resolve conflicts). It is safe to use even on branches that are publicly shared on remote repositories.

Next: LAB-18: Removing Commits from a Branch

Removing Commits from a Branch

The revert command of the previous section is a powerful command that lets us undo the effects of any commit in the repository. However, both the original commit and the “undoing” commit are visible in the branch history (using the git log command).

Often we make a commit and immediately realize that it was a mistake. It would be nice to have a “take back” command that would allow us to pretend that the incorrect commit never happened. The “take back” command would even prevent the bad commit from showing up the git log history. It would be as if the bad commit never happened.

The reset command

We’ve already seen the restore command and have used it to set the staging area to be consistent with a given commit (we used the HEAD commit in our previous lab). The reset command is a similar command that lets us completely remove commits from a branch.

When given a commit reference (i.e. a hash, branch or tag name), the reset command will:

  1. Rewrite the current branch to point to the specified commit
  2. Optionally reset the staging area to match the specified commit
  3. Optionally reset the working directory to match the specified commit

Check Our History

Let's do a quick check of our commit history.

> git hist
> git hist
* 567e59e 2022-08-16 | Revert "Oops, we didn't want this commit" (HEAD -> main) [Brenton Cleeland]
* 148b282 2022-08-16 | Oops, we didn't want this commit [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

We see that we have an “Oops” commit and a “Revert Oops” commit as the last two commits made in this branch. Let’s remove them using reset.

First, Mark this Branch

But before we remove the commits, let’s mark the latest commit with a tag so we can find it again.

> git tag oops

Reset to Before Oops

Looking at the log history (above), we see that the commit tagged v1 is the commit right before the bad commit. Let’s reset the branch to that point. Since that branch is tagged, we can use the tag name in the reset command (if it wasn’t tagged, we could just use the hash value).

> git reset --hard v1
> git hist
git reset --hard v1
HEAD is now at db1f4d1 Added a comment
* db1f4d1 2022-08-16 | Added a comment (HEAD -> main, tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

Our main branch now points to the v1 commit and the Oops commit and the Revert Oops commit are no longer in the branch. The --hard parameter indicates that the working directory should be updated to be consistent with the new branch head.

Nothing is Ever Lost

But what happened to the bad commits? It turns out that the commits are still in the repository. In fact, we can still reference them. Remember that at the beginning of this lab we tagged the reverting commit with the tag “oops”. Let’s look at all the commits.

git hist --all
git hist --all
* 567e59e 2022-08-16 | Revert "Oops, we didn't want this commit" (tag: oops) [Brenton Cleeland]
* 148b282 2022-08-16 | Oops, we didn't want this commit [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (HEAD -> main, tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

Here we see that the bad commits haven’t disappeared. They are still in the repository. It’s just that they are no longer listed in the main branch. If we hadn’t tagged them, they would still be in the repository, but there would be no way to reference them other than using their hash names. Commits that are unreferenced remain in the repository until the system runs the garbage collection software.

Dangers of Reset

Resets on local branches are generally safe. Any “accidents” can usually be recovered from by just resetting again with the desired commit.

However, if the branch is shared on remote repositories, resetting can confuse other users sharing the branch. As a general rule you should revert not reset if you have shared your branch by pushing it to a remote repository.

Removing tag oops

The oops tag has served its purpose. Let’s remove it and allow the commits it referenced to be garbage collected.

> git tag -d oops
> git hist --all
> git tag -d oops
Deleted tag 'oops' (was 567e59e)
> git hist --all
* db1f4d1 2022-08-16 | Added a comment (HEAD -> main, tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

The oops tag is no longer listed in the repository.

Next: LAB-19: Ammending Commits

Change the program, then commit

Let's add a simple author comment to the program.

# Default is World
# Author: <your name>
name = ARGV.first || "World"

puts "Hello, #{name}!"
> git add hello.rb
> git commit -m "Add an author comment"

Oops, Should have an Email

After you make the commit, you realize that any good author comment should have an email included. Update the hello program to include an email.

# Default is World
# Author: <your name> (me@example.org)
name = ARGV.first || "World"

puts "Hello, #{name}!"

Ammend the Previous Commit

We really don’t want a separate commit for just the email. Let’s amend the previous commit to include the email change.

> git add hello.rb
> git commit --amend -m "Add an author/email comment"
> git add hello.rb
> git commit --amend -m "Add an author/email comment"
[main af90521] Add an author/email comment
Date: Mon Aug 15 14:54:43 2022 +1000
1 file changed, 2 insertions(+), 1 deletion(-)

Review the History

> git hist
> git hist
* af90521 2022-08-16 | Add an author/email comment (HEAD -> main) [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

We can see the original “author” commit is now gone, and it is replaced by the “author/email” commit. You can achieve the same effect by resetting the branch back one commit and then recommitting the new changes.

Similarly to the reset command in the previous lab, you need to be careful not to ammend commits that you've already shared with others by pushing to a remote repository.

Next: LAB-20: Moving Files

Move the hello.rb file into a lib directory

We are now going to build up the structure of our little repository. Let’s move the program into a lib directory.

> mkdir lib
> git mv hello.rb lib
> git status
> mkdir lib
> git mv hello.rb lib
> git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
    renamed:    hello.rb -> lib/hello.rb

By using git to do the move, we inform git of 2 things

  1. That the file hello.rb has been deleted.
  2. The file lib/hello.rb has been created.

Both of these bits of information are immediately staged and ready to be committed. The git status command reports that the file has been moved.

Another way of moving files

One of the nice things about git is that you can forget about source control until the point you are ready to start committing code. What would happen if we used the operating system command to move the file instead of the git command?

It turns out the following set of commands is identical to what we just did. It’s a bit more work, but the result is the same.

We could have done:

> mkdir lib
> mv hello.rb lib
> git add lib/hello.rb
> git rm hello.rb

Commit the new directory

Let's commit this change before we move on.

> git commit -m "Moved hello.rb to lib"

Next: LAB-21: Git Internals - The .git Directory

The .git Directory

Let's take a quick detour and explore some of git's internals. First, from the root of your project directory:

> ls -C .git
> ls -C .git
COMMIT_EDITMSG    config        index        objects
HEAD        description    info        packed-refs
ORIG_HEAD    hooks        logs        refs

This is the magic directory where all the git “stuff” is stored. Let’s peek in the objects directory.

The Object Store

> ls -C .git/objects
> ls -C .git/objects
01    11    27    43    69    7d    98    af    c5    info
05    19    28    58    6b    94    9c    b5    e4    pack
09    24    37    59    78    97    a0    c4    e7

You should see a bunch of directories with 2 letter names. The directory names are the first two letters of the sha1 hash of the object stored in git.

Deeper into the Object Store

> ls -C .git/objects/<dir>
> ls -C .git/objects/01
10f9a5cf0047ac7cb1938734e238f48a47ffb4

Look in one of the two-letter directories. You should see some files with 38-character names. These are the files that contain the objects stored in git. These files are compressed and encoded, so looking at their contents directly won’t be very helpful, but we will take a closer look in a bit.

Config File

> cat .git/config
> cat .git/config
\[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true
\[user]
    name = Git User
    email = me@example.org

This is a project-specific configuration file. Config entries in here will override the config entries in the .gitconfig file in your home directory, at least for this project.

Branches and Tags

> ls .git/refs
> ls .git/refs/heads
> ls .git/refs/tags
> cat .git/refs/tags/v1

```output
> ls .git/refs
heads
tags
> ls .git/refs/heads
main
> ls .git/refs/tags
v1
v1-beta
> cat .git/refs/tags/v1
c51cd709b284ba82ae2c7d1578e242cd11dc8a0e

You should recognize the files in the tags subdirectory. Each file corresponds to a tag you created with the git tag command earlier. Its content is just the hash of the commit tied to the tag.

The heads directory is similar, but is used for branches rather than tags. We only have one branch at the moment, so all you will see is main in this directory.

The HEAD File

> cat .git/HEAD

```output
> cat .git/HEAD
ref: refs/heads/main

The HEAD file contains a reference to the current branch. It should be a reference to main at this point.

Next: LAB-22: Creating a Branch

Create a new branch

It’s time to do a major rewrite of the hello world functionality. Since this might take awhile, you’ll want to put these changes into a separate branch to isolate them from changes in main.

Let’s call our new branch greet.

> git checkout -b greet
> git status

NOTE: git checkout -b <branchname> is a shortcut for git branch <branchname> followed by a git switch <branchname>.

Notice that the git status command reports that you are on the greet branch.

Changes for Greet: Add a Greeter class

Create a new file called lib/greeter.rb with the following content:

class Greeter
    def initialize(who)
        @who = who
    end
    def greet
        "Hello, #{@who}"
    end
end

Then add and commit the new file:

> git add lib/greeter.rb
> git commit -m "Added greeter class"

Changes for Greet: Modify the main program

Update hello.rb to use the greeter class:

require_relative 'greeter'

# Default is World
name = ARGV.first || "World"

greeter = Greeter.new(name)
puts greeter.greet

Add and commit this change as well:

> git add lib/hello.rb
> git commit -m "Hello uses Greeter"

Up Next

We now have a new branch called greet with 2 new commits on it. Next we will learn how to navigate and switch between branches.

Next: LAB-23: Navigating Branches

Switch to the main branch

> git switch main

Create a README

Create a new file called README.md with the following content:

This is the Hello World example from the git tutorial.

Commit the README to main

> git add README.md
> git commit -m "Added README.md"

Next: LAB-25: Viewing Diverging Branches

View the Current Branches

We now have two diverging branches in the repository. Use the hist alias we created earlier to view the branches and how they diverge.

> git hist --all
* 22d8379 2022-08-16 | Added README.md (HEAD -> main) [Brenton Cleeland]
| * 9ca04ea 2022-08-16 | Hello uses Greeter (greet) [Brenton Cleeland]
| * 34c8660 2022-08-16 | Added greeter class [Brenton Cleeland]
|/
* 41ff07c 2022-08-16 | Moved hello.rb to lib [Brenton Cleeland]
* af90521 2022-08-16 | Add an author/email comment [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

Here is our first chance to see the --graph option on git hist in action. Adding the --graph option to git log causes it to draw the commit tree using simple ASCII characters. We can see both branches (greet and main), and that the main branch is the current HEAD.

The --all flag makes sure that we see all the branches. The default is to show only the current branch.

Next: LAB-26: Merging

Merge the branches

Merging brings the changes in two branches together. Let’s go back to the greet branch and merge main onto greet.

> git switch greet
> git merge main
> git hist --all
> git merge main
Merge made by the 'ort' strategy.
README.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 README.md
> git hist --all
*   4f17759 2022-08-16 | Merge branch 'main' into greet (HEAD -> greet) [Brenton Cleeland]
|\
| * 22d8379 2022-08-16 | Added README.md (main) [Brenton Cleeland]
* | 9ca04ea 2022-08-16 | Hello uses Greeter [Brenton Cleeland]
* | 34c8660 2022-08-16 | Added greeter class [Brenton Cleeland]
|/
* 41ff07c 2022-08-16 | Moved hello.rb to lib [Brenton Cleeland]
* af90521 2022-08-16 | Add an author/email comment [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

By merging main into your greet branch periodically, you can pick up any changes to main and keep your changes in greet compatible with changes in the mainline.

However, it does produce ugly commit graphs. Later we will look at the option of rebasing rather than merging.

Up Next

But first, what if the changes in main conflict with the changes in greet?

Next: LAB-27: Creating a Conflict

Switch back to main and create a conflict

Switch back to the main branch and make this change:

> git switch main

Update lib/hello.rb with the following:

puts "What's your name"
my_name = gets.strip

puts "Hello, #{my_name}!"
> git add lib/hello.rb
> git commit -m "Made interactive"

We now have changes to lib/hello.rb on both of our active branches. A common situation if you have more than one person working on your codebase!

View the branches

> git hist --all
> git hist --all
* ef40b54 2022-08-16 | Made interactive (HEAD -> main) [Brenton Cleeland]
| *   4f17759 2022-08-16 | Merge branch 'main' into greet (greet) [Brenton Cleeland]
| |\
| |/
|/|
* | 22d8379 2022-08-16 | Added README.md [Brenton Cleeland]
| * 9ca04ea 2022-08-16 | Hello uses Greeter [Brenton Cleeland]
| * 34c8660 2022-08-16 | Added greeter class [Brenton Cleeland]
|/
* 41ff07c 2022-08-16 | Moved hello.rb to lib [Brenton Cleeland]
* af90521 2022-08-16 | Add an author/email comment [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

Main at commit “Added README” has been merged to the greet branch, but there is now an additional commit on main that has not been merged back to greet.

Next: LAB-28: Resolving Conflicts

Merge main to greet

Now go back to the greet branch and try to merge the new main.

> git checkout greet
> git merge main
> git merge main
Auto-merging lib/hello.rb
CONFLICT (content): Merge conflict in lib/hello.rb
Automatic merge failed; fix conflicts and then commit the result.

If you open lib/hello.rb you will see git's conflict markers in the file:

<<<<<<< HEAD
require_relative 'greeter'

# Default is World
name = ARGV.first || "World"

greeter = Greeter.new(name)
puts greeter.greet
=======
puts "What's your name"
my_name = gets.strip

puts "Hello, #{my_name}!"
>>>>>>> main

The first section is the version on the head of the current branch (greet). The second section is the version on the main branch.

Fix the Conflict

You need to manually resolve the conflict. In this case we want to keep a version that supports both pieces of functionality. Modify lib/hello.rb to be the following:

require_relative 'greeter'

puts "What's your name"
my_name = gets.strip

greeter = Greeter.new(my_name)
puts greeter.greet

Commit the Conflict Resolution

> git add lib/hello.rb
> git commit -m "Merged main, fixed conflict."
> git add lib/hello.rb
> git commit -m "Merged main, fixed conflict."
[greet 8822978] Merged main, fixed conflict.

Advanced Merging

Git doesn't provide any graphical merge tools, but it will gladly work with any third party merge tool you with to use. Most modern editors (like VS Code) include merge tools that you can test. The Customising Git Configuration section of the Git Book includes details about configuring Perforce as your default merge tool.

Next: LAB-29: Merging vs Rebasing

Merging vs Rebasing

Let’s explore the differences between merging and rebasing. In order to do so, we will make a new change on the main branch, then use git rebase to move that onto our greet branch.

Start by switching to main:

> git switch main

Update the README.md

Add a thank you to the original tutorial author in the README.md:

This is the Hello World example from the git tutorial.

Thanks to Jim Weirich for the initial work on this tutorial for RailsConf 2010.

Now, lets commit that change on main.

> git add README.md
> git commit -m "Add a thank you to Jim"

View the history

While still on the main branch, check the history.

> git hist --all
git hist --all
* f6a7d74 2022-08-16 | Add a thank you to Jim (HEAD -> main) [Brenton Cleeland]
| *   8822978 2022-08-16 | Merged main, fixed conflict. (greet) [Brenton Cleeland]
| |\
| |/
|/|
* | ef40b54 2022-08-16 | Made interactive [Brenton Cleeland]
| *   4f17759 2022-08-16 | Merge branch 'main' into greet [Brenton Cleeland]
| |\
| |/
|/|
* | 22d8379 2022-08-16 | Added README.md [Brenton Cleeland]
| * 9ca04ea 2022-08-16 | Hello uses Greeter [Brenton Cleeland]
| * 34c8660 2022-08-16 | Added greeter class [Brenton Cleeland]
|/
* 41ff07c 2022-08-16 | Moved hello.rb to lib [Brenton Cleeland]
* af90521 2022-08-16 | Add an author/email comment [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

In the log you can see the new commit at the top. Switch to the greet branch and view the history again, it should look the same.

> git switch greet
> git hist --all
> git hist --all
* f6a7d74 2022-08-16 | Add a thank you to Jim (main) [Brenton Cleeland]
| *   8822978 2022-08-16 | Merged main, fixed conflict. (HEAD -> greet) [Brenton Cleeland]
| |\
| |/
|/|
* | ef40b54 2022-08-16 | Made interactive [Brenton Cleeland]
| *   4f17759 2022-08-16 | Merge branch 'main' into greet [Brenton Cleeland]
| |\
| |/
|/|
* | 22d8379 2022-08-16 | Added README.md [Brenton Cleeland]
| * 9ca04ea 2022-08-16 | Hello uses Greeter [Brenton Cleeland]
| * 34c8660 2022-08-16 | Added greeter class [Brenton Cleeland]
|/
* 41ff07c 2022-08-16 | Moved hello.rb to lib [Brenton Cleeland]
* af90521 2022-08-16 | Add an author/email comment [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

Rebase the change onto our branch

This time we will use the rebase command instead of the merge command to bring in the changes from the main branch.

> git rebase main
> git rebase main
Auto-merging lib/hello.rb
CONFLICT (content): Merge conflict in lib/hello.rb
error: could not apply 9ca04ea... Hello uses Greeter
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 9ca04ea... Hello uses Greeter

Ut, oh! We have the same conflict during this rebase that we resolved earlier. This is a great example of why it can be tricky to mix rebasing and merging together.

Resolve the conflict by updating lib/hello.rb with the expected content:

require_relative 'greeter'

puts "What's your name"
my_name = gets.strip

greeter = Greeter.new(my_name)
puts greeter.greet

Then continue through the rebase:

> git add lib/hello.rb
> git rebase --continue
> git add lib/hello.rb
> git rebase --continue
[detached HEAD 630033d] Hello uses Greeter
1 file changed, 4 insertions(+), 1 deletion(-)
Successfully rebased and updated refs/heads/greet.

Check the history

Finally, run git hist again to see what's happened to our "Add a thank you" commit.

> git hist
> git hist
* 630033d 2022-08-16 | Hello uses Greeter (HEAD -> greet) [Brenton Cleeland]
* 55d0c6b 2022-08-16 | Added greeter class [Brenton Cleeland]
* f6a7d74 2022-08-16 | Add a thank you to Jim (main) [Brenton Cleeland]
* ef40b54 2022-08-16 | Made interactive [Brenton Cleeland]
* 22d8379 2022-08-16 | Added README.md [Brenton Cleeland]
* 41ff07c 2022-08-16 | Moved hello.rb to lib [Brenton Cleeland]
* af90521 2022-08-16 | Add an author/email comment [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

Notice that our "Add a thank you" commit has moved before the new commits on our greet branch. This has made our commit history completely linear.

Merge VS Rebase

The final result of the rebase is very similar to the merge.

The greet branch now contains all of its changes, as well as all the changes from the main branch. However, the commit tree is quite different.

The commit tree for the greet branch has been rewritten so that the main branch is a part of the commit history. This leaves the chain of commits linear and much easier to read.

When to Rebase, When to Merge?

Don’t use rebase...

  • If the branch is public and shared with others. Rewriting publicly shared branches will tend to screw up other members of the team.
  • When the exact history of the commit branch is important (since rebase rewrites the commit history).

Given the above guidelines, I tend to use rebase for short-lived, local branches and merge for branches in the public repository.

Next: LAB-30: Merging Back to Main

Merge greet into main

We’ve kept our greet branch up to date with main (via rebase), now let’s merge the greet changes back into the main branch.

> git checkout main
> git merge greet
> git checkout main
Switched to branch 'main'
> git merge greet
Updating f6a7d74..630033d
Fast-forward
lib/greeter.rb | 8 ++++++++
lib/hello.rb   | 5 ++++-
2 files changed, 12 insertions(+), 1 deletion(-)
create mode 100644 lib/greeter.rb

Because the head of main is a direct ancestor of the head of the greet branch, git is able to do a fast-forward merge. When fast-forwarding, the branch pointer is simply moved forward to point to the same commit as the greeter branch.

There will never be conflicts in a fast-forward merge.

Review the logs

git hist
> git hist
* 630033d 2022-08-16 | Hello uses Greeter (HEAD -> main, greet) [Brenton Cleeland]
* 55d0c6b 2022-08-16 | Added greeter class [Brenton Cleeland]
* f6a7d74 2022-08-16 | Add a thank you to Jim [Brenton Cleeland]
* ef40b54 2022-08-16 | Made interactive [Brenton Cleeland]
* 22d8379 2022-08-16 | Added README.md [Brenton Cleeland]
* 41ff07c 2022-08-16 | Moved hello.rb to lib [Brenton Cleeland]
* af90521 2022-08-16 | Add an author/email comment [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

The greet and main branches are now identical.

Next: LAB-31: Multiple Repositories

Multiple Repositories

Up to this point we have been working with a single git repository. However, git excels at working with multiple repositories. These extra repositories may be stored locally, or may be accessed across a network connection. In modern workflows you will use a hosted remote repository (i.e. on Github or Azure DevOps) as a way of sharing your code with other developers on your team.

In the next section we will create a new repository called “cloned_hello”. We will show how to move changes from one repository to another, and how to handle conflicts when they arise between two repositories.

Image representing the hello repository with a cloned_hello copy of the repository

For now, we will be working with local repositories (i.e. repositories stored on your local hard disk), however most of the things learned in this section will apply to multiple repositories whether they are stored locally or remotely over a network.

NOTE: We are going be making changes to both copies of our repositories. Make sure you pay attention to which repository you are in at each step of the following labs.

Next: LAB-32: Cloning Repositories

Cloning Repositories

First, we need to move up one directory in our terminal.

> cd ..
> ls

In the output of ls you should see your hello directory.

> ls
hello

Create a clone of the hello repository

Let's make a clone of the repository.

> git clone hello cloned_hello
> ls
> git clone hello cloned_hello
Cloning into 'cloned_hello'...
done.
> ls
cloned_hello hello

There should now be two repositories in your working directory: the original “hello” repository and the newly cloned “cloned_hello” repository.

Next: LAB-33: Review the Cloned Repository

Look at the cloned repository

Let’s take a look at the cloned repository.

> cd cloned_hello
> ls
> ls
README.md lib

You should see a list of all the files in the top level of the original repository (README and lib). You've created a complete copy of the "hello" repository.

Review the repository history

> git hist --all
* 630033d 2022-08-16 | Hello uses Greeter (HEAD -> main, origin/main, origin/greet, origin/HEAD) [Brenton Cleeland]
* 55d0c6b 2022-08-16 | Added greeter class [Brenton Cleeland]
* f6a7d74 2022-08-16 | Add a thank you to Jim [Brenton Cleeland]
* ef40b54 2022-08-16 | Made interactive [Brenton Cleeland]
* 22d8379 2022-08-16 | Added README.md [Brenton Cleeland]
* 41ff07c 2022-08-16 | Moved hello.rb to lib [Brenton Cleeland]
* af90521 2022-08-16 | Add an author/email comment [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

You should now see a list of all the commits in the new repository, and it should (more or less) match the history of commits in the original repository. The only difference should be in the names of the branches.

You can see the main branch (along with HEAD) in the history list. But you will also have a number of strangely named branches (origin/main, origin/greet and origin/HEAD). We’ll talk about those in a bit.

Next: LAB-34: What is Origin

What is Origin?

Let's learn a little about naming remote repositories.

> git remote
> git remote
origin

We see that the cloned repository knows about a remote repository named origin. Let’s see if we can get more information about origin:

> git remote show origin
* remote origin
Fetch URL: /Users/brecleel/Dev/hello
Push  URL: /Users/brecleel/Dev/hello
HEAD branch: main
Remote branches:
    greet tracked
    main  tracked
Local branch configured for 'git pull':
    main merges with remote main
Local ref configured for 'git push':
    main pushes to main (up to date)

Now we see that the remote repository “origin” is simply the original hello repository on our filesystem.

Remote repositories typically live on a separate machine, possibly a centralized server. As we can see here, however, they can just as well point to a repository on the same machine.

There is nothing particularly special about the name “origin”, however the convention is to use the name “origin” for the primary centralized repository (if there is one).

Next: LAB-35: Remote Branches

Remote Branches

Let's look at the branches available in our cloned repository.

> git branch
> git branch
* main

That's it! Only the main branch is listed. Where is the greet branch? The git branch command only lists the local branches by default.

Listing remote branches

To see all branches you need to use the -a flag on git branch.

> git branch -a
* main
remotes/origin/HEAD -> origin/main
remotes/origin/greet
remotes/origin/main

Git has all the commits from the original repository, but branches in the remote repository are not treated as local branches here. If we want our own greet branch, we need to create it ourselves. We will see how to do that in a minute.

Next: LAB-36: Change the Original Repository

Make a change in the original hello repository

First, move back into the origin hello repository.

> cd ../hello

Now, make a change to the README.md file:

This is the Hello World example from the git tutorial.

Thanks to Jim Weirich for the initial work on this tutorial for RailsConf 2010.

(changed in original)

Add and commit the change.

> git add README.md
> git commit -m "Changed README in the original repo"

Up Next

The original repository now has later changes that are not in the cloned version. Next we will pull those changes across to the cloned repository.

Next: LAB-37: Fetching Changes

Fetching Changes

Move into the cloned_hello repository and check the history.

> cd ../cloned_hello
> git hist --all
> cd ../cloned_hello/
> git hist --all
* 630033d 2022-08-16 | Hello uses Greeter (HEAD -> main, origin/main, origin/greet, origin/HEAD) [Brenton Cleeland]
* 55d0c6b 2022-08-16 | Added greeter class [Brenton Cleeland]
* f6a7d74 2022-08-16 | Add a thank you to Jim [Brenton Cleeland]
* ef40b54 2022-08-16 | Made interactive [Brenton Cleeland]
* 22d8379 2022-08-16 | Added README.md [Brenton Cleeland]
* 41ff07c 2022-08-16 | Moved hello.rb to lib [Brenton Cleeland]
* af90521 2022-08-16 | Add an author/email comment [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

Notice that the new "Changed README in origin repo" commit isn't yet available in this repository.

Fetching changes from origin

The git fetch command is used to retrieve the latest version of the repository from our remote.

> git fetch
> git hist --all
> git fetch
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 333 bytes | 111.00 KiB/s, done.
From /Users/brecleel/Dev/hello
630033d..35263dc  main       -> origin/main
> git hist --all
* 35263dc 2022-08-17 | Changed README in the original repo (origin/main, origin/HEAD) [Brenton Cleeland]
* 630033d 2022-08-16 | Hello uses Greeter (HEAD -> main, origin/greet) [Brenton Cleeland]
* 55d0c6b 2022-08-16 | Added greeter class [Brenton Cleeland]
* f6a7d74 2022-08-16 | Add a thank you to Jim [Brenton Cleeland]
* ef40b54 2022-08-16 | Made interactive [Brenton Cleeland]
* 22d8379 2022-08-16 | Added README.md [Brenton Cleeland]
* 41ff07c 2022-08-16 | Moved hello.rb to lib [Brenton Cleeland]
* af90521 2022-08-16 | Add an author/email comment [Brenton Cleeland]
* db1f4d1 2022-08-16 | Added a comment (tag: v1) [Brenton Cleeland]
* cebd1e7 2022-08-16 | Added a default value (tag: v1-beta) [Brenton Cleeland]
* 3f2c477 2022-08-16 | Using ARGV [Brenton Cleeland]
* 5dced27 2022-08-16 | First Commit [Brenton Cleeland]

At this point the repository has all the commits from the original repository, but they are not integrated into the cloned repository’s local branches.

Find the “Changed README in original repo” commit in the history above. Notice that the commit includes “origin/main” and “origin/HEAD”.

Now look at the “Hello uses Greeter” commit. You will see that the local main branch points to this commit, not to the new commit that we just fetched.

The upshot of this is that the git fetch command will fetch new commits from the remote repository, but it will not merge these commits into the local branches.

Check the README

We can demonstrate that the cloned README is unchanged.

> cat README.md
This is the Hello World example from the git tutorial.

Thanks to Jim Weirich for the initial work on this tutorial for RailsConf 2010.

See, this is the original README.md contents from before we made our change in the hello repository.

Next: LAB-38: Merging Pulled Changes

Merge the fetch changes into local main

The local and origin main branches are different, and you can use git merge to bring them into line.

> git merge origin/main
> git merge origin/main
Updating 630033d..35263dc
Fast-forward
README.md | 2 ++
1 file changed, 2 insertions(+)

Check the README again

We should see the changes now.

> cat README.md
> cat README.md
This is the Hello World example from the git tutorial.

Thanks to Jim Weirich for the initial work on this tutorial for RailsConf 2010.

(changed in original)

Pulling changes (instead of fetching them)

We’re not going to go through the process of creating another change and merging it again, but we do want you to know that doing:

> git pull

Is the equivalent of doing the two steps we just did in a single command.

> git fetch
> git merge origin/main

Next: LAB-39: Create a bare repository

Create a bare repository

In order to push a change to a remote, the remote needs to be a "bare" repository. Bare repositories don't have working directories, and a effectively just the content of the .git directory. When you create a repository with a service like Github the repository that they host will be considered bare (though it might not look like it in the user interface).

Let's create a bare repository from our hello repository.

> cd ..
> git clone --bare hello hello.git
> ls hello.git
> git clone --bare hello hello.git
Cloning into bare repository 'hello.git'...
done.
> ls hello.git
HEAD        config      description hooks       info        objects     packed-refs refs

The convention is that repositories ending in ‘.git’ are bare repositories. We can see that there is no working directory in the hello.git repo. Essentially it is nothing but the .git directory of a non-bare repo.

Add the new remote to cloned_hello

Move back into the cloned_hello repository and add our new bare repository as a remote.

cd cloned_hello
git remote add shared ../hello.git

We've named the remote "shared" in the command above.

Next: LAB-40: Push a change

Make a change to our cloned_hello repo

Let's push a change to our new bare remote.

Update the README.md in your cloned_hello repository:

This is the Hello World example from the git tutorial.

Thanks to Jim Weirich for the initial work on this tutorial for RailsConf 2010.

(changed in original, then changed in cloned_hello)

Add and commit the change using the commands you've learned today.

> git add README.md
> git commit -m "Update README from our cloned repo"
> git commit -m "Update README from our cloned repo"
[main 1dc8db6] Update README from our cloned repo
1 file changed, 1 insertion(+), 1 deletion(-)

Use git push to publish our change

In the command below "shared" is the name of our remote, and "main" is the name of the branch we are pushing. It's possible to set up branches to "track" remote branches automatically (allowing you to simply git push) but being explicit about where and what your are pushing is good practice.

> git push shared main
git push shared main
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 364 bytes | 364.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
To ../hello.git
35263dc..1dc8db6  main -> main

Your change has now been pushed to the remote and is shared with others.

Next: LAB-41: Pulling shared changes

Pulling shared changes

Let's quickly hop over into the origin "hello" repository and pull the changes from the bare repository. At this point all of the commands below should be familiar.

> cd ../hello
> git remote add shared ../hello.git
> git pull shared main
> cat README.md
> cd ../hello
> git remote add shared ../hello.git
> git pull shared main
From ../hello
* branch            main       -> FETCH_HEAD
* [new branch]      main       -> shared/main
Updating 35263dc..1dc8db6
Fast-forward
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
> cat README.md
This is the Hello World example from the git tutorial.

Thanks to Jim Weirich for the initial work on this tutorial for RailsConf 2010.

(changed in original, then changed in cloned_hello)

Because we used git pull the changes from the remote were automatically merged into our main branch. Both the hello and cloned_hello repositories are now in sync.

Next: LAB-42: Hosting your repository on Github

Create a new repository on Github

Sign in to the account you created earlier and create a new repository on Github. Choose a name for your repo (I chose "hello" but you can choose anything you like).

Screenshot of the new repository screen on Github, with the name hello being used for the repo

Add the new repository as a remote

From the empty repository screen, copy the SSH URL for the repository. It should look similar to git@github.com:brntn-ps/hello.git but with your username instead of mine.

Screenshot of an empty repository on Github

Add the repository as a remote for our hello repo.

> git remote add github git@github.com:brntn-ps/hello.git

Push our code to the remote

Using the same git push command as we did before, we'll push our code to the new github remote.

> git push github main
git push github main
Enumerating objects: 41, done.
Counting objects: 100% (41/41), done.
Delta compression using up to 8 threads
Compressing objects: 100% (32/32), done.
Writing objects: 100% (41/41), 3.91 KiB | 572.00 KiB/s, done.
Total 41 (delta 4), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (4/4), done.
To github.com:brntn-ps/hello.git
* [new branch]      main -> main

If you navigate to the repository in your browser and refresh, you should now see our code.

Next: Next steps with Git

Git Immersion (2023 Edition)

A guided tour that walks through the fundamentals of Git, inspired by the premise that to know a thing is to do it.


git is an industry standard version control system. Today's workshop will give you a deep dive into how to use it on a project.

Large parts of this tutorial are based on the Git Immersion tutorial which was originally presented at RailsConf 2010 by Jim Weirich. That tutorial is licensed as CC-BY-SA and this updated tutorial retains that license.

Changes from the original:

  • Switch from master to main as the default branch
  • Use restore instead of checkout to roll back unstaged changes
  • Use restore --staged instead of reset to roll back staged changes
  • Remove steps that add the Rakefile to remove the dependency on having ruby available
  • Use switch instead of checkout to change branches
  • Create a new change to demonstrate the difference between merge and rebase, instead of resetting both branches
  • Round things out by pushing to a repository on Github

Lab Exercises

LAB-01: Getting things installedLAB-02: Github setupLAB-03: Configure GitLAB-04: Create a ProjectLAB-05: Checking StatusLAB-06: Making ChangesLAB-07: Staging ChangesLAB-08: Staging and CommittingLAB-09: Committing ChangesLAB-10: Changes, not FilesLAB-11: HistoryLAB-12: AliasesLAB-13: Getting Old VersionsLAB-14: Tagging versionsLAB-15: Undoing Local Changes Before StagingLAB-16: Undoing Local Changes Before CommittingLAB-17: Undoing Committed ChangesLAB-18: Removing Commits from a BranchLAB-19: Ammending CommitsLAB-20: Moving FilesLAB-21: Git Internals - The .git DirectoryLAB-22: Creating a BranchLAB-23: Navigating BranchesLAB-24: Changes in mainLAB-25: Viewing Diverging BranchesLAB-26: MergingLAB-27: Creating a ConflictLAB-28: Resolving ConflictsLAB-29: Merging vs RebasingLAB-30: Merging Back to MainLAB-31: Multiple RepositoriesLAB-32: Cloning RepositoriesLAB-33: Review the Cloned RepositoryLAB-34: What is OriginLAB-35: Remote BranchesLAB-36: Change the Original RepositoryLAB-37: Fetching ChangesLAB-38: Merging Pulled ChangesLAB-39: Create a bare repositoryLAB-40: Push a changeLAB-41: Pulling shared changesLAB-42: Hosting your repository on GithubNext steps with Git

This version of Git Immersion was updated in 2022 by Brenton Cleeland for Publicis Sapient Australia for our Early Careers program. Like the original, it is released under a Creative Commons Attribution Non-Commercial Share-Alike 4.0 International license.