Git and version control

Author

Murray Logan

Published

September 15, 2024

This tutorial will take a modular approach. The first section will provide an overview of the basic concepts of git. The second section will provide a quick overview of basic usage and the third and final section will cover intermediate level usage. In an attempt to ease understanding, the tutorial will blend together git commands and output, schematic diagrams and commentary in an attempt to ease understanding.

The following table surves as both a key and overview of the most common actions and git ‘verbs’.

Initialize git
git init Establish a git repository (within the current path if no path provided)
Staging
git add <file>
where file is one or more files to stage
Staging is indicating which files and their states are to be included in the next commit.
Committing
git commit -m "<Commit message>"
where <Commit message> is a message to accompany the commit
Commiting generates a ‘snapshot’ of the file system.
Checkout
git checkout "<commit>"
where <commit> is a reference to a commit to be reviewed
Explore the state associated with a specific commit
Reset
git reset --hard "<commit>"
where <commit> is a reference to a commit
Return to a previous state, effectively erasing subsequent commits..
Revert
git revert "<commit>"
where <commit> is a reference to a commit that should be nullified (inverted)
Generate a new commit that reverses the changes introduced by a commit thereby effectively rolling back to a previous state (the one prior to the nominated commit) whilst still maintaining full commit history.
Branching
git branch <name>
git checkout <name>
where <name> is a reference to a branch name (e.g. ‘Feature’)
Take edits in the project in a new direction to allow for modifications that will not affect the main (master) branch.
Merging
git checkout master
git branch <name>
where <name> is a reference to a branch name (e.g. ‘Feature’) that is to be merged back into master.
Incorporate changes in a branch into another branch (typically master).
Rebasing
git rebase -i HEAD~<number>
where <number> is the number of previous commits to squash together with head.
Combine multiple commits together into a single larger commit.
Pulling
git pull -u <remote> <branch>
where <remote> is the name of the remote (typically origin) and <branch> is the branch to sync with remote (typically master).
Pull changes from a branch of a remote repository.
Pushing
git push -u <remote> <branch>
where <remote> is the name of the remote (typically origin) and <branch> is the branch to sync with remote (typically master).
Push changes up to a branch of a remote repository.

1 Context

Git is a distributed versioning system. This means that the complete contents and history of a repository (in simplistic terms a repository is a collection of files and associated metadata) can be completely duplicated across multiple locations.

No doubt you have previously been working on a file (could be a document, spreadsheet, script or any other type of file) and got to a point where you have thought that you are starting to make edits that substantially change the file and therefore have considered saving the new file with a new name that indicates that it is a new version.

In the above diagram, new content is indicated in red and modifications in blue.

Whist this approach is ok, it is fairly limited and unsophisticated approach to versioning (keeping multiple versions of a file). Firstly, if you edit this file over many sessions and each time save with a different name, it becomes very difficult to either keep tract of what changes are associated with each version of the file, or the order in which the changes were made. This is massively compounded if a project comprises multiple files or has multiple authors.

Instead, imagine a system in which you could take a snapshot of state of your files and also provide a description outlining what changes you have made. Now imagine that the system was able to store and keep track of a succession of such versions in such a way that allows you to roll back to any previous versions of the files and exchange the entire history of changes with others collaborators - that is the purpose of git.

In the above diagram (which I must point out is not actually how git works), you can see that we are keeping track of multiple documents and potentially multiple changes within each document. What constitutes a version (as in how many changes and to what files) is completely arbitrary. Each individual edit can define a separate version.

One of the issues with the above system is that there is a lot of redundancy. With each new version an addition copy of the project’s entire filesystem (all its files) must be stored. In the above case, Version 2 and 3 both contain identical copies of fileA.doc. Is there a way of reducing the required size of the snapshots by only keeping copies of those that have actually changed? this is what git achieves. Git versions (or snapshots known as commits) store files that have changed since the previous and files that have not changed are only represented by links to instances of these files within previous snapshots.

Now consider the following:

  • You might have noticed that a new version can comprise multiple changes across multiple files. However, what if we have made numerous changes to numerous files over the course of an editing session (perhaps simultaneously addressing multiple different editing suggestions at a time), yet we did not want to lump all of these changes together into a single save point (snapshot). For example, the multiple changes might constitute addressing three independent issues, so although all edits were made simultaneously, we wish to record and describe the changes in three separate snapshots.

  • What if this project had multiple contributors some of whom are working on new components of the project and some whom are working simultaneously on the same set of files? How can the system ensure that all contributors are in sync with each other and that new components are only introduced to the project proper once they are stable and agreed upon?

  • What if there are files present within our project that we do not wish to keep track of. These files could be log files, compilation intermediates etc.

  • Given that projects can comprise many files (some of which can be large), is it possible to store compressed files so as to reduce the storage and bandwidth burden?

2 Overview of git

The above discussion provides context for understanding how git works. Within git, files can exist in one of four states:

  • untracked - these are files within the directory tree that are not to be included in the repository (not part of any snapshot)
  • modified - these are files that have changed since the last snapshot
  • staged - these are files that are nominated to be part of the next snapshot
  • committed - these are files that are represented in a stored snapshot (called a commit). One a snapshot is committed, it is a permanent part of the repositories history

Since untracked files are not part of a repository, we will ignore these for now.

Conceptually, there are three main sections of a repository:

  • Working directory - (or Workspace) is the obvious tree (set of files and folders) that is present on disc and comprises the actual files that you directly create, edit etc.
  • Staging area - (or index) is a hidden file that contains metadata about the files to be included in the next snapshot (commit)
  • Repository - the snapshots (commits). The commits are themselves just additional metadata pointing to a particular snapshot.

A superficial representation of some aspects of the git version control system follows. Here, the physical file tree in the workspace can be added to the staging area before this snapshot can be committed to the local repository.

After we add the two files (file 1 and file 2), both files will be considered in an untracked state. Adding the files to the staging area changes their state to staged. Finally when we commit, the files are in a committed state.

Now if we add another file (file 3) to our workspace, add this file to the staging area and then commit the change, the resulting committed snapshot in the local repository will resemble the workspace. Note, although the staging area contains all three files, only file 3 points to any new internal content - since file 1 and file 2 have unmodified, their instances in the staging area point to the same instances as previous. Similarly, the second commit in the Local repository will point to one new representation (associated with file 3) and two previous representations (associated with file 1 and file 2).

Initially, it might seem that there is an awful lot of duplication going on. For example, if we make a minor alteration to a file, why not just commit the change (delta) instead of an entirely new copy? Well, periodically, git will perform garbage collection on the repository. This process repacks the objects together into a single object that comprises only the original blobs and their subsequent deltas - thereby gaining efficiency. The process of garbage collection can also be forced at any time via:

git gc

During the evolution of most projects, situations arise in which we wish to start work on new components or features that might represent a substantial deviation from the main line of evolution. Often, we would very much like to be able to quarantine the main thread of the project from these new developments. For example, we may wish to be able to continue tweaking the main project files (in order to address minor issues and bugs), while at the same time, performing major edits that take the project in a different direction.

This is called branching. The main evolutionary thread of the project is referred to as the main branch. Deviations from the main branch are generally called branches and can be given any name (other than ‘main’ or ‘HEAD’). For example, we could start a new branch called ‘Feature’ where we can evolve the project in one direction whilst still being able to actively develop the main branch at the same time. ‘Feature’ and ‘main’ branches are depicted in the left hand sequence of circles of the schematic below.

The circles represent commits (stored snapshots). We can see that the first commit is the common ancestor of the ‘Feature’ and ‘main’ branch. HEAD is a special reference that points to the tip of the currently active commit. It indicates where the next commit will be built onto. In diagram above, HEAD is pointing to the last commit in main. Hence the next commit will build on this commit. To develop the Feature branch further, we first have to move HEAD to the tip of the Feature branch.

We can later merge the Feature branch into the main branch in order to make the new changes mainstream.

To support collaboration, there can also be a remote repository (referred to as origin and depicted by the squares in the figure above). Unlike a local repository, a remote repository does not contain a workspace as files are not directly edited in the remote repository. Instead, the remote repository acts as a permanently available conduit between multiple contributors.

In the diagram above, we can see that the remote repository (origin) has an additional branch (in this called dev). The collaborator whose local repository is depicted above has either not yet obtained (pulled) this branch or has elected not to (as perhaps it is not a direction that they are involved in).

We also see that the main branch on the remote repository has a newer (additional) commit than the local repository.

Prior to working on branch a collaborator should first get any updates to the remote repository. This is a two step process. Firstly, the collaborator fetches any changes and then secondly merges those changes into their version of the branch. Collectively, these two actions are called a pull.

To make local changes available to others, the collaborator can push commits up to the remote repository. The pushed changes are applied directly to the nominated branch so it is the users responsibility to ensure as much as possible, their local repository already included the most recent remote repository changes (by always pulling before pushing).

3 Installation

Git Bash (Command Line Version):

  1. Download the Git for Windows installer from Git for Windows
    • Click the Download button
    • Select the latest version from the list of Assets
  2. Run the installer and follow the installation prompts.
  3. Choose the default options unless you have specific preferences.
  4. Select the default text editor (usually Vim) or choose another editor like Nano or Notepad++.
  5. Choose to use Git from the Windows Command Prompt (recommended).
  6. Complete the installation.

Using Homebrew:

  1. Open Terminal.
  2. Install Homebrew if not installed:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  1. Install Git using Homebrew:
brew install git
  1. Open Terminal.

Ubuntu/Debian:

sudo apt update
sudo apt install git

Fedora:

sudo dnf install git

Arch Linux:

sudo pacman -S git

Linux (Red Hat/CentOS):

sudo yum install git

To verify that the software is installed and accessible, open a terminal and issue the following:

git --version
git version 2.46.0

Windows:

On Windows, you can access a terminal via one of the following:

  • via the command Prompt:
    • Press Win + R to open the Run dialog.
    • Type cmd and press Enter.
  • via PowerShell:
    • Press Win + X and select “Windows PowerShell.”
  • Git Bash (Optional):
    • if Git is installed (which we are hoping it is!), open “Git Bash” for a Unix-like terminal experience.

MacOS:

  • via Terminal:
    • Press Cmd + Space to open Spotlight.
    • Type terminal and press Enter.

Linux:

Oh please. You cannot seriously tell me that you are using Linux and don’t know how to access a terminal.

In the command above, pay particular attention to the number of hyphens in the above command - there are two in a row and no spaces between the -- and the word version.

If you get output similar to above (an indication of what version of git you have on your system), then it is likely to be properly installed. If instead you get an error message, then it is likely that git is not properly installed and you should try again.

4 Getting started

Before using git, it is a good idea to define some global (applied to all your gits) settings. These include your name and email address and whilst not essential, they are applied to all actions you perform so the it is easier for others to track the route of changes etc.

git config --global user.name "Your Name"
git config --global user.email "your_email@whatever.com"
Note

In the above, you should replace “Your Name” with your actual name. This need not be a username (or even a real name) it is not cross referenced anywhere. It is simply to use in collaboration so that your collaborators know who is responsible for your commits.

Similarly, you should replace “your_email@whatever.com” with an email that you are likely to monitor. This need not be the same email address you have used to register a Github account etc, it is just so that collaborators have a way of contacting you.

The remaining sections go through the major git versioning concepts. As previously indicated, git is a command driven program (technically a family of programs). Nevertheless, many other applications (such as RStudio) are able to interface directly with git for some of the more commonly used features. Hence, in addition to providing the command line syntax for performing each task, where possible, this tutorial will also provide instructions (with screen captures) for RStudio and emacs.

5 Setting up (initializing) a new repository

For the purpose of this tutorial, I will create a temporary folder the tmp folder of my home directory into which to create and manipulate repositories. To follow along with this tutorial, you are encouraged to do similarly.

5.1 Initialize local repository

We will start by creating a new directory (folder) which we will call Repo1 in which to place our repository. All usual directory naming rules apply since it is just a regular directory.

mkdir ~/tmp/Repo1

To create (or initialize) a new local repository, issue the git init command in the root of the working directory you wish to contain the git repository. This can be either an empty directory or contain an existing directory/file structure. The git init command will add a folder called .git to the directory. This is a one time operation.

cd ~/tmp/Repo1
git init 
Initialized empty Git repository in /home/runner/tmp/Repo1/.git/

The .git folder contains all the necessary metadata to manage the repository.

ls -al
total 12
drwxr-xr-x 3 runner docker 4096 Sep 15 01:57 .
drwxr-xr-x 3 runner docker 4096 Sep 15 01:57 ..
drwxr-xr-x 7 runner docker 4096 Sep 15 01:57 .git
tree -a --charset unicode
.
`-- .git
    |-- HEAD
    |-- branches
    |-- config
    |-- description
    |-- hooks
    |   |-- applypatch-msg.sample
    |   |-- commit-msg.sample
    |   |-- fsmonitor-watchman.sample
    |   |-- post-update.sample
    |   |-- pre-applypatch.sample
    |   |-- pre-commit.sample
    |   |-- pre-merge-commit.sample
    |   |-- pre-push.sample
    |   |-- pre-rebase.sample
    |   |-- pre-receive.sample
    |   |-- prepare-commit-msg.sample
    |   |-- push-to-checkout.sample
    |   |-- sendemail-validate.sample
    |   `-- update.sample
    |-- info
    |   `-- exclude
    |-- objects
    |   |-- info
    |   `-- pack
    `-- refs
        |-- heads
        `-- tags

10 directories, 18 files
config
this file stores settings such as the location of a remote repository that this repository is linked to.
description
lists the name (and version) of a repository
HEAD
lists a reference to the current checked out commit.
hooks
a directory containing scripts that are executed at various stages (e.g. pre-push.sample is an example of a script executed prior to pushing)
info
contains a file exclude that lists exclusions (files not to be tracked). This is like .gitignore, except is not versioned.
objects
this directory contains SHA indexed files being tracked
refs
a master copy of all the repository refs
logs
contains a history of each branch

