Formation GIT

Niveau débutant

Contenu du cours

Les systèmes de version de code (SVC / CVS)

Les commandes Git de base

  • init et clone
  • status, add, reset et commit

Travailler avec plusieurs versions

  • branch et checkout
  • diff et log

Travailler avec d'autres personnes

  • remotes, push, pull, merge
  • Résoudre des conflits

Les systèmes

de version de code

Passé

CVS - Concurent version

Approche Client/serveur, centralisé, facilité de faire des copies et modifier.

 

SVN - Subversion

Approche client/serveur, centralisé. Permet de plus facilement faire des merge car l'approche est basé sur une ligne évolutive au lieu de multiples branches.

Présent

GIT

Approche décentralisé qui enregistre les changements apportés dans un arbre historique.

 

Git est utilisé par de nombreux outils tels GitHub, GitLab, Bitbucket, etc.

CVS - Concurrent VS

SVN - Subversion

Git

Travailler

en solo

git init

  • Initialise un répertoire
  • Créé un répertoire de contrôle .git
git init
  • Retirer le support git d'un répertoire?
rm -rf .git

C'est quoi un repository

  • Un ensemble de fichiers de contrôle qui permet de stocker l'historique des fichiers
     
  • Permet de créer des copies (branches) et de travailler avec les différentes copies
     
  • Il est possible d'aller chercher ou d'envoyer les changements d'une personne à une autre en envoyant les changements entre les repository

git clone

  • Initialise un répertoire avec un sous-répertoire .git et obtient une copie existante d'un autre repository
git clone <url> <cible: optionnel>
  • git clone c'est l'équivalent de
git init
git remote add origin <l'url du repo>
git fetch origin
git checkout master

A votre tour

git status

  • Permet de voir le statut du repository
git status

On branch master

No commits yet

nothing to commit (create/copy files and use "git add" to track)
  • Sous git, il existe une zone de travail qui s'appelle le staging
     
  • Lorsque vous faites des opérations, cela affecte généralement le staging

git add

  • Ajoute un ou plusieurs fichiers au staging
git add <pattern>
  • Le point est utilisé pour la récursivité
  • Sinon, vous pouvez aussi utiliser le traditionnel pattern matching de type glob
git add .
git add sous-dir/.
git add *.txt

Allons-y

  • Créez un fichier "hello.txt" avec du contenu
  • Lancez la commande git status pour voir que le fichier est bien détecté
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        hello.txt

nothing added to commit but untracked files present (use "git add" to track)
  • Ajoutez le au staging avec git add

Allons-y

  • Utilisez la commande git status pour voir la différence
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        hello.txt

nothing added to commit but untracked files present (use "git add" to track)
  • Remarquez que le fichier est maintenant dans le staging avec une indication new file
On branch master

No commits yet

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

        new file:   hello.txt

Avant

Après

Sur-modification

  • Modifiez le fichier hello.txt a nouveau et utilisez la commande "git status" pour voir la différence
On branch master

No commits yet

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

        new file:   hello.txt
  • Remarquez que le fichier apparait maintenant en double, une fois dans le staging, une fois à l'extérieur
On branch master

No commits yet

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

        new file:   hello.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   hello.txt

Avant

Après

Appliquer les changements

  • Lancez la commande git commit et ajoutez une description.
On branch master

No commits yet

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

        new file:   hello.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   hello.txt
  • Remarquez que le fichier apparait maintenant hors staging seulement mais les changements ne sont pas perdus
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   hello.txt

no changes added to commit (use "git add" and/or "git commit -a")

Avant

Après

Retirer du staging

  • La commande git reset peut recevoir un pattern incluant le "." récursif
git reset
git reset repertoire/.
git reset *.pattern
  • Ceci n'annule pas les changements à un fichier, il retire le fichier du staging

Allons-y

  • Ajoutez "hello.txt" au staging avec git add
  • Lancez git status pour voir le fichier en staging
  • Lancez git reset pour annuler le staging
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   hello.txt
  • git reset retire tous les fichiers du staging actuel
