3 votes

Quelles sont les commandes git qui permettent de parcourir les fichiers poussés ?

J'essaie de mettre en place un pre-push git hook en python pour valider les fichiers avant qu'ils ne soient poussés vers le repo distant.

J'ai déjà écrit un pre-commit git hook pour valider les fichiers avant qu'ils ne soient livrés au repo local et pour obtenir une liste des fichiers dans le commit, j'ai lancé git diff-index --cached --name-status HEAD .

Pour les pre-push script, quelles commandes git puis-je exécuter pour parcourir tous les commits sur le point d'être poussés, et ensuite parcourir tous les fichiers dans les commits individuels afin que je puisse les valider ?

Jusqu'à présent, j'utilise la commande : git diff --name-status @{u}..

EDIT : Je pense qu'il est également important de noter que les mêmes fichiers peuvent être modifiés par plusieurs commits qui sont sur le point d'être poussés - il serait donc bon de ne pas valider le même fichier plusieurs fois.

SOLUTION_FINALE :

Voici le code que j'ai fini par utiliser grâce aux réponses de @Vampire et @Torek...

#!/usr/bin/env python

# read the args provided by git from stdin that are in the following format...
# <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF
# the line above represents a branch being pushed
# Note: multiple branches may be pushed at once

lines = sys.stdin.read().splitlines()

for line in lines:
    local_ref, local_sha1, remote_ref, remote_sha1 = line.split()

    if remote_sha1 == "0000000000000000000000000000000000000000":
        print_error("Local branch '%s' cannot be found on the remote repo - push only the branch without any commits first!" % local_ref)
        sys.exit(1)

    # get changed files
    changed_files = subprocess.check_output(["git", "diff", "--name-status", local_sha1, remote_sha1], universal_newlines=True)

    # get the non deleted files while getting rid of M\t or A\t (etc) characters from the diff output
    non_deleted_files = [ f[2:] for f in changed_files.split("\n") if f and not f.startswith("D") ]

    # validation here...
    if validation_failed:
        sys.exit(1) # terminate the push

sys.exit(0)

3voto

torek Points 25463

Problème 1 : commits

Obtenir la liste des s'engage n'est que modérément difficile, car vous devez surtout exécuter des git rev-list . Il existe toutefois des cas particuliers. En tant que la documentation sur les githooks dit :

Les informations sur ce qui doit être poussé sont fournies sur l'entrée standard du crochet sous la forme de lignes :

<local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF

Par exemple, si la commande git push origin master:foreign était exécuté, le crochet recevrait une ligne comme la suivante :

refs/heads/master 67890 refs/heads/foreign 12345

bien que les SHA-1 complets de 40 caractères soient fournis. Si le référent étranger n'existe pas encore, l'option <remote SHA-1> sera de 40 0 . Si une référence doit être supprimée, l'option <local ref> sera fournie sous la forme (delete) et le <local SHA-1> sera de 40 0 . Si le commit local a été spécifié par quelque chose d'autre qu'un nom qui peut être développé (comme HEAD~, ou un SHA-1), il sera fourni tel qu'il a été donné à l'origine.

