Dans ce tutoriel, nous allons aborder la synchronisation de données avec iTop et nous allons voir comment l’adapter à un besoin spécifique grâce aux collecteurs de données.
Nous utiliserons les API en libre accès mises à disposition sur le site https://api.gouv.fr/ afin de synchroniser les établissements de l’éducation nationale sur notre serveur iTop.
Vous pourrez par la suite ajouter d’autres données disponibles dans d’autres API du site.
Ce site référence les API du service public, mises à la disposition des collectivités, des ministères et des entreprises pour construire des services informatiques au service de tous.
api.gouv.fr
⏱️Durée approximative du tutoriel: 4 heures
Les étapes clés de ce tutoriel
1/ La structuration des données
2/ La création d’une extension pour iTop
3/ La création d’un collecteur pour iTop
4/ L’industrialisation de la solution
5/ Exercice
6/ Quizz
Prérequis
Un serveur iTop communautaire de version 2.7 ou supérieure installé en local ou sur une machine distante de votre réseau avec les droits utilisateurs pour apporter des modifications à l’installation.
Un environnement d’exécution PHP compatible avec la version d’iTop et quelques connaissances en PHP.
Un éditeur de texte Notepad++ ou autre.
Principe de synchronisation avec iTop
Le rôle du collecteur est de convertir les données externes au format CSV, de mettre à jour les réplicas avec ces données et enfin de synchroniser ces derniers avec les données d’iTop.
Une couche d’abstraction s’occupe de la phase d’importation et de synchronisation pour nous.
Pour notre collecteur spécifique REST, nous devons simplement extraire les données de l’API et les convertir en CSV.
Les étapes du tutoriel
1/ La structuration des données
Nous souhaitons synchroniser la liste des établissements de l’éducation nationale Française avec notre iTop. Les données seront en lecture seule côté iTop.
Nous utiliserons l’API annuaire éducation: https://api.gouv.fr/les-api/api-annuaire-education.
L’analyse
La première étape consiste à analyser l’API et à identifier les données que nous souhaitons remonter sur iTop.
En allant sur la page de présentation de l’API, nous pouvons découvrir les principales données disponibles.
La sélection des données à synchroniser
Nous choisissons les informations des établissements suivantes:
· L’identifiant unique (UAI)
· Le nom
· L’adresse postale
· Le type d’établissement
· L’information public / privé
· Le code académie
· Le nombre d’élèves
· La présence d’un service de restauration
· La date d’ouverture
Plusieurs types de données (texte, nombre, date, énumération et booléen) seront ainsi synchronisés.
La source de données
La requête de l’API que nous utiliserons est la suivante:
GET /catalog/datasets/fr-en-annuaire-education/records
Nous utiliserons les paramètres select
, limit
et offset
afin de ne sélectionner que les données qui nous intéresse et nous pourrons ainsi, parcourir l’ensemble des enregistrements en plusieurs itérations. Il est en effet impossible de sélectionner l’ensemble des établissements, la limite du nombre de résultats fixée par l’API étant de 100 établissements par requête.
Vous pouvez effectuer quelques tests pour prendre en main l’API ici: https://api.gouv.fr/documentation/api-annuaire-education.
2/ La création d’une extension pour iTop
Nous allons créer une extension pour nous permettre de customiser le modèle de données d’iTop et ainsi nous permettre de collecter des données de type établissement d’éducation. Nous étendrons la classe localisation et ajouterons les informations spécifiques aux établissements de l’éducation nationale. Nous apporterons également quelques modifications au menu latéral d’iTop afin d’accueillir les menus associés aux API du site api.gouv.fr.
Génération du squelette
Commencez par générer un squelette d’extension en saisissant les informations suivantes dans le formulaire de création.
Cliquez sur le bouton Generate
et téléchargez ensuite l’archive créée.
Décompressez l’archive téléchargée dans le dossier extensions sur le serveur iTop.
Company name: MyCompany
Extension name: gouv-api-extension
Extension label: api.gouv.fr data synchronisation
Version: 1.0.0
Dependencies: itop-structure/3.0.0
Pour aller plus loin dans la création d’extensions, visitez la documentation officielle.
Création de la classe du modèle de données
Nous allons créer notre classe pour stocker les informations des établissement scolaires. Nous faisons le choix de dériver la classe Location
et héritons ainsi des données de localisation.
Informations générales
Initialisation de la classe Establishment
qui dérive de la classe Location
.
Ajoutez les informations générales de la classe dans le fichier de modèle de données comme suit:
datamodel.gouv-edu-extension.xml
<?xml version="1.0" encoding="UTF-8"?> <itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.6"> … <classes> <class id="Establishment" _delta="define"> <parent>Location</parent> <properties> <category>bizmodel,searchable</category> <abstract>false</abstract> <key_type>autoincrement</key_type> <db_table>establishment</db_table> <db_key_field>id</db_key_field> <db_final_class_field/> <naming> <format>%1$s</format> <attributes> <attribute id="name"/> </attributes> </naming> <display_template/> <icon>assets/img/school.png</icon> </properties> </class> </classes> … </itop_design>
💡La balise <icon>
permets d’associer une image à la classe.
Ajout des attributs de la classe du modèle de données
Déclaration des attributs de la classe Establishment. Pour chacun de ces attributs nous spécifions son type de donnée xsi:type="AttributeXXX"
.
Nous rendons obligatoires les attributs identifier
, type
et private_public
.
Ajoutez les attributs de la classe dans le fichier de modèle de données comme suit:
datamodel.gouv-edu-extension.xml
<classes> <class id="Establishment" _delta="define"> … <fields> <!-- identifier--> <field id="identifier" xsi:type="AttributeString"> <sql>identifier</sql> <default_value/> <is_null_allowed>false</is_null_allowed> </field> <!-- type --> <field id="type" xsi:type="AttributeString"> <sql>type</sql> <default_value/> <is_null_allowed>false</is_null_allowed> </field> <!-- private_public --> <field id="private_public" xsi:type="AttributeEnum"> <values> <value>unknown</value> <value>private</value> <value>public</value> </values> <sql>private_public</sql> <default_value/> <is_null_allowed>false</is_null_allowed> <display_style>radio_horizontal</display_style> </field> <!-- academy_code --> <field id="academy_code" xsi:type="AttributeString"> <sql>code_academie</sql> <default_value/> <is_null_allowed>true</is_null_allowed> </field> <!-- catering_service --> <field id="catering_service" xsi:type="AttributeBoolean"> <sql>catering_service</sql> <default_value/> <is_null_allowed>true</is_null_allowed> </field> <!-- students_count --> <field id="students_count" xsi:type="AttributeInteger"> <sql>students_count</sql> <default_value/> <is_null_allowed>false</is_null_allowed> </field> <!-- opening_date --> <field id="opening_date" xsi:type="AttributeDate"> <sql>opening_date</sql> <default_value/> <is_null_allowed>false</is_null_allowed> </field> </fields> </class> </classes>
Ajoutez les traductions de nos attributs de classe dans le fichier d’internationalisation.
Par défaut, seul le fichier de traduction anglais est créé, ajoutez d’autres langues au besoin.
en.dict.gouv-api-extension.php
Dict::Add('EN US', 'English', 'English', array( 'Class:Establishment/Attribute:identifier' => 'Establishment code', 'Class:Establishment/Attribute:identifier+' => 'Code UAI', 'Class:Establishment/Attribute:type' => 'Establishment type', 'Class:Establishment/Attribute:private_public' => 'Private / Public', 'Class:Establishment/Attribute:private_public/Value:private' => 'Private', 'Class:Establishment/Attribute:private_public/Value:public' => 'Public', 'Class:Establishment/Attribute:private_public/Value:unknown' => 'Unknown', 'Class:Establishment/Attribute:academy_code_academie' => 'Academy code', 'Class:Establishment/Attribute:catering_service' => 'Catering service', 'Class:Establishment/Attribute:students_count' => 'Students count', 'Class:Establishment/Attribute:opening_date' => 'Opening date', ));
💡Le symbole + permets de joindre une bulle de survol au formulaire pour un attribut donné: ‘Class:Establishment/Attribute:identifier+
‘
Ajout des données de présentation
Déclaration des différents modes d’affichage de notre classe.
La section détails
determine l’affichage de notre classe lors de la visualisation ou de l’édition d’objet.
La section list
determine les attributs de notre classe utilisés comme colonnes dans les listes.
La section search
quant à elle définit les attributs utilisés comme critères lors des recherches.
Ajoutez les modes d’affichage comme suit:
datamodel.gouv-edu-extension.xml
<presentation> <details> <items> <item id="type"> <rank>30</rank> </item> <item id="identifier"> <rank>10</rank> </item> <item id="private_public"> <rank>40</rank> </item> <item id="academy_code"> <rank>50</rank> </item> <item id="catering_service"> <rank>60</rank> </item> <item id="students_count"> <rank>70</rank> </item> <item id="opening_date"> <rank>80</rank> </item> <item id="org_id"> <rank>90</rank> </item> <item id="address"> <rank>100</rank> </item> <item id="postal_code"> <rank>110</rank> </item> <item id="city"> <rank>120</rank> </item> <item id="country"> <rank>130</rank> </item> </items> </details> <search> <items> <item id="type"> <rank>10</rank> </item> <item id="private_public"> <rank>20</rank> </item> <item id="academy_code"> <rank>30</rank> </item> </items> </search> <list> <items> <item id="identifier"> <rank>10</rank> </item> <item id="private_public"> <rank>30</rank> </item> <item id="city"> <rank>40</rank> </item> <item id="opening_date"> <rank>50</rank> </item> <item id="catering_service"> <rank>60</rank> </item> <item id="students_count"> <rank>70</rank> </item> </items> </list> </presentation>
Ajout des données de réconciliation
Déclaration des attributs de réconciliation de notre classe.
Nous nous servons de l’identifiant de l’établissement scolaire pour la réconciliation car il est unique.
🎓La réconciliation définit un ou plusieurs attributs garants de l’identification d’un objet.
Ajoutez les informations de réconciliation de la classe dans le fichier de modèle de données comme suit:
datamodel.gouv-edu-extension.xml
<classes> <class id="Establishment" _delta="define"> … <properties> … <reconciliation> <attributes> <attribute id="identifier"/> </attributes> </reconciliation> </properties> </class> </classes>
Personnalisation du menu iTop
Nous allons organiser nos données issues du site api.gouv.fr dans un menu intégré au menu latéral d’iTop.
Vous pourrez par la suite ajouter autant d’entrées dans ce menu que d’API que vous implémenterez.
Ajoutez les informations de menu de la classe dans le fichier de modèle de données comme suit:
datamodel.gouv-edu-extension.xml
<?xml version="1.0" encoding="UTF-8"?> <itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.6"> … <menus> <menu id="GouvAPIMenu" xsi:type="MenuGroup" _delta="define"> <rank>10</rank> <style> <decoration_classes>fas fa-dice-d20</decoration_classes> </style> </menu> <menu id="EstablishmentSearchMenu" xsi:type="SearchMenuNode" _delta="define"> <rank>10</rank> <parent>GouvAPIMenu</parent> <class>Establishment</class> </menu> </menus> … </itop_design>
💡La balise <decoration_classes>
permets de définir une icône pour le groupe de menu.
Les codes pour les icônes sont disponibles sur le site de Font Awesome: https://fontawesome.com/.
Ajoutez les traductions de nos menus dans le fichier d’internationalisation comme suit:
en.dict.gouv-api-extension.php
Dict::Add('EN US', 'English', 'English', array( 'Menu:GouvAPIMenu' => 'api.gouv.fr', 'Menu:EstablishmentSearchMenu' => 'National Education Establishments', ));
Installation de notre extension
Copiez l’extension dans le dossier extensions de votre serveur iTop.
Ajoutez les autorisations d’écriture au fichier de configuration d’iTop se trouvant dans le dossier conf/production/config-itop.php
puis accédez à l’adresse web de votre iTop suivante: http://{adresse-serveur-itop}/setup
dans un navigateur internet afin de lancer l’installation.
Confirmez les étapes de l’installation et assurez-vous que notre nouvelle extension soit cochée à la fin de l’assistant d’installation.
Et voilà ! Nous pouvons à présent accueillir des établissements de l’éducation nationale dans notre iTop.
Essayez de créer un établissement pour vérifier que tout est opérationnel.
Naviguez dans le menu api.gouv.fr > Etablissements éducation nationale et cliquez sur create a establishment.
Dans le chapitre suivant, nous verrons comment synchroniser les données grâce à un collecteur.
3/ La création d’un collecteur pour iTop
Installation iTop data collector
Télécharger l’extension data collector base.
Et décompressez le contenu dans le dossier de votre choix. (Ne pas décompresser dans le dossier www
de votre serveur afin de ne pas en exposer le contenu)
Il est tout à fait possible de décompresser sur une autre machine du réseau du moment qu’elle possède un environnement d’exécution PHP 7.x.
Tout savoir sur Data collector base dans notre documentation officielle.
Configuration de la source de données
La configuration de la source de données est décrite au format JSON.
Le plus simple est de paramétrer la source de données dans l’interface utilisateur de la console d’iTop et de la transformer au format JSON via un script fourni par l’extension iTop data collector.
Créez une nouvelle source de données dans le menu configuration > sources de dé données de synchronisation.
Saisissez les informations suivantes et laissez les autres champs à leur valeur par défaut.
Name: Synchro établissements éducation nationale
Description: https://api.gouv.fr/documentation/api-annuaire-education
Target class: Establishment
Contact to notify: Your_email
Datatable: data_national_education_establishment
Choisissez l’attribut identifier pour la réconciliation.
Choisissez la clé de réconciliation Full name pour l’attribut Owner organization ce qui nous permettra d’indiquer l’organisation par son nom et être ainsi plus lisible.
Générez ensuite la définition JSON de cette source de données dans le répertoire collectors.
php toolkit/dump_tasks.php --task_name="Synchro établissements éducation nationale" > collectors/DataEducationCollector.class.inc.json
Pour aller plus loin sur ce sujet, consultez notre documentation officielle.
Paramétrage du collecteur
Nous devons préparer un fichier de paramétrage pour l’utilisation du collecteur.
Dupliquez le fichier de configuration params.distrib.xml
en le nommant params.local.xml
.
Mettez à jour le fichier de paramétrage avec vos paramètres comme suit:
conf/params.local.xml
<?xml version="1.0" encoding="UTF-8"?> <parameters> <itop_url>itop_server_url</itop_url> <itop_login>your_login</itop_login> <itop_password>your_password</itop_password> <itop_login_mode></itop_login_mode> <synchro_user>admin</synchro_user> <contact_to_notify>your_email</contact_to_notify> …
Création de la classe collector
Création de la classe DataEducationCollector
dérivée de la classe Collector
.
On vérifie la présence de curl
et on initialise en supprimant les anciennes données collectées.
DataEducationCollector.class.inc.php
class DataEducationCollector extends Collector { /** @var array education establishment data */ protected $aData = []; /** @var integer current process element */ protected $iCurrent; /** @var integer fetched elements count */ protected $idx; /** * Constructor. * * @throws Exception */ public function __construct() { parent::__construct(); // test curl presence if(!function_exists('curl_version')){ throw new Exception('Curl is mandatory, check your PHP installation'); } // initialization $this->init(); } /** * Initialization. * * @return void */ private function init() : void { // remove data files $this->RemoveDataFiles(); } }
On déclare notre fonction principale pour l’appel à l’API.
Cette méthode retourne un booléen pour signifier qu’il reste des données à récolter.
On itère par paquets de 100 éléments (max définit par l’API) et on se limite à 300 résultats pour l’instant.
DataEducationCollector.class.inc.php
class DataEducationCollector extends Collector { … /** * Call rest api to acquire data. * * @return bool true if more data available */ private function callRestApi() : bool { // open curl $oCurl = curl_init(); // check limit $limit = 100; $max = 300; if($this->iCurrent + $limit > $max){ $limit = $max - $this->iCurrent; } // construct API url $url = "https://data.education.gouv.fr/api/v2/catalog/datasets/fr-en-annuaire-education/records?limit=$limit&offset=$this->iCurrent"; // curl options curl_setopt($oCurl, CURLOPT_URL, $url); curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, true); curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, false); // execute $aResult = json_decode(curl_exec($oCurl), true); echo "curl_exec: $url\n"; curl_close($oCurl); // retrieve records $this->aData = $aResult['records']; $this->iCurrent += count($aResult['records']); // processing flags $hasMoreData = $this->iCurrent < $aResult['total_count']; $hasReachMax = $this->iCurrent >= $max; return $hasMoreData && !$hasReachMax; } }
Le mécanisme de fond de la collecte.
On appelle la fonction prépare pour collecter un lot puis on dépile les enregistrements de ce lots en les ajoutant dans des fichiers csv respectant une taille maximum de données.
On recommence tant qu’il y a des données à collecter.
DataEducationCollector.class.inc.php
class DataEducationCollector extends Collector { … /** * Prepare data. * * @return bool true if more data available */ public function Prepare() : bool { // call rest API for new block of data $this->idx = 0; return $this->callRestApi(); // return true if more data available } /** * Fetch next block of data. * * @return bool|array current element data or false if no more data to fetch */ public function Fetch() { // has something to fetch ? if ($this->idx < count($this->aData)) { // next value to fetch $aElement = $this->aData[$this->idx++]['record']; // table conversion establishment private public enumeration $tableConvPrivatePublicEnum = [ 'Privé' => 'private', 'Public' => 'public', '' => 'unknown', ]; return array( 'primary_key' => $aElement['id'], 'identifiant' => $aElement["fields"]['identifiant_de_l_etablissement'], 'name' => $aElement["fields"]['nom_etablissement'], 'org_id' => $this->aSynchroFieldsToDefaultValues['org_id'], 'address' => $aElement['fields']['adresse_1'], 'city' => $aElement['fields']['nom_commune'], 'postal_code' => $aElement['fields']['code_postal'], 'country' => 'France', 'status' => 'active', 'type' => $aElement['fields']['type_etablissement'], 'private_public' => $tableConvPrivatePublicEnum[$aElement['fields']['statut_public_prive']], 'code_academie' => $aElement['fields']['code_academie'], 'restauration' => $aElement['fields']['restauration'] == '' ? '0' : $aElement['fields']['restauration'], 'nombre_eleves' => $aElement['fields']['nombre_d_eleves'] == null ? 0 : $aElement['fields']['nombre_d_eleves'], 'date_ouverture' => $aElement['fields']['date_ouverture'] ); } return false; // no more data to fetch } /** * Collect data. * * @return bool true if data has been collected */ public function Collect($iMaxChunkSize = 0) { $idx = 0; do{ // block preparation $continue = $this->Prepare(); // fetch elements... while ($aRow = $this->Fetch()) { if (($idx == 0) || (($iMaxChunkSize > 0) && (($idx % $iMaxChunkSize) == 0))) { $this->NextCSVFile(); $this->AddHeader($this->aColumns); } $this->AddRow($aRow); $idx++; } } while($continue); // next block // cleanup $this->Cleanup(); return $idx > 0; } }
Inscription du collecteur
Nous allons à présent déclarer notre nouveau collecteur à l’orchestrateur.
Créez un fichier main.php
à la racine du dossier collectors et initialisez le comme suit:
collectors/main.php
Orchestrator::AddCollector(1, 'DataEducationCollector');
Lancement de la synchronisation
Vous pouvez à présent déclencher la synchronisation avec la commande suivante.
php .\exec.php
Naviguez dans le menu api.gouv.fr > Etablissements éducation nationale.
Vous devriez retrouver vos 300 établissements synchronisés.
4/ Industrialisation de la solution
Nous souhaitons à présent finaliser notre solution en externalisant des paramètres de notre collecteur dans le fichier de paramétrage.
Ajout de paramètres
Ajoutez les membres de classe comme suit:
DataEducationCollector.class.inc.php
class DataEducationCollector extends Collector { const PRIMARY_KEY = 'primary_key'; /** @var string rest API url */ protected $sRestUrl; /** @var integer rest results amount limit */ protected $iRestLimit; /** @var integer max elements to synchronize */ protected $iMaxElements; /** @var array data columns */ protected $aSynchroCols; /** @var array data translation */ protected $aSynchroTranslation; /** @var array default values */ protected $aSynchroDefaultValues; /** @var array class fields */ protected $aSynchroClassFields; … /** * Initialization. * * @return void */ private function init() : void { $this->iCurrent = 0; $this->iMaxElements = -1; // extract configuration values $this->extractConfigurationValues(); // remove data files $this->RemoveDataFiles(); } /** * Extract configuration values. * * @return void * @throws Exception */ private function extractConfigurationValues() : void { // read configuration values $aConfigurationValues = Utils::GetConfigurationValue(strtolower(get_class($this))); // rest url if (array_key_exists('rest_url', $aConfigurationValues)) { $this->sRestUrl = $aConfigurationValues['rest_url']; } // rest limit if (array_key_exists('rest_limit', $aConfigurationValues)) { $this->iRestLimit = $aConfigurationValues['rest_limit']; } // max elements if (array_key_exists('max_elements', $aConfigurationValues)) { $this->iMaxElements = $aConfigurationValues['max_elements']; } // fields association if (array_key_exists('class_fields', $aConfigurationValues)) { $this->aSynchroClassFields = array(Self::PRIMARY_KEY => null); $this->aSynchroClassFields = array_merge($this->aSynchroClassFields, $aConfigurationValues['class_fields']); $this->aSynchroCols = array_keys($this->aSynchroClassFields); } // translation if (array_key_exists('translations', $aConfigurationValues)) { $this->aSynchroTranslation = $aConfigurationValues['translations']; foreach ($this->aSynchroTranslation as $key => $aTranslation) { $this->aSynchroTranslation[$key] = array_flip($this->aSynchroTranslation[$key]); } } // default values if (array_key_exists('defaults', $aConfigurationValues)) { $this->aSynchroDefaultValues = $aConfigurationValues['defaults']; } } /** * Call rest api to acquire data. * * @return void * @throws Exception */ private function callRestApi() : bool { … // check limit $limit = $this->iRestLimit; if($this->iCurrent + $this->iRestLimit > $this->iMaxElements){ $limit = $this->iMaxElements - $this->iCurrent; } // construct API url $url = "$this->sRestUrl?limit=$limit&offset=$this->iCurrent"; … } /** * Fetch next data. * * @return bool|array current element data or false if no more data to tech */ public function Fetch() { // has something to fetch ? if ($this->idx < count($this->aData)) { // next value to fetch $aElement = $this->aData[$this->idx++]['record']; // fetched data $fetchedData = array(); // fill values foreach ($this->aSynchroClassFields as $classField => $value) { if($classField == Self::PRIMARY_KEY){ $fetchedData[$classField] = $aElement['id']; } else if(array_key_exists($value, $aElement["fields"]) && $aElement["fields"][$value] != ''){ $fetchedData[$classField] = $this->getFieldValue($classField, $aElement["fields"][$value]); } else{ $fetchedData[$classField] = $this->getFieldDefaultValue($classField); } } return $fetchedData; } return false; // no more data to fetch }
Versionner le collecteur
Créez un fichier de déclaration de module à la racine du dossier collectors et initialisez le come suit:
collectors/module.gouv-api-collectors-extension.php
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file 'gouv-api-collectors-extension/1.0.0', array( // Identification // 'label' => 'api.gouv.fr API data synchronisation collectors', 'category' => 'business', // Setup // 'dependencies' => array( 'gouv-api-extension/1.0.0' ), 'mandatory' => false ) );
5/ Exercice en autonomie !
Lister les établissements AFPA de la France entière
https://api.gouv.fr/documentation/api_etablissements_publics
Ajouter une nouvelle entrée dans le menu latéral.
6/ Quizz
– Comment créer un squelette d’extension rapidement ?
🔲 En la créant avec un éditeur de texte
🔲 Grâce au pages du wiki
🔲 Avec une autre extension
– Qu’est-ce que la réconciliation ?
🔲 La clé primaire d’une classe
🔲 L’affichage du nom intelligible d’un objet
🔲 Un ou ensemble d’attributs d’identification
– Ou sont définies les traductions des textes ?
🔲 Dans la balise class du modèle de donnée
🔲 Dans la balise dictionaries du modèle de donnée
🔲 Dans la fonction Dict::Add en PHP
– Comment installer son extension ?
🔲 Avec iTop hub
🔲 En exécutant un script d’installation
🔲 En plaçant l’extension dans le dossier extensions du serveur iTop.
– Comment donner une version à son collecteur
🔲 En nommant le dossier avec la version
🔲 En créant un fichier module
🔲 En le sauvegardant sous Git