Unstaged changes after reset:
M       hello.txt

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   hello.txt

no changes added to commit (use "git add" and/or "git commit -a")

Avant reset

Après reset

Concepts

Un peu de théorie

Changeset

Git fait un suivi de changesets; il garde une trace des fichiers modifiés et leur contenus.

 

Pour bien organiser son contenu, il créé des arbres de fichiers.

Références

Chaque changeset fait référence à un autre changeset qui fait de même.

 

Ceci créé un arbre de changement qui éventuellement se reconnectent les uns aux autres.

Arbres

Git fait un suivi des contenus et changements en créant des arbres internes.

 

Les branches des différents arbres font référence à des contenus de fichier et c'est de cette façon qu'on évalue les changements entre les versions de chaque fichiers.

Contenus

Lorsqu'un fichier change, on garde en général la totalité du contenu ou du moins le delta dans un fichier.

 

Les arbres font ensuite référence à ces contenus pour organiser les différentes versions de fichiers.

Avant

Après

Un fichier change

Dans cet exemple, 3 fichiers existent mais 1 seul change. Les blobs dans 2 autres fichiers restent intact et 1 nouveau blob est créé.

Commit

Un commit est une référence à un arbre d'objet et à un autre commit.

 

Un commit peut faire référence à plusieurs commit surtout lorsque vous migrez plusieurs versions ensemble.

Travailler

avec des versions

Branches

Une branche est une séparation dans l'arbre des commit. C'est une façon de permettre à plusieurs version d'un même fichier d'exister avec du contenu différent.

 

Les branches sont essentielles dans le processus de développement pour permettre une revue de code et un développement progressif.

Structure interne

Une branche n'est pas un concept très compliqué, c'est un fait une simple référence à un commit.

 

Lorsque vous créez une branche, vous créez une référence à un commit et les prochains commit créerons un arbre de commit différents.

git branch

  • Affiche les branches existante
git branch

* master
  • Créer une nouvelle branche à partir du point actuel
git branch dev
git branch

  dev
* master
  • En ajoutant un nom après git branch vous créez cette nouvelle branche
  • La branche avec une étoile est la branche active

git branch -d

  • Supprime une branche existante
git branch -d dev

Deleted branch dev (was ab86f85)
  • Si votre branche détient des changements, il faut utiliser "-D" pas "-d"
git branch -d dev
error: The branch 'dev' is not fully merged.
If you are sure you want to delete it, run 'git branch -D dev'.

git branch -D dev

Deleted branch dev (was ab86f85)

git checkout

  • Change la branche active et applique son contenu
git checkout dev
Switched to branch 'dev'
  • À ce moment, tous les fichiers qui sont dans votre répertoire sont maintenant les fichiers de dev

git checkout

  • Faisons un test pour voir cet effet
  • Changez le fichier "hello.txt"
  • Utilisez git add et git commit pour créer une nouvelle version de "hello.txt"
  • Retournez sur la branche master
  • Inspectez le contenu de "hello.txt"
echo "BLAH" > hello.txt
git add .
git commit -m "dev changes"

git checkout master

cat hello.txt
Test

Inspecter le log

  • Assurez vous d'être sur la branche master
  • Lancez un git log
  • Le log de la branche master s'affiche
commit ab86f859a0dd1333ec8413e44f9ee7d6176432fb (HEAD -> master)
Author: Mathieu Dumoulin <thecrazycodr@gmail.com>
Date:   Sat May 23 10:46:49 2020 -0400

    Test commit
  • Vous voyez le log de la branche master seulement

Inspecter le log

  • Allez sur la branche dev
  • Lancez un git log
  • Les informations seront différentes
commit 81e7bdeeb21172b8ee33ad133fc7f1893092ec41 (HEAD -> dev)
Author: Mathieu Dumoulin <thecrazycodr@gmail.com>
Date:   Sat May 23 11:52:47 2020 -0400

    Test 2