The repository that we are going to create in this demonstration could be considered to be a new standalone analysis. In Rstudio, this would be considered a project. So, we will initialise the git repository while we create a new Rstudio project. To do so:

  1. click on the Project selector in the top right of the Rstudio window (as highlighted by the red ellipse in the image below.

  2. select New Project from the dropdown menu

  3. select New Directory form the Create Project panel

  4. select New Project from the Project Type panel

  5. Provide a name for the new directory to be created and use the Browse button to locate a suitable position for this new directory. Ensure that the Create a git repository checkbox is checked

  6. Click the Create Project button

If successful, you should notice a couple of changes - these are highlighted in the following figure:

  • a new Git tab will appear in the top right panel
  • the contents of this newly created project/repository will appear in the Files tab of the bottom right panel

If the files and directories that begin with a . do not appear, click on the More file commands cog and make sure the Show Hidden Files option is ticked.

The newly created files/folders are:

  • .git - this directory houses the repository information and should not generally be edited directly
  • .gitignore - this file defines files/folders to be excluded from the repository. We will discuss this file more later
  • .Rhistory - this file will accrue a history of the commands you have evaluated in R within this project
  • .Rproj.user - this folder stores some project-specific temporary files
  • Repo1.Rproj - contains the project specific settings

Note that on the left side of the Rstudio window there are two panels - one called “Console”, the other called “Terminal”. The console window is for issuing R commands and the terminal window is for issuing system (bash, shell) commands. Throughout this tutorial, as an alternative to using the point and click Rstudio methods, you could instead issue the Terminal instructions into the “Terminal” panel. Indeed, there are some git commands that are not supported directly by Rstudio and can only be entered into the terminal

Note, at this stage, no files are being tracked, that is, they are not part of the repository.

To assist in gaining a greater understanding of the workings of git, we will use a series of schematics diagrams representing the contents of four important sections of the repository. Typically, these figures will be contained within callout panels that expand/collapse upon clicking. However, for this first time, they will be standalone.

In the first figure below, the left hand panel represents the contents of the root directory (excluding the .git folder) - this is the workspace and is currently empty.

The three white panels represent three important parts of the inner structure of the .git folder. A newly initialized repository is relatively devoid of any specific metadata since there are no staged or committed files. In the root of the .git folder, there is a file called HEAD.

The figure is currently very sparse. However, as the repository grows, so the figure will become more complex.

The second figure provides the same information, yet via a network diagram. Again, this will not be overly meaningful until the repository contains some content.

5.2 Initializing other types of repositories

The above demonstrated how to initialise a new local repository from scratch. However, there are times when we instead want to:

  • create a git repository from an existing directory or project
  • collaborate with someone on an existing repository
  • create a remote repository

These situations are briefly demonstrated in the following sections.

5.2.1 Initializing a shared (remote) repository

The main repository for sharing should not contain the working directory as such - only the .git tree and the .gitignore file. Typically the point of a remote repository is to act as a perminantly available repository from which multiple uses can exchange files. Consequently, those accessing this repository should only be able to interact with the .git metadata - they do not directly modify any files.

Since a remote repository is devode of the working files and directories, it is referred to as bare.

To create a bare remote repository, issue the git init --bare command after logging in to the remote location.

git init --bare

Use the instructions for the Terminal

5.2.2 Cloning an existing repository

To get your own local copy of an existing repository, issue the git clone <repo url> command in the root of the working directory you wish to contain the git repository. The repo url points to the location of the existing repository to be cloned. This is also a one time operation and should be issued in an otherwise empty directory.

The repo url can be located on any accessible filesytem (local or remote). The cloning process also stores a link back to the original location of the repository (called origin). This provides a convenient way for the system to keep track of where the local repository should exchange files.

Many git repositories are hosted on sites such as github, gitlab or bitbucket. Within an online git repository, these sites provide url links for cloning.

git clone "url.git"

where "url.git" is the url of the hosted repository.

  1. click on the Project selector in the top right of the Rstudio window (as highlighted by the red ellipse in the image below.
  2. select New Project from the dropdown menu
  3. select Version Control form the Create Project panel
  4. select Git from the Create Project from Version Control panel
  5. paste in the address of the repository that you want to clone, optionally a name for this repository (if you do not like the original name) and use the Browse button to locate a suitable position for this new directory.
  6. Click the Create Project button

5.2.3 Initializing a repository in an existing directory

This is the same as for a new directory.

git init
  1. click on the Project selector in the top right of the Rstudio window (as highlighted by the red ellipse in the image below.
  2. select New Project from the dropdown menu
  3. select Existing Directory form the Create Project panel
  4. use the Browse button to locate the existing directory
  5. Click the Create Project button

6 Tracking files

The basic workflow for tracking files is a two step process in which one or more files are first added to the staging area before they are committed to the local repository. The staging area acts as a little like a snapshot of what the repository will look like once the changes have been committed. The staging area also acts like a buffer between the files in the workspace (actual local copy of files) and the local repository (committed changes).

The reason that this is a two step process is that it allows the user to make edits to numerous files, yet block the commits in smaller chunks to help isolate changes in case there is a need to roll back to previous versions.

6.1 Staging files

When a file is first added to the staging area, a full copy of that file is added to the staging area (not just the file diffs as in other versioning systems).

To demonstrate lets create a file (a simple text file containing the string saying ‘File 1’) and add it to the staging area.

echo 'File 1' > file1

Now lets add this file to the staging area

git add file1

To see the status of the repository (that is, what files are being tracked), we issue the git status command

git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
    new file:   file1

This indicates that there is a single file (file1) in the staging area

To demonstrate lets create a file (a simple text file containing the string saying ‘File 1’) and add it to the staging area.

  1. Click the green “New File” button followed by the “Text File” option (or click the equivalent option from the “File” menu)

  2. Type File 1 in the panel with the flashing cursor. This panel represents the contents of the yet to be named file that we are creating.

  3. Click the “Save” or “Save all” buttons (or select the equivalent items from the “File” menu) and name the file “file1”

    Switch to the Git tab and you should notice a number of items (including the file we just created) in the panel. These are files that git is aware of, but not yet tracking. This panel acts as a status window. The yellow “?” symbol indicates that git considers these files “untracked”

  4. To stage a file, click on the corresponding checkbox - the status symbol should change to a green “A” (for added)

Our simple overview schematic represents the staging of file 1.

A schematic of the internal working of git shows in .git/objects a blob has been created. This is a compressed version of file1. Its filename is a 40 digit SHA-1 checksum has representing the contents of the file1. To re-iterate, the blob name is a SHA-1 hash of the file contents (actually, the first two digits form a folder and the remaining 38 form the filename).

We can look at the contents of this blob using the git cat-file command. This command outputs the contents of a compressed object (blob, tree, commit) from either the objects name (or unique fraction thereof) or its tag (we will discuss tags later).

git cat-file blob 50fcd
File 1

The add (staging) process also created a index file. This file simply points to the blob that is part of the snapshot. The git internals schematic illustrates the internal changes in response to staging a file.

6.2 Commit to local repository

To commit a set of changes from the staging area to the local repository, we issue the git commit command. We usually add the -m switch to explicitly supply a message to be associated with the commit. This message should ideally describe what the changes the commit introduces to the repository.

git commit -m 'Initial repo and added file1'
[main (root-commit) bab510e] Initial repo and added file1
 1 file changed, 1 insertion(+)
 create mode 100644 file1

We now see that the status has changed. It indicates that the tree in the workspace is in sync with the repository.

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

To commit a set of changes from the staging area to the local repository:

  1. click on the “Commit” button to open the “Review Changes” window

    This box will list the files to be committed (in this case “file1”), the changes in this file since the previous commit (as this is the first time this file has been committed, the changes are the file contents)

  2. you should also provide a commit message (in the figure above, I entered “Initial commit”. This message should ideally describe what the changes the commit introduces to the repository.

  3. click the “Commit” button and you will be presented with a popup message.

    This message provides feedback to confirm that your commit was successful.

  4. close the popup window and the “Review Changes” window

file1 should now have disappeared from the git status panel.

Our simple overview schematic represents the staging of file 1.

The following modifications have occurred (in reverse order to how they actually occur):

  • The main branch reference was created. There is currently only a single branch (more on branches later). The branch reference point to (indicates) which commit is the current commit within a branch.

    cat .git/refs/heads/main
    bab510ee0f7c10b81baaa46e41e145680bd308bc
  • A commit was created. This points to a tree (which itself points to the blob representing file1) as well as other important metadata (such as who made the commit and when). Since the time stamp will be unique each time a snapshot is commited, so too the name of the commit (as a SHA-1 checksum hash) will differ. To reiterate, the names of blobs and trees are determined by contents alone, commit names are also incorporate commit timestamp and details of the committer - and are thus virtually unique.

    git cat-file commit bab51
    tree 07a941b332d756f9a8acc9fdaf58aab5c7a43f64
    author pcinereus <i.obesulus@gmail.com> 1726365453 +0000
    committer pcinereus <i.obesulus@gmail.com> 1726365453 +0000
    
    Initial repo and added file1
  • A tree object was created. This represents the directory tree of the snapshot (commit) and thus points to the blobs.

    git ls-tree bab51
    100644 blob 50fcd26d6ce3000f9d5f12904e80eccdc5685dd1  file1

    Or most commonly (if interested in the latest commit):

    git ls-tree HEAD
    100644 blob 50fcd26d6ce3000f9d5f12904e80eccdc5685dd1  file1

The schematic now looks like

Committing staged changes creates an object under the .git tree.

tree -a --charset unicode
.
|-- .git
|   |-- COMMIT_EDITMSG
|   |-- HEAD
|   |-- branches
|   |-- config
|   |-- description
|   |-- hooks
|   |   |-- applypatch-msg.sample
|   |   |-- commit-msg.sample
|   |   |-- fsmonitor-watchman.sample
|   |   |-- post-update.sample
|   |   |-- pre-applypatch.sample
|   |   |-- pre-commit.sample
|   |   |-- pre-merge-commit.sample
|   |   |-- pre-push.sample
|   |   |-- pre-rebase.sample
|   |   |-- pre-receive.sample
|   |   |-- prepare-commit-msg.sample
|   |   |-- push-to-checkout.sample
|   |   |-- sendemail-validate.sample
|   |   `-- update.sample
|   |-- index
|   |-- info
|   |   `-- exclude
|   |-- logs
|   |   |-- HEAD
|   |   `-- refs
|   |       `-- heads
|   |           `-- main
|   |-- objects
|   |   |-- 07
|   |   |   `-- a941b332d756f9a8acc9fdaf58aab5c7a43f64
|   |   |-- 50
|   |   |   `-- fcd26d6ce3000f9d5f12904e80eccdc5685dd1
|   |   |-- ba
|   |   |   `-- b510ee0f7c10b81baaa46e41e145680bd308bc
|   |   |-- info
|   |   `-- pack
|   `-- refs
|       |-- heads
|       |   `-- main
|       `-- tags
`-- file1

16 directories, 27 files
git cat-file -p HEAD
tree 07a941b332d756f9a8acc9fdaf58aab5c7a43f64
author pcinereus <i.obesulus@gmail.com> 1726365453 +0000
committer pcinereus <i.obesulus@gmail.com> 1726365453 +0000

Initial repo and added file1
git cat-file -p HEAD^{tree}
100644 blob 50fcd26d6ce3000f9d5f12904e80eccdc5685dd1    file1
git log --oneline
bab510e Initial repo and added file1

6.3 More changes

Whenever a file is added or modified, if the changes are to be tracked, the file needs to be added to the staging area. Lets demonstrate by modifying file1 and adding an additional file (file2), this time to a subdirectory (dir1).

echo '---------------' >> file1
mkdir dir1
echo '* Notes' > dir1/file2
git add file1 dir1/file2

Now if we re-examine the status:

git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   dir1/file2
    modified:   file1
  1. modify file1 by adding a number of hyphens under the File 1 like in the figure below

  2. save the file. As you do so, you should notice that the file reappears in the status panel (this time with a blue “M” to signify that the file has been modified)

  3. to create the subdirectory, click on the “Add a new folder” icon and then enter a name for the subdirectory in the popup box (as per figure below)

  4. navigate to this new directory (dir1)

  5. click the “Create a new blank file in current directory” button and select “Text file”

  6. enter a new filename (file2) into the popup box

  7. enter some text into this file (like in the figure below)

  8. save the file and notice that the dir1 directory is now also in the git status panel (yet its status is “untracked”)

  9. stage both file1 and dir1 (click on the corresponding checkboxes)

And now our schematic looks like:

So when staging, the following has been performed:

  • the index file has been updated

    git ls-files --stage
    100644 4fcc8f85f738deb6cbb17db1ed3da241ad6cdf39 0 dir1/file2
    100644 28ed2456cbfa8a18a280c8af5b422e91e88ff64d 0 file1
  • two new blobs have been generated. One representing the modified file1 and the other representing the newly created file2 in the dir1 folder. The blob that represented the original file1 contents is still present and indeed is still the one currently committed. Blobs are not erased or modified.

Now we will commit this snapshot.

git commit -m 'Modified file1 and added file2 (in dir1)'
[main 235d5df] Modified file1 and added file2 (in dir1)
 2 files changed, 2 insertions(+)
 create mode 100644 dir1/file2
  1. click the “Commit” button

  2. you might like to explore the changes associated with each file

  3. enter a commit message (as in the figure below)

  4. click the “Commit” button

  5. after checking that the “Git Commit” popup does not contain any errors, close the popup

  6. to explore the repository history, click towards the “History” button on the top left corner of the “Review Changes” window

    This provides a graphical list of commits (in reverse chronological order)

  7. once you have finished exploring the history, you can close the “Review Changes” window

The following modifications occur:

  • the master branch now points to the new commit.

    cat .git/refs/heads/main
    235d5dfe6ce2ac1c509d5aaded38cf0d1953cb29
    git reflog
    235d5df HEAD@{0}: commit: Modified file1 and added file2 (in dir1)
    bab510e HEAD@{1}: commit (initial): Initial repo and added file1
  • a new commit was created. This points to a new root tree object and also points to the previous commit (its parent).

    git cat-file commit 235d5
    tree 2b61e2b3db9d1708269cf9d1aeaae2b0a2af1a23
    parent bab510ee0f7c10b81baaa46e41e145680bd308bc
    author pcinereus <i.obesulus@gmail.com> 1726365459 +0000
    committer pcinereus <i.obesulus@gmail.com> 1726365459 +0000
    
    Modified file1 and added file2 (in dir1)
  • new root tree was created. This points to a blob representing the modified file1 as well as a newly created sub-directory tree representing the dir1 folder.

    git ls-tree 2b61e
    040000 tree f2fa54609fe5e918f365e0d5ffaf9a3aea88d541  dir1
    100644 blob 28ed2456cbfa8a18a280c8af5b422e91e88ff64d  file1
    git cat-file -p HEAD^{tree}
    040000 tree f2fa54609fe5e918f365e0d5ffaf9a3aea88d541  dir1
    100644 blob 28ed2456cbfa8a18a280c8af5b422e91e88ff64d  file1
  • a new sub-directory root tree was created. This points to a blob representing the modified file1 as well as a newly created subtree tree representing the file2 file within the dir1 folder.

    git ls-tree 235d5
    040000 tree f2fa54609fe5e918f365e0d5ffaf9a3aea88d541  dir1
    100644 blob 28ed2456cbfa8a18a280c8af5b422e91e88ff64d  file1

    OR,

    git ls-tree HEAD
    040000 tree f2fa54609fe5e918f365e0d5ffaf9a3aea88d541  dir1
    100644 blob 28ed2456cbfa8a18a280c8af5b422e91e88ff64d  file1

Committing staged changes creates an object under the .git tree.

tree -a --charset unicode
.
|-- .git
|   |-- COMMIT_EDITMSG
|   |-- HEAD
|   |-- branches
|   |-- config
|   |-- description
|   |-- hooks
|   |   |-- applypatch-msg.sample
|   |   |-- commit-msg.sample
|   |   |-- fsmonitor-watchman.sample
|   |   |-- post-update.sample
|   |   |-- pre-applypatch.sample
|   |   |-- pre-commit.sample
|   |   |-- pre-merge-commit.sample
|   |   |-- pre-push.sample
|   |   |-- pre-rebase.sample
|   |   |-- pre-receive.sample
|   |   |-- prepare-commit-msg.sample
|   |   |-- push-to-checkout.sample
|   |   |-- sendemail-validate.sample
|   |   `-- update.sample
|   |-- index
|   |-- info
|   |   `-- exclude
|   |-- logs
|   |   |-- HEAD
|   |   `-- refs
|   |       `-- heads
|   |           `-- main
|   |-- objects
|   |   |-- 07
|   |   |   `-- a941b332d756f9a8acc9fdaf58aab5c7a43f64
|   |   |-- 23
|   |   |   `-- 5d5dfe6ce2ac1c509d5aaded38cf0d1953cb29
|   |   |-- 28
|   |   |   `-- ed2456cbfa8a18a280c8af5b422e91e88ff64d
|   |   |-- 2b
|   |   |   `-- 61e2b3db9d1708269cf9d1aeaae2b0a2af1a23
|   |   |-- 4f
|   |   |   `-- cc8f85f738deb6cbb17db1ed3da241ad6cdf39
|   |   |-- 50
|   |   |   `-- fcd26d6ce3000f9d5f12904e80eccdc5685dd1
|   |   |-- ba
|   |   |   `-- b510ee0f7c10b81baaa46e41e145680bd308bc
|   |   |-- f2
|   |   |   `-- fa54609fe5e918f365e0d5ffaf9a3aea88d541
|   |   |-- info
|   |   `-- pack
|   `-- refs
|       |-- heads
|       |   `-- main
|       `-- tags
|-- dir1
|   `-- file2
`-- file1

22 directories, 33 files
git cat-file -p HEAD
tree 2b61e2b3db9d1708269cf9d1aeaae2b0a2af1a23
parent bab510ee0f7c10b81baaa46e41e145680bd308bc
author pcinereus <i.obesulus@gmail.com> 1726365459 +0000
committer pcinereus <i.obesulus@gmail.com> 1726365459 +0000

Modified file1 and added file2 (in dir1)
git cat-file -p HEAD^{tree}
040000 tree f2fa54609fe5e918f365e0d5ffaf9a3aea88d541    dir1
100644 blob 28ed2456cbfa8a18a280c8af5b422e91e88ff64d    file1
git log --oneline
235d5df Modified file1 and added file2 (in dir1)
bab510e Initial repo and added file1

Now you might be wondering… What if I have modified many files and I want to stage them all. Do I really have to add each file individually? Is there not some way to add multiple files at a time? The answer of course is yes. To stage all files (including those in subdirectories) we issue the git add . command (notice the dot).

git add .

6.4 .gitignore

Whilst it is convenient to not have to list every file that you want to be staged (added), what about files that we don’t want to get staged and committed. It is also possible to define a file (called .gitignore) that is a list of files (or file patterns) that are to be excluded when we request all files be added. This functionality is provided via the .gitignore file that must be in the root of the repository working directory.

For example, we may have temporary files or automatic backup files or files generated as intermediates in a compile process etc that get generated. These files are commonly generated in the process of working with files in a project, yet we do not necessarily wish for them to be tracked. Often these files have very predictable filename pattern (such as ending with a # or ~ symbol or having a specific file extension such as .aux.

As an example, when working with a project in Rstudio, files (such as .Rhistory) and directories (such as .Rproj.user) are automatically added to the file system and thus appear as untracked files in git status.

Hence, we can create a.gitignore to exclude these files/directories. Indeed, if you are using Rstudio, you might have noticed that a .gitignore file was automatically created when you created the project.

Lets start by modifying the file2 and creating a new file f.tmp (that we want to ignore).

echo '---' >> dir1/file2
echo 'temp' > dir1/f.tmp
  1. navigate to the dir1 directory and open file2 for editing (or just make sure you are on the file2 tab.
  2. edit the file such that it just contains three hyphens (---) before saving the file
  3. in the same dir1 directory add another new text file (f.tmp) and edit this file to contain the word temp (then save the file)

The Git status panel should display both of these as untracked files.

To ignore the f.tmp file, we could either explicitly add this file as a row in a .gitignore file, or else we could supply a wildcard version that will ignore all files ending in .tmp.

echo '*.tmp' > .gitignore
cat .gitignore
*.tmp
  1. navigate back to the root of the project

  2. click on the gitignore file to open it up for editing

  3. navigate to the end of this file and add a newline containing the text *.tmp

You will notice that this .gitignore file already had items in it before you started editing it. These were added by Rstudio when you first created the new project.

The first item is .Rproj.user and its presence in this file is why it does not appear in the git status panel.

Once we save the .gitignore file, notice how the f.tmp file is similarly removed from the git status panel - since via .gitignore we have indicated that we want to ignore this file (not track it as part of our version control system).

Entry Meaning
file1 DO NOT stage (add) file1
*.tmp DO NOT stage (add) any file ending in .tmp
/dir1/* DO NOT stage (add) the folder called dir1 (or any of its contents) unless this is specifically negated (see next line)
!/dir1/file2 DO stage (add) the file called file2 in the dir1 folder

Now when we go to add all files to the staging area, those that fall under the exclude rules will be ignored

git add .
git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   .gitignore
    modified:   dir1/file2

You will notice that .gitignore was added as a new file and dir1/file2 was marked as modified yet dir1/f.tmp was totally ignored.

You will notice that .gitignore was added as a new file and dir1/file2 was marked as modified yet dir1/f.tmp was totally ignored.

  1. check the boxes next to each of the files listed in the status panel

Lets now commit these changes.

git commit -m 'Modified file2, added .gitignore'
[main d540207] Modified file2, added .gitignore
 2 files changed, 2 insertions(+)
 create mode 100644 .gitignore
git status
On branch main
nothing to commit, working tree clean
  1. click on the “Commit” button
  2. add a commit message (such as Modified file2, added .gitignore)
  3. click the “Commit” button
  4. close the popup
  5. close the “Review Changes” window

For those still interested in the schematic…

Committing staged changes creates an object under the .git tree.

tree -a --charset unicode
.
|-- .git
|   |-- COMMIT_EDITMSG
|   |-- HEAD
|   |-- branches
|   |-- config
|   |-- description
|   |-- hooks
|   |   |-- applypatch-msg.sample
|   |   |-- commit-msg.sample
|   |   |-- fsmonitor-watchman.sample
|   |   |-- post-update.sample
|   |   |-- pre-applypatch.sample
|   |   |-- pre-commit.sample
|   |   |-- pre-merge-commit.sample
|   |   |-- pre-push.sample
|   |   |-- pre-rebase.sample
|   |   |-- pre-receive.sample
|   |   |-- prepare-commit-msg.sample
|   |   |-- push-to-checkout.sample
|   |   |-- sendemail-validate.sample
|   |   `-- update.sample
|   |-- index
|   |-- info
|   |   `-- exclude
|   |-- logs
|   |   |-- HEAD
|   |   `-- refs
|   |       `-- heads
|   |           `-- main
|   |-- objects
|   |   |-- 07
|   |   |   `-- a941b332d756f9a8acc9fdaf58aab5c7a43f64
|   |   |-- 14
|   |   |   `-- 3a8bb5a2cc05a91f83a87af18c8eb5885a375c
|   |   |-- 19
|   |   |   `-- 44fd61e7c53bcc19e6f3eb94cc800508944a25
|   |   |-- 23
|   |   |   `-- 5d5dfe6ce2ac1c509d5aaded38cf0d1953cb29
|   |   |-- 28
|   |   |   `-- ed2456cbfa8a18a280c8af5b422e91e88ff64d
|   |   |-- 2b
|   |   |   `-- 61e2b3db9d1708269cf9d1aeaae2b0a2af1a23
|   |   |-- 3c
|   |   |   `-- 7af0d3ccea71c9af82fa0ce68532272edcf1b8
|   |   |-- 4f
|   |   |   `-- cc8f85f738deb6cbb17db1ed3da241ad6cdf39
|   |   |-- 50
|   |   |   `-- fcd26d6ce3000f9d5f12904e80eccdc5685dd1
|   |   |-- ba
|   |   |   `-- b510ee0f7c10b81baaa46e41e145680bd308bc
|   |   |-- c4
|   |   |   `-- 26a67af50d13828ec73b3c560b2648e2f3dc08
|   |   |-- d5
|   |   |   `-- 402077556abfd38a986b131a44b42c10fd29ba
|   |   |-- f2
|   |   |   `-- fa54609fe5e918f365e0d5ffaf9a3aea88d541
|   |   |-- info
|   |   `-- pack
|   `-- refs
|       |-- heads
|       |   `-- main
|       `-- tags
|-- .gitignore
|-- dir1
|   |-- f.tmp
|   `-- file2
`-- file1

27 directories, 40 files
git cat-file -p HEAD
tree 3c7af0d3ccea71c9af82fa0ce68532272edcf1b8
parent 235d5dfe6ce2ac1c509d5aaded38cf0d1953cb29
author pcinereus <i.obesulus@gmail.com> 1726365461 +0000
committer pcinereus <i.obesulus@gmail.com> 1726365461 +0000

Modified file2, added .gitignore
git cat-file -p HEAD^{tree}
100644 blob 1944fd61e7c53bcc19e6f3eb94cc800508944a25    .gitignore
040000 tree c426a67af50d13828ec73b3c560b2648e2f3dc08    dir1
100644 blob 28ed2456cbfa8a18a280c8af5b422e91e88ff64d    file1
git log --oneline
d540207 Modified file2, added .gitignore
235d5df Modified file1 and added file2 (in dir1)
bab510e Initial repo and added file1

7 Inspecting a repository

For this section, will will be working on the repository built up in the previous section. If you did not follow along with the previous section, I suggest that you expand the following callout and run the provided code in a terminal.

If you already have the repository, you can ignore the commands to create the repository.

Issue the following commands in your terminal

rm -rf ~/tmp/Repo1
mkdir ~/tmp/Repo1
cd ~/tmp/Repo1
git init 
echo 'File 1' > file1
git add file1
git commit -m 'Initial repo and added file1'
echo '---------------' >> file1
mkdir dir1
echo '* Notes' > dir1/file2
git add file1 dir1/file2
git commit -m 'Modified file1 and added file2 (in dir1)'
echo '---' >> dir1/file2
echo 'temp' > dir1/f.tmp
echo '*.tmp' > .gitignore
git add .
git commit -m 'Modified file2, added .gitignore'
tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 9 files

7.1 Status of workspace and staging area

Recall that within the .git environment, files can be in one of four states:

  • untracked
  • modified
  • staged
  • committed

To inspect the status of files in your workspace, you can issue the git status command (as we have done on numerous occasions above). This command displays the current state of the workspace and staging area.

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

The output of git status partitions all the files into (staged: Changes to be committed, unstaged: Changes not staged for commit and Untracked) as well as hints on how to either promote or demote the status of these files.

Examine the git status panel - ideally it should be empty thereby signalling that all your important files are tracked andcommitted.

7.1.1 log of commits

The git log command allows us to review the history of committed snapshots

git log
commit d5402077556abfd38a986b131a44b42c10fd29ba
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:41 2024 +0000

    Modified file2, added .gitignore

commit 235d5dfe6ce2ac1c509d5aaded38cf0d1953cb29
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:39 2024 +0000

    Modified file1 and added file2 (in dir1)

commit bab510ee0f7c10b81baaa46e41e145680bd308bc
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:33 2024 +0000

    Initial repo and added file1

We can see that in my case some fool called ‘Murray Logan’ has made a total of three commits. We can also see the date/time that the commits were made as well as the supplied commit comment.

Over time repositories accumulate a large number of commits, to only review the last 2 commits, we could issue the git log -n 2 command.

git log -n 2 
commit d5402077556abfd38a986b131a44b42c10fd29ba
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:41 2024 +0000

    Modified file2, added .gitignore

commit 235d5dfe6ce2ac1c509d5aaded38cf0d1953cb29
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:39 2024 +0000

    Modified file1 and added file2 (in dir1)
Option Example
--oneline
Condensed view
git log --oneline
d540207 Modified file2, added .gitignore
235d5df Modified file1 and added file2 (in dir1)
bab510e Initial repo and added file1
--stat
Indicates number of changes
git log --stat
commit d5402077556abfd38a986b131a44b42c10fd29ba
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:41 2024 +0000

    Modified file2, added .gitignore

 .gitignore | 1 +
 dir1/file2 | 1 +
 2 files changed, 2 insertions(+)

commit 235d5dfe6ce2ac1c509d5aaded38cf0d1953cb29
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:39 2024 +0000

    Modified file1 and added file2 (in dir1)

 dir1/file2 | 1 +
 file1      | 1 +
 2 files changed, 2 insertions(+)

commit bab510ee0f7c10b81baaa46e41e145680bd308bc
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:33 2024 +0000

    Initial repo and added file1

 file1 | 1 +
 1 file changed, 1 insertion(+)
-p
Displays the full diff of each commit
git log -p
commit d5402077556abfd38a986b131a44b42c10fd29ba
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:41 2024 +0000

    Modified file2, added .gitignore

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1944fd6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.tmp
diff --git a/dir1/file2 b/dir1/file2
index 4fcc8f8..143a8bb 100644
--- a/dir1/file2
+++ b/dir1/file2
@@ -1 +1,2 @@
 * Notes
+---

commit 235d5dfe6ce2ac1c509d5aaded38cf0d1953cb29
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:39 2024 +0000

    Modified file1 and added file2 (in dir1)

diff --git a/dir1/file2 b/dir1/file2
new file mode 100644
index 0000000..4fcc8f8
--- /dev/null
+++ b/dir1/file2
@@ -0,0 +1 @@
+* Notes
diff --git a/file1 b/file1
index 50fcd26..28ed245 100644
--- a/file1
+++ b/file1
@@ -1 +1,2 @@
 File 1
+---------------

commit bab510ee0f7c10b81baaa46e41e145680bd308bc
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:33 2024 +0000

    Initial repo and added file1

diff --git a/file1 b/file1
new file mode 100644
index 0000000..50fcd26
--- /dev/null
+++ b/file1
@@ -0,0 +1 @@
+File 1
--author="<name>"
Filter by author
git log --author="Murray"
--grep="<pattern>"
Filter by regex pattern of commit message
git log --grep="Modified"
commit d5402077556abfd38a986b131a44b42c10fd29ba
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:41 2024 +0000

    Modified file2, added .gitignore

commit 235d5dfe6ce2ac1c509d5aaded38cf0d1953cb29
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:39 2024 +0000

    Modified file1 and added file2 (in dir1)
<file>
Filter by filename
git log file1
commit 235d5dfe6ce2ac1c509d5aaded38cf0d1953cb29
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:39 2024 +0000

    Modified file1 and added file2 (in dir1)

commit bab510ee0f7c10b81baaa46e41e145680bd308bc
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:33 2024 +0000

    Initial repo and added file1
--decorate --graph
git log --graph --decorate --oneline
* d540207 (HEAD -> main) Modified file2, added .gitignore
* 235d5df Modified file1 and added file2 (in dir1)
* bab510e Initial repo and added file1
--all
All branches
git log --all
commit d5402077556abfd38a986b131a44b42c10fd29ba
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:41 2024 +0000

    Modified file2, added .gitignore

commit 235d5dfe6ce2ac1c509d5aaded38cf0d1953cb29
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:39 2024 +0000

    Modified file1 and added file2 (in dir1)

commit bab510ee0f7c10b81baaa46e41e145680bd308bc
Author: pcinereus <i.obesulus@gmail.com>
Date:   Sun Sep 15 01:57:33 2024 +0000

    Initial repo and added file1

To explore the history of a repository, click on the clock icon (“View history of previous commits” button). This will open up the “Review Changes” window in the “History” tab.

Along with the reverse chronological list of commits, for each commit (and file thereof), you can explore the changes (diffs) that occurred.

Text that appears over a green background represents text that have been added as part of the current commit. Text that appears over a red background represents text that have been removed.

If we scroll down and explore the changes in dir1/file2 for the most recent commit, we see that the text * Notes was removed and then * Notes and --- were added. At first this might seem a bit odd - why was * Notes deleted and then added back?

Git works on entire lines of text. So the first line was replaced because in the newer version, the first line had a carriage return (newline character). Although we cant see this character, it is there - we see it more via its effect (sending the text after it to the next line). Hence, in fact, two lines of text were actually changed in the most recent commit.

7.1.2 reflog

Another way to explore the commit history is to look at the reflog. This is a log of the branch references. This approach is more useful when we have multiple branches and so will be visited in the section on branching. It displays all repository activity, not just the commits.

git reflog
d540207 HEAD@{0}: commit: Modified file2, added .gitignore
235d5df HEAD@{1}: commit: Modified file1 and added file2 (in dir1)
bab510e HEAD@{2}: commit (initial): Initial repo and added file1

Some of this sort of information can be gleaned from the git “History”. Just make sure you select (“all branches”) from the “Switch branch” menu.

7.1.3 diff

Whilst some of these actions described in this section are available from the “History” tab of the “Review Changes” window in Rstudio, most are only available as terminal commands.

Two of the three commits in our repository involved modifications to a file (dir1/file2). To further help illustrate commands to compare files indifferent states, we will additionally make a further change to dir1/file2. The git diff allows us to explore differences between:

  • the workspace and the staging area (index)

    # lets modify dir1/file2
    echo 'Notes' >> dir1/file2
    git diff
    diff --git a/dir1/file2 b/dir1/file2
    index 143a8bb..f12af0a 100644
    --- a/dir1/file2
    +++ b/dir1/file2
    @@ -1,2 +1,3 @@
     * Notes
     ---
    +Notes

    The output indicates that we are comparing the blob representing dir1/file2 in the index (staging area) with the newly modified dir1/file2. The next couple of rows indicate that the indexed version will be represented by a ‘-’ sign and the new version will be represented by a ‘+’ sign. The next row (which is surrounded in a pair of @ signs, indicates that there are two lines that have changed. Finally the next two rows show that a charrage return has been added to the end of the first line and the new version has added the word ‘Notes’ to the next line.

  • the staging area and the last commit

    git add .
    git diff --cached
    diff --git a/dir1/file2 b/dir1/file2
    index 143a8bb..f12af0a 100644
    --- a/dir1/file2
    +++ b/dir1/file2
    @@ -1,2 +1,3 @@
     * Notes
     ---
    +Notes

    Once we stage the modifications, we see that the same differences are recorded.

  • the index and a tree (in this case, the current tree)

    git diff --cached HEAD^{tree}
    diff --git a/dir1/file2 b/dir1/file2
    index 143a8bb..f12af0a 100644
    --- a/dir1/file2
    +++ b/dir1/file2
    @@ -1,2 +1,3 @@
     * Notes
     ---
    +Notes
  • the workspace and the current commit

    git diff HEAD
    diff --git a/dir1/file2 b/dir1/file2
    index 143a8bb..f12af0a 100644
    --- a/dir1/file2
    +++ b/dir1/file2
    @@ -1,2 +1,3 @@
     * Notes
     ---
    +Notes
  • two commits (e.g. previous and current commits)

    git diff HEAD^ HEAD
    diff --git a/.gitignore b/.gitignore
    new file mode 100644
    index 0000000..1944fd6
    --- /dev/null
    +++ b/.gitignore
    @@ -0,0 +1 @@
    +*.tmp
    diff --git a/dir1/file2 b/dir1/file2
    index 4fcc8f8..143a8bb 100644
    --- a/dir1/file2
    +++ b/dir1/file2
    @@ -1 +1,2 @@
     * Notes
    +---
  • two trees (first example, the current and previous commit trees)

    git diff HEAD^{tree} HEAD^^{tree}
    diff --git a/.gitignore b/.gitignore
    deleted file mode 100644
    index 1944fd6..0000000
    --- a/.gitignore
    +++ /dev/null
    @@ -1 +0,0 @@
    -*.tmp
    diff --git a/dir1/file2 b/dir1/file2
    index 143a8bb..4fcc8f8 100644
    --- a/dir1/file2
    +++ b/dir1/file2
    @@ -1,2 +1 @@
     * Notes
    ----
    git diff 07a94 2b61e
    diff --git a/dir1/file2 b/dir1/file2
    new file mode 100644
    index 0000000..4fcc8f8
    --- /dev/null
    +++ b/dir1/file2
    @@ -0,0 +1 @@
    +* Notes
    diff --git a/file1 b/file1
    index 50fcd26..28ed245 100644
    --- a/file1
    +++ b/file1
    @@ -1 +1,2 @@
     File 1
    +---------------
  • two blobs (indeed any two objects)

    git diff 50fcd 28ed2
    diff --git a/50fcd b/28ed2
    index 50fcd26..28ed245 100644
    --- a/50fcd
    +++ b/28ed2
    @@ -1 +1,2 @@
     File 1
    +---------------

7.1.4 ls-files

Similar to the previous section, the following is only really available via the terminal.

We can list the files that comprise the repository by:

git ls-files 
.gitignore
dir1/file2
file1

The change to dir1/file2 above was just to illustrate the git diff. In doing so we now have a modified version of this file that has not been committed Before we move on, I am going to remove these changes so that the dir1/file2 is not in a modified state and reflects the state of the current commit. To do so, I will use perform a hard reset (git reset --hard). More will be discussed about the git reset command later in this tutorial - for now all that is important is to know that it restores the workspace to a previous state.

In addition to the git reset --hard, I will also clean and prune the repository.

git reset --hard 
git clean -qfdx
git reflog expire --expire-unreachable=now --all
git gc --prune=now
HEAD is now at d540207 Modified file2, added .gitignore

7.2 Tags

Although it is possible to track the history of a repository via its commit sha1 names, most find it more convenient to apply tags to certain milestone commits. For example, a particular commit might represent a specific point in the history of a project - such as a release version. Git tags allow us to apply more human readable flags.

git tag V.1

In the above, V.1 is the tag we are applying to the most recent commit. For this example, V.1 simply denotes something like “version 1”. The tag must not contain any spaces (just replace space characters with underscores or periods).

git log --graph --decorate --oneline
* d540207 (HEAD -> main, tag: V.1) Modified file2, added .gitignore
* 235d5df Modified file1 and added file2 (in dir1)
* bab510e Initial repo and added file1
git reflog
d540207 HEAD@{0}: reset: moving to HEAD
d540207 HEAD@{1}: commit: Modified file2, added .gitignore
235d5df HEAD@{2}: commit: Modified file1 and added file2 (in dir1)
bab510e HEAD@{3}: commit (initial): Initial repo and added file1

The functionality to add tags to commits is not directly supported in Rstudio. in order to apply a tag, you will need to switch the terminal and enter a command like:

git tag V.1

In the above, V.1 is the tag we are applying to the most recent commit. For this example, V.1 simply denotes something like “version 1”. The tag must not contain any spaces (just replace space characters with underscores or periods).

Now if we return to the “History” tab of the “Review Changes” window, we can see the tag represented by a yellow tag in the commit diagram.

8 Branching

Again we will start with our repository. For this section, will be working on the repository built up in the previous section.

rm -rf ~/tmp/Repo1
mkdir ~/tmp/Repo1
cd ~/tmp/Repo1
git init 
echo 'File 1' > file1
git add file1
git commit -m 'Initial repo and added file1'
echo '---------------' >> file1
mkdir dir1
echo '* Notes' > dir1/file2
git add file1 dir1/file2
git commit -m 'Modified file1 and added file2 (in dir1)'
echo '---' >> dir1/file2
echo 'temp' > dir1/f.tmp
echo '*.tmp' > .gitignore
git add .
git commit -m 'Modified file2, added .gitignore'
git tag V.1
Initialized empty Git repository in /home/runner/tmp/Repo1/.git/
[main (root-commit) cf111fb] Initial repo and added file1
 1 file changed, 1 insertion(+)
 create mode 100644 file1
[main 8c416f9] Modified file1 and added file2 (in dir1)
 2 files changed, 2 insertions(+)
 create mode 100644 dir1/file2
[main 3534d11] Modified file2, added .gitignore
 2 files changed, 2 insertions(+)
 create mode 100644 .gitignore

The following are a couple of different visuals of the repository.

tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 9 files

Lets assume that the current commit represents a largely stable project. We are about to embark on a substantial modification in the form of a new feature that will involve editing file1 and adding a new file to dir1. At the same time, we wish to leave open the possibility of committing additional minor changes to the current commit in order to address any bugs or issues that might arise.

In essence what we want to do is start a new branch for the new feature. This is performed in two steps:

  1. use the git branch <name> command to generate a new branch reference. In the following example, I will call the new branch Feature, but it can be anything.
git branch Feature

To create a new branch, click on the “New branch” icon (see figure below) and enter a name (e.g. Feature) for the branch in the popup box.

Note

Note, at this stage we have only created a reference to a new branch, until we commit to this branch, its contents will be the same as the main branch.

git log --graph --decorate --oneline
* 3534d11 (HEAD -> main, tag: V.1, Feature) Modified file2, added .gitignore
* 8c416f9 Modified file1 and added file2 (in dir1)
* cf111fb Initial repo and added file1
tree -ra -L 2 --charset ascii
git reflog
.
|-- file1
|-- dir1
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 9 files
3534d11 HEAD@{0}: commit: Modified file2, added .gitignore
8c416f9 HEAD@{1}: commit: Modified file1 and added file2 (in dir1)
cf111fb HEAD@{2}: commit (initial): Initial repo and added file1

  1. use the git checkout <name> command to move the HEAD to the tip of this new branch (Feature).
git checkout Feature
Switched to branch 'Feature'

To checkout a branch in Rstudio, click on the “Switch branch” button and select the branch name (in this case Feature) from the dialog box.

If we now examine the “History” tab of the “Review Changes” window, we will see that the most recent commit has both main and Feature branch markers. However, the main connection is with the Feature branch indicating that the HEAD is currently on the Feature branch.

The only noticable change is that we are now considered to be on the “Feature” branch (notice that HEAD is pointing to Feature). Any new commits will be applied to this new branch.

git log --graph --decorate --oneline
* 3534d11 (HEAD -> Feature, tag: V.1, main) Modified file2, added .gitignore
* 8c416f9 Modified file1 and added file2 (in dir1)
* cf111fb Initial repo and added file1
tree -ra -L 2 --charset ascii
git reflog
.
|-- file1
|-- dir1
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 9 files
3534d11 HEAD@{0}: checkout: moving from main to Feature
3534d11 HEAD@{1}: commit: Modified file2, added .gitignore
8c416f9 HEAD@{2}: commit: Modified file1 and added file2 (in dir1)
cf111fb HEAD@{3}: commit (initial): Initial repo and added file1

Now if we make and commit a change (such as an edit to file1 and an addition of file3 within dir1), we will be operating on a separate branch.

echo 'b' >> file1
echo 'File 3' > dir1/file3
git add .
git commit -m 'New feature'
[Feature 91826ac] New feature
 2 files changed, 2 insertions(+)
 create mode 100644 dir1/file3
  1. modify file1 by adding a carriage return followed by some additional text (e.g. a b) like in the figure below

  2. navigate to this new directory (dir1)

  3. click the “Create a new blank file in current directory” button and select “Text file”

  4. enter a new filename (file3) into the popup box

  5. enter some text into this file (such as File 3)

  6. stage (add) the two modified files using their respective checkboxes

  7. click the “Commit” button, provide a commit message (sch as “New feature”)

  8. close the popup

  9. goto the “History” tab

Notice that we have advanced one commit on this new branch.

git log --graph --decorate --oneline
* 91826ac (HEAD -> Feature) New feature
* 3534d11 (tag: V.1, main) Modified file2, added .gitignore
* 8c416f9 Modified file1 and added file2 (in dir1)
* cf111fb Initial repo and added file1
tree -ra -L 2 --charset ascii
git reflog
.
|-- file1
|-- dir1
|   |-- file3
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 10 files
91826ac HEAD@{0}: commit: New feature
3534d11 HEAD@{1}: checkout: moving from main to Feature
3534d11 HEAD@{2}: commit: Modified file2, added .gitignore
8c416f9 HEAD@{3}: commit: Modified file1 and added file2 (in dir1)
cf111fb HEAD@{4}: commit (initial): Initial repo and added file1

So we can now continue to develop the Feature branch. But what if we now decided that we wanted to make a change to the main branch (perhaps addressing a bug or issue).

  1. switch over to the main branch
git checkout main
Switched to branch 'main'

Use the “Switch branch” selector to checkout the main branch

git log --graph --decorate --oneline --all
* 91826ac (Feature) New feature
* 3534d11 (HEAD -> main, tag: V.1) Modified file2, added .gitignore
* 8c416f9 Modified file1 and added file2 (in dir1)
* cf111fb Initial repo and added file1
tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 9 files
git reflog
3534d11 HEAD@{0}: checkout: moving from Feature to main
91826ac HEAD@{1}: commit: New feature
3534d11 HEAD@{2}: checkout: moving from main to Feature
3534d11 HEAD@{3}: commit: Modified file2, added .gitignore
8c416f9 HEAD@{4}: commit: Modified file1 and added file2 (in dir1)
cf111fb HEAD@{5}: commit (initial): Initial repo and added file1

  1. make the necessary changes to the files and commit them on the main branch
echo ' another bug fix' >> dir1/file2
git add .
git commit -m 'Bug fix in file1'
[main 8c73b66] Bug fix in file1
 1 file changed, 1 insertion(+)
  1. make the following edits to dir1/file2

  2. stage and commit the changes

git log --graph --decorate --oneline --all
* 8c73b66 (HEAD -> main) Bug fix in file1
| * 91826ac (Feature) New feature
|/  
* 3534d11 (tag: V.1) Modified file2, added .gitignore
* 8c416f9 Modified file1 and added file2 (in dir1)
* cf111fb Initial repo and added file1
tree -ra -L 2 --charset ascii
git reflog
.
|-- file1
|-- dir1
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 9 files
8c73b66 HEAD@{0}: commit: Bug fix in file1
3534d11 HEAD@{1}: checkout: moving from Feature to main
91826ac HEAD@{2}: commit: New feature
3534d11 HEAD@{3}: checkout: moving from main to Feature
3534d11 HEAD@{4}: commit: Modified file2, added .gitignore
8c416f9 HEAD@{5}: commit: Modified file1 and added file2 (in dir1)
cf111fb HEAD@{6}: commit (initial): Initial repo and added file1

We could simultaneously make additional modifications to the Feature branch just by simply checking out the Feature branch and commiting those modifications. To illustrate, we will make another change to the dir1/file3 file.

For this demonstration we are deliberately avoiding making edits to either file1 or dir1/file2. This is because if we did, there is a chance that we might introduce conflicting edits of the same lines of files across the two branches (main and Feature).

In a later section, we WILL deliberately introduce a conflict so that we can see how to resolve conflicts.

git checkout Feature
echo ' a modification' >> dir1/file3
git add .
git commit -m 'Feature complete'
Switched to branch 'Feature'
[Feature aa38a67] Feature complete
 1 file changed, 1 insertion(+)
  1. checkout the Feature branch using the “Switch branch” selector

  2. modify dir1/file3 by adding a carriage return followed by some additional text (e.g. a modification) like in the figure below

  3. stage (add) this file

  4. commit the change with the message of Feature complete

git log --graph --decorate --oneline --all
git reflog
* aa38a67 (HEAD -> Feature) Feature complete
* 91826ac New feature
| * 8c73b66 (main) Bug fix in file1
|/  
* 3534d11 (tag: V.1) Modified file2, added .gitignore
* 8c416f9 Modified file1 and added file2 (in dir1)
* cf111fb Initial repo and added file1
aa38a67 HEAD@{0}: commit: Feature complete
91826ac HEAD@{1}: checkout: moving from main to Feature
8c73b66 HEAD@{2}: commit: Bug fix in file1
3534d11 HEAD@{3}: checkout: moving from Feature to main
91826ac HEAD@{4}: commit: New feature
3534d11 HEAD@{5}: checkout: moving from main to Feature
3534d11 HEAD@{6}: commit: Modified file2, added .gitignore
8c416f9 HEAD@{7}: commit: Modified file1 and added file2 (in dir1)
cf111fb HEAD@{8}: commit (initial): Initial repo and added file1
tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   |-- file3
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 10 files

8.1 Merge branches

Finally, (if we are satisfied that Feature is stable and complete), we might like to introduce these changes into the main branch so that they become a part of the main project base. This operation is called a merge and is completed with the git merge <branch> command where <branch> is the name of the branch you want to merge the current branch (that pointed to by HEAD) with. Typically we want to merge the non-main branch with the main branch. Therefore we must be checkout the main branch before merging.

git checkout main
Switched to branch 'main'
git merge Feature --no-edit
Merge made by the 'ort' strategy.
 dir1/file3 | 2 ++
 file1      | 1 +
 2 files changed, 3 insertions(+)
 create mode 100644 dir1/file3
  1. checkout the main branch using the “Switch branch” selector

  2. merging is not directly supported in Rstudio, so go to the terminal and enter the git merge command as shown below

  3. if you now review the “History” tab of the “Review Changes” window, you will see the confluence of the two branches reflected in the commit graphic

git log --graph --decorate --oneline --all
*   cbc5949 (HEAD -> main) Merge branch 'Feature'
|\  
| * aa38a67 (Feature) Feature complete
| * 91826ac New feature
* | 8c73b66 Bug fix in file1
|/  
* 3534d11 (tag: V.1) Modified file2, added .gitignore
* 8c416f9 Modified file1 and added file2 (in dir1)
* cf111fb Initial repo and added file1
tree -ra -L 2 --charset ascii
git reflog
.
|-- file1
|-- dir1
|   |-- file3
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- ORIG_HEAD
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 11 files
cbc5949 HEAD@{0}: merge Feature: Merge made by the 'ort' strategy.
8c73b66 HEAD@{1}: checkout: moving from Feature to main
aa38a67 HEAD@{2}: commit: Feature complete
91826ac HEAD@{3}: checkout: moving from main to Feature
8c73b66 HEAD@{4}: commit: Bug fix in file1
3534d11 HEAD@{5}: checkout: moving from Feature to main
91826ac HEAD@{6}: commit: New feature
3534d11 HEAD@{7}: checkout: moving from main to Feature
3534d11 HEAD@{8}: commit: Modified file2, added .gitignore
8c416f9 HEAD@{9}: commit: Modified file1 and added file2 (in dir1)
cf111fb HEAD@{10}: commit (initial): Initial repo and added file1

Warning

If, when issuing a git merge command, you get a conflict message, please refer to the section on resolving conflicts below.

8.2 Delete a branch

Once the purpose of the branch has been fulfilled (for example to develop a new feature) and the branch has been merged back into the main branch, you might consider deleting the branch so as to simplify the commit history.

Importantly, this action should only ever be performed after the branch has been successfully merged into the main branch (in fact `git will not allow you to delete an un-merged branch unless you really fight for it).

Note also, this can only be performed on a local repository.

This procedure is only available via the terminal.

git branch -d Feature
Deleted branch Feature (was aa38a67).

git log --graph --decorate --oneline --all
*   cbc5949 (HEAD -> main) Merge branch 'Feature'
|\  
| * aa38a67 Feature complete
| * 91826ac New feature
* | 8c73b66 Bug fix in file1
|/  
* 3534d11 (tag: V.1) Modified file2, added .gitignore
* 8c416f9 Modified file1 and added file2 (in dir1)
* cf111fb Initial repo and added file1
tree -ra -L 2 --charset ascii
git reflog
.
|-- file1
|-- dir1
|   |-- file3
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- ORIG_HEAD
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 11 files
cbc5949 HEAD@{0}: merge Feature: Merge made by the 'ort' strategy.
8c73b66 HEAD@{1}: checkout: moving from Feature to main
aa38a67 HEAD@{2}: commit: Feature complete
91826ac HEAD@{3}: checkout: moving from main to Feature
8c73b66 HEAD@{4}: commit: Bug fix in file1
3534d11 HEAD@{5}: checkout: moving from Feature to main
91826ac HEAD@{6}: commit: New feature
3534d11 HEAD@{7}: checkout: moving from main to Feature
3534d11 HEAD@{8}: commit: Modified file2, added .gitignore
8c416f9 HEAD@{9}: commit: Modified file1 and added file2 (in dir1)
cf111fb HEAD@{10}: commit (initial): Initial repo and added file1

In order to facilitate the rest of the tutorial, I am going to reset the repository to a commit that precedes the merge. The process of resetting will be covered later on in this tutorial.

git reset --hard HEAD~1
git clean -qfdx
git reflog expire --expire-unreachable=now --all
git gc --prune=now
HEAD is now at 8c73b66 Bug fix in file1

tree -ra -L 2 --charset ascii
git log --graph --decorate --oneline --all
git reflog
.
|-- file1
|-- dir1
|   `-- file2
|-- .gitignore
`-- .git
    |-- refs
    |-- packed-refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- ORIG_HEAD
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 10 files
* aa38a67 (Feature) Feature complete
* 91826ac New feature
| * 8c73b66 (HEAD -> main) Bug fix in file1
|/  
* 3534d11 (tag: V.1) Modified file2, added .gitignore
* 8c416f9 Modified file1 and added file2 (in dir1)
* cf111fb Initial repo and added file1
8c73b66 HEAD@{0}: checkout: moving from Feature to main
aa38a67 HEAD@{1}: commit: Feature complete
91826ac HEAD@{2}: checkout: moving from main to Feature
8c73b66 HEAD@{3}: commit: Bug fix in file1
3534d11 HEAD@{4}: checkout: moving from Feature to main
91826ac HEAD@{5}: commit: New feature
3534d11 HEAD@{6}: checkout: moving from main to Feature
3534d11 HEAD@{7}: commit: Modified file2, added .gitignore
8c416f9 HEAD@{8}: commit: Modified file1 and added file2 (in dir1)
cf111fb HEAD@{9}: commit (initial): Initial repo and added file1

8.3 Rebasing

Branches are great for working on new features without disturbing a stable codebase. However, the main branch may have changed or progressed since the branch started. As a result, it may be building upon old code that may either no longer work or no longer be appropriate.

Whilst you could attempt to manually apply the newer main code changes into your branch, this is likely to be tedious and error prone. Rebasing is the process of changing the root (base) of the branch from one commit to another. In this way, the base of the branch can be moved to the current HEAD of the main branch, thereby absorbing all the updates from the main branch into the feature branch.

This section builds on the repository created up to this point in the tutorial. To remind you, the repository currently looks like:

rm -rf ~/tmp/Repo1
mkdir ~/tmp/Repo1
cd ~/tmp/Repo1
git init 
echo 'File 1' > file1
git add file1
git commit -m 'Initial repo and added file1'
echo '---------------' >> file1
mkdir dir1
echo '* Notes' > dir1/file2
git add file1 dir1/file2
git commit -m 'Modified file1 and added file2 (in dir1)'
echo '---' >> dir1/file2
echo 'temp' > dir1/f.tmp
echo '*.tmp' > .gitignore
git add .
git commit -m 'Modified file2, added .gitignore'
git tag V.1
git branch Feature
git checkout Feature
echo 'b' >> file1
echo 'File 3' > dir1/file3
git add .
git commit -m 'New feature'
git checkout main
echo ' another bug fix' >> dir1/file2
git add .
git commit -m 'Bug fix in file1'
git checkout Feature
echo ' a modification' >> dir1/file3
git add .
git commit -m 'Feature complete'
Initialized empty Git repository in /home/runner/tmp/Repo1/.git/
[main (root-commit) 7bdfcce] Initial repo and added file1
 1 file changed, 1 insertion(+)
 create mode 100644 file1
[main 0261f74] Modified file1 and added file2 (in dir1)
 2 files changed, 2 insertions(+)
 create mode 100644 dir1/file2
[main 2c47c61] Modified file2, added .gitignore
 2 files changed, 2 insertions(+)
 create mode 100644 .gitignore
Switched to branch 'Feature'
[Feature 571359f] New feature
 2 files changed, 2 insertions(+)
 create mode 100644 dir1/file3
Switched to branch 'main'
[main c345300] Bug fix in file1
 1 file changed, 1 insertion(+)
Switched to branch 'Feature'
[Feature c745fdb] Feature complete
 1 file changed, 1 insertion(+)

tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   |-- file3
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 10 files
git log --graph --decorate --oneline --all
* c745fdb (HEAD -> Feature) Feature complete
* 571359f New feature
| * c345300 (main) Bug fix in file1
|/  
* 2c47c61 (tag: V.1) Modified file2, added .gitignore
* 0261f74 Modified file1 and added file2 (in dir1)
* 7bdfcce Initial repo and added file1
git reflog
c745fdb HEAD@{0}: commit: Feature complete
571359f HEAD@{1}: checkout: moving from main to Feature
c345300 HEAD@{2}: commit: Bug fix in file1
2c47c61 HEAD@{3}: checkout: moving from Feature to main
571359f HEAD@{4}: commit: New feature
2c47c61 HEAD@{5}: checkout: moving from main to Feature
2c47c61 HEAD@{6}: commit: Modified file2, added .gitignore
0261f74 HEAD@{7}: commit: Modified file1 and added file2 (in dir1)
7bdfcce HEAD@{8}: commit (initial): Initial repo and added file1
git checkout Feature
Already on 'Feature'
git rebase main
Rebasing (1/2)
Rebasing (2/2)

                                                                                
Successfully rebased and updated refs/heads/Feature.

Rebasing is not directly supported by Rstudio.

  1. checkout the Feature branch using the “Switch branch” selector

  2. in the terminal, type git rebase main to rebase the Feature branch on the end of the main branch.

  3. if you now review the “History” tab of the “Review Changes” window, you will now see that the history is linear and the Feature branch stems from the end of the main branch. That is, we have moved the base of the Feature branch.

tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   |-- file3
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- ORIG_HEAD
    |-- HEAD
    |-- COMMIT_EDITMSG
    `-- AUTO_MERGE

8 directories, 12 files
git log --graph --decorate --oneline --all
* 9ce032d (HEAD -> Feature) Feature complete
* b77c352 New feature
* c345300 (main) Bug fix in file1
* 2c47c61 (tag: V.1) Modified file2, added .gitignore
* 0261f74 Modified file1 and added file2 (in dir1)
* 7bdfcce Initial repo and added file1
git reflog
9ce032d HEAD@{0}: rebase (finish): returning to refs/heads/Feature
9ce032d HEAD@{1}: rebase (pick): Feature complete
b77c352 HEAD@{2}: rebase (pick): New feature
c345300 HEAD@{3}: rebase (start): checkout main
c745fdb HEAD@{4}: checkout: moving from Feature to Feature
c745fdb HEAD@{5}: commit: Feature complete
571359f HEAD@{6}: checkout: moving from main to Feature
c345300 HEAD@{7}: commit: Bug fix in file1
2c47c61 HEAD@{8}: checkout: moving from Feature to main
571359f HEAD@{9}: commit: New feature
2c47c61 HEAD@{10}: checkout: moving from main to Feature
2c47c61 HEAD@{11}: commit: Modified file2, added .gitignore
0261f74 HEAD@{12}: commit: Modified file1 and added file2 (in dir1)
7bdfcce HEAD@{13}: commit (initial): Initial repo and added file1

If the rebased commits were previously on a remote repository (hopefully you checked to make sure noone was relying on any of the commits that have been squashed), then it will be necessary to force a push on this repository.

9 Undoing (rolling back) changes

One of the real strengths of a versioning system is the ability to roll back to a previous state when changes have been found to introduce undesirable or unintended consequences. There are also multiple different stages from which to roll back. For example, do we want to revert from committed states or just unstage a file or files.

To illustrate the various ways to roll back within a repository, we will start with a small repository comprising of a single branch and just three commits. This repository will mimic a repository created earlier in this tutorial (before we started branching).

rm -rf ~/tmp/Repo1
mkdir ~/tmp/Repo1
cd ~/tmp/Repo1
git init 
echo 'File 1' > file1
git add file1
git commit -m 'Initial repo and added file1'
echo '---------------' >> file1
mkdir dir1
echo '* Notes' > dir1/file2
git add file1 dir1/file2
git commit -m 'Modified file1 and added file2 (in dir1)'
echo '---' >> dir1/file2
echo 'temp' > dir1/f.tmp
echo '*.tmp' > .gitignore
git add .
git commit -m 'Modified file2, added .gitignore'
git tag V.1
Initialized empty Git repository in /home/runner/tmp/Repo1/.git/
[main (root-commit) 1401d56] Initial repo and added file1
 1 file changed, 1 insertion(+)
 create mode 100644 file1
[main 6898aca] Modified file1 and added file2 (in dir1)
 2 files changed, 2 insertions(+)
 create mode 100644 dir1/file2
[main b284e9b] Modified file2, added .gitignore
 2 files changed, 2 insertions(+)
 create mode 100644 .gitignore
cat .git/refs/heads/main
tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 9 files
git log --graph --decorate --oneline --all
* b284e9b (HEAD -> main, tag: V.1) Modified file2, added .gitignore
* 6898aca Modified file1 and added file2 (in dir1)
* 1401d56 Initial repo and added file1
b284e9b HEAD@{0}: commit: Modified file2, added .gitignore
6898aca HEAD@{1}: commit: Modified file1 and added file2 (in dir1)
1401d56 HEAD@{2}: commit (initial): Initial repo and added file1

The above diagram shows that both HEAD and main point at the same stage (all three files). Again, remember that the SHA-1 has values will be different in your repo so in the following, you will need to use the SHA value that corresponds to the item in your list.

With additional commits and activity, the above schematic will rapidly become very busy and complex. As a result, we will now switch to a simpler schematic that focuses only on the commits and references thereof (HEAD, main and branches).

Recall that a git repository comprises multiple levels in which changes are recorded:

  • there is the Workspace (which is essentially the actual files and folders that you directly edit).
  • there is the Staging area (or index which is a record of which files are next to be committed).
  • there is the Local repository (the actual commits).
  • and finally, three is the remote repository (a remote store of commits).

As such, there are multiple levels from which changes could be undone. Furthermore, we might want to undo changes at the commit or individual file level. For example, we might decide that we have made a local commit that introduced an issue and we now wish to return back to the state prior to this commit. Alternatively, we might have just accidentally staged a file (yet not committed it) and now we want to unstage it.

Action Command Notes
Commit level
Undo to a particular local commit
git reset --soft <commit> HEAD is moved to the nominated . IT DOES NOT alter index or the workspace
Roll back to the the previous commit
git reset --hard <commit> Resets the Index and Workspace
Roll back over the last two commits
git reset --hard HEAD~2 Roll back over the last two commits
Inspect an old commit
git checkout <commit> moves the HEAD and modifies the workspace to reflect its state at
Roll back the changes introduced by commit so that a new commit resembles a previous state
git revert HEAD Creates a new commit that reverses the changes introduced by the last commit. Revert creates a new revision history that adds onto existing history and is therefore safe to use on a branch that has been pushed to a remote.

Now lets say we wanted to roll back to the state before we added .gitignore and modified dir1/file2. That is, we want to roll-back to commit 6898aca. We have three main choices:

  • reset - this allows us to remove all commits back to a nominated commit. Resetting is a irreversible process as it totally removes commits from the history. A reset should only ever be used if you are sure you want to permanently remove the changes introduced via one or more commits. A reset should never be performed on a branch that exists in a remote repository

  • revert - this allows us to skip the most recent commit. That is, a revert rolls back to a previous commit and then apply that state to a new commit. Unlike a reset, all commits remain safely in the git history and can target a single commit.

  • branch - this allows us to safely take the project (or part of the project) in an experimental direction that might involve dramatic deviations in files without interrupting the main thread of the project. At some point, if the new direction proves useful, the changes can be merged back into the main branch. We will expore branching in the section on branching.

Normally we would not perform all three. Rather, we would select the most appropriate one depending on the context and goal. Nevertheless, this is a tutorial and therefore we will perform all three. In order to ensure that we start from the same point for each demonstration, prior to each demonstration, we will aggressively reset the repository back to the state it was at commit 6898aca.

9.1 Reset

Reset is not directly supported by Rstudio - use the terminal for this section.

9.1.1 Soft reset

When we perform a soft reset, we move the head to the nominated commit, but the workspace is unchanged.

git reset --soft 6898aca
tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- ORIG_HEAD
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 10 files
git log --graph --decorate --oneline --all
* b284e9b (tag: V.1) Modified file2, added .gitignore
* 6898aca (HEAD -> main) Modified file1 and added file2 (in dir1)
* 1401d56 Initial repo and added file1
6898aca HEAD@{0}: reset: moving to 6898aca
b284e9b HEAD@{1}: commit: Modified file2, added .gitignore
6898aca HEAD@{2}: commit: Modified file1 and added file2 (in dir1)
1401d56 HEAD@{3}: commit (initial): Initial repo and added file1

tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   |-- file2
|   `-- f.tmp
|-- .gitignore
`-- .git
    |-- refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- ORIG_HEAD
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 10 files
git log --graph --decorate --oneline --all
* b284e9b (tag: V.1) Modified file2, added .gitignore
* 6898aca (HEAD -> main) Modified file1 and added file2 (in dir1)
* 1401d56 Initial repo and added file1
6898aca HEAD@{0}: reset: moving to 6898aca
b284e9b HEAD@{1}: commit: Modified file2, added .gitignore
6898aca HEAD@{2}: commit: Modified file1 and added file2 (in dir1)
1401d56 HEAD@{3}: commit (initial): Initial repo and added file1

9.1.2 Hard reset

When we perform a hard reset, we not only move the head to the nominated commit, but the workspace is altered to reflect the workspace that existed when that commit was originally performed.

As I am about to demonstrate this on a repo that I have just performed a soft reset on, I am first going to start by re-establishing the original repository. If you have not just run a soft reset, then ignore the following.

git reset --hard V.1
git clean -qfdx
git reflog expire --expire-unreachable=now --all
git gc --prune=now
HEAD is now at b284e9b Modified file2, added .gitignore

Now we are in a position to perform the hard reset.

git reset --hard 6898aca
HEAD is now at 6898aca Modified file1 and added file2 (in dir1)
tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   `-- file2
`-- .git
    |-- refs
    |-- packed-refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- ORIG_HEAD
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 9 files

Notice that .gitignore is not not present.

git log --graph --decorate --oneline --all
* b284e9b (tag: V.1) Modified file2, added .gitignore
* 6898aca (HEAD -> main) Modified file1 and added file2 (in dir1)
* 1401d56 Initial repo and added file1
git reflog
6898aca HEAD@{0}: reset: moving to 6898aca
b284e9b HEAD@{1}: reset: moving to V.1
6898aca HEAD@{2}: reset: moving to 6898aca
b284e9b HEAD@{3}: commit: Modified file2, added .gitignore
6898aca HEAD@{4}: commit: Modified file1 and added file2 (in dir1)
1401d56 HEAD@{5}: commit (initial): Initial repo and added file1

Note, however, if we looked at the log, it would be as if the previous commit had not occurred. For this reason, care must be exercised when using reset on remote repositories since others may be relying on a specific point in the repo history that you may have just erased.

If we now make a change (such as a change to file1 and adding file3) and commit, it would be as if any commits after 6898aca had never occurred.

echo 'End' > file1
echo 'File3' >> dir1/file3
git add file1 dir1/file3
git commit -m 'Modified file1 and added file3'
[main c7b47fa] Modified file1 and added file3
 2 files changed, 2 insertions(+), 2 deletions(-)
 create mode 100644 dir1/file3
git log --graph --decorate --oneline --all
* c7b47fa (HEAD -> main) Modified file1 and added file3
| * b284e9b (tag: V.1) Modified file2, added .gitignore
|/  
* 6898aca Modified file1 and added file2 (in dir1)
* 1401d56 Initial repo and added file1
git reflog
c7b47fa HEAD@{0}: commit: Modified file1 and added file3
6898aca HEAD@{1}: reset: moving to 6898aca
b284e9b HEAD@{2}: reset: moving to V.1
6898aca HEAD@{3}: reset: moving to 6898aca
b284e9b HEAD@{4}: commit: Modified file2, added .gitignore
6898aca HEAD@{5}: commit: Modified file1 and added file2 (in dir1)
1401d56 HEAD@{6}: commit (initial): Initial repo and added file1
tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   |-- file3
|   `-- file2
`-- .git
    |-- refs
    |-- packed-refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- ORIG_HEAD
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 10 files

Notice the addition of file3 in dir1

git ls-files
dir1/file2
dir1/file3
file1

9.2 Revert

As with git reset, git revert is not directly supported by Rstudio, hence the methods used in this section should be performed in the terminal. There is one exception to this, Rstudio is able to revert an modified file back to its state in the last commit.

git reset --hard V.1
git clean -qfdx
git reflog expire --expire-unreachable=now --all
git gc --prune=now
HEAD is now at b284e9b Modified file2, added .gitignore

Revert generates a new commit that removes the changes that were introduced by one or more of the most recent commits. Note, it does not revert to a particular commit, but rather undoes a commit. So, to roll back to 6898aca (the second last commit), we just have to revert the last commit (HEAD).

git revert HEAD
[main 9ea61a5] Revert "Modified file2, added .gitignore"
 Date: Sun Sep 15 01:58:12 2024 +0000
 2 files changed, 2 deletions(-)
 delete mode 100644 .gitignore

However, if we explore the reflog, we can see the entire history

git reflog
9ea61a5 HEAD@{0}: revert: Revert "Modified file2, added .gitignore"
6898aca HEAD@{1}: reset: moving to 6898aca
b284e9b HEAD@{2}: reset: moving to V.1
6898aca HEAD@{3}: reset: moving to 6898aca
b284e9b HEAD@{4}: commit: Modified file2, added .gitignore
6898aca HEAD@{5}: commit: Modified file1 and added file2 (in dir1)
1401d56 HEAD@{6}: commit (initial): Initial repo and added file1
git log --graph --decorate --oneline --all
* 9ea61a5 (HEAD -> main) Revert "Modified file2, added .gitignore"
* b284e9b (tag: V.1) Modified file2, added .gitignore
* 6898aca Modified file1 and added file2 (in dir1)
* 1401d56 Initial repo and added file1
tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   `-- file2
`-- .git
    |-- refs
    |-- packed-refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- ORIG_HEAD
    |-- HEAD
    |-- COMMIT_EDITMSG
    `-- AUTO_MERGE

8 directories, 10 files

Notice the absence of .gitignore. Notice also that dir1/f.tmp is also present. Although this file was added at the same time as .gitignore, it was never committed and therefore is not altered with repo manipulations.

If we list the files that are part of the repo:

git ls-files
dir1/file2
file1

we will see that we are back to the state where only file1 and dir1/file2 are present.

git reset --hard V.1
git clean -qfdx
git reflog expire --expire-unreachable=now --all
git gc --prune=now
HEAD is now at b284e9b Modified file2, added .gitignore

If we had actually wanted to roll back to commit 1401d56, then we could do so by sequentially issuing git revert:

git revert --no-commit HEAD
git revert --no-commit HEAD~1
git commit -m 'Rolled back'
[main f6fbba8] Rolled back
 3 files changed, 4 deletions(-)
 delete mode 100644 .gitignore
 delete mode 100644 dir1/file2
git reflog
f6fbba8 HEAD@{0}: commit: Rolled back
6898aca HEAD@{1}: reset: moving to 6898aca
b284e9b HEAD@{2}: reset: moving to V.1
6898aca HEAD@{3}: reset: moving to 6898aca
b284e9b HEAD@{4}: commit: Modified file2, added .gitignore
6898aca HEAD@{5}: commit: Modified file1 and added file2 (in dir1)
1401d56 HEAD@{6}: commit (initial): Initial repo and added file1
git log --graph --decorate --oneline --all
* f6fbba8 (HEAD -> main) Rolled back
* b284e9b (tag: V.1) Modified file2, added .gitignore
* 6898aca Modified file1 and added file2 (in dir1)
* 1401d56 Initial repo and added file1
tree -ra -L 2 --charset ascii
.
|-- file1
`-- .git
    |-- refs
    |-- packed-refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- ORIG_HEAD
    |-- HEAD
    `-- COMMIT_EDITMSG

7 directories, 8 files

Notice that file2 is now also absent. If we list the files that are part of the repo:

git ls-files
file1

we will see that we are back to the state where only file is present

9.3 Checkout and branching

Once again, the methods outlined in this section are not directly supported by Rstudio. Please use the terminal instead.

git reset --hard V.1
git clean -qfdx
git reflog expire --expire-unreachable=now --all
git gc --prune=now
HEAD is now at b284e9b Modified file2, added .gitignore

If we wanted to review the state of files corresponding to commit 6898aca, we could checkout the code from that commit. This provides a way to travel back in time through your commits and explore the (tracked) files exactly as they were.

git checkout 6898a
Note: switching to '6898a'.

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 6898aca Modified file1 and added file2 (in dir1)
git reflog
6898aca HEAD@{0}: checkout: moving from main to 6898a
6898aca HEAD@{1}: reset: moving to 6898aca
b284e9b HEAD@{2}: reset: moving to V.1
6898aca HEAD@{3}: reset: moving to 6898aca
b284e9b HEAD@{4}: commit: Modified file2, added .gitignore
6898aca HEAD@{5}: commit: Modified file1 and added file2 (in dir1)
1401d56 HEAD@{6}: commit (initial): Initial repo and added file1
git log --graph --decorate --oneline --all
* b284e9b (tag: V.1, main) Modified file2, added .gitignore
* 6898aca (HEAD) Modified file1 and added file2 (in dir1)
* 1401d56 Initial repo and added file1
tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   `-- file2
`-- .git
    |-- refs
    |-- packed-refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    |-- ORIG_HEAD
    |-- HEAD
    `-- COMMIT_EDITMSG

8 directories, 9 files

Notice that file2 is now also absent. If we list the files that are part of the repo:

git ls-files
dir1/file2
file1

we will see that we are back to the state where only file is present

If we go to the “History” tab of the “Review Changes” window, you will notice that the commit history has been truncated to reflect that we have gone back in commit history.

Nevertheless, if we select “All branches” from the dropdown menu, we can see the full commit history.

The output advises us that we are in a detached HEAD state. This occurs when a commit is checked out rather than a branch. Normally, when changes are committed, the new commit is added to the HEAD of the current branch. However, in a detached HEAD state, any commits that are made are not associated with any branch and will effectively be lost next time you checkout.

So if for example, we then added another file (file3)..

echo 'END' > file3
git add file3
git commit -m 'END added to file3'
[detached HEAD 0c83489] END added to file3
 1 file changed, 1 insertion(+)
 create mode 100644 file3
git reflog
0c83489 HEAD@{0}: commit: END added to file3
6898aca HEAD@{1}: checkout: moving from main to 6898a
6898aca HEAD@{2}: reset: moving to 6898aca
b284e9b HEAD@{3}: reset: moving to V.1
6898aca HEAD@{4}: reset: moving to 6898aca
b284e9b HEAD@{5}: commit: Modified file2, added .gitignore
6898aca HEAD@{6}: commit: Modified file1 and added file2 (in dir1)
1401d56 HEAD@{7}: commit (initial): Initial repo and added file1
git log --graph --decorate --oneline --all
* 0c83489 (HEAD) END added to file3
| * b284e9b (tag: V.1, main) Modified file2, added .gitignore
|/  
* 6898aca Modified file1 and added file2 (in dir1)
* 1401d56 Initial repo and added file1

Now if we checked out main, the commit we made whilst in detached head mode would be lost.

git checkout main
Warning: you are leaving 1 commit behind, not connected to
any of your branches:

  0c83489 END added to file3

If you want to keep it by creating a new branch, this may be a good time
to do so with:

 git branch <new-branch-name> 0c83489

Switched to branch 'main'
git reflog
b284e9b HEAD@{0}: checkout: moving from 0c83489041cbfe25707f8a2823a1304b4e0a2954 to main
0c83489 HEAD@{1}: commit: END added to file3
6898aca HEAD@{2}: checkout: moving from main to 6898a
6898aca HEAD@{3}: reset: moving to 6898aca
b284e9b HEAD@{4}: reset: moving to V.1
6898aca HEAD@{5}: reset: moving to 6898aca
b284e9b HEAD@{6}: commit: Modified file2, added .gitignore
6898aca HEAD@{7}: commit: Modified file1 and added file2 (in dir1)
1401d56 HEAD@{8}: commit (initial): Initial repo and added file1
git log --graph --decorate --oneline --all
* b284e9b (HEAD -> main, tag: V.1) Modified file2, added .gitignore
* 6898aca Modified file1 and added file2 (in dir1)
* 1401d56 Initial repo and added file1

If, having reviewed the state of a commit (by checking it out), we decided that we wanted to roll back to this state and develop further (make additional commits), we are effectively deciding to start a new branch that splits off at that commit. See the section on Branching for more details on how to do that.

10 Synching with remote repository

When a project has multiple contributors, it is typical for there to be a remote repository against which each contributor can exchange their contributions. The remote repository comprises only the .git folder (and its contents), it never has a workspace. Files are rarely edited directly on the remote repository. Instead, it acts as a constantly available ‘main’ conduit between all contributors.

A remote repository can be anywhere that you have permission to at least read from. Obviously, if you also want to contribute your local commits to the remote repository, you also need write access to that location. If you intend to collaborate, then the remote repository also needs to be in a location that all users can access at any time.

For this demonstration, we will start by re-generating a repository that we made earlier on in this tutorial. This repository comprises a main branch along with an un-merged Feature branch.

rm -rf ~/tmp/Repo1
mkdir ~/tmp/Repo1
cd ~/tmp/Repo1
git init 
echo 'File 1' > file1
git add file1
git commit -m 'Initial repo and added file1'
echo '---------------' >> file1
mkdir dir1
echo '* Notes' > dir1/file2
git add file1 dir1/file2
git commit -m 'Modified file1 and added file2 (in dir1)'
echo '---' >> dir1/file2
echo 'temp' > dir1/f.tmp
echo '*.tmp' > .gitignore
git add .
git commit -m 'Modified file2, added .gitignore'
git branch Feature
git checkout Feature
echo 'b' >> file1
echo 'File 3' > dir1/file3
git add .
git commit -m 'New feature'
git checkout main
echo ' another bug fix' >> dir1/file2
git add .
git commit -m 'Bug fix in file1'
git checkout Feature
echo ' a modification' >> dir1/file3
git add .
git commit -m 'Feature complete'
git checkout main

git reflog
git log --graph --decorate --oneline --all
Initialized empty Git repository in /home/runner/tmp/Repo1/.git/
[main (root-commit) 5cdb90a] Initial repo and added file1
 1 file changed, 1 insertion(+)
 create mode 100644 file1
[main 8a7c777] Modified file1 and added file2 (in dir1)
 2 files changed, 2 insertions(+)
 create mode 100644 dir1/file2
[main 3b0c29d] Modified file2, added .gitignore
 2 files changed, 2 insertions(+)
 create mode 100644 .gitignore
Switched to branch 'Feature'
[Feature 4fc0c5f] New feature
 2 files changed, 2 insertions(+)
 create mode 100644 dir1/file3
Switched to branch 'main'
[main feb72a7] Bug fix in file1
 1 file changed, 1 insertion(+)
Switched to branch 'Feature'
[Feature dcd4b57] Feature complete
 1 file changed, 1 insertion(+)
Switched to branch 'main'
feb72a7 HEAD@{0}: checkout: moving from Feature to main
dcd4b57 HEAD@{1}: commit: Feature complete
4fc0c5f HEAD@{2}: checkout: moving from main to Feature
feb72a7 HEAD@{3}: commit: Bug fix in file1
3b0c29d HEAD@{4}: checkout: moving from Feature to main
4fc0c5f HEAD@{5}: commit: New feature
3b0c29d HEAD@{6}: checkout: moving from main to Feature
3b0c29d HEAD@{7}: commit: Modified file2, added .gitignore
8a7c777 HEAD@{8}: commit: Modified file1 and added file2 (in dir1)
5cdb90a HEAD@{9}: commit (initial): Initial repo and added file1
* dcd4b57 (Feature) Feature complete
* 4fc0c5f New feature
| * feb72a7 (HEAD -> main) Bug fix in file1
|/  
* 3b0c29d Modified file2, added .gitignore
* 8a7c777 Modified file1 and added file2 (in dir1)
* 5cdb90a Initial repo and added file1

10.1 Simulate a remote repository locally

For the purpose of this tutorial, we will create a remote repository that is on the same computer as the above repository that we have been working on. Whilst not the typical situation, it does mean that an external location and account is not necessary to follow along with the tutorial. As previously mentioned, the actual location of the remote repository is almost irrelevant to how you interact with it. Therefore, whether the remote repository is on the same computer or elsewhere in the world makes little difference (other than permissions and connections).

This step is not supported directly by Rstudio - please use the terminal.

cd ~/tmp/RemoteRepo1
git init --bare
Initialized empty Git repository in /home/runner/tmp/RemoteRepo1/

Now that we have a remote repository - albeit empty at this stage - we return to our local repository and declare (add) the location of the remote repository using the git remote add <name> <url> command. In this command, an optional name can be supplied to refer to the remote repository (<name>). The compulsory <url> argument is the address (location) of the remote repository.

This step is not supported directly by Rstudio - please use the terminal.

git remote add origin ~/tmp/RemoteRepo1

To see what this has achieved, we can have a quick look at the .git/config

cat .git/config
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
[remote "origin"]
    url = /home/runner/tmp/RemoteRepo1
    fetch = +refs/heads/*:refs/remotes/origin/*

You should notice that there is now a ‘remote’ section with the name of ‘origin’ and the ‘url’ points to the location we nominated.

10.1.1 Pushing

Currently the remote repository is empty. We will now push our local commit history to the remote repository. This is achieved via the git push -u <name> <ref> command. Here, <name> is the name of the remote repository (‘origin’) and <ref> is a reference the head of the commit chain we want to sync.

git push -u origin main
To /home/runner/tmp/RemoteRepo1
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

Rstudio does not have a direct means by which we can define the remote repository. Thus, we must start by entering the following into the terminal.

git push -u origin main

Thereafter, you might notice that some up and down (push and pull respectively) buttons become active within the “git” panel.

Now, after each subsequent commit, you can “push” your code to the remote repository simply by pushing the up (push) arrow.

git reflog
git log --graph --decorate --oneline --all
feb72a7 HEAD@{0}: checkout: moving from Feature to main
dcd4b57 HEAD@{1}: commit: Feature complete
4fc0c5f HEAD@{2}: checkout: moving from main to Feature
feb72a7 HEAD@{3}: commit: Bug fix in file1
3b0c29d HEAD@{4}: checkout: moving from Feature to main
4fc0c5f HEAD@{5}: commit: New feature
3b0c29d HEAD@{6}: checkout: moving from main to Feature
3b0c29d HEAD@{7}: commit: Modified file2, added .gitignore
8a7c777 HEAD@{8}: commit: Modified file1 and added file2 (in dir1)
5cdb90a HEAD@{9}: commit (initial): Initial repo and added file1
* dcd4b57 (Feature) Feature complete
* 4fc0c5f New feature
| * feb72a7 (HEAD -> main, origin/main) Bug fix in file1
|/  
* 3b0c29d Modified file2, added .gitignore
* 8a7c777 Modified file1 and added file2 (in dir1)
* 5cdb90a Initial repo and added file1

Note that when we pushed the commits to the remote repository, we only pushed the main branch. Consequently, the remote repository only has a single branch.

10.2 Cloning

To collaborate with others on a repository, we start by cloning the repository you wish to collaborate on. So at the moment, we have the original repository (~/tmp/Repo1) created by user 1. We also have a remote repository (~/tmp/RemoteRepo1).

To demonstrate cloning (and collaborating), we will also assume the personal of user 2 and we will clone the remote repository to yet another local path (~/tmp/MyRepo1). Of course, this would not normally be on the same machine as the original repository, we are just doing it this way to simulate multiple users on the same machine.

git clone ~/tmp/RemoteRepo1 ~/tmp/MyRepo1
Cloning into '/home/runner/tmp/MyRepo1'...
done.
  1. click on the Project selector in the top right of the Rstudio window (as highlighted by the red ellipse in the image below.

  2. select New Project from the dropdown menu

  3. select Version Control form the Create Project panel

  4. select Git from the Create Project from Version Control panel

  5. provide a path to a remote repository. Normally this URL would be for a location on a server such as Github, Gitlab, Bitbucket etc.
    However, for this demostration we will point to the remote repository that we set up in the previous section (~/tmp/RemoteRepo1)

  6. provide a directory name in which to store this new cloned repository. Normally this field is populated based on the name give in the URL. However, in this case, it would suggest a name of RemoteRepo1 which already exists (for the repository we are trying to clone), and we don’t wish to overwrite that one. I will instead offer an alternative name (MyRepo1).

  7. we also need to supply a path to where this cloned repository will be stored.

  8. click the “Create Project” button.

The contents (and state) of ~/tmp/MyRepo1 should match that of ~/tmp/Repo1 (other than any files excluded due to a .gitignore or files not yet committed).

tree -ra -L 2 --charset ascii
.
|-- file1
|-- dir1
|   `-- file2
|-- .gitignore
`-- .git
    |-- refs
    |-- packed-refs
    |-- objects
    |-- logs
    |-- info
    |-- index
    |-- hooks
    |-- description
    |-- config
    |-- branches
    `-- HEAD

8 directories, 8 files

Note that when cloning repository, all branches in the remote repository are cloned. However, since the remote repository only had one branch (main), so too the clone only has one branch.

Now as the collaborator (user 2), lets make a modification and push this change up to the remote repository.

Important info about pushing to a remote repository

Before pushing any changes, it is absolutely vital that you adhere to the following steps:

  1. commit your changes - so that you have something new to push and they are safe before the next step.
  2. pull (and if necessary reconcile - see the next section below) the latest from the remote repository. This is critical as it ensures that the changes you are pushing are against the latest stage of the repository. Without this step, you might be pushing changes that are based on a stage that is not longer current.
  3. push your changes
git pull
Already up to date.
echo 'Something else' > file4
git add file4
git commit -m 'Added file4'
[main 24abf69] Added file4
 1 file changed, 1 insertion(+)
 create mode 100644 file4
git push -u origin main
To /home/runner/tmp/RemoteRepo1
   feb72a7..24abf69  main -> main
branch 'main' set up to track 'origin/main'.
  1. start by pulling the latest from the remote repository just incase there has been an change
  2. click on the “Create new blank file in the current directory” button and select “Text file” - name it file4
  3. edit this file by adding the contents Something else
  4. save the file
  5. stage (add) the file
  6. commit the change with a message of “Added file4”
  7. push this commit either by clicking on the green up (push) arrow in the “Review Changes” window or the same arrow in the git tab of the main Rstudio window.

Notice how the second (cloned. MyRepo1) repository and the remote repository (RemoteRepo1) are one commit ahead of the original local repository (Repo1). For Repo1 to be in sync with MyRepo1, the original user will have to pull the remote repository changes manually.

10.3 Pulling

Retrieving a commit chain (pulling) from a remote repository is superficially the opposite of pushing. Actually, technically it is two actions:

  • a fetch that retrieves the remote information and uses it to create a branch off your local repository (the name of this branch is made from the name of the remote and the branch that was fetched - e.g. origin/master).

  • a merge that merges this branch into the main repository.

These actions can be performed individually, however, they are more typically performed together via the git pull command.

To illustrate, lets return to being user 1 and we will pull the changes contributed by user 2 in the section above.

git pull
From /home/runner/tmp/RemoteRepo1
   feb72a7..24abf69  main       -> origin/main
Updating feb72a7..24abf69
Fast-forward
 file4 | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 file4

The associated message informs us that upon pulling, a file (file4) has been added. Any conflicts arising from the merging stage of the pull can be resolved in the usual manner of opening the conflicted file(s) making manual edits and then committing the changes.

git reflog
24abf69 HEAD@{0}: pull: Fast-forward
feb72a7 HEAD@{1}: checkout: moving from Feature to main
dcd4b57 HEAD@{2}: commit: Feature complete
4fc0c5f HEAD@{3}: checkout: moving from main to Feature
feb72a7 HEAD@{4}: commit: Bug fix in file1
3b0c29d HEAD@{5}: checkout: moving from Feature to main
4fc0c5f HEAD@{6}: commit: New feature
3b0c29d HEAD@{7}: checkout: moving from main to Feature
3b0c29d HEAD@{8}: commit: Modified file2, added .gitignore
8a7c777 HEAD@{9}: commit: Modified file1 and added file2 (in dir1)
5cdb90a HEAD@{10}: commit (initial): Initial repo and added file1
git log --graph --decorate --oneline --all
* 24abf69 (HEAD -> main, origin/main) Added file4
* feb72a7 Bug fix in file1
| * dcd4b57 (Feature) Feature complete
| * 4fc0c5f New feature
|/  
* 3b0c29d Modified file2, added .gitignore
* 8a7c777 Modified file1 and added file2 (in dir1)
* 5cdb90a Initial repo and added file1
git reflog
24abf69 HEAD@{0}: commit: Added file4
feb72a7 HEAD@{1}: clone: from /home/runner/tmp/RemoteRepo1
git log --graph --decorate --oneline --all
* 24abf69 (HEAD -> main, origin/main, origin/HEAD) Added file4
* feb72a7 Bug fix in file1
* 3b0c29d Modified file2, added .gitignore
* 8a7c777 Modified file1 and added file2 (in dir1)
* 5cdb90a Initial repo and added file1
git reflog
git log --graph --decorate --oneline --all
* 24abf69 (HEAD -> main) Added file4
* feb72a7 Bug fix in file1
* 3b0c29d Modified file2, added .gitignore
* 8a7c777 Modified file1 and added file2 (in dir1)
* 5cdb90a Initial repo and added file1

10.4 Github as a remote repository

GitHub provides the world’s leading centralized platform for version control, collaboration, and project management, facilitating seamless teamwork, tracking changes, and ensuring the integrity and accessibility of code repositories throughout the software development lifecycle.

Although anyone can explore (read) public repositories on github, only those with github accounts can contribute and collaborate.

10.4.1 Setup Github account

To create a free github account:

  1. visit https://github.com and click “Sign up for github”
  2. register by providing your prefered email address, a username and a password when prompted
  3. to complete the account activation, you will need to verify your details via an email sent to your nominated email address

As of the start of 2024, github now requires Two-Factor Authentication (2FA) for enhanced security. Whenever you login to github (or are prompted for a password, you will also need to use 2FA. To setup 2FA:

  1. click on your profile picture in the top right corner.
  2. select “Settings” from the dropdown menu.
  3. select “Password and authentication” in the left sidebar.
  4. under “Two-factor authentication” section, click “Enable”.
  5. choose your preferred method (authenticator app or SMS) and follow the prompts to set it up.

Passwords and Two-Factor Authentication (2FA) are used when you (as a human) securely login and interact directly with the GitHub website. However, it is also possible to have other tools (such as git) interact with Github on your behalf via an Application Programming Interfacet (API). Passwords/2FA are not appropriate to authenticate these machine to machine communications. Instead, Github requires the use of a Personal Access Token (PAT). PATs offer a more secure and granular approach, allowing users to control access without exposing their account password.

To generate a Personal Access Token (PAT):

  1. click on your profile picture in the top right corner.

  2. select “Settings” from the dropdown menu.

  3. select “Developer settings” from the bottom of the left sidebar.

  4. select “Personal access tokens” from the left sidebar.

  5. select “Tokens (classic)” from the dropdown menu

  6. click “Generate new token”

  7. select “Generate new token (classic)” from the dropdown menu

  8. at this point you will likely be prompted for your password

  9. provide a “note” - this is more of a short description of what the token is to be used for (in the example below, I have entered “git push/pull” to remind me that this is a simple token for regular push/pull interaction between my local and remote repositories).

    You also need to provide an expiration. Although not secure or recommended, I have selected “No expiration” as I don’t want to have to re-do my PAT across multiple machines too regularly.

    Finally, you also need to indicate scope (what activities you are granting permission for the tools to be able to perform). In this case, I have ticked the “repo” box. This grants general rea/write access to my repositories. I have not granted permission for more administration like activities such as managing teams, deleting repositories, etc - these activities I am happy to perform myself via the website.

  10. click “Generate token” and securely copy the generated token. Until this is stored safely (see below) do not close the page, because Github will never show you this PAT again.

Important

Important: Store your PAT safely as you won’t be able to see it again! Ideally, you should store this PAT in a digital wallet. Digital wallets vary according to operating systems. R users might like to use the r function from the asdf package (which you will need to install prior) as follows in order to store the PAT.

In an R console, enter:

gitcreds::gitcreds_set()

When propted for a password, paste in the copied PAT that hopefully is still in your clipboard - else you might need to re-copy it.

To confirm that you have successfully stored your PAT in your wallet, you can:

gitcreds::gitcreds_get()

and confirm that it indicates that there is a hidden password.

10.4.2 Create remote Github repository

  1. login to your Github account

  2. either:

    1. click on the “Create new..” button (with the plus sign) to the right of your profile picture in the top right corner and select “New repository” from the dropdown menu

    2. click on “Repositories” from the top horizontal menu followed by the big green “New” button

  3. fill out the details of the Create a new repository for similar to the following

    In particular:

    • give the repository a name. Typically use the same name as you used for the local repository to avoid confusion

    • provide a description. Along with the name, this field is searchable so the more detailed it is, the more likely your repository will be discoverable by others as well as yourself in the future

    • indicate the privacy level. This affects whether your repository is discoverable and readable by anyone (public) or just those you invite (private)

    • ideally, you also want to include a README file and license in your repository. However, if you enable either of these options in the form, Github will bypass providing a screen with some additional instructions that many find useful for linking your local and remote repository. So on this occasion, we will leave these options as they are

  4. click the “Create repository” button at the bottom of the page

  5. Github will present you with the following page:

    This page presents three alternative sets of instructions that you run locally (on your machine) in order to establish a link between the local and remote repository. You need to run the appropriate set of commands in your local terminal

    • if no local repository exists, follow the first set of instructions
    • if you already have a local repository (as is the case with this demonstration), follow the second set of instructions
    • if you intend to import a repository from a different versioning system, follow the last set of instructions
  6. once you have run the above commands locally, you can refresh the Github page and you will be presented with your remote repository. From here you can navigate through your code, manage privileges etc.

If you would like to allow others to collaborate with you on your repository, then regardless of whether the repository is public or private, you will need to invite them as a collaborator. To do so:

  1. click on “Settings” from the horizontal menu bar
  2. click on “Collaborators” from the left sidebar (you may then be asked to submit your password)
  3. click on the green “Add people” button
  4. in the popup, enter either the username, full name or email address of the person you want to invite to collaborate with you. Once you click the “Select a collaborator above” and select the appropriate candidate, this person will be sent an invite via email.
  5. nominate the role that this collaborator can assume (e.g. what capacity does the collaborator have to edit, invite others, alter settings, delete the repository etc)
  6. repeat steps 3-4 for each additional collaborator you wish to invite

11 Resolving conflicts

In Git, conflicts arise when changes made in different branches cannot be automatically merged. This typically happens when two branches modify the same part of a file and the changes overlap. Think of it like two writers revising the same sentence differently. Conflicts usually arise when merging or rebasing branches.

When git identifies a conflict, it will mark the conflicting areas (with sets of plain text fences - see below), and it’s then up to the user to resolve them manually by making edits the conflicted file(s) and choosing which changes to keep. After resolving conflicts, the changes can be staged, and the merge or rebase can be completed. Conflict resolution is an essential skill in collaborative Git workflows, ensuring smooth integration of changes from multiple contributors.

Recall that pulling from a remote repository is a to stage process. Firstly the new commits are “fetched” to a new temporary branch and then this branch is merged into the local repository. Hence, conflicts can occur when pulling from remote repositories. Conflict resolution in such cases is as outlined above.

To illustrate a git conflict, we will start with a very simple repository, create a branch and then concurrently make edits to the same part of the same file on each branch before attempting to merge the branches together.

rm -rf ~/tmp/Repo1
mkdir ~/tmp/Repo1
cd ~/tmp/Repo1
git init 
echo 'File 1' > file1
git add file1
git commit -m 'Initial repo and added file1'
echo '---------------' >> file1
mkdir dir1
echo '* Notes' > dir1/file2
git add file1 dir1/file2
git commit -m 'Modified file1 and added file2 (in dir1)'
echo '---' >> dir1/file2
echo 'temp' > dir1/f.tmp
echo '*.tmp' > .gitignore
git add .
git commit -m 'Modified file2, added .gitignore'
git branch Feature
git checkout Feature
echo 'some text added on Feature branch' >> file1
echo 'File 3' > dir1/file3
git add .
git commit -m 'New feature'
git checkout main
echo ' a bug fix on the main branch' >> file1
git add .
git commit -m 'Bug fix in file1'

git reflog
git log --graph --decorate --oneline --all
Initialized empty Git repository in /home/runner/tmp/Repo1/.git/
[main (root-commit) 2b9ba19] Initial repo and added file1
 1 file changed, 1 insertion(+)
 create mode 100644 file1
[main e97dcbb] Modified file1 and added file2 (in dir1)
 2 files changed, 2 insertions(+)
 create mode 100644 dir1/file2
[main f267fa7] Modified file2, added .gitignore
 2 files changed, 2 insertions(+)
 create mode 100644 .gitignore
Switched to branch 'Feature'
[Feature 87647d8] New feature
 2 files changed, 2 insertions(+)
 create mode 100644 dir1/file3
Switched to branch 'main'
[main 419ec60] Bug fix in file1
 1 file changed, 1 insertion(+)
419ec60 HEAD@{0}: commit: Bug fix in file1
f267fa7 HEAD@{1}: checkout: moving from Feature to main
87647d8 HEAD@{2}: commit: New feature
f267fa7 HEAD@{3}: checkout: moving from main to Feature
f267fa7 HEAD@{4}: commit: Modified file2, added .gitignore
e97dcbb HEAD@{5}: commit: Modified file1 and added file2 (in dir1)
2b9ba19 HEAD@{6}: commit (initial): Initial repo and added file1
* 87647d8 (Feature) New feature
| * 419ec60 (HEAD -> main) Bug fix in file1
|/  
* f267fa7 Modified file2, added .gitignore
* e97dcbb Modified file1 and added file2 (in dir1)
* 2b9ba19 Initial repo and added file1

git merge Feature --no-edit
Auto-merging file1
CONFLICT (content): Merge conflict in file1
Automatic merge failed; fix conflicts and then commit the result.

Merging is not directly supported by Rstudio, please use the terminal.

Hmmm. It appears that there is a conflict (although we should not be supprised since we deliberately set the repository up to have a conflict!). If we explore the a git diff, we will see that on the master and Feature branches have incompatible changes.

git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Changes to be committed:
    new file:   dir1/file3

Unmerged paths:
  (use "git add <file>..." to mark resolution)
    both modified:   file1

The above status informs us that whilst we were able to successfully merge in the changes in dir1/file3, the modifications in file1 remain unmerged (due to conflicts).

If we run a git diff on the two branches, we will be able to identify all the differences within all comparable files between the two branches.

git diff main Feature
diff --git a/dir1/file3 b/dir1/file3
new file mode 100644
index 0000000..8cf9e18
--- /dev/null
+++ b/dir1/file3
@@ -0,0 +1 @@
+File 3
diff --git a/file1 b/file1
index 11c945a..8a32d91 100644
--- a/file1
+++ b/file1
@@ -1,3 +1,3 @@
 File 1
 ---------------
- a bug fix on the main branch
+some text added on Feature branch

The output indicates that dir/file3 does not exist on the main branch - this is not a conflict. However, for file1, we see that line three differs between the two branches.

It is not so much that they have both made changes to the same file, it is more that the changes are to the same part of the file. Lets look at the contents of file1 in the commit that is the common ancester of both branches:

git cat-file -p main^:file1
File 1
---------------

So prior to branching, the file1 file had two lines of text.

In the latest commit on the main branch, the file1 has added a third line with text about a bug fix.

git cat-file -p main:file1
File 1
---------------
 a bug fix on the main branch

Yet on the Feature branch, the third line of the file1 file contains some text added on Feature branch.

git cat-file -p Feature:file1
File 1
---------------
some text added on Feature branch

So the source of this conflict evidently is the third line of the file1 file.

We can see that the changes made to file1 are inconsistent. We need to decide which edits (if any) we want to use. Recall that the change made in main was to address a bug or issue. Perhaps this bug or issue does not arise with the new Feature and thus is superfluous. Alternatively, it might be that this bug fix is required by both branches (if so, we probably should have introduced it to the Feature branch at the same time as the main anyway….

Lets address the conflict by rolling back file1 from the main branch.

The following figures depict the file1 file with conflicts (left) and once resolved (right).

Normally we would resolve this issue using a code editor to edit the actual changes in the file back to the desired condition. However as we only have the one change and this demo is fully scripted, I will instead roll back (checkout) this one file from the earlier commit on the main branch.

git checkout main^ file1
git add .
git commit -m 'Merge in Feature'
Updated 1 path from 3c7af0d
[main ca2b6e7] Merge in Feature
  1. open file1 for editing
  2. make the necessary changes to the text
  3. save the file
  4. stage (add) the file (you may have to click on the checkbox twice before the tick appears)
  5. commit the changes with a message like “Merge in Feature”
git reflog
ca2b6e7 HEAD@{0}: commit (merge): Merge in Feature
419ec60 HEAD@{1}: commit: Bug fix in file1
f267fa7 HEAD@{2}: checkout: moving from Feature to main
87647d8 HEAD@{3}: commit: New feature
f267fa7 HEAD@{4}: checkout: moving from main to Feature
f267fa7 HEAD@{5}: commit: Modified file2, added .gitignore
e97dcbb HEAD@{6}: commit: Modified file1 and added file2 (in dir1)
2b9ba19 HEAD@{7}: commit (initial): Initial repo and added file1
git log --graph --decorate --oneline --all
*   ca2b6e7 (HEAD -> main) Merge in Feature
|\  
| * 87647d8 (Feature) New feature
* | 419ec60 Bug fix in file1
|/  
* f267fa7 Modified file2, added .gitignore
* e97dcbb Modified file1 and added file2 (in dir1)
* 2b9ba19 Initial repo and added file1