Git Basics#
Git is a tool to exchange commits (file changes) and branches (organized of commits) with other developers.
As a distributed revision control system, Git does not have the notion of a central server. However, for Sage development, Git communicates with other developers via the Sage repository on GitHub. Hence we assume that throughout this guide.
Git authentication through SSH#
In order to push changes securely to a remote repository, Git uses public-key cryptography. This section will show you how to set up the necessary cryptographic keys for the case that you want to use SSH(Secure Shell) protocol to authenticate your Git to GitHub, instead of HTTPS protocol.
Generating your SSH keys#
Check whether you already have suitable SSH keys by inspecting .ssh
directory in your home directory. If you don’t have suitable SSH keys yet, you
can create a key pair with the ssh-keygen
tool.
Follow either the detailed instructions or the following brief instructions:
[alice@localhost ~]$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/alice/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/alice/.ssh/id_rsa.
Your public key has been saved in /home/alice/.ssh/id_rsa.pub.
The key fingerprint is:
ce:32:b3:de:38:56:80:c9:11:f0:b3:88:f2:1c:89:0a alice@localhost
The key's randomart image is:
+--[ RSA 2048]----+
| .... |
| .. |
| .o+ |
| o o+o. |
|E + . .S |
|+o . o. |
|. o +.o |
| oB |
| o+.. |
+-----------------+
This will generate a new random private RSA key
in the .ssh
folder in your home directory. By default, they are
~/.ssh/id_rsa
Your private key. Keep safe. Never hand it out to anybody.
~/.ssh/id_rsa.pub
The corresponding public key. This and only this file can be safely disclosed to third parties.
The ssh-keygen
tool will let you generate a key with a different
file name, or protect it with a passphrase. Depending on how much you
trust your own computer or system administrator, you can leave the
passphrase empty to be able to login without any human intervention.
Adding your public key for authentication to GitHub#
Follow the procedure Adding a new SSH key to your GitHub account. Then check that it works by:
[alice@localhost sage]$ git remote add origin [email protected]:alice/sage.git
[alice@localhost sage]$ git remote -v
origin [email protected]:alice/sage.git (fetch)
origin [email protected]:alice/sage.git (push)
Pushing your changes to a remote#
Push your branch to the remote origin
with either
[alice@localhost sage]$ git push --set-upstream origin HEAD:my_branch
or
[alice@localhost sage]$ git push origin HEAD:my_branch
if your branch already has an upstream branch. Here “upstream” means the the
remote origin
, which is upstream to your local Git repo.
Here, HEAD
means that you are pushing the most recent commit (and, by
extension, all of its parent commits) of the current local branch to the remote
branch.
Checking out a PR#
If you want to work with the changes of a PR branch, you must make a local copy of the branch. In particular, Git has no concept of directly working with the remote branch, the remotes are only bookmarks for things that you can get from/to the remote server. Hence, the first thing you should do is to get everything from the branch into your local repository. This is achieved by:
[alice@localhost sage]$ git fetch upstream pull/12345/head
remote: Enumerating objects: 12, done.
remote: Counting objects: 100% (12/12), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 12 (delta 9), reused 11 (delta 9), pack-reused 0
Unpacking objects: 100% (12/12), 2.22 KiB | 206.00 KiB/s, done.
From https://github.com/sagemath/sage
* branch refs/pull/12345/head -> FETCH_HEAD
The pull/12345/head
branch refers to the branch of the PR #12345 of the
remote upstream
. The branch is now temporarily (until you fetch something
else) stored in your local Git database under the alias FETCH_HEAD
. In the
second step, we make it available as a new local branch and switch to it. Your
local branch can have a different name, for example:
[alice@localhost sage]$ git checkout -b my_branch FETCH_HEAD
Switched to a new branch 'my_branch'
creates a new branch in your local Git repository named my_branch
and modifies your local Sage filesystem tree to the state of the files
in the branch. You can now edit files and commit changes to your
local branch.
Getting changes from a remote#
A common task during development is to synchronize your local copy of the branch with the branch on the GitHub Sage repo. In particular, assume you downloaded the branch of a PR made by someone else, say Bob, and made some suggestions for improvements on the PR. Now Bob incorporated your suggestions into his branch, and you want to get the added changes to complete your review. Assuming that you originally got your local branch as in Checking out a PR, you can just issue:
[bob@localhost sage]$ git pull upstream pull/12345/head
From https://github.com/sagemath/sage
* branch refs/pull/35608/head -> FETCH_HEAD
Merge made by the 'ort' strategy.
src/doc/common/python3.inv | Bin 98082 -> 131309 bytes
src/doc/common/update-python-inv.sh | 7 ++++---
2 files changed, 4 insertions(+), 3 deletions(-)
This command downloads the changes from the branch of the PR and merges them into your local branch.
Updating develop#
The develop
branch can be updated just like any other branch. However, your
local copy of the develop branch should stay identical to the GitHub Sage repo develop
branch.
If you accidentally added commits to your local copy of develop
, you must
delete them before updating the branch.
One way to ensure that you are notified of potential problems is to use git
pull --ff-only
, which will raise an error if a non-trivial merge would be
required:
[alice@localhost sage]$ git checkout develop
[alice@localhost sage]$ git pull --ff-only upstream develop
If this pull fails, then something is wrong with the local copy of the master branch. To switch to the correct Sage master branch, use:
[alice@localhost sage]$ git checkout develop
[alice@localhost sage]$ git reset --hard upstream/develop
Merging and rebasing#
Sometimes, a new version of Sage is released while you work on a Git branch.
Let us assume you started my_branch
at commit B
. After a while, your
branch has advanced to commit Z
, but you updated develop
(see
Updating develop) and now your Git history looks like this (see
The history):
X---Y---Z my_branch
/
A---B---C---D develop
How should you deal with such changes? In principle, there are two ways:
Rebase: The first solution is to replay commits
X,Y,Z
atop of the newdevelop
. This is called rebase, and it rewrites your current branch:git checkout my_branch git rebase -i develop
In terms of the commit graph, this results in:
X'--Y'--Z' my_branch / A---B---C---D develop
Note that this operation rewrites the history of
my_branch
(see Rewriting history). This can lead to problems if somebody began to write code atop of your commitsX,Y,Z
. It is safe otherwise.Alternatively, you can rebase
my_branch
while updatingdevelop
at the same time (see Getting changes from a remote):git checkout my_branch git pull -r develop
Merging your branch with
develop
will create a new commit above the two of them:git checkout my_branch git merge develop
The result is the following commit graph:
X---Y---Z---W my_branch / / A---B---C-------D develop
Pros: you did not rewrite history (see Rewriting history).The additional commit is then easily pushed to the git repository and distributed to your collaborators.
Cons: it introduced an extra merge commit that would not be there had you used rebase.
Alternatively, you can merge
my_branch
while updatingdevelop
at the same time (see Getting changes from a remote):git checkout my_branch git pull develop
In case of doubt use merge rather than rebase. There is less risk involved, and rebase in this case is only useful for branches with a very long history.
Merge tools#
Simple conflicts can be easily solved with Git only (see Conflict resolution)
For more complicated ones, a range of specialized programs are available. Because the conflict marker includes the hash of the most recent common parent, you can use a three-way diff:
[alice@laptop]$ git mergetool
This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
meld opendiff kdiff3 [...] merge araxis bc3 codecompare emerge vimdiff
Merging:
fibonacci.py
Normal merge conflict for 'fibonacci.py':
{local}: modified file
{remote}: modified file
Hit return to start merge resolution tool (meld):
If you don’t have a favourite merge tool we suggest you try meld (cross-platform). The result looks like the following screenshot.
The middle file is the most recent common parent; on the right is Bob’s version and on the left is Alice’s conflicting version. Clicking on the arrow moves the marked change to the file in the adjacent pane.
Conflict resolution#
Merge conflicts happen if there are overlapping edits, and they are an unavoidable consequence of distributed development. Fortunately, resolving them is common and easy with Git. As a hypothetical example, consider the following code snippet:
def fibonacci(i):
"""
Return the `i`-th Fibonacci number
"""
return fibonacci(i-1) * fibonacci(i-2)
This is clearly wrong. Two developers, namely Alice and Bob, decide to fix it. Bob corrected the seed values:
def fibonacci(i):
"""
Return the `i`-th Fibonacci number
"""
if i > 1:
return fibonacci(i-1) * fibonacci(i-2)
return [0, 1][i]
and turned those changes into a new commit:
[bob@laptop sage]$ git add fibonacci.py
[bob@laptop sage]$ git commit -m 'return correct seed values'
He made his changes a PR to the GitHub Sage repo and got it merged to the
develop
branch. His fibonacci
function is not yet perfect but is
certainly better than the original.
Meanwhile, Alice changed the multiplication to an addition since that is the correct recursion formula:
def fibonacci(i):
"""
Return the `i`-th Fibonacci number
"""
return fibonacci(i-1) + fibonacci(i-2)
and merged her branch with the latest develop
branch fetched from the GitHub Sage repo:
[alice@home sage]$ git add fibonacci.py
[alice@home sage]$ git commit -m 'corrected recursion formula, must be + instead of *'
[alice@home sage]$ git fetch upstream develop:develop
[alice@home sage]$ git merge develop
...
CONFLICT (content): Merge conflict in fibonacci.py
Automatic merge failed; fix conflicts and then commit the result.
The file now looks like this:
def fibonacci(i):
"""
Return the `i`-th Fibonacci number
"""
<<<<<<< HEAD
return fibonacci(i-1) + fibonacci(i-2)
=======
if i > 1:
return fibonacci(i-1) * fibonacci(i-2)
return [0, 1][i]
>>>>>>> 41675dfaedbfb89dcff0a47e520be4aa2b6c5d1b
The conflict is shown between the conflict markers <<<<<<<
and
>>>>>>>
. The first half (up to the =======
marker) is Alice’s
current version, the second half is Bob’s version. The 40-digit hex
number after the second conflict marker is the SHA1 hash of the most
recent common parent of both.
It is now Alice’s job to resolve the conflict by reconciling their changes, for example by editing the file. Her result is:
def fibonacci(i):
"""
Return the `i`-th Fibonacci number
"""
if i > 1:
return fibonacci(i-1) + fibonacci(i-2)
return [0, 1][i]
And then upload both her original change and her merge commit to the GitHub Sage repo:
[alice@laptop sage]$ git add fibonacci.py
[alice@laptop sage]$ git commit -m "merged Bob's changes with mine"
The resulting commit graph now has a loop:
[alice@laptop sage]$ git log --graph --oneline
* 6316447 merged Bob's changes with mine
|\
| * 41675df corrected recursion formula, must be + instead of *
* | 14ae1d3 return correct seed values
|/
* 14afe53 initial commit
[alice@laptop sage]$ git push origin
This time, there is no merge conflict since Alice’s branch already merged the develop
branch.