commit ab86f859a0dd1333ec8413e44f9ee7d6176432fb (master)
Author: Mathieu Dumoulin <thecrazycodr@gmail.com>
Date:   Sat May 23 10:46:49 2020 -0400

    Test commit
  • Vous voyez le log de master en plus de dev car dev provient de master

Log à travers les branches

  • Allez sur la branche dev
  • Lancez un git log --graph
  • Les informations seront différentes, on voit 
* commit 81e7bdeeb21172b8ee33ad133fc7f1893092ec41 (HEAD -> dev)
| Author: Mathieu Dumoulin <thecrazycodr@gmail.com>
| Date:   Sat May 23 11:52:47 2020 -0400
| 
|     Test 2
| 
* commit ab86f859a0dd1333ec8413e44f9ee7d6176432fb
  Author: Mathieu Dumoulin <thecrazycodr@gmail.com>
  Date:   Sat May 23 10:46:49 2020 -0400
  
      Test commit
  • Vous voyez le lien entre les commit

Ajoutez des modifications

  • Sur master, ajoutez 1 ou 2 commit
  • Créez une nouvelle branche à partir de dev et ajoutez des commits
  • Sur dev, ajoutez 1 commit
  • Lancez git log --all --graph
* commit c5a6fd48bd3372f1962a3e21ef982d7e80397c6f (HEAD -> dev2)
| Author: Mathieu Dumoulin <thecrazycodr@gmail.com>
| Date:   Sat May 23 14:30:01 2020 -0400
| 
|     New feature
|   
| * commit aee11b272543efa8d7411e0ff90628823694b9d5 (dev)
|/  Author: Mathieu Dumoulin <thecrazycodr@gmail.com>
|   Date:   Sat May 23 14:29:30 2020 -0400
|   
|       dev change
| 
* commit 81e7bdeeb21172b8ee33ad133fc7f1893092ec41
| Author: Mathieu Dumoulin <thecrazycodr@gmail.com>
| Date:   Sat May 23 11:52:47 2020 -0400
| 
|     Test 2
|   
| * commit 0d5905bac31d17e07e5e57198c9f7398bd926b25 (master)
| | Author: Mathieu Dumoulin <thecrazycodr@gmail.com>
| | Date:   Sat May 23 14:28:52 2020 -0400
| | 
| |     Some new file
| | 
| * commit 2430aef3f3bf7e95ad4dac2e5572821c45ee7c9d
|/  Author: Mathieu Dumoulin <thecrazycodr@gmail.com>
|   Date:   Sat May 23 14:26:22 2020 -0400
|   
|       Test3
| 
* commit ab86f859a0dd1333ec8413e44f9ee7d6176432fb
  Author: Mathieu Dumoulin <thecrazycodr@gmail.com>
  Date:   Sat May 23 10:46:49 2020 -0400
  
      Test commit

Naviguer le log

  • Vous pouvez utiliser les flèches pour naviguer le log
  • Une autre option très utile est
    git log --all --graph --oneline
* c5a6fd4 (HEAD -> dev2) New feature
| * aee11b2 (dev) dev change
|/  
* 81e7bde Test 2
| * 0d5905b (master) Some new file
| * 2430aef Test3
|/  
* ab86f85 Test commit
  • Remarquez les noms des branches: HEAD, dev2, dev, master

Voir les différences

  • Une option très importante dans git est de voir la différence entre 2 versions
  • Faites un git checkout dev et ensuite
    git log --oneline
aee11b2 (HEAD -> dev) dev change
81e7bde Test 2
ab86f85 Test commit
  • Les aee11b2, 81e7bde, ab86f85 sont des commit hash, ils sont uniques
  • Prenez note de 2 de vos commit hash car ils seront différent des miens

Voir les différences

  • Utilisez git diff <version1>:<version2> pour voir les différences
git diff aee11b2 81e7bde

diff --git a/hello.txt b/hello.txt
index d075906..110c58e 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-dev version
+Test 2
  • On voit que dans "hello.txt", le texte "dev version" à été remplacé par "Test 2". Mais est-ce bien le cas?
