From aff9af832df426bdf1316715a0189b15b08829ad Mon Sep 17 00:00:00 2001
From: "steven.liatti" <steven@liatti.ch>
Date: Wed, 11 Nov 2020 19:55:31 +0100
Subject: [PATCH] Add list functionality

---
 README.md |  21 ++++++++-
 pwm       | 128 ++++++++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 134 insertions(+), 15 deletions(-)
 mode change 100755 => 100644 pwm

diff --git a/README.md b/README.md
index 54f9fd7..2a508c5 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,18 @@ Programme python pour gérer les travaux pratiques des étudiants avec la contra
 
 ## TL;DR
 
-- Créer un groupe et les repositories en une seule commande (avec un repository "image" optionnel) : `python 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 : `python pwm -t TOKEN clone ID DIRECTORY`
+- Créer un groupe et les repositories en une seule commande, avec un repository "image" optionnel et un fichier d'emails (voir [Syntaxe du fichier YAML (REPOS_FILE)](#syntaxe-du-fichier-yaml-repos_file) :
+  ```bash
+  python pwm -t TOKEN group_repos GROUP_NAME REPOS_FILE [-i IMPORT_URL]
+  ```
+- Clone tous les projets des étudiants d'un groupe gitlab (`ID`) dans un répertoire créé à la volée :
+  ```bash
+  python pwm -t TOKEN clone ID DIRECTORY
+  ```
+- Liste les noms des projets dans un groupe :
+  ```bash
+  python pwm list ID
+  ```
 
 - [Practical Work Manager (pwm)](#practical-work-manager-pwm)
   - [TL;DR](#tldr)
@@ -15,6 +25,7 @@ Programme python pour gérer les travaux pratiques des étudiants avec la contra
     - [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)
+    - [Liste des projets d'un groupe ou des membres d'un projet](#liste-des-projets-dun-groupe-ou-des-membres-dun-projet)
   - [Syntaxe du fichier YAML (REPOS_FILE)](#syntaxe-du-fichier-yaml-repos_file)
     - [Noms et emails](#noms-et-emails)
     - [Emails seulement](#emails-seulement)
@@ -89,6 +100,12 @@ python pwm clone [-h] [-g | -f] [-u UNTIL_DATE] ID DIRECTORY
 ```
 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.
 
+### Liste des projets d'un groupe ou des membres d'un projet
+```bash
+python pwm list [-h] [-p | -m] [-s SHOW] ID
+```
+Liste les projets d'un groupe ou les membres d'un projet selon l'id `ID`. L'argument `--show` (ou `-s`) permet de choisir quelles informations à afficher (par défaut le nom) : `[all | name | url | ssh]`.
+
 ## Syntaxe du fichier YAML (REPOS_FILE)
 Le fichier YAML doit respecter une des deux syntaxes suivantes.
 
diff --git a/pwm b/pwm
old mode 100755
new mode 100644
index 9f85c32..523c97c
--- a/pwm
+++ b/pwm
@@ -8,11 +8,13 @@ Steven Liatti
 
 from argparse import Namespace
 import os
-from typing import Dict, List, Optional
+from typing import Any, Dict, List, Optional
 import requests
 import subprocess
 import argparse
+from requests.models import Response
 import yaml
+import json
 
 BASE_URL: str = 'https://gitedu.hesge.ch/api/v4'
 
