diff --git a/README.md b/README.md index a8d7e912637125b61ae23688de1071205d421f3e..1430045da052a1ae753d8de40fc34f7f954d184d 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,126 @@ # Practical Work Manager (pwm) -Ce repository contient différents scripts python pour gérer la réalisation de travaux pratiques par les étudiants avec la contrainte d'utiliser le gitlab d'HEPIA. +Programme python pour gérer les travaux pratiques des étudiants avec la contrainte d'utiliser le gitlab d'HEPIA. + +## TL;DR + +- Créer un groupe et les repositories en une seule commande (avec un repository "image" optionnel) : `./pwm -t TOKEN group_repos GROUP_NAME REPOS_FILE [-i IMPORT_URL]`, voir [Syntaxe du fichier YAML (REPOS_FILE)](#syntaxe-du-fichier-yaml-repos_file) +- Clone tous les projets des étudiants d'un groupe gitlab (`ID`) dans un répertoire créé à la volée : `./pwm -t TOKEN clone ID DIRECTORY` + +- [Practical Work Manager (pwm)](#practical-work-manager-pwm) + - [TL;DR](#tldr) + - [Workflow d'utilisation](#workflow-dutilisation) + - [Utilisation de pwm](#utilisation-de-pwm) + - [Création d'un groupe et des projets](#création-dun-groupe-et-des-projets) + - [Création d'un groupe seulement](#création-dun-groupe-seulement) + - [Création d'un sous-projet dans le groupe](#création-dun-sous-projet-dans-le-groupe) + - [Clone de tous les repositories](#clone-de-tous-les-repositories) + - [Syntaxe du fichier YAML (REPOS_FILE)](#syntaxe-du-fichier-yaml-repos_file) + - [Noms et emails](#noms-et-emails) + - [Emails seulement](#emails-seulement) ## Workflow d'utilisation Pour expliquer la démarche d'utilisation, prenons le scénario suivant : -- L'enseignant Michel Albuquerque prépare un nouveau travail pratique (TP). Il souhaite forcer les étudiants à utiliser git et githepia pour qu'ils versionnent leur code et pour qu'il puisse visualiser et recevoir leur rendus. +- L'enseignant Michel Albuquerque prépare un nouveau travail pratique (TP). Il souhaite forcer les étudiants à utiliser git et gitedu pour qu'ils versionnent leur code et pour qu'il puisse visualiser et recevoir leur rendus. - Les TPs sont à faire par groupe ou de manière individuelle. - Pour transmettre son énoncé et des fichiers (exemples, squelette de code, librairies, binaires, etc.) aux étudiants, Michel Albuquerque crée un repository git accessible publiquement, nommé "super-tp". - Grâce à ce repository "super-tp", il peut mettre à jour le contenu distribué aux étudiants, en leur offrant la possibilité de visualiser les changements incrémentaux survenus. -Le moment est venu de créer les dépôts git pour chaque groupe/étudiant suivant le cours et devant réaliser le TP. Sur la base d'une liste de groupes ou d'étudiants, Michel Albuquerque pourra utiliser les scripts suivants pour : +Le moment est venu de créer les dépôts git pour chaque groupe/étudiant suivant le cours et devant réaliser le TP. Sur la base d'une liste de groupes ou d'étudiants, Michel Albuquerque pourra utiliser le programme pour : 1. Créer le groupe (namespace) dédié au cours/TP, contenant tous les repositories des étudiants. 1. Créer chaque repository pour chaque groupe/étudiant avec les contraintes nécessaires (privé, accessible à (aux) l'étudiant(s) concerné(s), aux enseignants, etc.) 1. Récupérer (clone) sur sa machine tous les repositories d'un seul coup, dans des répertoires séparés, au moment du rendu par exemple. -La section suivante décrit l'utilisation des scripts. +## Utilisation de pwm -## Utilisation des scripts +Ce programme est écrit en python et testé avec la version 3.9.0, avec les dépendances suivantes (voir `requirements.txt`) : -Tous les scripts sont (actuellement) écrits en python et testés avec la version 3.6.8, sans dépendances à des librairies externes. Ils se présentent sous la forme de programmes à lancer dans un shell en ligne de commande. Ils nécessitent tous un `token` gitlab, pouvant être généré [sur cette page](https://gitedu.hesge.ch/profile/personal_access_tokens), en cochant la case "api". Certains attendent également un `project_id` correspond à celui affiché sur la page de repo : +``` +requests +pyyaml +``` - +Pour rappel, pour ne pas à avoir à installer ces deux dépendances au niveau système, les commandes suivantes génèrent un environnement virtuel : -### create_group.py ```bash -python3 create_group.py <token> <group_name> <visibility> +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt ``` -Crée un groupe au sens gitlab du terme, nommé `group_name`, avec la visibilité optionnelle `visibility` (`private`, `internal` ou `public`), par défaut privée. Si le groupe existe déjà, ne fait rien. Retourne le `group_id` du groupe créé, nécessaire pour `create_repo_for_students.py` par exemple. -### create_repo_for_students.py +pwm se présente sous la forme d'une CLI. Il nécessite un `token` gitlab, pouvant être généré [sur cette page](https://gitedu.hesge.ch/profile/personal_access_tokens), en cochant la case "api". Ce `token` peut ensuite être utilisé de trois manières : + +1. Écrit dans le fichier `~/.gitedu_token` +2. Placé dans la variable d'environnement `GITEDU_TOKEN` +3. Donné en argument de `pwm` avec l'option `-t` ou `--token` + +Selon les commandes, un `project_id` ou `group_id` est également nécessaire, il correspond à celui affiché sur la page du groupe / projet : + + + +L'exécution du programme sans arguments affiche l'aide et le détail pour chaque sous-commande. + +### Création d'un groupe et des projets ```bash -python3 create_repo_for_students.py <token> <import_url> <group_id> <project_name> <student-mail1,student-mail2,...,student-mailN> <expires_at> +python pwm group_repos GROUP_NAME REPOS_FILE [-h] [--visibility VISIBILITY] [-i IMPORT_URL] [-x EXPIRES_AT] ``` -Crée un dépôt git (projet) au sein d'un groupe à partir de l'URL d'un projet existant pour une liste d'emails d'étudiants. Détail des arguments : -- `token` : le token gitlab. -- `import_url` : l'URL (http) du projet (repository) existant. Ce projet doit être public. -- `group_id` : l'identifiant du groupe dédié au cours/TP, créé précédemment (avec `create_group.py` par exemple). -- `project_name` : le nom du nouveau repository à créer pour le ou les étudiants concernés. -- `student-mail1,student-mail2,...,student-mailN` : une liste d'emails des étudiants. Les emails sont séparés par une virgule. Peut contenir un seul email. -- `expires_at`: optionnel, au format `AAAA-MM-DD`, supprime les étudiants ajoutés à la date donnée. - -### clone_all_repos_in_group.py +Exécute les opérations de création de groupe et de repositories à partir d'un fichier YAML (voir [Syntaxe du fichier YAML (REPOS_FILE)](#syntaxe-du-fichier-yaml-repos_file)). Voir les sous-sections suivantes pour les détails des sous-commandes. + +### Création d'un groupe seulement ```bash -python3 clone_all_repos_in_group.py <token> <group_id> <directory> <until_date> +python pwm group [-h] [--visibility VISIBILITY] GROUP_NAME ``` -Clone tous les repositories d'un groupe `group_id` donné dans un répertoire nommé `directory`. Si une date `until_date` (au format `AAAA-MM-DD hh:mm`) est donnée, exécute un `git checkout` sur le premier commit précédant cette date. Affiche sur la sortie standard les membres du groupe, l'url web du repo et dans quel sous-répertoire se trouvent les fichiers. +Crée un groupe au sens gitlab du terme, nommé `GROUP_NAME`, avec la visibilité optionnelle `VISIBILITY` (`private`, `internal` ou `public`), par défaut privée. Si le groupe existe déjà, ne fait rien. Retourne le `group_id` du groupe créé, nécessaire pour la création des sous-projets par exemple. -### clone_all_forks.py +### Création d'un sous-projet dans le groupe ```bash -python3 clone_all_forks.py <token> <project_id> <directory> <until_date> +python pwm repo [-h] [-n NAME] [-i IMPORT_URL] [-x EXPIRES_AT] GROUP_ID EMAILS ``` -Clone tous les forks d'un projet `project_id` donné dans un répertoire nommé `directory`. Si une date `until_date` (au format `AAAA-MM-DD hh:mm`) est donnée, exécute un `git checkout` sur le premier commit précédant cette date. Affiche sur la sortie standard les membres du groupe (avec un droit d'accès supérieur à *Reporter*), l'url web du repo et dans quel sous-répertoire se trouvent les fichiers. +Crée un dépôt git (projet) au sein d'un groupe à partir de l'URL d'un projet existant pour une liste d'emails d'étudiants. Détail des arguments : +- `NAME` : optionnel, le nom du nouveau repository à créer pour le ou les étudiants concernés. Si non renseigné, prend la première partie du premier email dans `EMAILS`. +- `IMPORT_URL` : optionnel, l'URL (http) du projet (repository) existant. Ce projet doit être public. +- `EXPIRES_AT`: optionnel, au format `YYYY-MM-DD`, supprime les étudiants ajoutés à la date donnée (ils ne peuvent plus `push`). +- `GROUP_ID` : l'identifiant du groupe dédié au cours/TP, créé précédemment. +- `EMAILS` : une liste d'emails des étudiants. Les emails sont séparés par une virgule. Peut contenir un seul email. -## "Convenient" script +### Clone de tous les repositories ```bash -./create_group_and_repos.sh <token> <group_name> <import_url> <repos_students> +python pwm clone [-h] [-g | -f] [-u UNTIL_DATE] ID DIRECTORY ``` -Un script bash est également disponible, `create_group_and_repos.sh` qui permet de "batcher" les opérations de création de groupe et de repositories à partir d'un fichier texte `repos_students` formaté ainsi : +Clone tous les repositories d'un groupe (`-g`) ou tous les forks d'un projet (`-f`) selon l'id (`ID`) donné dans un répertoire nommé `DIRECTORY`. Si une date `UNTIL_DATE` (au format `YYYY-MM-DD hh:mm`) est donnée, exécute un `git checkout` sur le premier commit précédant cette date. Affiche sur la sortie standard les membres du groupe (avec un droit d'accès supérieur à *Reporter*), l'url web du repo et dans quel sous-répertoire se trouvent les fichiers. + +## Syntaxe du fichier YAML (REPOS_FILE) +Le fichier YAML doit respecter une des deux syntaxes suivantes. + +### Noms et emails +Pour chaque projet créé, un nom et une liste d'emails doivent être renseignés : + +```yaml +- name: group1 + emails: + - prenom.nom11@hesge.ch + - prenom.nom12@hesge.ch +- name: group2 + emails: + - prenom.nom21@hesge.ch + - prenom.nom22@hesge.ch +- name: group3 + emails: + - prenom.nom31@hesge.ch + - prenom.nom32@hesge.ch ``` -repository1;email1,email2 -repository2;email3,email4 + +### Emails seulement +Si uniquement les emails sont fournis, prend le premier nom de chaque email pour nom de projet : + +```yaml +- emails: + - prenom.nom11@hesge.ch # project_name = prenom.nom11 + - prenom.nom12@hesge.ch +- emails: + - prenom.nom21@hesge.ch # project_name = prenom.nom21 + - prenom.nom22@hesge.ch +- emails: + - prenom.nom31@hesge.ch # project_name = prenom.nom31 + - prenom.nom32@hesge.ch ``` diff --git a/create_group_and_repos.sh b/create_group_and_repos.sh deleted file mode 100755 index 15bc4bba545c305440b397492b461b79370ab27c..0000000000000000000000000000000000000000 --- a/create_group_and_repos.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -if [[ $# != 4 ]]; then - echo "Usage: $0 <token> <group_name> <import_url> <repos_students>" - exit 1 -fi - -token=$1 -group_name=$2 -import_url=$3 -repos_students=$4 - -group=$(scripts/create_group.py $token $group_name) -group_id=$(echo $group | cut -d';' -f2) -printf "$group\n\n" - -for line in $(cat $repos_students); do - project_name=$(echo $line | cut -d';' -f1) - students=$(echo $line | cut -d';' -f2) - new_repo=$(scripts/create_repo_for_students.py $token $import_url $group_id "$project_name" $students) - printf "$new_repo\n\n" -done diff --git a/doc/project_id.png b/doc/project_id.png deleted file mode 100644 index 2671a3fb443acc0b326d14adaf81f5473fb2ea0a..0000000000000000000000000000000000000000 Binary files a/doc/project_id.png and /dev/null differ diff --git a/project_id.png b/project_id.png new file mode 100644 index 0000000000000000000000000000000000000000..37a6aa7b7f25ebf52924164b71f87dacbe004c7f Binary files /dev/null and b/project_id.png differ diff --git a/pwm b/pwm index ec1aaf85e4d17d8a2e58cc956aeaf8b9e29cd105..51b0a323ec8feff0893c37bb9779ed430f5003a4 100755 --- a/pwm +++ b/pwm @@ -248,7 +248,7 @@ if __name__ == '__main__': parser_group_repos.add_argument( "group_name", metavar="GROUP_NAME", help="The group name.") parser_group_repos.add_argument( - "repos_file", metavar="REPOS_FILE", help="A file with projects names and students emails.") + "repos_file", metavar="REPOS_FILE", help="YAML file with projects names and/or students emails.") parser_group_repos.add_argument( "--visibility", help="Group visibility. By default private.") parser_group_repos.add_argument("-i", "--import_url", diff --git a/scripts/clone_all.py b/scripts/clone_all.py deleted file mode 100755 index f02c74adc97c1799322c75e67568047b20e2a1a6..0000000000000000000000000000000000000000 --- a/scripts/clone_all.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 - -import os -import requests -import subprocess -import argparse - -parser = argparse.ArgumentParser() -group = parser.add_mutually_exclusive_group() -group.add_argument("-g", "--group", action="store_true", help="Clone repositories from a group (with group_id) or forks of a project (with project_id) (default behavior).") -group.add_argument("-f", "--forks", action="store_true", help="Clone forks of a project (with project_id).") -parser.add_argument( - "token", metavar="TOKEN", help="Create a token here: https://gitedu.hesge.ch/profile/personal_access_tokens") -parser.add_argument( - "id", metavar="ID", help="The group_id (int) of the projects or the project_id (int) of the forks.") -parser.add_argument( - "directory", metavar="DIRECTORY", help="Local directory where clone all repositories.") -parser.add_argument( - "-u", "--until_date", help="Do a git checkout for all repositories at given date, format \"YYYY-MM-DD hh:mm\" (optional).") -args = parser.parse_args() - -try: - os.mkdir(args.directory) -except OSError: - print("Creation of the directory '%s' failed, exit\n" % args.directory) - exit(1) - -base_url = 'https://gitedu.hesge.ch/api/v4/' -params = {'simple': 'true', 'per_page': 100} -headers = {'PRIVATE-TOKEN': args.token} - -if args.forks: - url = base_url + 'projects/' + args.id + '/forks' -else: - url = base_url + 'groups/' + args.id + '/projects' - -repositories = requests.get(url, params=params, headers=headers).json() -if 'message' in repositories: - print('Error retrieving repositories: ' + repositories['message']) - exit(1) - -for repo in repositories: - repo_url = base_url + '/projects/' + str(repo['id']) + '/members' - members = requests.get(repo_url, headers=headers).json() - if 'message' in members: - print('Error retrieving members: ' + members['message']) - exit(1) - - ssh_url_to_repo = repo['ssh_url_to_repo'] - web_url = repo['web_url'] - members_names = '' - - for member in members: - if member['access_level'] > 20: # Access level greater than "Reporter" - members_names += member['username'] + ', ' - - if args.forks: - repo_local_name = repo['namespace']['path'] - else: - repo_local_name = repo['path'] - - print('Members: ' + members_names) - print('Web url: ' + web_url) - print('Cloning in "' + args.directory + '/' + repo_local_name + '"') - - subprocess.run(["git", "clone", "-q", ssh_url_to_repo, - args.directory + '/' + repo_local_name]) - if args.until_date: - commit_id = subprocess.check_output([ - "git", "rev-list", "-n", "1", "--before=\"" + args.until_date + "\"", - "master"], cwd=args.directory + '/' + repo_local_name).decode('utf-8').rstrip() - subprocess.run( - ["git", "checkout", "-q", str(commit_id)], - cwd=args.directory + '/' + repo_local_name) - print("Checkout at " + str(commit_id) + "\n") - else: - print() diff --git a/scripts/create_group.py b/scripts/create_group.py deleted file mode 100755 index 209e4c314e4d9df4a09bd7d3af47bee10e1193ec..0000000000000000000000000000000000000000 --- a/scripts/create_group.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import requests - -parser = argparse.ArgumentParser() -parser.add_argument( - "token", metavar="TOKEN", help="Create a token here: https://gitedu.hesge.ch/profile/personal_access_tokens") -parser.add_argument( - "group_name", metavar="GROUP_NAME", help="The group name.") -parser.add_argument( - "--visibility", help="Group visibility. By default private.") -args = parser.parse_args() - -if args.visibility: - visibility = args.visibility -else: - visibility = 'private' - -base_url = 'https://gitedu.hesge.ch/api/v4/' -params = {'path': args.group_name, - 'name': args.group_name, 'visibility': visibility} -headers = {'PRIVATE-TOKEN': args.token} - -group = requests.post(base_url + '/groups', - params=params, headers=headers).json() -if 'message' in group: - print('Error in creating group: %s' % group) - exit(1) - -print("Group '" + group['name'] + "' with id '" + str(group['id']) + "' and visibility '" + - group['visibility'] + "' available at '" + group['web_url'] + "' ;" + str(group['id'])) diff --git a/scripts/create_repo_for_students.py b/scripts/create_repo_for_students.py deleted file mode 100755 index 89480aa4f4299845e0bafd2c4b08596801643693..0000000000000000000000000000000000000000 --- a/scripts/create_repo_for_students.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import requests - -parser = argparse.ArgumentParser() -parser.add_argument( - "token", metavar="TOKEN", help="Create a token here: https://gitedu.hesge.ch/profile/personal_access_tokens") -parser.add_argument( - "group_id", metavar="GROUP_ID", help="The group id (int) where to store the created new project.") -parser.add_argument( - "emails", metavar="EMAILS", help="Emails list of students working in this project, separated by commas (email1,email2).") -parser.add_argument( - "-n", "--name", help="The project name. If blank, take the first student name (from email) as name.") -parser.add_argument("-i", "--import_url", - help="Import the publicly accessible project by URL given here (optional).") -parser.add_argument("-x", "--expires_at", - help="Expiration date to kick off students from this project, at 00:00:00. YYYY-MM-DD format (optional).") -args = parser.parse_args() - -base_url = 'https://gitedu.hesge.ch/api/v4' -headers = {'PRIVATE-TOKEN': args.token} - -# split '@' in the case when project name = student's email -if args.name: - name = args.name -else: - name = args.emails.split('@')[0] - -# Get students ids from their emails -users_emails = args.emails.split(',') -user_ids = [] -for email in users_emails: - user_requested = requests.get( - base_url + '/users', params={'search': email}, headers=headers).json() - if len(user_requested) == 0: - print('No user %s found, operation aborted' % email) - exit(1) - user_id = user_requested[0]['id'] - user_ids.append(user_id) - -# Create project from name, import_url (if given) and group_id -params = {'name': name, 'namespace_id': args.group_id, 'visibility': 'private'} -if args.import_url: - params['import_url'] = args.import_url -project = requests.post(base_url + '/projects', - params=params, headers=headers).json() -if 'message' in project: - print('Error in creating project: %s' % project) - exit(1) -print("Project '" + project['name'] + "' at '" + - project['web_url'] + "' created") - -# Allow users with developer access level to push and merge on master -access_level = 30 -params = {'name': 'master', 'push_access_level': str( - access_level), 'merge_access_level': str(access_level)} -requests.post(base_url + '/projects/' + - str(project['id']) + '/protected_branches', params=params, headers=headers).json() - -# Add each student as project's developer (level 30) -for user_id in user_ids: - params = {'user_id': user_id, 'access_level': access_level} - if args.expires_at: - params['expires_at'] = args.expires_at - new_user = requests.post(base_url + '/projects/' + str( - project['id']) + '/members', params=params, headers=headers).json() - if 'message' in new_user: - print('Error in adding user: %s' % new_user) - else: - out = ("Adding '" + new_user['name'] + "' (" + new_user['username'] + ") in '" - + project['name'] + "' with access level: " + str(new_user['access_level'])) - if args.expires_at: - out += ", expires at: " + new_user['expires_at'] - print(out) - -# Do not forget : students have to add second remote in their local repositories for pulling last changes.