layout | title | navtitle | excerpt | permalink |
---|---|---|---|---|
page |
Git Mastery in 1 Hour |
Git Mastery |
Git Mastery via Concepts First! Concept mastery makes you an expert. Let search engines handle the 'how' of Git. |
/git-lesson/ |
{% if jekyll.environment == "production" %}
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>{% else %}
<script src="../assets/jquery-3.2.1.min.js"></script>{% endif %}
<script src="../assets/toc.js"></script> <script src="../assets/git-obj-id.js"></script>- TOC {:toc}
{% include commit-id-forms.html %}
This Git lesson focuses on giving you a firm grasp of key Git concepts. Concept mastery makes you an expert. You should leave the "how" to search engines (like Google). Knowing how to cut wood doesn't make you a carpenter!
This Git lesson brings you through a typical workflow with Git, consisting 3 areas.
- Local clone
- Remote operations
- Collaboration techniques
Git is primarily a collaboration tool.
Even if you only use Git on your own, bear in mind that version control is necessary for you to "collaborate with yourself". We typically go through a decent length project often asking questions like "why did I do that last month?", "what was I thinking when I made this decision?", "what if I take a new fangled approach I just learned?", and so on.
This Git lesson is written to be as trim as possible. You can safely ignore side notes like this:
But you should especially note important tips like this:
Grasp concepts first, lookup technical details later. Learning before working, looking before leaping!
Important tips are places where you should pause and grasp the concepts just demonstrated.
Shoot me an email if any part of this lesson bogs you down and impedes you from progressing rapidly.
However, you should look out for fast-forward suggestions like this:
Lastly, you must execute shell (Bash) commands presented like this: ls -la
(inline). And like this:
In short, read and understand the concepts explained. The explanation is mostly demonstrated rather than described abstractly. Follow the demonstrations by executing commands that are styled as mentioned above.
Before we start, let's remind ourselves that "concepts matter most". There will be no in-depth treatment of "how" to do stuff; you can google "how to create Git branch" and easily find git branch <name> <ref>
. The focus of this Git lesson is on concepts (what) and rationale (why).
This Git lesson is taught using a *nix platform (eg Linux, MacOS), in particular Bash.
If you're on Linux or MacOS, you're a productive and efficient coder, and you should expect to blaze through this Git lesson in less than an hour.
If you're on Windows, you can install Git for Windows, and use Git Bash. Meantime, keep bugging [Jon](https://bitbucket.org/{{ site.bitbucket_username }}) to complete his "Crash Course for Productivity on Linux/MacOS".
We will start by creating our first Git commit. There will be a few first steps to do before we achieve that.
A Git repo (short for repository) contains:
- A copy of the files that you're working on (aka working copy)
- The history of your work on said files.
You start a local Git repo like this:
A Git repo tracks a folder of files, so the said files are really the files in that folder.
A Git repo resides in a *folder*, and can potentially (and usually does) track all files in that *folder*.
That means you really shouldn't `git init` in a *top-level folder* like `~/Documents` or `/usr/local`! You want to track the progress of your projects, not every single file in your computer.
ls -la
will show you the .git
folder that was created when you started a new Git repo. This hidden folder contains Git data --- data regarding the history of your work, data about your credential, and other stuff we want to ignore for now.
The `.git` folder is what defines your Git repo.
We're now interested in .git/config
, where your credential is stored. The content of that file (do cat .git/config
) should currently be (with some omissions):
{% highlight conf %}
[core]
filemode = true # false if you're on Windows
bare = false
...
{% endhighlight %}
Let's input a credential for you now, using fictitious, but important, values. Follow along, please! {% assign git-name = site.data.git-lesson.git-credential.name %} {% assign git-email = site.data.git-lesson.git-credential.email %}
Now, these new lines will have been inserted into .git/config
. Doing cat .git/config
shows:
{% highlight conf %}
[user]
name = {{ git-name }}
email = {{ git-email }}
{% endhighlight %}
As you would intuitively perceive, you can have different credentials for different projects. A typical use case would be having one credential for work (eg, user\[email protected]) and another for personal projects (eg, user\[email protected]).
Manually editing that config file is possible and equivalent to performing git config
.
If there is a credential you almost always use, you can put it in the global Git config at `~/.gitconfig` via a command like `git config --global user.name "{{ git-name }}"`.
Local Git config parameters override global ones.
Create file story.txt
(eg. emacs story.txt
), and enter into it these 3 lines:
{% assign linenos = "1 2 3" | split: " " %}
{% include linenos.html numbers=linenos %}
{% highlight text %}
Once upon a time, there was a unicorn.
The unicorn looked around. {% endhighlight %}
Everything outside of the `.git` folder is a **working copy** of the files you are working on for the project.
git status
will show this:
On branch master
Initial commit
Untracked files:
(use "git add <file>..." to include in what will be committed)
story.txt
nothing added to commit but untracked files present (use "git add" to track)
You have added a new file to your Git repo. New files --- files not yet tracked by your Git repo --- are called untracked files.
Ignore all other information in that output for now.
We'll follow Git's advice and work towards adding our new file to the Git repo.
`git status` is a command you will use very often:
- Before you start work on a new set of changes (initial check) - In the process of staging files (progress check) - Before you commit changes (final check) - Whenever in doubt, and so on.When telling Git to commit your new work, Git only commits what you place in the staging area.
git add story.txt
will put story.txt
into the staging area, and a subsequent git status
will show:
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
story.txt
You can unstage work using git rm --cached <file>
. Feel free to practice unstaging the staged work and re-staging that work.
The *staging area* allows you to work on multiple ideas rapidly --- as and when they come to mind --- but yet still be able to organize your changes into *coherent* and *integral* units.
To demonstrate the purpose of having a staging area, create a new file rough_thoughts.txt
(eg. emacs rough_thoughts.txt
) and enter this line:
{% highlight text %}
Random disorganized thoughts. Don't want to git-track this.
{% endhighlight %}
git status
will now show:
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
story.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
rough_thoughts.txt
In the above demonstration, you might have some new ideas you want to quickly write down in rough_thoughts.txt
before you forget those ideas. Yet, you might not want those underdeveloped new ideas to be committed in your next set of changes.
{% assign commit-msg-file = site.data.git-lesson.commit-msg-file %}
You can use any text editor to create file {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
). Enter into it these 5 lines:
{% highlight text %}
Adds first work on the story
I'd think up more descriptive information here if I could. That first line above should be a short summary, with no ending period. Then comes a blank line, and then details and descriptions follow. {% endhighlight %}
You then commit your work by doing git commit -F {{ commit-msg-file }}
.
If you want to use Emacs as a *commit message editor*, you can configure Git to use Emacs. Do the configuration with `git config --global core.editor emacs`. The default editor is [vi][vi]. You can then verify your editor configuration in `~/.gitconfig`. That file can also be edited by hand.
You can then do just `git commit -v` and see your chosen text editor pop up, enter your commit message, save like you're saving a file (you're actually saving `{{ commit-msg-file }}`), and finally exit the text editor. That sequence of actions will commit your changes.
It is usually better to collect your thoughts in a file when you construct your commit message. The `-F` option lets you specify a file that contains your commit message.
{% assign first-commit = site.data.git-lesson.git-commits.first.id %} {% assign first-commit-date = site.data.git-lesson.git-commits.first.date %} {% assign first-commit-timestamp = site.data.git-lesson.git-commits.first.timestamp %}
git log --decorate --graph
will show your first commit. Later on when you have more commits, git log
will show a connected graph (timeline) of all your commits. Right now, you only have 1 commit:
{% include git-log/ch.html commit-id=first-commit class="first-commit" full-id=true head=true attached="master" %}
Author: {{ git-name }} <{{ git-email }}>
Date: {{ first-commit-date }}
Adds first work on the story
I'd think up more descriptive information here if I could.
That first line above should be a short summary, with no ending period.
Then comes a blank line, and then whatever descriptive things I wanna say.
Especially note the credential attached to that commit. Recall from a previous section where you configured your credential for this Git repo. This credential is what gets attached to every commit you make in this Git repo.
git log
shows the log in a less
window (navigation tips here), which coincidentally is navigated similarly to a vi
window.
Shorten the height of your bash
terminal such that git log --decorate --graph
output exceeds that height. The log will be displayed in a less
window, and you can practice navigating in that window.
We start with Git objects, so that we can understand how Git commits are built and hence grasp what Git commits essentially are --- snapshots of your project (all its folders and files).
Soon after that, we get a handle on some internals that are actually crucial to normal use of Git --- some of Git's vocabulary (or terminology).
A Git Commit is collection of folders and files contained in that commit. In short, Git is really a tracker for a filesystem.
That's almost all you need to know about a Git Commit. Besides being a snapshot of all your project files, a Git Commit also has relations to other Git Commits.
Technically, under the hood, Git represents folders with trees and files with blobs.
Now, we shall see how a Git Commit is a snapshot and how it relates to other Git Commits.
Create a subfolder folder-A
and a file file-A.txt
inside:
Create a subfolder folder-B
inside folder-A
, and files file-B.txt
and file-C.txt
inside:
Stage our new changes with git add folder-A
.
Check our staging area with git status
:
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: folder-A/file-A.txt
new file: folder-A/folder-B/file-B.txt
new file: folder-A/folder-B/file-C.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
rough_thoughts.txt
We construct our commit message by editing {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
):
{% highlight text %}
Adds nested folder structure
Just testing. We want to see Git Objects. We should be seeing Commits, Trees and Blobs. {% endhighlight %}
And now, we commit with git commit -F {{ commit-msg-file }}
.
{% assign second-commit = site.data.git-lesson.git-commits.second.id %} {% assign second-commit-date = site.data.git-lesson.git-commits.second.date %} {% assign second-commit-timestamp = site.data.git-lesson.git-commits.second.timestamp %}
A look at Git Log via git log --decorate --graph
shows:
{% include git-log/ch.html commit-id=second-commit class="second-commit" full-id=true head=true attached="master" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
|
| Just testing. We want to see Git Objects.
| We should be seeing Commits, Trees and Blobs.
|
{% include git-log/ch.html commit-id=first-commit class="first-commit" full-id=true %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
I'd think up more descriptive information here if I could.
That first line above should be a short summary, with no ending period.
Then comes a blank line, and then details and descriptions follow.
{% assign first-commit-short = first-commit | slice: 0, 7 %} {% assign second-commit-short = second-commit | slice: 0, 7 %}
In the following instructions, replace my commit IDs with your own.
{% assign second-tree = site.data.git-lesson.git-trees.second %}
Let's look at the second-commit via
git cat-file -p {{ second-commit-short }}
tree {{ second-tree }} parent {{ first-commit }} author Author A {{ second-commit-timestamp }} +0800 committer Author A {{ second-commit-timestamp }} +0800
Adds nested folder structure
Just testing. We want to see Git Objects. We should be seeing Commits, Trees and Blobs.
From the third line of that output, we see the commit message for our second-commit.
The second line indicates that our second-commit is linked to our first-commit (via the property parent
).
{% assign first-tree = site.data.git-lesson.git-trees.first %}
And to confirm that our second-commit really links to our first-commit, we take a peek at the first-commit by doing:
git cat-file -p {{ first-commit-short }}
tree {{ first-tree }} author Author A {{ first-commit-timestamp }} +0800 committer Author A {{ first-commit-timestamp }} +0800
Adds first work on the story
I'd think up more descriptive information here if I could. That first line above should be a short summary, with no ending period. Then comes a blank line, and then details and descriptions follow.
{% assign first-tree-short = first-tree | slice: 0, 7 %} {% assign second-tree-short = second-tree | slice: 0, 7 %}
The first-tree and second-tree are 2 different snapshots, the first being taken by our first-commit and the second by our second-commit. Based on the folders and files we created so far, we know both trees look like: {% highlight text %} first-tree | |-- story.txt
second-tree | |-- story.txt |-- folder-A | |-- file-A.txt |-- folder-B | |-- file-B.txt |-- file-C.txt {% endhighlight %}
Just a little more down the rabbit hole...
{% assign git-tree-folder-A = site.data.git-lesson.git-trees.folder-A %} {% assign git-tree-folder-A-short = git-tree-folder-A | slice: 0, 7 %} {% assign git-tree-folder-B = site.data.git-lesson.git-trees.folder-B %} {% assign git-tree-folder-B-short = git-tree-folder-B | slice: 0, 7 %} {% assign git-tree-file-B = site.data.git-lesson.git-trees.file-B %} {% assign git-tree-file-B-short = git-tree-file-B | slice: 0, 7 %}
Let's confirm that our second-tree does indeed contain the hierarchy we conjectured above.
git cat-file -p {{ second-tree-short }}
040000 tree {{ git-tree-folder-A }} folder-A
100644 blob 2a8621f3fe966d677330a471450bd54a539162a2 story.txt
Indeed, our second-tree has a tree (folder-A
) and a blob (story.txt
).
Chase that nested tree further down by:
git cat-file -p {{ git-tree-folder-A-short }}
100644 blob 0e82526a4ea4a0031220e1e872d2c6abab945ccb file-A.txt
040000 tree {{ git-tree-folder-B }} folder-B
And further down by:
git cat-file -p {{ git-tree-folder-B-short }}
100644 blob {{ git-tree-file-B }} file-B.txt
100644 blob 8458d5e043e7546ff08a0292699a75536f87bcaa file-C.txt
And finally, a peek into a Blob Git object by:
git cat-file -p {{ git-tree-file-B-short }}
{% assign git-tree-file-B = site.data.git-lesson.git-trees.file-B %} {% assign git-tree-file-B-short = git-tree-file-B | slice: 0, 7 %}
To fully take stock of all the Git objects we currently have, find .git/objects -type f
shows:
{% highlight text %}
.git/objects/{{ first-tree | slice: 0, 2 }}/{{ first-tree | slice: 2, 100 }}
.git/objects/{{ second-tree | slice: 0, 2 }}/{{ second-tree | slice: 2, 100 }}
... and so on ...
{% endhighlight %}
You can find your recorded commit IDs among the above output.
To check their types (**Commit**, **Tree**, **Blob**), you can do `find .git/objects -type f | cut -d'/' -f3,4 | sed 's/\///' | xargs -I %ID -t git cat-file -t %ID`.
Or simply do them 1 by 1: `git cat-file -t <1st-7-digits-of-ID>`.
The types of Git references are:
- Branch (local and remote)
- Tag
HEAD
(special kind of reference, special semantics)
We currently have 1 branch --- "master". (git branch
shows all branches)
This is actually a reference to our second-commit, as seen by git log --decorate --oneline master
:
{{ second-commit-short }} (HEAD -> master) Adds nested folder structure
{{ first-commit-short }} Adds first work on the story
Confirm this by doing cat .git/refs/heads/master
and also git branch -v
. Note that the Git object ID is the same in both places:
{{ second-commit }}
* master {{ second-commit-short }} Adds nested folder structure
In the spirit of Git references, let's make Git Object IDs more human-friendly too.
We set a parameter for git log
like this: git config log.abbrevCommit true
.
A subsequent git log --decorate --graph
shows 7-character commit IDs:
{% include git-log/ch.html commit-id=second-commit class="second-commit" head=true attached="master" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
Since we're on the topic of making things more human-friendly, you can explore Git Aliases. Here's a quick example for an oft-used git log --decorate --graph
: git config alias.lg "log --decorate --graph"
You can add option --global
(like git config --global
) if you want that alias for all your Git repos.
Now, try git lg
. You'll see that it is exactly equivalent to git log --decorate --graph
.
Have fun making aliases for long and cumbersome Git commands!
git log --decorate --oneline master~1
shows us our first-commit, which is 1 step upstream of our second-commit:
{{ first-commit-short }} Adds first work on the story
There is no way to swim downstream. Git commits do not have a property that is a counterpart to parent
.
Your next new commit will have this current commit as its parent, and that new commit becomes the new current commit.
That is the definition, so to speak, of the HEAD
. It concisely describes everything that the HEAD
does (its function), besides what the HEAD
is (a Git reference).
We will next look at how to manually move the HEAD
.
There are other ways of moving the HEAD
, the most commonly seen of which is the advancement via a git commit
. As per the definition of the HEAD
, the HEAD
is advanced to the newly created commit upon a git commit
. This method of moving the HEAD
is referred to as creating a commit, and is not a manual movement of the HEAD
. We've seen this happen when we created our first-commit and second-commit.
The HEAD
can have 3 states --- attached, detached and initial.
When we created our Git repo via git init
, these happened:
- The default branch master is readied (not yet created)
- The
HEAD
is attached to master. - The
HEAD
refers to no commit (none exist yet) --- an initial state.
This is the only time when the HEAD
exists in that initial state.
We need to know nothing more of this initial state. We don't work with this initial state.
As can be seen, the usual state of the HEAD
is attached (to branch master by default). This is true even when the HEAD
is in its initial state.
The usual state of the HEAD
, during your normal use of Git, is that of being attached to a branch. We will explore this usual state next.
This is the usual state of the HEAD
, that of being attached to a branch.
When we created our first commit, the HEAD
was advanced to our newly created first-commit. And similarly for second-commit.
Note that the branch to which the HEAD
is attached is advanced similarly.
We can see that the HEAD
is currently attached to branch master via git log --decorate --graph
:
{% include git-log/ch.html commit-id=second-commit class="second-commit" head=true attached="master" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
HEAD -> branch-name
A detached HEAD
is used to take a look-see at any commit, especially commits that are not at any branch head.
In fact, performing a checkout with just the branch reference itself is the only way to attach HEAD
.
To see a detached HEAD
, we do a checkout via git checkout master~0
, to which Git issues this warning:
Note: checking out 'master~0'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b <new-branch-name>
HEAD is now at {{ second-commit-short }}... Adds first work on the story
The subsequent git log --decorate
shows that the HEAD
is not attached to any branch:
{% include git-log/ch.html commit-id=second-commit class="second-commit" head=true branch="master" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
Even though we're still on the same commit that branch master is on, the HEAD
is detached.
We re-attach the HEAD
to branch master with git checkout master
. Then, a git log --decorate --graph
shows:
{% include git-log/ch.html commit-id=second-commit class="second-commit" head=true attached="master" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
Git reflog is like an "Undo history" (recent history) for Git references. For example, the reflog for the HEAD
shows the recent changes to the HEAD
.
Reflogs only exist for branches and for the HEAD
.
It is meaningless to have reflogs for tags, since tags do not (and are not meant to) move like branches and the HEAD
do.
git reflog <branch>
shows the reflog for branch <branch>
git reflog
or git reflog HEAD
shows the reflog for the HEAD
We will almost always be using reflog for the HEAD
git reflog
or git reflog HEAD
shows the reflog for the HEAD
.
So far, we have witnessed 2 types of HEAD
change: commit and checkout. Let's also witness them in the reflog for the HEAD
.
A git reflog
shows us these recent changes to the HEAD
:
{{ second-commit-short }} HEAD@{0}: checkout: moving from {{ second-commit }} to master
{{ second-commit-short }} HEAD@{1}: checkout: moving from master to master~0
{{ second-commit-short }} HEAD@{2}: commit: Adds nested folder structure
{{ first-commit-short }} HEAD@{3}: commit (initial): Adds first work on the story
Assuming you followed this Git lesson closely, your HEAD
's reflog should look exactly like the above.
Let's recall our past actions and match them with the reflog above, from the earliest (HEAD@{3}
) to the latest (HEAD@{0}
).
When we created our Git repo (git init
), Git created a branch master. That branch is where the HEAD
starts, where the HEAD
is attached.
How did we arrive at HEAD@{3}
?
{{ first-commit-short }} HEAD@{3}: commit (initial): Adds first work on the story
Our first actions were to:
- Create a new file
story.txt
and enter some lines of text into it. - Stage our work on
story.txt
(git add
) - Commit our work (
git commit
)
Yet, only the git commit
action was recorded on the reflog. This gives us an important lesson:
You will see that HEAD@{3}
corresponds with your first-commit.
How did we arrive at HEAD@{2}
?
{{ second-commit-short }} HEAD@{2}: commit: Adds nested folder structure
Our next action was a git commit
that commited some nested folders and files in order to explore Git Objects. This corresponds with your second-commit.
How did we arrive at HEAD@{1}
?
{{ second-commit-short }} HEAD@{1}: checkout: moving from master to master~0
We performed a checkout via git checkout master~0
to detach the HEAD
. Since ~0
means "zero steps upstream of master", the HEAD
points to the same commit that master points to. This corresponds with our second-commit.
How did we arrive at HEAD@{0}
?
{{ second-commit-short }} HEAD@{0}: checkout: moving from {{ second-commit }} to master
We performed a checkout via git checkout master
to attach the HEAD
to branch master.
git reflog <branch>
shows the reflog for said branch.
Reflogs for branches work about the same way as reflog for the HEAD
.
Since branches can be deleted, thereby erasing all traces of their movements, there is less use for reflogs for branches.
The HEAD
can never be deleted, nor can its reflog.
We will first demonstrate that a Git branch is simply a Git reference (pointer).
Currently, the only branch we have is the "master" branch, shown by git branch
.
Our "master" branch is pointing to our second commit, as shown by git log --decorate --graph
:
{% include git-log/ch.html commit-id=second-commit class="second-commit" head=true attached="master" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
Let's create a new branch named "temp" at our first-commit by doing git branch temp master~1
. A git log --decorate --graph
shows:
{% include git-log/ch.html commit-id=second-commit class="second-commit" head=true attached="master" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
{% include git-log/ch.html commit-id=first-commit branch="temp" class="first-commit" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
The fact that branches are really just Git references tells us that branches are simply pointers. We confirm this by comparing cat .git/refs/heads/temp
with git branch -v
:
* master {{ second-commit-short }} Adds nested folder structure
temp {{ first-commit-short }} Adds first work on the story
{{ first-commit }}
Although branches are technically merely pointers, Git still wants to use the term "branch" to actually denote a branch --- a line of connected commits. That would be like a branch of timeline --- yes, we'll be using time travel as a fitting analog --- aka an alternate history.
Still with the time travel analog, 2 connected branches would split (or stem) from a fork upstream. Relax, fork isn't a Git term, so we can forget that term.
We will continue writing our story in story.txt
from branch master, and create a new branch git-obj-study that points to our prior study of Git objects:
We can now witness our first git reset
in the reflog via git reflog
:
{{ first-commit-short }} HEAD@{0}: reset: moving to master~1
Our story will continue properly from the first-commit. The second-commit was really a digression to understand Git objects. git log --decorate git-obj-study
shows:
{% include git-log/ch.html commit-id=second-commit class="second-commit" branch="git-obj-study" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
{% include git-log/ch.html commit-id=first-commit class="first-commit" head=true attached="master" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
Edit file story.txt
(eg. emacs story.txt
) to contain (new lines 3-6):
{% assign linenos = "1 2 3 4 5 6 7" | split: " " %}
{% include linenos.html numbers=linenos %}
{% highlight text %}
Once upon a time, there was a unicorn.
The unicorn saw a rainbow.
The unicorn felt nothing about it.
The unicorn looked around. {% endhighlight %}
Add our new work to the staging area by doing git add story.txt
.
Edit file {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
) to contain:
{% highlight text %}
Unicorn encounters a rainbow
{% endhighlight %}
Commit our new work by doing git commit -F {{ commit-msg-file }}
.
{% assign third-commit = site.data.git-lesson.git-commits.third.id %} {% assign third-commit-short = third-commit | slice: 0, 7 %} {% assign third-commit-date = site.data.git-lesson.git-commits.third.date %} {% assign third-commit-timestamp = site.data.git-lesson.git-commits.third.timestamp %}
A git log --decorate --graph git-obj-study master
shows:
{% include git-log/ch.html commit-id=third-commit class="third-commit" attached="master" head=true %}
| Author: Author A
| Date: {{ third-commit-date }}
|
| Unicorn encounters a rainbow
|
| {% include git-log/ch.html commit-id=second-commit class="second-commit" branch="git-obj-study" %}
|/ Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
In the above git log
, we can see that there are 2 branches --- master whose head is at the third-commit, and git-obj-study whose head is at the second-commit.
The fork for these 2 branches is at our first-commit, just FYI.
Branches are important because they provide the only (normal) way for you to access commits.
That is, it is generally infeasible to refer to commits via their Git Object Identifiers.
Even if you strive hard to remember an unreferenced commit's Git object ID, you won't be able to retrieve that commit after Git's garbage collector has deleted it.
We will demonstrate how the garbage collector deletes an unreferenced commit. We first create a commit we intend to lose.
Edit story.txt
(emacs story.txt
) to add lines 8-9 at the end:
{% assign linenos = "1 2 3 4 5 6 7 8 9" | split: " " %}
{% include linenos.html numbers=linenos %}
{% highlight text %}
Once upon a time, there was a unicorn.
The unicorn saw a rainbow.
The unicorn felt nothing about it.
The unicorn looked around.
This change will be intentionally lost. {% endhighlight %}
Edit {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
) to be:
{% highlight text %}
Adds a commit we intend to lose
{% endhighlight %}
Do git add story.txt
and then git commit -F {{ commit-msg-file }}
.
{% assign to-lose-commit = site.data.git-lesson.git-commits.to-lose.id %} {% assign to-lose-commit-short = to-lose-commit | slice: 0, 7 %} {% assign to-lose-commit-date = site.data.git-lesson.git-commits.to-lose.date %} {% assign to-lose-commit-timestamp = site.data.git-lesson.git-commits.to-lose.timestamp %}
A git log --decorate --graph
shows our to-lose-commit:
{% include git-log/ch.html commit-id=to-lose-commit class="to-lose-commit" head=true attached="master" %}
| Author: Author A
| Date: {{ to-lose-commit-date }}
|
| Adds a commit we intend to lose
|
{% include git-log/ch.html commit-id=third-commit class="third-commit" %}
| Author: Author A
| Date: {{ third-commit-date }}
|
| Unicorn encounters a rainbow
|
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
We now retreat (move upstream) our branch master by doing git reset --hard HEAD~1
. A git log --decorate --graph
shows:
{% include git-log/ch.html commit-id=third-commit class="third-commit" head=true attached="master" %}
| Author: Author A
| Date: {{ third-commit-date }}
|
| Unicorn encounters a rainbow
|
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
We confirm that our to-lose-commit still exists by doing:
git cat-file -p {{ to-lose-commit-short }}
tree 2913ba48160d3b6b713135243c06d7e15034bbcc parent {{ third-commit }} author Author A {{ to-lose-commit-timestamp }} +0800 committer Author A {{ to-lose-commit-timestamp }} +0800
Adds a commit we intend to lose
We can see that our to-lose-commit is now unreachable by doing git fsck --no-reflogs
:
dangling commit {{ to-lose-commit }}
Reachable commits render their parent(s) reachable.
After accounting for all reachable commits, all other commits are unreachable.
It is now clear that our to-lose-commit is unreachable. But it is not unreferenced! Let's see.
Git's reflog still stores a reference to our to-lose-commit. A git reflog
reveals our to-lose-commit is still referenced at HEAD@{1}
:
{{ third-commit-short }} HEAD@{0}: reset: moving to HEAD~1
{{ to-lose-commit-short }} HEAD@{1}: commit: Adds a commit we intend to lose
{{ third-commit-short }} HEAD@{2}: commit: Unicorn encounters a rainbow
{{ first-commit-short }} HEAD@{3}: reset: moving to master~1
Now, Git's default parameters for its garbage collector means that Git only deletes unreferenced commits that are older than 14 days. To impede our current experiment more, Git still retains references to our to-lose-commit in its reflog, because reflog references to unreachable commits are only deleted if older than 30 days. Our experiment can only work if we wait 30 days from now!
A simple test proves it. We do git gc
and see that our to-lose-commit is still in existence:
git cat-file -p {{ to-lose-commit-short }}
tree 2913ba48160d3b6b713135243c06d7e15034bbcc parent {{ third-commit }} author Author A {{ to-lose-commit-timestamp }} +0800 committer Author A {{ to-lose-commit-timestamp }} +0800
Adds a commit we intend to lose
We give immediacy to the garbage collector by passing in 2 parameters via these commands:
Now, git gc
will delete our to-lose-commit, as can be seen by:
git cat-file -p {{ to-lose-commit-short }}
fatal: Not a valid object name {{ to-lose-commit-short }}
As expected, git reflog
shows that our top 2 entries were deleted:
{{ third-commit-short }} HEAD@{0}: commit: Unicorn encounters a rainbow
{{ first-commit-short }} HEAD@{1}: reset: moving to master~1
The removal of those 2 entries rendered our to-lose-commit unreferenced, not just unreachable. That is why the garbage collector was able to delete our to-lose-commit.
Now remove that immediacy we just mandated! We don't want Git immediately deleting our commits. We like the 30-day grace period for us to perform any undo required!
Reset the garbage collector to default parameters by doing git config --remove-section gc
! Do that now!
What does it mean to work on a branch? How is a branch advanced?
Hence, a HEAD
attached to some branch allows us to in effect work on the branch, commit to the branch, and advance the branch.
This was seen when we committed our first-commit and second-commit while on branch master. We did not have to manually advance the branch with every new commit.
We shall soon see that this automatic advancing of a branch does not occur if the HEAD
is not attached to any branch.
As we will soon see, checking out a tag will result in a detached HEAD
.
Create a tag at the second-commit by doing git checkout git-obj-study
and then git tag our-tag
. Then git log --decorate --graph
shows:
{% include git-log/ch.html commit-id=second-commit attached="git-obj-study" head=true tag="our-tag" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
{% include git-log/ch.html commit-id=first-commit %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
Checkout tag our-tag by doing git checkout our-tag
. Git issues a warning:
Note: checking out 'our-tag'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b <new-branch-name>
HEAD is now at {{ second-commit-short }} ... Adds nested folder structure
Recall: performing a checkout with just the branch reference itself is the only way to attach the HEAD
.
A git log --decorate --graph
shows that the HEAD
is on its own:
{% include git-log/ch.html commit-id=second-commit class="second-commit" branch="git-obj-study" head=true tag="our-tag" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
We will now show what happens when a new commit is made on a detached HEAD
.
Add 2 lines (4-5) at the end of story.txt
(eg. emacs story.txt
):
{% assign linenos = "1 2 3 4 5" | split: " " %}
{% include linenos.html numbers=linenos %}
{% highlight text %}
Once upon a time, there was a unicorn.
The unicorn looked around.
This change will be committed while detached. {% endhighlight %}
Edit {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
) to be:
{% highlight text %}
Adds a commit while detached
{% endhighlight %}
Commit that new change with git add story.txt
and then git commit -F {{ commit-msg-file }}
.
{% assign detached-commit = site.data.git-lesson.git-commits.detached.id %} {% assign detached-commit-short = detached-commit | slice: 0, 7 %} {% assign detached-commit-date = site.data.git-lesson.git-commits.detached.date %} {% assign detached-commit-timestamp = site.data.git-lesson.git-commits.detached.timestamp %}
Then git log --decorate --graph
shows:
{% include git-log/ch.html commit-id=detached-commit class="detached-commit" head=true %}
| Author: Author A
| Date: {{ detached-commit-date }}
|
| Adds a commit while detached
|
{% include git-log/ch.html commit-id=second-commit class="second-commit" branch="git-obj-study" tag="our-tag" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
Let's complete our demonstration of how a commit created while detached can be lost.
Checkout branch git-obj-study via git checkout git-obj-study
, and we see Git telling us:
Warning: you are leaving 1 commit behind, not connected to
any of your branches:
{{ detached-commit-short }} Adds a commit while detached
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> {{ detached-commit-short }}
Switched to branch 'git-obj-study'
In fact, despite Git's polite warning, that is about the only time you may keep that unreachable commit. You likely won't remember the commit ID of our detached-commit after this warning disappears. Moreover, Git's garbage collector may delete that unreachable commit some time later (30 days) before you decide to retrieve it.
That brings us to an important tip:
To repeat for reinforcement, branches are the only normal way to access (reach) commits. In Git, we work with branches, not tags. You create multiple/alternate timelines (approaches, "what-if's"), which involve branches.
Worse than tags, the HEAD
is not at all intended to keep commits. TheHEAD
changes depending on which branch you checkout, and can potentially leave commits behind.
To see that the HEAD
has moved to branch git-obj-study, and has left our detached-commit behind:
git log --decorate --graph {{ detached-commit-short }}
{% include git-log/ch.html commit-id=detached-commit class="detached-commit" %} | Author: Author A | Date: {{ detached-commit-date }} | | Adds a commit while detached | {% include git-log/ch.html commit-id=second-commit class="second-commit" head=true attached="git-obj-study" tag="our-tag" %} | Author: Author A | Date: {{ second-commit-date }} | | Adds nested folder structure ... {% include git-log/ch.html commit-id=first-commit class="first-commit" %} Author: Author A Date: {{ first-commit-date }}
Adds first work on the story
...
A detached HEAD
does have its uses. You can move the HEAD
to any commit (snapshot) in any timeline to take a look-see.
Just remember that you need to attach the HEAD
to a branch before you start work.
Let's jump to an earlier time in branch git-obj-study. Suppose we want to reminisce about how we started off when we created this branch.
A git checkout git-obj-study~1
puts us 1 commit upstream:
Note: checking out 'git-obj-study~1'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b <new-branch-name>
HEAD is now at {{ first-commit-short }}... Adds first work on the story
And git log --decorate
shows:
{% include git-log/ch.html commit-id=first-commit class="first-commit" head=true lone=true %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
I'd think up more descriptive information here if I could.
That first line above should be a short summary, with no ending period.
Then comes a blank line, and then details and descriptions follow.
Let's take a look-see. A ls -a
shows:
./ .git/ story.txt
../ rough_thoughts.txt
And cat story.txt
shows:
{% highlight text %}
Once upon a time, there was a unicorn.
The unicorn looked around. {% endhighlight %}
Aw, how sweet. That was how we started, with just 3 lines in story.txt
.
Alright, enough nostalgia. We need progress. We're done with learning about detached HEAD
.
The reflog for the HEAD
gives us the ability to undo actions we recently performed, if the actions involve the movement of the HEAD
.
There are a few ways to perform an undo, and all of them are based on the idea that the reflog contains a list of commits the HEAD
touched on. Some of those commits may have become left behind (aka lost).
To retrieve a "left behind" commit, you can simply place a branch on it: git branch <branch-name> <commit>
.
Our git reflog
should currently look like (omitted top 2 entries):
{{ detached-commit-short }} HEAD@{2}: commit: Adds a commit while detached
{{ second-commit-short }} HEAD@{3}: checkout: moving from git-obj-study to our-tag
{{ second-commit-short }} HEAD@{4}: checkout: moving from master to git-obj-study
{{ third-commit-short }} HEAD@{5}: commit: Unicorn encounters a rainbow
Our detached-commit is still unreachable, as seen by git fsck --no-reflogs
:
dangling commit {{ detached-commit }}
Creating a new branch on {{ detached-commit-short }} will make it reachable:
git branch reclaim-detached {{ detached-commit-short }}
And now, git fsck --no-reflogs
should show that all commits are reachable.
If you're not yet sure you want to create a new branch to reclaim a "left behind" commit --- perhaps when you're swamped with tons of flippantly created branches --- you can choose to simply backtrack the HEAD
.
Force move the HEAD
back in time. In this case, the reflog "time" entry we want to go back to is HEAD@{2}
(2 steps back into the past).
{{ detached-commit-short }} HEAD@{2}: commit: Adds a commit while detached
We're currently still on the first-commit, as seen by git log --decorate
:
{% include git-log/ch.html commit-id=first-commit class="first-commit" head=true lone=true %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
Move the HEAD
back into the past by 2 steps: git reset --hard HEAD@{2}
Now, git log --decorate --graph
shows:
{% include git-log/ch.html commit-id=detached-commit class="detached-commit" head=true branch="reclaim-detached" %}
| Author: Author A
| Date: {{ detached-commit-date }}
|
| Adds a commit while detached
|
{% include git-log/ch.html commit-id=second-commit class="second-commit" branch="git-obj-study" tag="our-tag" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
Notice that branch reclaim-detached still exists. We merely force moved the HEAD
to a different commit (our detached-commit); we didn't actually "turn back time" to when we haven't created branch reclaim-detached.
Recall that each commit is a snapshot of the entire project at some point in time.
Think of it this way. We created a snapshot (our detached-commit) right after that at second-commit. Then we moved the HEAD
somewhere else and gave up on detached-commit; it was unreachable by any branch since we committed while detached. After that, we changed our mind about losing detached-commit. So we backtracked the HEAD
to the point where it landed again on detached-commit.
A git reflog
shows that we backtracked the HEAD
:
{{ detached-commit-short }} HEAD@{0}: reset: moving to HEAD@{2}
The "undo history" (reflogs) for branches work the same way as the reflog for the HEAD
.
In case you accidentally shift a branch via git branch -f <branch> <wrong-commit>
, you can check its reflog to see which "correct commit" it was on before that accident.
Edit {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
) to be:
{% highlight text %}
Adds a commit while detached
We decided to include more details in the commit message. This is an amended commit. {% endhighlight %}
Then git commit --amend -F {{ commit-msg-file }}
to amend our last commit.
{% assign amended-commit = site.data.git-lesson.git-commits.amended.id %} {% assign amended-commit-short = amended-commit | slice: 0, 7 %} {% assign amended-commit-date = site.data.git-lesson.git-commits.amended.date %} {% assign amended-commit-timestamp = site.data.git-lesson.git-commits.amended.timestamp %}
We can see that a new commit --- our amended-commit --- has been created. A git log --decorate --graph reclaim-detached HEAD
shows:
{% include git-log/ch.html commit-id=amended-commit class="amended-commit" head=true %}
| Author: Jon Wong
| Date: Mon May 15 20:54:09 2017 +0800
|
| Adds a commit while detached
|
| We decided to include more details in the commit message.
| This is an amended commit.
|
| {% include git-log/ch.html commit-id=detached-commit class="detached-commit" branch="reclaim-detached" %}
|/ Author: Author A
| Date: {{ detached-commit-date }}
|
| Adds a commit while detached
|
{% include git-log/ch.html commit-id=second-commit class="second-commit" branch="git-obj-study" tag="our-tag" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
In this case, it just so happens that we have branch reclaim-detached at our original commit; normally, the original commit would have been left behind and become unreachable. The HEAD
was detached when we created the new commit, so the HEAD
did not bring any branch along with it.
Since it is generally not a good practice to go back and perfect past commits over and over, we won't be doing this section.
This requires learning about Git rebase.
To explore merging, we first have to do some branching. We already learned about branches earlier, and now we will practice working on a branch.
Recall: To work on a branch, simply git checkout <branch>
.
We move back to branch master via git checkout master
to continue our story. (You'll get a warning that you will be leaving behind our amended-commit; let that go, we don't need that anymore.)
Let's do some branching right now.
Recall that Agile's first key paradigms is iterative and incremental.
We perform a short unit of work via a leapfrog loop (my terminology, not formal Git). But why don't we perfectly do that short unit of work within 1 commit?
Consider that even a short unit of work can involve multiple commits --- you should commit often (good practice), because Git tracks your work only when you commit. Additionally, consider that multiple commits may be required due to your making mistakes while working fast (and you should work fast). That is why a typical short unit of work ("leapfrog") can involve more than 1 commit.
Let's start a leapfrog.
Create a new branch and checkout that branch in one command: git checkout -b author-A/daydream
. A git log --decorate --graph
should show:
{% include git-log/ch.html commit-id=third-commit class="third-commit" head=true attached="author-A/daydream" branch="master"%}
| Author: Author A
| Date: Tue May 16 18:23:02 2017 +0800
|
| Unicorn encounters a rainbow
|
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: Tue May 16 17:48:34 2017 +0800
Adds first work on the story
...
We will now make 2 commits on branch author-A/daydream.
Edit story.txt
(eg. emacs story.txt
) to add lines 8-9 at the end:
{% assign linenos = "1 2 3 4 5 6 7 8 9" | split: " " %}
{% include linenos.html numbers=linenos %}
{% highlight text %}
Once upon a time, there was a unicorn.
The unicorn saw a rainbow.
The unicorn felt nothing about it.
The unicorn looked around.
The unicorn thought about cotton candy. {% endhighlight %}
Edit {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
to contain:
{% highlight text %}
Adds cotton candy thought
{% endhighlight %}
Add your work via git add story.txt
, and commit via git commit -F {{ commit-msg-file }}
.
This first commit simulates a case where we're quick to commit our work and forgot a line about "clouds" which we will be adding in a second commit coming up next.
Edit story.txt
(eg. emacs story.txt
) to add lines 9-10:
{% assign linenos = "1 2 3 4 5 6 7 8 9 10 11" | split: " " %}
{% include linenos.html numbers=linenos %}
{% highlight text %}
Once upon a time, there was a unicorn.
The unicorn saw a rainbow.
The unicorn felt nothing about it.
The unicorn looked around.
The unicorn dreamed of clouds.
The unicorn thought about cotton candy. {% endhighlight %}
Edit {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
to contain:
{% highlight text %}
Adds cloud thought
{% endhighlight %}
Add your work via git add story.txt
, and commit via git commit -F {{ commit-msg-file }}
.
This second commit simulates an "afterthought" where we insert a line about "clouds" we missed out in the first commit.
This is the reason we use a "leapfrog loop" to house a unit of work. We can work fast, make mistakes and make corrections. Hindsight is 20/20. Exercise some foresight, but don't forget to use lots of hindsight!
{% assign leapfrog-one-commit = site.data.git-lesson.git-commits.leapfrog-one.id %} {% assign leapfrog-one-commit-short = leapfrog-one-commit | slice: 0, 7 %} {% assign leapfrog-one-commit-date = site.data.git-lesson.git-commits.leapfrog-one.date %} {% assign leapfrog-one-commit-timestamp = site.data.git-lesson.git-commits.leapfrog-one.timestamp %}
{% assign leapfrog-two-commit = site.data.git-lesson.git-commits.leapfrog-two.id %} {% assign leapfrog-two-commit-short = leapfrog-two-commit | slice: 0, 7 %} {% assign leapfrog-two-commit-date = site.data.git-lesson.git-commits.leapfrog-two.date %} {% assign leapfrog-two-commit-timestamp = site.data.git-lesson.git-commits.leapfrog-two.timestamp %}
We return to branch master with a git checkout master
. A git log --decorate --graph author-A/daydream
shows:
{% include git-log/ch.html commit-id=leapfrog-two-commit class="leapfrog-two-commit" branch="author-A/daydream" %}
| Author: Author A
| Date: {{ leapfrog-two-commit-date }}
|
| Adds cloud thought
|
{% include git-log/ch.html commit-id=leapfrog-one-commit class="leapfrog-one-commit" %}
| Author: Author A
| Date: {{ leapfrog-one-commit-date }}
|
| Adds cotton candy thought
|
{% include git-log/ch.html commit-id=third-commit class="third-commit" head=true attached="master" %}
| Author: Author A
| Date: Tue May 16 18:23:02 2017 +0800
|
| Unicorn encounters a rainbow
|
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: Tue May 16 17:48:34 2017 +0800
Adds first work on the story
...
Edit {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
to contain:
{% highlight text %}
Merge branch 'author-A/daydream'
The unicorn daydreams. {% endhighlight %}
We perform a merge with git merge --no-ff --no-commit author-A/daydream
.
Finally, we commit the merge with git commit -F {{ commit-msg-file }}
.
{% assign merge-commit = site.data.git-lesson.git-commits.merge.id %} {% assign merge-commit-short = merge-commit | slice: 0, 7 %} {% assign merge-commit-date = site.data.git-lesson.git-commits.merge.date %} {% assign merge-commit-timestamp = site.data.git-lesson.git-commits.merge.timestamp %}
And here is the loop with git log --decorate --graph
:
{% include git-log/ch.html commit-id=merge-commit class="merge-commit" head=true attached="master" %}
|\ Merge: {{ third-commit-short }} {{ leapfrog-two-commit-short }}
| | Author: Author A
| | Date: {{ merge-commit-date }}
| |
| | Merge branch 'author-A/daydream'
| |
| | The unicorn daydreams.
| |
| {% include git-log/ch.html commit-id=leapfrog-two-commit class="leapfrog-two-commit" branch="author-A/daydream" %}
| | Author: Author A
| | Date: {{ leapfrog-two-commit-date }}
| |
| | Adds cloud thought
| |
| {% include git-log/ch.html commit-id=leapfrog-one-commit class="leapfrog-one-commit" %}
|/ Author: Author A
| Date: {{ leapfrog-one-commit-date }}
|
| Adds cotton candy thought
|
{% include git-log/ch.html commit-id=third-commit class="third-commit" %}
| Author: Author A
| Date: Tue May 16 18:23:02 2017 +0800
|
| Unicorn encounters a rainbow
|
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: Tue May 16 17:48:34 2017 +0800
Adds first work on the story
...
A git log --decorate --graph --first-parent
shows:
{% include git-log/ch.html commit-id=merge-commit class="merge-commit" head=true attached="master" %}
| Merge: {{ third-commit-short }} {{ leapfrog-two-commit-short }}
| Author: Author A
| Date: {{ merge-commit-date }}
|
| Merge branch 'author-A/daydream'
|
| The unicorn daydreams.
|
{% include git-log/ch.html commit-id=third-commit class="third-commit" %}
| Author: Author A
| Date: Tue May 16 18:23:02 2017 +0800
|
| Unicorn encounters a rainbow
|
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: Tue May 16 17:48:34 2017 +0800
Adds first work on the story
...
Whatever mistakes and mishaps we had in our leapfrog is hidden from view like this.
And that is also why we employ leapfrog loops --- to hide away the messy details of our work from the main branch. In this case, our messy details are in branch author-A/daydream, and our main branch is branch master.
A bare Git repo contains the history of your work on your files, but does not keep a working copy of your files.
Make room for the bare Git repo by doing:
Create the bare repo, which we shall call remote repo, by doing:
Notice that the contents of folder remote
looks exactly like the contents in folder clone-A/.git
.
In our case here, we have a clone in folder clone-A
.
Our remote in this case resides in folder remote
.
Point our clone to the remote:
Checking for our added remote with git remote -v
:
{% highlight text %}
orign ../remote (fetch)
orign ../remote (push)
{% endhighlight %}
We now push our current branch master up to origin
by doing git push -u origin master
:
{% highlight text %}
Counting objects: 13, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (13/13), 1.31 KiB | 0 bytes/s, done.
Total 13 (delta 2), reused 0 (delta 0)
To ../remote
- [new branch] master -> master {% endhighlight %}
Shoot me an email if you want me to rush out a tutorial for the above use case (very common for staff in SUTD).
Everything you saw in your .git
folder inside your clone will be inside the remote (the bare Git repo).
Both these commands should show you the same files (../remote/objects
has less files):
(The remote shows only the commits from branch master because we only pushed that branch up.)
You can see that the bare repo does not reserve any space for working copies of project files; all Git data is stored at the top-level folder (../remote
).
A look at Git log via git log --decorate --graph --first-parent
tells us our remote now has branch master too (origin/master):
{% include git-log/ch.html commit-id=merge-commit class="merge-commit" head=true attached="master" remote="origin/master" %}
| Merge: {{ third-commit-short }} {{ leapfrog-two-commit-short }}
| Author: Author A
| Date: {{ merge-commit-date }}
|
| Merge branch 'author-A/daydream'
|
| The unicorn daydreams.
|
{% include git-log/ch.html commit-id=third-commit class="third-commit" %}
| Author: Author A
| Date: Tue May 16 18:23:02 2017 +0800
|
| Unicorn encounters a rainbow
|
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: Tue May 16 17:48:34 2017 +0800
Adds first work on the story
...
Branches (and consequently the commits rendered reachable only by them) are not visible to the rest of your team if you don't push them to the remote.
We will demonstrate this fact. We first clone the repo into folder clone-B
:
We then set our credential and some parameters:
A git log --decorate --graph --first-parent
shows:
{% include git-log/ch.html commit-id=merge-commit class="merge-commit" head=true attached="master" remote="origin/master" remote2="origin/HEAD" %}
| Merge: {{ third-commit-short }} {{ leapfrog-two-commit-short }}
| Author: Author A
| Date: {{ merge-commit-date }}
|
| Merge branch 'author-A/daydream'
|
| The unicorn daydreams.
|
{% include git-log/ch.html commit-id=third-commit class="third-commit" %}
| Author: Author A
| Date: Tue May 16 18:23:02 2017 +0800
|
| Unicorn encounters a rainbow
...
We delete branch origin/HEAD in our clone-B
via git branch -r -d origin/HEAD
.
The above Git log tells us our clone-B
has access to branch master. But what about branch git-obj-study? Let's see with git log --decorate --graph git-obj-study
:
{% highlight text %}
fatal: ambiguous argument 'git-obj-study': unknown revision or path not in the working tree.
{% endhighlight %}
Even git branch
tells us that clone-B
's only branch is master.
Let's go back into clone-A
to publish branch git-obj-study:
And we get clone-B
to pull in the newly pushed branch:
Now, git log --decorate --graph
shows that clone-B
can see branch git-obj-study:
{% include git-log/ch.html commit-id=second-commit class="second-commit" head=true attached="git-obj-study" remote="origin/git-obj-study" %}
| Author: Author A
| Date: {{ second-commit-date }}
|
| Adds nested folder structure
...
{% include git-log/ch.html commit-id=first-commit class="first-commit" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
We will contrast remote branch versus branch on remote.
A remote branch is like a pointer that resides in the clone. A branch on the remote resides in the remote.
To see that concept demonstrated, we will create a remote branch, delete it, and then see that the remote repo still has the corresponding branch.
We create branch temp at our first-commit via git branch temp HEAD~1
. We then push that branch up via git push -u origin temp
. Finally, git log --decorate temp
shows:
{% include git-log/ch.html commit-id=first-commit class="first-commit" branch="temp" remote="origin/temp" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
We confirm that the remote repo actually has branch temp. We enter the remote repo with cd ../remote
. We then check the log via git log --decorate temp
:
{% include git-log/ch.html commit-id=first-commit class="first-commit" full-id=true branch="temp" %}
Author: Author A
Date: {{ first-commit-date }}
Adds first work on the story
...
A git branch -v
also shows:
temp {{ first-commit-short }} Adds first work on the story
We now return to clone-B
and delete the remote branch origin/temp and also local branch temp:
Our first-commit no longer holds remote branch origin/temp nor local branch temp. A git log temp
and a git log origin/temp
both show:
{% highlight text %}
fatal: ambiguous argument 'temp': unknown revision or path not in the working tree.
{% endhighlight %}
We will see that the remote repo still has branch temp:
{% include git-log/ch.html commit-id=first-commit class="first-commit" full-id=true branch="temp" %} Author: Author A Date: {{ first-commit-date }}
Adds first work on the story
...
As can be seen, a remote branch is merely a tracking branch that tracks (corresponds with) a branch on a remote.
We now return to clone-B
to delete the branch on the remote (we normally don't have direct access to a remote repo):
Now, the remote repo truly has branch temp deleted:
A typical collaboration technique is the "peer review".
To demonstrate such collaboration, we will be getting "Author A" to commit some changes on a branch and then to notify someone else to do a review of those changes. Finally, after the review, the work will be merged back into branch master.
We enter clone-A
now with cd ../clone-A
. We then create a new branch and checkout the branch (recall section Leapfrog Loop) via git checkout -b author-A/rainbow
.
Our branch author-A/rainbow should be at our merge-commit, as seen via git log --decorate --graph --first-parent
:
{% include git-log/ch.html commit-id=merge-commit class="merge-commit" head=true branch="master" attached="author-A/rainbow" remote="origin/master" %}
| Merge: {{ third-commit-short }} {{ leapfrog-two-commit-short }}
| Author: Author A
| Date: {{ merge-commit-date }}
|
| Merge branch 'author-A/daydream'
|
| The unicorn daydreams.
|
{% include git-log/ch.html commit-id=third-commit class="third-commit" %}
| Author: Author A
| Date: Tue May 16 18:23:02 2017 +0800
|
| Unicorn encounters a rainbow
...
Edit story.txt
(eg. emacs story.txt
) to change line 5:
{% assign linenos = "1 2 3 4 5 6 7 8 9 10 11" | split: " " %}
{% include linenos.html numbers=linenos %}
{% highlight text %}
Once upon a time, there was a unicorn.
The unicorn saw a rainbow.
The unicorn found it unremarkable.
The unicorn looked around.
The unicorn dreamed of clouds.
The unicorn thought about cotton candy. {% endhighlight %}
Edit {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
to contain:
{% highlight text %}
Unicorn finds rainbow unremarkable
{% endhighlight %}
{% assign work-A-commit = site.data.git-lesson.git-commits.work-A.id %} {% assign work-A-commit-short = work-A-commit | slice: 0, 7 %} {% assign work-A-commit-date = site.data.git-lesson.git-commits.work-A.date %} {% assign work-A-commit-timestamp = site.data.git-lesson.git-commits.work-A.timestamp %}
Add your work via git add story.txt
, and then commit it via git commit -F {{ commit-msg-file }}
. Then git log --decorate --graph --first-parent
shows:
{% include git-log/ch.html commit-id=work-A-commit class="work-A-commit" head=true attached="author-A/rainbow" %}
| Author: Author A
| Date: {{ work-A-commit-date }}
|
| Unicorn finds rainbow unremarkable
|
{% include git-log/ch.html commit-id=merge-commit class="merge-commit" branch="master" remote="origin/master" %}
| Merge: {{ third-commit-short }} {{ leapfrog-two-commit-short }}
| Author: Author A
| Date: {{ merge-commit-date }}
|
| Merge branch 'author-A/daydream'
|
| The unicorn daydreams.
...
"Author A" will now publish the work, so that someone else can review it.
Do git push -u origin author-A/rainbow
to publish the work. A git log --decorate --graph --first-parent
shows:
{% include git-log/ch.html commit-id=work-A-commit class="work-A-commit" head=true attached="author-A/rainbow" remote="origin/author-A/rainbow" %}
| Author: Author A
| Date: {{ work-A-commit-date }}
|
| Unicorn finds rainbow unremarkable
|
{% include git-log/ch.html commit-id=merge-commit class="merge-commit" branch="master" remote="origin/master" %}
| Merge: {{ third-commit-short }} {{ leapfrog-two-commit-short }}
| Author: Author A
| Date: {{ merge-commit-date }}
|
| Merge branch 'author-A/daydream'
|
| The unicorn daydreams.
...
There is a concept called "Pull Request" (PR). A PR is a piece of communication going from the work/change publisher ("Author A", in our case) to the reviewer ("Author C", which we will soon create).
PRs are not a part of Git. GitHub, BitBucket and GitLab (terms it as "Merge Request") had to implement this feature themselves.
For now, we only need to know that a PR is merely a message of this structure: {% highlight yaml %} From Branch: origin/author-A/rainbow To Branch: origin/master Description: Makes rainbow more integal to the story. {% endhighlight %}
That is all a PR essentially is --- a request to merge (from) a work branch onto a main branch. That request is sent to a reviewer.
Suppose "Author A" sends the above PR to "Author C".
We now simulate an "Author C". Create a clone clone-C
by doing:
We then set the credential for "Author C", as well as some parameters:
Next, we pull updates from the remote, and then checkout the work we need to review:
Git has a related program called diff-highlight
that makes git diff
more visually informative.
Install the diff-highlight
program:
Check if the diff-highlight
program is on your executable path: Doing which diff-highlight
should show:
{% highlight text %}
/Users//bin/diff-highlight
{% endhighlight %}
If there was no output from the above command, you will have to add ~/bin
to your executable path:
If you're using MacOS, you must also ensure that your ~/.bash_profile
has this line:
{% highlight text %}
[[ -s ~/.bashrc ]] && source ~/.bashrc
{% endhighlight %}
Usually, a MacOS's ~/.bash_profile
should contain only that 1 line.
After adding ~/bin
to your executable path, you must restart your Bash terminal.
If you're on MacOS, you can try to do source /etc/profile
instead. Yes, MacOS is a little non-standard in the way it handles shell configuration.
Now, tell Git to always use diff-highlight
:
The reviewer looks at the work (or changes).
In this case, branch author-A/rainbow contains the new changes to be applied onto branch master. Therefore, we do git diff master author-A/rainbow
:
diff --git a/story.txt b/story.txt
index 700b75e..6806d4f 100644
--- a/story.txt
+++ b/story.txt
@@ -2,7 +2,7 @@ Once upon a time, there was a unicorn.
The unicorn saw a rainbow.
-The unicorn felt nothing about it.
+The unicorn found it unremarkable.
The unicorn looked around.
It is very obvious that "Author A" replaced "elt nothing about it" with "ound it unremarkable".
In this case, the changes are simple and easy to understand.
But to learn about how discussions are typically conducted, let's assume the reviewer still has concerns, and chooses to hold a discussion with "Author A". For example, the reviewer may question the purpose of the "rainbow" in the story, since the unicorn had no response to the "rainbow" at all. In response, "Author A" may justify the existence of the "rainbow" by claiming that the "rainbow" will have a purpose only later on in the story, via a "surprise plot twist" perhaps. Moreover, "Author A" can claim that deeming the "rainbow" "unremarkable" is a non-trivial response.
Let's assume that the reviewer accepts the justification given by "Author A", and therefore accepts the change put forward by "Author A".
The reviewer will now merge the work into branch master.
Create {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
) to contain:
{% highlight text %}
Merge branch 'author-A/rainbow'
{% endhighlight %}
Perform the merge, commit it, and then push it:
When working on a GitHub, BitBucket or GitLab platform, a PR is merged on the remote repo, not from a clone like what was seen in the above merge process. Don't worry, such a platform-facilitated merge is a lot more convenient than the above manual merge process.
{% assign work-A-merge-commit = site.data.git-lesson.git-commits.work-A-merge.id %} {% assign work-A-merge-commit-short = work-A-merge-commit | slice: 0, 7 %} {% assign work-A-merge-commit-date = site.data.git-lesson.git-commits.work-A-merge.date %} {% assign work-A-merge-commit-timestamp = site.data.git-lesson.git-commits.work-A-merge.timestamp %}
A git log --decorate --graph --first-parent
now should show:
{% include git-log/ch.html commit-id=work-A-merge-commit class="work-A-merge-commit" head=true attached="master" remote="origin/master" %}
| Merge: {{ merge-commit-short }} {{ work-A-commit-short }}
| Author: Author C
| Date: {{ work-A-merge-commit-date }}
|
| Merge branch 'author-A/rainbow'
|
{% include git-log/ch.html commit-id=merge-commit class="merge-commit" %}
| Merge: {{ third-commit-short }} {{ leapfrog-two-commit-short }}
| Author: Author A
| Date: {{ merge-commit-date }}
|
| Merge branch 'author-A/daydream'
|
| The unicorn daydreams.
...
To demonstrate a merge conflict, we get "Author B" to create work that changes the same lines affected by the work of "Author A".
The workflow is about the same as the case without merge conflict, but with conflict discussion and resolution added:
- Work is done.
- PR is created, and conflict is discovered.
- Conflict is discussed among relevant parties.
- Conflict is resolved.
- PR is sent to reviewer.
- Work is reviewed, and then merged.
Note that the "PR creation" step will now be fleshed out properly. It is more involved than previously described.
"Author B" will now create a branch author-B/rainbow:
A git log --decorate --graph --first-parent
shows:
{% include git-log/ch.html commit-id=merge-commit class="merge-commit" head=true branch="master" attached="author-B/rainbow" remote="origin/master" %}
| Merge: {{ third-commit-short }} {{ leapfrog-two-commit-short }}
| Author: Author A
| Date: {{ merge-commit-date }}
|
| Merge branch 'author-A/daydream'
|
| The unicorn daydreams.
|
{% include git-log/ch.html commit-id=third-commit class="third-commit" %}
| Author: Author A
| Date: Tue May 16 18:23:02 2017 +0800
|
| Unicorn encounters a rainbow
...
Notice that we're starting our work from an older tip of branch master --- there is no sign of work from "Author A". This is intentional in this demonstration. It is a typical scenario.
In this case, we're assuming that "Author B" started work on branch author-B/rainbow before "Author A" even finished work on branch author-A/rainbow.
We now do some work.
Edit story.txt
(eg. emacs story.txt
) to change line 5 and add lines 9-10:
{% assign linenos = "1 2 3 4 5 6 7 8 9 10 11 12 13" | split: " " %}
{% include linenos.html numbers=linenos %}
{% highlight text %}
Once upon a time, there was a unicorn.
The unicorn saw a rainbow.
The unicorn found it exciting.
The unicorn looked around.
The unicorn moved towards the rainbow.
The unicorn dreamed of clouds.
The unicorn thought about cotton candy. {% endhighlight %}
A git diff story.txt
shows:
diff --git a/story.txt b/story.txt
index 700b75e..b669429 100644
--- a/story.txt
+++ b/story.txt
@@ -2,7 +2,7 @@ Once upon a time, there was a unicorn.
The unicorn saw a rainbow.
-The unicorn felt nothing about it.
+The unicorn found it exciting.
The unicorn looked around.
+The unicorn moved towards the rainbow.
+
The unicorn dreamed of clouds.
The unicorn thought about cotton candy.
Create {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
) to contain:
{% highlight text %}
Unicorn finds rainbow exciting
{% endhighlight %}
{% assign work-B-commit = site.data.git-lesson.git-commits.work-B.id %} {% assign work-B-commit-short = work-B-commit | slice: 0, 7 %} {% assign work-B-commit-date = site.data.git-lesson.git-commits.work-B.date %} {% assign work-B-commit-timestamp = site.data.git-lesson.git-commits.work-B.timestamp %}
Add your work via git add story.txt
, and then commit it via git commit -F {{ commit-msg-file }}
. Finally, we push the work via git push -u origin author-B/rainbow
. Then git log --decorate --graph --first-parent
shows:
{% include git-log/ch.html commit-id=work-B-commit class="work-B-commit" head=true attached="author-B/rainbow" remote="origin/author-B/rainbow" %}
| Author: Author B
| Date: {{ work-B-commit-date }}
|
| Unicorn finds rainbow exciting
|
{% include git-log/ch.html commit-id=merge-commit class="merge-commit" branch="master" remote="origin/master" %}
| Merge: {{ third-commit-short }} {{ leapfrog-two-commit-short }}
| Author: Author A
| Date: {{ merge-commit-date }}
|
| Merge branch 'author-A/daydream'
|
| The unicorn daydreams.
...
We next have "Author B" create the following PR: {% highlight yaml %} From Branch: origin/author-B/rainbow To Branch: origin/master Description: Unicorn responds to the rainbow {% endhighlight %}
"Author B" now has the responbility to perform a "test merge" of the work onto branch master.
For learning purposes, we continue to perform the "test merge" ourselves.
"Author B" performs the "test merge" via:
Git tells us there's a merge conflict: {% highlight text %} Auto-merging story.txt CONFLICT (content): Merge conflict in story.txt Automatic merge failed; fix conflicts and then commit the result. {% endhighlight %}
Editing story.txt
(eg. emacs story.txt
) shows:
{% assign linenos = "3 4 5 6 7 8 9 10 11 12 13 14 15" | split: " " %}
{% include linenos.html numbers=linenos %}
The unicorn saw a rainbow.
<<<<<<< HEAD
The unicorn found it unremarkable.
||||||| merged common ancestors
The unicorn felt nothing about it.
=======
The unicorn found it exciting.
>>>>>>> author-B/rainbow
The unicorn looked around.
The unicorn moved towards the rainbow.
The above colorful highlighting is provided by emacs
. Even if your text editor doesn't show those colors, you should be able to compare the text easily.
Recall: To view the work/changes, do git diff <ours> <theirs>
, where <ours>
is the commit on which we will be applying changes (via git merge
) from <theirs>
. In our case now, <ours>
would be branch master and <theirs>
would be branch author-B/rainbow.
The red section shows the "old" value, the value already on branch master.
The green section shows the "new" value, the value on branch author-B/rainbow.
The yellow section shows the "common value" from which both the "new" and "old" values stemmed (forked).
Let's see where that "fork" is, via git log --decorate --graph --first-parent master author-B/rainbow
:
{% include git-log/ch.html commit-id=work-B-commit class="work-B-commit" head=true attached="author-B/rainbow" remote="origin/author-B/rainbow" %}
| Author: Author B
| Date: {{ work-B-commit-date }}
|
| Unicorn finds rainbow exciting
|
| {% include git-log/ch.html commit-id=work-A-merge-commit class="work-A-merge-commit" head=true attached="master" remote="origin/master" %}
|/ Author: Author C
| Date: {{ work-A-merge-commit-date }}
|
| Merge branch 'author-A/rainbow'
|
{% include git-log/ch.html commit-id=merge-commit class="merge-commit" %}
| Merge: {{ third-commit-short }} {{ leapfrog-two-commit-short }}
| Author: Author A
| Date: {{ merge-commit-date }}
|
| Merge branch 'author-A/daydream'
|
| The unicorn daydreams.
...
The fork is obviously at our merge-commit, where we merged in the unicorn's thoughts about "clouds and cotton candy", and where the unicorn still "felt nothing about" the rainbow.
We now abort the "test merge" via git merge --abort
. We don't have the right to merge our work into branch master now, because our work is not yet reviewed.
At this point of the workflow, "Author B" should discuss the conflict with "Author A".
A typical start of such a discussion would be these question from "Author B" to "Author A":
- "Did you intend to have the unicorn find the rainbow unremarkable?"
- "Will I break anything if I now decide that the unicorn finds the rainbow exciting?"
In our case, let's simplify the scenario and have "Author A" tell "Author B" to "go ahead with your changes because I now agree that the unicorn should not find the rainbow unremarkable". In short, "Author A" agrees to have her own changes overwritten by changes from "Author B".
"Author B" goes back to branch author-B/rainbow and attempts to incorporate the "latest updates" on branch master onto the work branch (branch author-B/rainbow):
Edit story.txt
(eg. emacs story.txt
to see the merge conflict:
{% assign linenos = "3 4 5 6 7 8 9 10 11 12 13 14 15" | split: " " %}
{% include linenos.html numbers=linenos %}
The unicorn saw a rainbow.
<<<<<<< HEAD
The unicorn found it exciting.
||||||| merged common ancestors
The unicorn felt nothing about it.
=======
The unicorn found it unremarkable.
>>>>>>> author-B/rainbow
The unicorn looked around.
The unicorn moved towards the rainbow.
The above line 15 is an example. It is a new line added.
"Author B" easily sees that our intended changes now involves removing lines 7-11 and line 5. We want to keep our changes (on branch author-B/rainbow), and discard their changes (on branch master). The final contents of story.txt
should be:
{% assign linenos = "1 2 3 4 5 6 7 8 9 10 11 12 13" | split: " " %}
{% include linenos.html numbers=linenos %}
{% highlight text %}
Once upon a time, there was a unicorn.
The unicorn saw a rainbow.
The unicorn found it exciting.
The unicorn looked around.
The unicorn moved towards the rainbow.
The unicorn dreamed of clouds.
The unicorn thought about cotton candy. {% endhighlight %}
Edit {{ commit-msg-file }}
(eg. emacs {{ commit-msg-file }}
to contain:
{% highlight text %}
Pulls in updates from 'master'
{% endhighlight %}
Commit the merge via git commit -F {{ commit-msg-file }}
.
Finally, the PR is sent to the reviewer, and the review proceeds. And that concludes our demonstration of merge conflicts.
Do not work on branches belonging to other team members.
Notice in section Leapfrog Loop that we created for "Author A" a branch named author-A/daydream.
Work (create commits) only on your own branches.
In future, if I do write about force push (git push -f
), I can explain why we only ever work on our own branches.
As for branch master, usually only the project manager should have the authority to "work" on that --- the project manager merges branches (leapfrog loops) into branch master.
We're done with the Git lesson! Feel free to create commits with both "Author A" and "Author B", and get a feel for how collaboration can work on Git repositories via remote and clones.
As a final tidbit, here's a clue about how to ignore the rough_thoughts.txt
file we created. Create a .gitignore
file in folder clone-A
. And clone-B
too, if you want to continue pretending to be both "Author A" and "Author B".
Specifying paths are a little more involved:
- A leading
/
targets only the folder wheregitignore
resides.- Eg.
/config.cfg
ignores/config.cfg
but not/subfolder/config.cfg
.
- Eg.
- No leading
/
targets all folders, including subfolders, recursively.- Eg.
config.cfg
ignores allconfig.cfg
files, even in subfolders.
- Eg.
BitBucket allows your academic email address to get an unlimited account --- you can have any number of private repositories.
GitHub is probably where you want to publish your proudest achievements.
For security, you should use SSH keys to login to your Bitbucket and GitHub accounts. Feel free to contact me to ask for a write-up on this. Or you can contact BitBucket and GitHub to ask their support staff for help.
Each SSH key has 2 parts --- private and public. The private key never leaves your harddisk, never travels onto any network. You can read briefly into Public-key Cryptography to get an idea of how SSH keys work.