git diff 81e7bde aee11b2

diff --git a/hello.txt b/hello.txt
index 110c58e..d075906 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-Test 2
+dev version

Voir les différences

  • Il est donc possible de voir la différence entre 2 commit même s'il ne sont pas dans le même ordre.
  • Lancez git status pour vous assurer que tout est sans changement et provoquez en un mais ne faites pas git add. Ensuite, faites git diff tout simplement.
diff --git a/hello.txt b/hello.txt
index d075906..b36e8ac 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-dev version
+dev version 2
  • Vous voyez les changements entre les fichiers locaux et la dernière version sauvegardée.

Voir les différences

  • Ajoutez le changement au staging et répétez la commande. Aucun résultat? Normal!
  • Essayez avec git diff --staged
diff --git a/hello.txt b/hello.txt
index d075906..b36e8ac 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-dev version
+dev version 2
  • Provoquez un nouveau changement sans l'ajouter au staging et utilisez git diff --staged et ensuite testez git diff normalement.
  • Le diff de base est toujours en fonction du staging area. Le staging c'est le dernier commit + les fichiers du staging area...

Utiliser HEAD

  • C'est quoi HEAD. Nous l'avons vu ici et là mais qu'est-ce que c'est en fait?
  • HEAD c'est l'endroit où vous vous trouvez présentement. C'est un équivalent direct au nom de la branche active.
git diff HEAD

<résultat>

git diff dev

<même résultat>
  • Pourquoi utiliser HEAD alors? Pour éviter d'avoir a taper le nom de la branche que vous ne vous rappellerez peut-être pas.

Références arithmétiques

  • Lors de git diff, vous pouvez utiliser une convention arithmétique fort simple pour comparer une version et une autre par rapport à une branche ou par rapport à HEAD.
git diff HEAD HEAD~1

diff --git a/hello.txt b/hello.txt
index d075906..110c58e 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-dev version
+Test 2
  • Vous pouvez utiliser HEAD~3 HEAD~7 si vous voulez. L'opérateur ~ signale à git de reculer de X commits à partir de la référence. Essayez le avec le nom d'une branche.

Travailler

avec d'autres personnes

Git est décentralisé

  • Tous détiennent sa propre copie
  • Personne n'est donc la référence officielle
  • Vous pouvez partager une portion de votre repository avec qui bon vous semble et prendre une portion de n'importe quel autre à votre besoin
  • Perte de contrôle des changements
  • Difficulté de synchronisation lors de larges changements
  • Contrôle des accès et récupération des données

Avantages

Désavantages

Les remotes

Un remote c'est une référence à un repository différent du votre. Il faut lui donner un nom et importer son contenu dans le votre pour travailler avec.

 

Un remote ce peut être:

  • Un répertoire différent sur votre machine
  • Un serveur web tel que GitHub ou GitLab qui permet un accès via HTTPS
  • Une machine d'un autre développeur ou un serveur que vous accédez par SSH

Le clonage

Au début de cette formation, nous avons vu git clone et vous avez vu que git clone est en fait une série de commandes:

En fait, git clone ajoute un remote à votre repository et fait un git fetch. Nous verrons ces commandes dans ce module.

git init
git remote add origin <l'url du repo>
git fetch origin
git checkout master

Ajouter un remote

  • Pour ajouter un remote, il vous faut un url d'accès. Cet url peut être:
    • un chemin d'accès relatif
    • un URL HTTP(s)
    • un url SSH
  • Ensuite, il vous faut lui donner un nom
  • Finalement, vous devez appeler la commande git remote add <nom> <url>:
git remote add <nom> <l'url du repo>

Allons-y

  1. Allez dans un autre répertoire que le repository en cours et créez un nouveau repository avec git init
  2. Retournez dans votre précédent repository
  3. Lancez git remote add relatif ../<chemin-relatif>
  4. Lancez ensuite git remote
