#!/usr/bin/env python import argparse import json import os import pathlib import sys import time import yaml from anytree import RenderTree, Node, AnyNode from anytree.importer import JsonImporter from gitlab import Gitlab def read_cli_arguments(argv: list=None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Build a tree of Gitlab groups and projects") group = parser.add_mutually_exclusive_group() group.add_argument("-d", "--dict", action="store_true", help="Output as Python dictionary") group.add_argument("-j", "--json", action="store_true", help="Output as JSON") group.add_argument("-t", "--tree", action="store_true", help="Output as tree (default)") parser.add_argument("-f", "--force", action="store_true", help="Force cache refresh (~/.config/glgtree/cache.json)") parser.add_argument("-g", "--group", type=int, help="Gitlab group to base output from") return parser.parse_args(argv) def create_cache() -> None: cache_dir = pathlib.Path.home() / '.config/glgtree/' if not cache_dir.is_dir(): cache_dir.mkdir() cache_file = pathlib.Path(cache_dir / 'cache.json') cache_file.touch() return cache_file def read_cache() -> str: cache_dir = pathlib.Path.home() / '.config/glgtree/' cache_file = pathlib.Path(cache_dir / 'cache.json') with open(cache_file) as f: json_data = json.load(f) return json_data def update_cache() -> None: token = read_token() connection = create_connection(token) # Gather groups, projects and establish hierarchy projects = read_projects(connection) groups = read_groups(connection) tree = organise_items(projects, groups) json_data = json.dumps(tree) cache_dir = pathlib.Path.home() / '.config/glgtree/' cache_file = pathlib.Path(cache_dir / 'cache.json') with open(cache_file, 'w') as f: json.dump(json_data, f) def read_token() -> str: # Check if GITLAB_TOKEN environment variable exists if 'GITLAB_TOKEN' in os.environ: return os.environ['GITLAB_TOKEN'] # Check if PAT token exists in glab configuration file glab_config_path = os.path.expanduser("~/.config/glab-cli/config.yml") if os.path.exists(glab_config_path): with open(glab_config_path, 'r') as config_file: glab_config = yaml.safe_load(config_file) if 'gitlab_token' in glab_config: return glab_config['gitlab_token'] # Prompt the user to enter a PAT token return input("Enter your GitLab PAT token: ") def create_connection(gitlab_token: str) -> Gitlab: gl = Gitlab(private_token=gitlab_token) gl.auth() return gl def read_projects(gitlab_connection: Gitlab) -> list: project_list = [] for project in gitlab_connection.projects.list(get_all=True, visibility='private'): project_dict = { 'id': project.id, 'name': project.name, 'parent_id': project.namespace['id'], 'object_kind':'project', 'object_icon':'📗' } project_list.append(project_dict) return project_list def read_groups(gitlab_connection: Gitlab) -> list: group_list = [] for group in gitlab_connection.groups.list(get_all=True, visibility='private'): group_dict = { 'id': group.id, 'name': group.name, 'parent_id': group.parent_id, 'object_kind':'group', 'object_icon':'📁', 'children': [] } group_list.append(group_dict) return group_list def organise_items(projects: list, groups: list) -> dict: # merge projects and groups # Create a dictionary where each parent_id is a key and its value is a list of groups with that parent_id parent_groups = {} for group in groups: parent_id = group['parent_id'] if parent_id is None: continue if parent_id not in parent_groups: parent_groups[parent_id] = [] parent_groups[parent_id].append(group) # Assign children to their respective parent groups for group in groups: group['children'] = parent_groups.get(group['id'], []) # Move the projects to their parent groups for project in projects: parent_id = project['parent_id'] # Find the parent group parent_group = next((group for group in groups if group['id'] == parent_id), None) if parent_group: parent_group['children'].append(project) for parent in groups: if parent['parent_id'] == None: parent_dict = parent return parent_dict def read_json_child(json_data: dict, id: int) -> dict: if json_data.get("id") == id: return json_data # Check if the current object has children if "children" in json_data: # Iterate through children recursively for child in json_data["children"]: result = read_json_child(child, id) if result: return result # If the id is not found in the current object or its children return None def print_tree(json_data: str) -> None: importer = JsonImporter() root = importer.import_(json_data) for pre, fill, node in RenderTree(root): print("%s%s %s (%s)" % (pre, node.object_icon, node.name, node.id)) def main(argv): # Gather command line arguments args = read_cli_arguments(argv) cache_file = pathlib.Path.home() / '.config/glgtree/cache.json' if args.force: cache_timeout = 0 else: cache_timeout = 24 * 3600 # check for cache file existance, create if missing if (not pathlib.Path(cache_file).exists()) or (not pathlib.Path(cache_file).is_file()): create_cache() update_cache() # check for cached JSON file expiry if (time.time() - pathlib.Path(cache_file).stat().st_mtime > cache_timeout) or (pathlib.Path(cache_file).stat().st_size == 0): update_cache() json_string = read_cache() json_data = json.loads(json_string) if args.group: json_data = read_json_child(json_data, args.group) if args.json: print(json.dumps(json_data)) elif args.dict: print(json_data) elif args.tree: print_tree(json.dumps(json_data)) else: print_tree(json.dumps(json_data)) if __name__ == "__main__": main(sys.argv[1:])