@@ -112,6 +114,59 @@ def create_repository(token: str, group_id: str, emails: List[str], name: str, i
             print(out)
 
 
+def paginate_responses(url: str, headers: Dict[str, str], params: Dict[str, Any]) -> List[Response]:
+    """
+    Manage gitlab pagination, max 100 results by request
+    """
+    responses = [requests.get(url, params=params, headers=headers)]
+    last_response = responses[len(responses) - 1]
+
+    while last_response.status_code == 200 and len(last_response.headers['X-Next-Page']) != 0:
+        next_page = last_response.headers['X-Next-Page']
+        params['page'] = next_page
+        responses.append(requests.get(url, params=params, headers=headers))
+        last_response = responses[len(responses) - 1]
+
+    return responses
+
+
+def get_members(token: str, id: str) -> List:
+    """
+    Return members list from given id
+    """
+    url = BASE_URL + '/projects/' + id + '/members'
+
+    headers = {'PRIVATE-TOKEN': token}
+    params = {'simple': 'true', 'order_by': 'name',
+              'sort': 'asc', 'per_page': 100}
+    responses = paginate_responses(url, headers, params)
+
+    members = []
+    for r in responses:
+        members += r.json()
+    return members
+
+
+def get_projects(token: str, id: str, source: str = 'group') -> List:
+    """
+    Return projects list from given id and source ('group' or 'forks')
+    """
+    if source == 'forks':
+        url = BASE_URL + '/projects/' + id + '/forks'
+    else:
+        url = BASE_URL + '/groups/' + id + '/projects'
+
+    headers = {'PRIVATE-TOKEN': token}
+    params = {'simple': 'true', 'order_by': 'name',
+              'sort': 'asc', 'per_page': 100}
+    responses = paginate_responses(url, headers, params)
+
+    projects = []
+    for r in responses:
+        projects += r.json()
+    return projects
+
+
 def clone_all(token: str, id: str, directory: str, until_date: Optional[str], source: str = 'group'):
     """
     Clone all repositories (from a group or "forks of") from id (group or
@@ -123,18 +178,8 @@ def clone_all(token: str, id: str, directory: str, until_date: Optional[str], so
         print("Creation of the directory '%s' failed, exit\n" % directory)
         exit(1)
 
-    params = {'simple': 'true', 'per_page': 100}
     headers = {'PRIVATE-TOKEN': token}
-
-    if source == 'forks':
-        url = BASE_URL + '/projects/' + id + '/forks'
-    else:
-        url = BASE_URL + '/groups/' + id + '/projects'
-
-    repositories = requests.get(url, params=params, headers=headers).json()
-    if 'message' in repositories:
-        print('Error retrieving repositories: ' + repositories['message'])
-        exit(1)
+    repositories = get_projects(token, id, source)
 
     for repo in repositories:
         repo_url = BASE_URL + '/projects/' + str(repo['id']) + '/members'
@@ -234,6 +279,50 @@ def command_clone_all(args):
         clone_all(args.token, args.id, args.directory, args.until_date)
 
 
+def command_list(args):
+    """
+    Call get_projects or get_members
+    """
+    if args.members:
+        members = get_members(args.token, args.id)
+        if args.show:
+            if args.show == 'all':
+                print(json.dumps(members, indent=2))
+            elif args.show == 'url':
+                results = list(map(lambda p: p['web_url'], members))
+                for r in results:
+                    print(r)
+            else:
+                names = list(map(lambda p: p['username'], members))
+                for name in names:
+                    print(name)
+        else:
+            names = list(map(lambda p: p['username'], members))
+            for name in names:
+                print(name)
+    else:
+        projects = get_projects(args.token, args.id)
+        if args.show:
+            if args.show == 'all':
+                print(json.dumps(projects, indent=2))
+            elif args.show == 'url':
+                results = list(map(lambda p: p['http_url_to_repo'], projects))
+                for r in results:
+                    print(r)
+            elif args.show == 'ssh':
+                results = list(map(lambda p: p['ssh_url_to_repo'], projects))
+                for r in results:
+                    print(r)
+            else:
+                names = list(map(lambda p: p['name'], projects))
+                for name in names:
+                    print(name)
+        else:
+            names = list(map(lambda p: p['name'], projects))
+            for name in names:
+                print(name)
+
+
 if __name__ == '__main__':
     parser = argparse.ArgumentParser(description='Practical Work Manager - \
         Manage students PW - Create group, projects or clone repositories')
@@ -241,7 +330,7 @@ if __name__ == '__main__':
     parser.add_argument("-t", "--token", metavar="TOKEN",
                         help="Create a token here: https://gitedu.hesge.ch/profile/personal_access_tokens")
     subparsers = parser.add_subparsers(
-        metavar='(group_repos | group | repo | clone)')
+        metavar='(group_repos | group | repo | clone | list)')
 
     parser_group_repos = subparsers.add_parser(
         'group_repos', help='Create group and repos associated')
@@ -292,6 +381,19 @@ if __name__ == '__main__':
         "-u", "--until_date", help="Do a git checkout for all repositories at given date, format \"YYYY-MM-DD hh:mm\" (optional).")
     parser_clone.set_defaults(func=command_clone_all)
 
+    parser_list = subparsers.add_parser(
+        'list', help="List group's projects or project's members")
+    group_list = parser_list.add_mutually_exclusive_group()
+    group_list.add_argument(
+        "-p", "--projects", action="store_true", help="List group's projects (default")
+    group_list.add_argument(
+        "-m", "--members", action="store_true", help="List project's members")
+    parser_list.add_argument(
+        "id", metavar="ID", help="The group_id or the project_id (int).")
+    parser_list.add_argument(
+        "-s", "--show", help="Amount of informations (default name) : [all | name | url | ssh]")
+    parser_list.set_defaults(func=command_list)
+
     args = parser.parse_args()
 
     if not args.token:
-- 
GitLab