git remote
relative
  • Faîtes de même avec un URL HTTPS et SSH d'un repository sur https://github.com
git remote
httpsremote
relative
sshremote

Gérer les remote

  • Il existe plusieurs commandes pour gérer les remote d'un repository. La structure est toujours pareille:
    git remote <sous-commande> <name>
  • Voici différentes commandes que vous pouvez utiliser:
Nom Description
rename Renome le remote à un nouveau nom
remove Supprime ce remote
set-url Change l'url de ce remote
get-url Affiche l'url de ce remote

Récupérer

  • Le fait d'avoir configuré un remote ne garanti pas que vous avez le code de ce remote en local, il faut le récupérer
  • Lancez la commande git fetch <remote-ssh-github>
Warning: Permanently added the RSA host key for IP address '140.82.112.4' to the list of known hosts.
warning: no common commits
remote: Enumerating objects: 32, done.
remote: Counting objects: 100% (32/32), done.
remote: Compressing objects: 100% (22/22), done.
remote: Total 32 (delta 13), reused 27 (delta 8), pack-reused 0
Unpacking objects: 100% (32/32), done.
From github.com:crazycodr/silencer
 * [new branch]      master     -> sshremote/master
 * [new tag]         0.1.0      -> 0.1.0
 * [new tag]         0.2.0      -> 0.2.0
 * [new tag]         0.2.1      -> 0.2.1
 * [new tag]         0.3.1      -> 0.3.1
 * [new tag]         0.3.2      -> 0.3.2
 * [new tag]         0.3.3      -> 0.3.3
  • Ce résultat démontre que j'ai été chercher la plus récente version de ce repository et je l'ai mis en local

Faits importants sur la récuppération

  • Récupérer doit être fait de façon récurrente
  • Récupérer de GitHub, par exemple, ne permet pas d'obtenir le code des autres développeurs qui se retrouve sur leur machine
  • Récupérer le repository en ligne ne mets pas à jour le code de votre propre repository

Synchroniser les changements

  • Une fois que vous avez les changements d'un remote en local, vous pouvez synchroniser les changements entre une branche remote et votre branche locale avec git merge:
git checkout dev
git reset --hard 0.1.0

git merge sshremote/master

Updating b931b57..2cf1629
Fast-forward
 .gitignore           |   3 ++-
 README.md            |  38 +++++++++++++++++++++++++++++++-------
 silencer-config.json |   5 +++++
 silencer.py          | 188 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
 4 files changed, 202 insertions(+), 32 deletions(-)
 create mode 100644 silencer-config.json
 mode change 100644 => 100755 silencer.py

Particularités de merge

  • L'opération merge est en fait une copie des différents commit de la branche que vous avez spécifié dans la branche active
     
  • Vous pouvez faire un merge entre deux branches locales, pas obligé de le faire à partir d'une branche remote
     
  • Lorsque vous faites un merge, vous pouvez obtenir des conflits

Des conflits

C'est quoi?

Changements en double

Le code ou texte d'un fichier semble avoir changé exactement au même endroit où vous avez fait des changements.

 

Git n'est pas capable de savoir ce qui est bon ou mauvais.

Fichiers supprimés et modifiés

Un fichier à été supprimé ou modifié alors que vous avez fait le contraire.

 

Git ne sait plus s'il doit conserver le fichier ou le supprimer.

Comment régler ces conflits?

  1. Il faut analyser les changements proposés
  2. Corriger les fichiers au besoin
  3. Supprimer les fichiers au besoin
  4. Faire un commit

Conflit de chang.

Un conflit de modification aura l'air de ceci:

git merge dev
Auto-merging hello.txt
CONFLICT (content): Merge conflict in hello.txt
Automatic merge failed; fix conflicts and then commit the result.

Le contenu du fichier "hello.txt" aura l'air de ceci:

<<<<<<< HEAD
Dancing
=======
Foo Bar
>>>>>>> dev

