Git is certainly the most popular Source Code Management (SCM) software: it is broadly used in almost every recent open source project, and even a lot of emblazoned legacy projects switched to it over the years.
In the previous post we thoroughly learned how to use it to version control sources, working only on personal - so local - repositories.
GIT Tutorial - A thorough Git Howto About Using Remotes completes our trip on learning how to professionally use Git, showing you how to link the personal local repository to shared remote bare repositories.
Knowing how to deal with this topic is of course a mandatory skill, since this is the only way you have to cooperate and work with other developers.
Setup The Lab
As always, also in this post we see things in action: since every example make use of SSH as the transport protocol, it is convenient to launch the SSH agent so to avoid having to type each time the password:
eval $(ssh-agent)
ssh-add
enter the password to unlock your SSH secret key: by now the ssh-agent will take care of automatically unlocking the SSH private key each time you login to a host using SSH.
It is of course also convenient to create an playground, so let's create a directory where to store its contents:
mkdir ~/playground
Of course we need some shared remote bare Git repositories to play with - as shown in the previous post, we can create them directly from any client using SSH, by running remote commands onto the host where we want to create them.
So let's create the "foo.git", "bar.git" and "baz.git" as follows:
ssh git@scm.carcano.local git init --bare --shared=true foo.git
ssh git@scm.carcano.local git init --bare --shared=true bar.git
ssh git@scm.carcano.local git init --bare --shared=true baz.git
If you want to know more about how to set up your environment so to be able to operate this way, please read create the remote shared bare repository as described in the previous post.
We also have to put some contents into these remotes - more specifically we create some branches and tags into the "baz.git" repository:
git clone ssh://git@scm.carcano.local:/home/git/baz.git
cd baz
touch Readme.md
git add --all && git commit -m "Initial commit" && git push -u origin master
git checkout -b develop
touch develop.txt
git add --all && git commit -m "develop - first commit" && git push -u origin develop
git checkout -b hotfix
touch hotfix.txt
git add --all && git commit -m "hotfix - first commit" && git push -u origin hotfix
git checkout -b f-fancy develop
touch f-fancy.txt
git add --all && git commit -m "f-fancy - first commit" && git push -u origin f-fancy
git checkout -b f-wow develop
touch f-wow.txt
git add --all && git commit -m "f-wow - first commit" && git push -u origin f-wow
git tag -a r-1.0.0 -m "Release 1.0.1 - Bounty"
git tag r-1.0.1
git tag -a d-1.1.3 -m "Development - 1st milestone"
git tag -a d-1.1.5 -m "Development - 2nd milestone"
git push --tags
cd ..
rm -rf baz
Working With Remote Repositories
Git enables you to link your local repository to remote shared bare repositories: this means that, whenever you want to make the changes you made to a branch so far publicly available, after committing you must always push that branch into the remote relevant branch of the shared repository.
Git calls "remotes" the list of these remote repositories configured to pull from and push to the branch.
There are two ways to work with remote repositories: clone an existing remote, or configure your local repo to use a remote as upstream.
Cloning A Remote Repository
This is the easiest way to start, since this way the remote repository gets automatically linked.
For example, you can clone the "foo" git repo from "ssh://scm.carcano.local/home/foo.git" as follows:
git clone ssh://git@scm.carcano.local:/home/git/foo.git
In this example we are using SSH as the transport protocol, but if you were using a Git service, either self hosted such as Gitea, or online such as Github, you could of course also use HTTP or HTTPS as transport protocol.
In this case, if the remote repository requires authentication, it is also convenient to include the username in the URL of the clone statement - for example:
git clone https://mcarcano@github.com/myorg/myrepo.git
this way, since the username has been permanently set, whenever you interact with the remote you just have to type the password.
Let's change directory into the repository we have just cloned, and create some sample contents:
cd ~/foo
mkdir -p bar/baz
touch xyzzy.txt bar/baz/qux.txt bar/baz/plugh.txt
cp -dR /usr/share/doc/git/howto .
then let's add them to the Staging Area:
git add --all
commit them:
git commit -m "Initial commit"
and eventually push them:
git push
Archiving A Remote Repository
To see this in action, we need a directory where to store the archived repository, so let's change to the playground directory we previously created and create the "archive" directory:
cd ~/playground
mkdir archive
we can now see how it is easy to extract the contents of a specific branch of a remote repository without its metadata and store it into a tar archive:
git archive --format=tar --remote=ssh://git@scm.carcano.local:/home/git/foo.git --output=archive/foo.tar master
If instead you just need a copy of the contents of a specific branch without its metadata, you can exploit the same command this time piping the contents to the tar command line utility and so extract the contents from the tar archive that was generated on the fly - of course create the directory where to extract the contents:
mkdir foo
and then run git as follows:
git archive --format=tar --remote=ssh://git@scm.carcano.local:/home/git/foo.git master | tar xf - -C foo
Listing Remote Repositories (Remotes)
After cloning a shared Git repository, Git automatically configures a reference to the upstream remote repository assigning the name "origin" as a label.
Change directory to get into the "foo" repository we previously cloned:
cd ~/foo
we can easily list the remotes already configured, as follows:
git remote -v
the output is as follows:
origin ssh://git@scm.carcano.local:/home/git/foo.git (fetch)
origin ssh://git@scm.carcano.local:/home/git/foo.git (push)
Please note how there are two different entries of the same remote ("origin"):
- the first is used to fetch
- the second is used to pull
So a remote is nothing but the link to a remote shared bare Git repository used to host the remote versions of one or more local branches.
Linking To A Remote Repository
When creating your own local repository from scratch you have to set-up things for the first time by yourself: this means that things such as linking the remote "origin" must be manually done.
For example, let's create the "bar" directory and put some data into it: new personal (and so local) Git repository:
mkdir ~/bar
cd ~/bar
cp -dR /usr/share/doc/git/howto .
new let's create a personal (and so local) Git repository into it, add all the contents to the Staging Area and commit:
git init
git add --all
git commit -m "Initial commit"
now we want to link the "bar" remote repository we previously created using SSH as the transport protocol - the syntax is "git remote add <label> <URL>":
git remote add origin ssh://git@scm.carcano.local/home/git/bar.git
Same as explained before, in this example we are using SSH as the transport protocol, but if you are using a Git service, either self hosted such as Gitea, or online such as Github, you can of course also use HTTP or HTTPS as transport protocol. In this case, if the remote repository requires authentication, it is convenient to include also the username in the URL of the clone statement:
git remote add origin https://mcarcano@git.carcano.local/foo.git
this way, since the username has been permanently set, whenever you interact with the remote you just have to type the password.
Mind that we have just set the link to a remote repository and assigned the label "origin" to it: we have not configured the default upstream remote for the current branch. This can be achieved as follows:
git push -u origin master
Branches
Listing Remote Branches
Same way as personal Git repositories, obviously also shared remote bare repositories can host more than just one branch: let's clone the "baz" repository we previously created and change directory into it:
git clone ssh://git@scm.carcano.local:/home/git/baz.git
cd baz
then let's list the remote branches as follows:
git branch -r
an example output can be:
origin/HEAD -> origin/master
origin/develop
origin/f-fancy
origin/f-wow
origin/hotfix
origin/master
in this example the remote repository, besides the "master" branch, contains also the following branches:
- develop
- hotfix
- f-fancy
- f-wow
"f-fancy" and "f-wow" are feature branches, whereas "hotfix" and "develop" are long lasting branches: it is very likely that this remote is used in compliance with the Gitflow branching model.
Another way is to use "git ls-remote" including only heads:
git ls-remote --heads
in the same scenario of the previous example, the output would be as follows:
From ssh://git@scm.carcano.local:/home/git/baz.git
5e2ad15b2d6391881ba4e753001cdd01a017d34d refs/heads/develop
8883a514972c25596deba04fc63aa5e3018435ed refs/heads/f-fancy
80c3023fd115760464a63560e1da6ec230479c61 refs/heads/f-wow
8bed706121790fb7f9cc418db23836301b38157c refs/heads/hotfix
bface8ede1a9da67221ad8c2f6a23020b67eaa19 refs/heads/master
As we have just seen, besides configuring remotes, Git requires us to specify what is the default remote for either pull or push each of the available local branches.
We can display all these settings as a whole by typing "git remote show <link-alias-to-a-remote-repo-name>":
git remote show origin
the output in the scenario of the previous example would be:
* remote origin
Fetch URL: ssh://git@scm.carcano.local:/home/git/baz.git
Push URL: ssh://git@scm.carcano.local:/home/git/baz.git
HEAD branch: master
Remote branches:
develop tracked
f-fancy tracked
f-wow tracked
hotfix tracked
master tracked
Local branch configured for 'git pull':
master merges with remote master
Local ref configured for 'git push':
master pushes to master (up to date)
Please note how beneath "Local branch" and "Local ref" is listed only the "master" branch: this branch has been locally configured to merge contents from the remote "main" when running "git pull", and to push committed changes to the remote "main" branch when running "git push".
Mind that "master" is the only branch listed locally since when cloning, besides metadata such as the list of the remote branches, only the default branch gets actually retrieved.
The previous output highlights also that the contents of the "master" local branch is up to date to contents of "master" remote branch.
If we checkout the "f-wow" branch:
git checkout f-wow
and we repeat the same previous show command:
git remote show origin
the output changes as follow (it is cut so to show only the interesting part):
* remote origin
…
Local branches configured for 'git pull':
f-wow merges with remote f-wow
master merges with remote master
Local refs configured for 'git push':
f-wow pushes to f-wow (up to date)
master pushes to master (up to date)
Assigning The Default Upstream Remote Branch
Conversely from when you checkout a remote branch, when you create a local branch it is not automatically configured as the default remote branch to push into or to pull from. This means that you must manually configure - this is often said as “set the upstream repository”. This can be achieved by specifying the "-u" option using the syntax: "git push -u <remote-alias> <remote-branch-name>".
For example, first create the new branch called "f-amazing":
git checkout -b f-amazing
then we need to manually set the branch called "f-amazing" of the remote repository "origin" as the upstream for the current checked-out branch:
git push -u origin f-amazing
when running this statement it asks you for the password to login to the remote repository: this is because you are also immediately pushing the branch to the remote repository.
Of course, if the branch does not exist yet into the remote it gets created.
Deleting A Remote Branch
If you need to delete a remote branch, it is not to be used the "git branch" statement: instead you must use the "git push" statement with the "--delete" command line switch:
git push origin --delete f-amazing
if you check, the branch has actually been deleted on the remote:
git branch -r
the output is:
origin/HEAD -> origin/master
origin/develop
origin/f-fancy
origin/f-wow
origin/hotfix
origin/master
Please mind that same way you did, anyone else that have write access to the remote Git repository can delete branches: mind that when you pull (git pull) or fetch (git fetch) git does not check if the local references are referring to branches that do not exist anymore on the remote repository.
These orphan references are deleted only when pruning - the statement to type is:
git fetch --prune
if you wish you can also configure Git to automatically prune each time you fetch as follows:
git config --global fetch.prune true
Pulling From And Pushing To
Once checked out a branch and configured its default remote, you can "pull commits from" and "push commits to" it.
- Pull means fetching new commits from the remote branch and merging them into the current checked-out branch.This can of course cause merge conflicts that in the worst case must be manually sorted out
- Push means sending new commits of the local branch to the remote branch.
Pulling From The Upstream Remote Branch
You can pull new commits of the current checked-out branch from the remote repository simply by typing:
git pull
as already said, if the repository is a private one, you'll be asked for the password (and the username, if you haven't already included it when cloning).
Pulling From A Remote Branch
You can however merge into the current branch a remote branch different from the upstream of the current checked-out branch. For example, let's say we are into "f-fancy" branch: we can merge commits from remote "f-wow" branch into the local "f-fancy "branch by typing:
git checkout f-fancy
git pull origin f-wow
if we type "git log -1", we'll see the following:
commit 87eab9b2817d767b642696802b618ada90c84d17 (HEAD -> f-fancy, origin/f-wow, f-wow)
that shows that this last commit is the HEAD of "f-fancy", but is used also by "origin/f-wow2" and "f-wow" branches. Note how it is not used by "origin/f-fancy": this means that it has not been pushed to the remote origin repository yet.
Pushing The Current Branch To Its Upstream Remote Branch
Same way, you can push current commits of the current checked-out branch to the remote repository simply by typing:
git push
as already seen, if the repository is a private one, you'll be asked for the password (and the username, if you haven't already included it when cloning).
Pushing Multiple Branches To Their Upstream Remote Branch
If necessary, you can also push multiple branches at a time to their respective remote upstream branch within a single push statement: you simply have to provide the repository they are stored into and the name of the branch as additional arguments.
For example, to push both "f-fancy" and "f-wow" to their respective remote branch issue:
git push origin f-fancy f-wow
the output is as follows:
Password for 'ssh://git@scm.carcano.local':
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 450 bytes | 450.00 KiB/s, done.
Total 4 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To ssh://git@scm.carcano.local/baz.git
80951cc..80949a7 f-fancy -> f-fancy
57978bb..577d74c f-wow -> f-wow
Pushing A Local Branch To A Remote Branch
You may want to push any local branch to any remote, even to a remote branch different from the configured default.
Since remotes are repository-wide objects, each time you want to push you must explicitly specify what local branch you want to push into what remote branch of a specific repository.
To see this in action, let's create the "qux" shared remote bare repository in the home directory of the "git" user of the "scm.carcano.local" as explained in the previous post:
ssh git@scm.carcano.local git init --bare --shared=true qux.git
now we can configure it as an additional remote of the "baz" local git repository:
git remote add replica ssh://git@scm.carcano.local/home/git/qux.git
now we set up everything is necessary to push the contents of the "f-fancy" local branch into the "f-super" branch of the remote shared repository identified by the "replica":
git push replica f-fancy:f-super
it the remote repository is a private one it asks you for the password, then the contents are pushed and if the remote branch does not exist yet it gets created.
Pushing All The Branches With Modifications To Their Upstream Remote Branch As A Whole
You can even push commits for every local branch that have received commits as a whole:
git push --all
the output is as follows:
Password for 'ssh://git@scm.carcano.local':
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 2 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 589 bytes | 589.00 KiB/s, done.
Total 6 (delta 3), reused 0 (delta 0)
remote: Resolving deltas: 100% (3/3), completed with 1 local object.
To ssh://git@scm.carcano.local/foo.git
80949a7..5d11e18 f-fancy -> f-fancy
577d74c..8f83b18 f-wow -> f-wow
You may be wondering why some branches, such as the "master" in the previous output are not pushed to their respective upstream: that is simply because they have not received local commits, so they are not "ahead" their upstream.
TAGS
Listing Remote Tags
When a repository is cloned. Let's see it in action in the "baz" repository we cloned:
cd ~baz
let's list the tags that are already cached in the metadata as follows:
git ls-remote --tags
the output is:
From ssh://git@scm.carcano.local:/home/git/baz.git
800ae849f4b2fb495ede6635902f6f5f67b5815e refs/tags/d-1.1.3
f30ded6eb7b3b5727c110c3a00118fc3d90f4bdc refs/tags/d-1.1.3^{}
c7787330c35e7cddf6f421b83a099b7e8b58da49 refs/tags/d-1.1.5
f30ded6eb7b3b5727c110c3a00118fc3d90f4bdc refs/tags/d-1.1.5^{}
59397d527caa8af2bd298e82de9cac4ad51fecfe refs/tags/r-1.0.0
f30ded6eb7b3b5727c110c3a00118fc3d90f4bdc refs/tags/r-1.0.0^{}
f30ded6eb7b3b5727c110c3a00118fc3d90f4bdc refs/tags/r-1.0.1
mind that since the repository is shared people working on it may create other tags at any time.
To refresh them, you can easily fetch tags metadata from the remotes as follows:
git fetch --tags
mind however that you can refresh the tags cached metadata even when you pull by supplying the "--all" command line switch:
git pull --all
Pushing Tags To A Remote Repository
Of course you can push also tags to a remote repository: let's create a new tag:
git tag r-1.0.2
and push it to the remote origin:
git push origin tags/r-1.0.2
the output is as follows:
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To ssh://scm.carcano.local:/home/git/baz.git
* [new tag] r-1.0.2 -> r-1.0.2
if necessary, you can also push all the tags that have been added as a whole - let's create some new tags first:
git tag r-1.0.3
git tag r-1.0.4
and then let's push them as a whole:
git push --tags
the output is as follows:
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To ssh://scm.carcano.local:/home/git/baz.git
* [new tag] r-1.0.3 -> r-1.0.3
* [new tag] r-1.0.4 -> r-1.0.4
Deleting Tags From The Remote Repository
Sometimes it is necessary to delete a tag from a remote repository: you easily achieve this by using the "--delete" option of the "git push" command.
For example, to delete the "r-1.0.4" tag from the "origin" remote repository, type:
git push --delete origin tags/r-1.0.4
the output is:
To ssh://scm.carcano.local:/home/git/baz.git
- [deleted] r-1.0.4
Hooks
As we saw in in the previous post, GIT provides a set of hooks that can be exploited to intercept events and perform additional actions: when dealing with remotes, since they are "bare" repositories, the repository itself is the ".git" directory, so the directory where the samples files are stored, and of course the directory where you must put your custom hooks into, is the "hooks" directory.
Mind that the environment for remote hooks is different from the one of the local hooks: for example, a trick like the one explained in the previous post, that relies on parsing the files of the Staging Area to verify their syntax and accept the commit only if it is valid would be very difficult to implement using remote hooks: the problem is that remote repositories are "bare" repositories, so you don't have the extracted version of the repository, ... you only have the metadata files.
Anyway, putting syntax validation or any kind of checks that makes sense before accepting the commit into remote hooks is a non-sense, since they are triggered only during push or merges.
The only thing that may make sense on remote hooks may be trigger actions to perform additional checks. For example you can exploit the "update" hook to trigger a call to the API endpoint of the project management suite, such as Taiga or JIRA, and make sure that the commit message contains the ID of a ticket, checks its state and such.
Some Git based Source Management Suites, such as Gitea, provides additional Hooks (Gitea calls them "Web Hooks") that provides a more granular control on when they are triggered. This lets you execute something for example when a pull request gets actually merged: for example adding the Gitea web page of this merge as a linked item to the Taiga (or JIRA) ticket. When running Gitea, this can be easily achieved by writing a Flask RESTful service that acts as a bridge to the Project Management suite and adding a Web Hook that calls this endpoint.
Footnotes
Here ends this post on using Git for version control: we saw how to professionally operate it when dealing with remote shared repositories. These two posts prepared you to face the very most of the situations that pop up daily when working with this great piece of software. Happy developing with Git.