Vous devez donc lire chaque ligne stdin et l'analyser en ses composants, puis décider :

  • S'agit-il d'un branche une mise à jour ? (Par exemple, la référence à distance a-t-elle la forme refs/heads/* (comme une correspondance globale ?) Si ce n'est pas le cas, voulez-vous vérifier les commits de toute façon ?
  • La référence est-elle créée ou détruite ? Dans l'affirmative, que devez-vous faire ?
  • Disposez-vous de l'objet spécifié par le hachage étranger (si ce n'est pas le cas) ? et la poussée réussit - elle peut très bien échouer - ce qui entraînera l'abandon d'un certain nombre d'objets commit, mais vous ne pouvez pas savoir lesquels. De plus, vous ne pouvez pas lister correctement les commits locaux qui seront transférés : vous savez ce que vous leur demandez de faire fixer mais vous ne savez pas quels sont les engagements que vous et eux ont en commun puisque vous ne pouvez pas parcourir leur historique).

En supposant que vous ayez déterminé les réponses à ces questions - disons qu'elles sont "no", "skip it", et "locally reject pushes that are not analyzable" - nous passons à la liste des commits, et ce n'est que le résultat de :

git rev-list remotehash..localhash

que vous pourriez utiliser :

proc = subprocess.Popen(['git', 'rev-list',
    '{}..{}'.format(remotehash, localhash)], stdout=subprocess.PIPE)
text = proc.stdout.read()
if proc.wait():
    raise ... # some appropriate error, as Git failed here
if not isinstance(text, str):   # i.e., if python3
    text = text.decode('utf-8') # convert bytes to str
lines = text.split('\n')
# now work with each commit hash

Il convient de noter que cette git rev-list l'appel échouera (sortie avec un statut non nul) si le hash distant ou local est all-zeros, ou si le hash distant correspond à un objet qui n'existe pas dans votre référentiel local (vous pouvez le vérifier en utilisant la commande git rev-parse --verify --quiet et vérifier le statut de retour, ou peut-être utiliser l'échec ici comme indication que vous ne pouvez pas vérifier les commits, bien qu'il y ait d'autres options lors de la création d'une nouvelle branche).

Notez que vous devez exécuter le programme git rev-list pour chaque référence qui doit être mis à jour. Il est possible que les mêmes commits, ou un sous-ensemble des mêmes commits, soient envoyés pour différentes références. Par exemple :

git push origin HEAD:br1 HEAD:br2 HEAD~3:br3

demanderait que la mise à jour à distance de trois branches soit effectuée br1 à travers br3 , réglage br1 y br2 au même commit que HEAD et la mise en place br3 à l'engagement trois pas en arrière de HEAD . Nous ne savons pas (et ne pouvons pas savoir) quels commits sont vraiment nouveaux - le crochet de pré-réception à l'autre bout pourrait le déterminer, mais nous ne le pouvons pas - mais si les commits de l'autre bout sont vraiment nouveaux, nous ne pouvons pas le savoir. br1 y br2 sont tous deux mis à jour à partir de HEAD~3 a HEAD et la fonction br3 est mis à jour à partir de HEAD~2 à l'envers a HEAD~3 , au maximum les commits HEAD~1 à travers HEAD peut être nouvelle. Que vous souhaitiez vérifier HEAD~2 puisqu'il est désormais susceptible d'apparaître sur le site de l'Union européenne. br1 y br2 dans l'autre référentiel (même s'il est était déjà sur br3 ), c'est aussi à vous de décider.

Problème 2 : fichiers

Vous avez maintenant un problème plus difficile à résoudre. Vous avez mentionné dans un éditorial que :

EDIT : Je pense qu'il est également important de noter que les mêmes fichiers peuvent être modifiés par plusieurs commits qui sont sur le point d'être poussés - il serait donc bon de ne pas valider le même fichier plusieurs fois.

Chaque livraison à envoyer a un aperçu complet du référentiel. En d'autres termes, chaque livraison contient tous les fichiers. Je n'ai aucune idée de la validation que vous avez l'intention d'effectuer, mais vous avez raison : si vous envoyez, disons, six livraisons au total, il est très probable que la plupart des fichiers des six livraisons soient identiques et que seuls quelques fichiers soient modifiés. Cependant, les fichiers foo.py pourrait être modifié lors de l'engagement 1234567 (en ce qui concerne les 1234567 ), et a ensuite modifié le commit parent de à nouveau dans l'engagement fedcba9 et vous devriez probablement vérifier les deux versions .

En outre, lorsqu'un engagement est un fusionner s'engager, il a (au moins) deux parents différents. Devriez-vous vérifier un fichier s'il est différent de soit parent ? Ou bien faut-il le vérifier uniquement s'il diffère de à la fois parents, indiquant qu'il contient des modifications provenant des "deux côtés" de la fusion ? S'il ne contient que des modifications d'un seul côté, le fichier est probablement "pré-vérifié" par les vérifications qui ont eu lieu pour le commit qui se trouve sur la page d'accueil. autres et, par conséquent, il n'est peut-être pas nécessaire de procéder à une nouvelle vérification (bien que cela dépende évidemment du type de vérification).

(Dans le cas d'une fusion de pieuvres, c'est-à-dire d'une fusion avec plus de deux parents, cette question est beaucoup plus difficile à résoudre).

Il est relativement facile de voir quels fichiers sont modifié dans un commit, par rapport à son ou ses parents : il suffit de lancer git diff-tree avec les options appropriées (notamment, -r pour rechercher des sous-arbres du commit). Le format de sortie par défaut est tout à fait analysable par la machine, bien que vous puissiez ajouter -z pour le rendre plus facile à manipuler directement dans Python. Si vous les faites une à la fois - ce qui est tout à fait possible - vous voudrez probablement aussi utiliser --no-commit-id de sorte que vous n'ayez pas besoin de lire et de sauter l'en-tête du commit.

C'est à vous de décider si vous voulez activer la détection des renommages et, si oui, à partir de quel seuil. En fonction, encore une fois, de ce que vous faites précisément pour vérifier les fichiers, il est souvent préférable de ne pas activer la détection des renommages : ainsi, vous "verrez" un fichier renommé comme une suppression de l'ancien chemin et un ajout du nouveau chemin.

La sortie de git diff-tree -r --no-commit-id sur une livraison particulière ressemble à ceci :

:000000 100644 0000000000000000000000000000000000000000 b0b4c36f9780eaa600232fec1adee9e6ba23efe5 A  Documentation/RelNotes/2.13.0.txt
:100755 100755 6a208e92bf30c849028268b5fca54b902f671bbd 817d1cf7ef2a2a99ab11e5a88a27dfea673fec79 M  GIT-VERSION-GEN
:120000 120000 d09c3d51093ac9e4da65e8a127b17ac9023520b5 125bf78f3b9ed2f1444e1873ed02cce9f0f4c5b8 M  RelNotes

Les ID de hachage sont les anciens et les nouveaux hachages de blob ; les codes de lettres et les noms de chemin sont tels que documentés. Vous pouvez ensuite récupérer le contenu du fichier en utilisant git cat-file -p sur le nouvel ID de hachage. Si votre Git est suffisamment récent, vous pouvez même obtenir n'importe quel .gitattributes -Le filtrage basé sur l'utilisation et la conversion de fin de ligne sont appliqués par l'ajout des éléments suivants --textconv --filters y --path=<path> (ou en utilisant le chemin d'accès au fichier ainsi que l'ID du commit, au lieu de --path=... pour nommer le hachage de l'objet à extraire). Vous pouvez également utiliser la forme de l'objet stocké dans le référentiel, si les filtres ne sont pas importants.

En fonction de ce que vous vérifiez, vous pouvez avoir besoin d'extraire l'intégralité du commit dans un arbre de travail temporaire (par exemple, un analyseur statique peut vouloir exécuter n'importe quel import s.) Dans ce cas, vous pouvez tout aussi bien exécuter git checkout à l'aide de l'outil GIT_INDEX_FILE (transmettez-la par l'intermédiaire de la variable d'environnement subprocess comme d'habitude) pour spécifier un fichier d'index temporaire afin que l'index principal ne soit pas perturbé. Spécifiez un arbre de travail alternatif avec --work-tree= ou par l'intermédiaire du GIT_WORK_TREE variable d'environnement. Dans tous les cas, la variable d'environnement git diff-tree vous indiquera les fichiers qui ont été modifié et doit donc être vérifiée (vous pouvez utiliser la fonction shutil.rmtree pour se débarrasser de l'arbre de travail temporaire une fois les tests terminés).

Si vous souhaitez vérifier les commits de fusion, prêtez une attention particulière à l'élément description de Différences combinées fait pour les fusions car ils nécessiteront un traitement quelque peu différent (ou la division de la fusion avec des -m ).

Edit : un peu de code pour montrer ce que je veux dire

Voici un petit bout de code qui permet d'obtenir toutes les données et de montrer que chaque engagement est ajouté à chaque branche étrangère. Notez que la liste des ajouté sera vide si les commits sont seulement en cours de traitement. supprimée . Ce document n'a été que très légèrement testé et n'est pas censé être robuste, maintenable, de bon style, etc.

import re, subprocess, sys

lines = sys.stdin.read().splitlines()
for line in lines:
    localref, localhash, foreignref, foreignhash = line.split()
    if not foreignref.startswith('refs/heads/'):
        print('skip {}'.format(foreignref))
        continue
    if re.match('0+$', localhash):
        print('deleting {}, do nothing'.format(foreignref))
        continue
    if re.match('0+$', foreignhash):
        print('creating {}, too hard for now'.format(foreignref))
        continue
    proc = subprocess.Popen(['git', 'rev-parse', '--quiet', '--verify',
            foreignhash],
        stdout=subprocess.PIPE)
    _ = proc.stdout.read()
    status = proc.wait()
    if status:
        print('we do not have {} for {}, try '
            'git fetch'.format(foreignhash, foreignref))
        # can try to run git fetch here ourselves, but for now:
        continue
    print('sending these commits for {}:'.format(foreignref))
    subprocess.call(['git', 'rev-list', '{}..{}'.format(localhash, foreignhash)])

1voto

Vampire Points 515

Utilisation @{u}.. est peu utile, car il diffère l'amont de l'autoroute. HEAD contre HEAD s'il existe une définition en amont. Mais cela n'a pas nécessairement à voir avec ce qui est poussé, puisque vous pouvez pousser n'importe quelle branche ou en fait n'importe quel commit, indépendamment de ce qui est vérifié actuellement et vers n'importe quelle branche distante que vous souhaitez, indépendamment de la définition de l'amont.

Selon la documentation de githooks Vous obtenez le nom et l'emplacement à distance comme paramètres de votre script et sur stdin vous obtenez une ligne par "chose" poussée avec le ref local et à distance et le sha local et à distance. Il faut donc itérer sur stdin et différencier le sha distant qui est poussé par rapport au sha local que vous poussez pour obtenir les fichiers qui sont différents.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X