Le "<<<<<< HEAD" indique que le contenu qui suit fait parti de votre modification. Le "=======" indique la séparation avec l'autre branche. Finalement ">>>>>> dev" indique la fin du conflit.

Quoi faire?

Il n'existe aucun truc, il faut simplement observer et corriger. Au besoin, parlez avec vos collègues pour savoir quelle est la version finale:

Dancing
Foo Bar
Dancing
Foo Bar

Juste le Head?

Les deux?

Juste l'autre branche?

De plus, si vous voulez mettre les deux cas ensemble, il faut savoir que l'ordre peut être important.

Conflit de suppress.

Lorsqu'un fichier est supprimé et que vous l'avez changé, la règle générale est qu'il doit être supprimé. Donc le problème est encore plus complexe:

Comment allez vous reproduire le changement originel maintenant que le fichier que vous aviez modifié est supprimé?

Il n'existe aucun truc, il faut simplement observer les nouveaux changements et intégrer vos changements de façon différente. Au besoin, parlez avec vos collègues pour savoir quelle sera l'approche.

git pull

Cette commande est un tout en un pour vous simplifier la vie. Une fois un remote mis en place vous pouvez faire un fetch et un merge en une seule commande:

git checkout dev
git reset --hard 0.1.0

git pull sshremote/master

Updating b931b57..2cf1629
Fast-forward
 .gitignore           |   3 ++-
 README.md            |  38 +++++++++++++++++++++++++++++++-------
 silencer-config.json |   5 +++++
 silencer.py          | 188 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
 4 files changed, 202 insertions(+), 32 deletions(-)
 create mode 100644 silencer-config.json
 mode change 100644 => 100755 silencer.py

Cette commande est un tout en un pour vous simplifier la vie. Une fois un remote mis en place vous pouvez faire un fetch et un merge en une seule commande:

Un git pull c'est l'équivalent d'un git fetch <remote> suivi d'un git merge <remote>/<branch>.

git push

Cette commande est la plus importante dans la collaboration. Elle permet de pousser les changements en local dans un remote.

git push origin HEAD:test

Counting objects: 13, done.
Delta compression using up to 12 threads.
Compressing objects: 100% (10/10), done.
Writing objects: 100% (13/13), 5.59 KiB | 5.59 MiB/s, done.
Total 13 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
remote: 
remote: Create a pull request for 'test' on GitHub by visiting:
remote:      https://github.com/crazycodr/standard-exceptions/pull/new/test
remote: 
To https://github.com/crazycodr/standard-exceptions.git
 * [new branch]      HEAD -> test

git push

Il existe plusieurs façons d'utiliser git push:

 

  • git push <remote>
  • git push <remote> HEAD:<branch>
  • git push <remote> <branch>:<branch>

 

Chaque approche permet de faire sensiblement la même chose.

Les outils

pour collaborer

Quels sont les outils?

  • www.github.com

     
  • www.gitlab.com

     
  • www.bitbucket.com

GitHub

  • Simple et facile
  • Peu couteux
  • Offre beaucoups d'outils tiers via un marketplace
  • Plus facile pour le modèle fork
  • N'offre pas de CI intégré
  • La gestion des versions et des projets est compliquée

Avantages

Désavantages

GitLab

  • Outil très complet
  • Offre une superbe intégration avec un CI
  • Permet la version de repository NPM et Docker
  • Dispendieux
  • Difficile à configurer surtout pour de gros projets mais toujours mieux que les autres
  • Peu d'intégrations externes

Avantages

Désavantages

BitBucket

  • Offre une belle complémentarité avec les autres outils d'Atlassian (Jira, Confluence, etc)
  • Offre des repository privés gratuits
  • Très peu coûteux pour les équipes de moins de 10 personnes
  • Interface difficile d'utilisation
  • Configuration de la sécurité compliquée à cause du single sign-on et des applications Atlassian
  • Pas de CI facilement intégré

Avantages

Désavantages

Pratique

adaptée aux besoins

Merci

Questions?

Made with Slides.com