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.

If you have not read the post "Git Tutorial - A Thorough Version Control With Git HowTo" yet, I strongly encourage you to read it now, since many concepts explained in that post are necessary to fully understand this second post.

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.

Mind that you can manually specify the branch to push, or you can specify a default target. If necessary, you can even push all the branches as a whole.

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
Don't worry for the message "warning: You appear to have cloned an empty repository": we have just created the "foo.git" "bar.git" and "baz.git" repositories, so they are of course empty.

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.

This is a GIT repository wide operation, so you don't need to repeat it every time you create a branch.
Mind that "origin" is the commonly assigned label to the first (and often only) linked remote repository – although you can assign any name to this alias, it is wise to follow the common convention to avoid unnecessary entropy (and headaches).

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.

Mind that for performance reasons this list contains only the branches that have been retrieved and cached by the last "git fetch" or "git checkout" - remember that git checkout is an alias to both run "git fetch" and "git pull" as a whole. If you want to list the remote branches  including the ones that may have been created in the meantime too, you must run a "git fetch" before running the "git branch -r" statement.

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.
Mind that both push and pull commands by default refer only to the currently checked-out branch and its upstream.

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.

Mind that in a clean design it is always best to use the KISS approach, ... so both the local and remote branch names must match: I did this example only to show that it is technically possible.

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
Note that to push a specific tag, you must prepend "tags/" to the name of the tag, otherwise Git thinks that the name of the object you are pushing is a branch.

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.

I hate blogs with pop-ups, ads and all the (even worse) other stuff that distracts from the topics you're reading and violates your privacy. I want to offer my readers the best experience possible for free, ... but please be wary that for me it's not really free: on top of the raw costs of running the blog, I usually spend on average 50-60 hours writing each post. I offer all this for free because I think it's nice to help people, but if you think something in this blog has helped you professionally and you want to give concrete support, your contribution is very much appreciated: you can just use the above button.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>