|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 4 | + |
| 5 | +import argparse |
| 6 | +import configparser |
| 7 | +import filecmp |
| 8 | +import os |
| 9 | +import pathlib |
| 10 | +import shutil |
| 11 | +import subprocess |
| 12 | +import sys |
| 13 | +import tempfile |
| 14 | + |
| 15 | +def directory_compare(dir1, dir2, ignore): |
| 16 | + compared = filecmp.dircmp(dir1, dir2, ignore=ignore) |
| 17 | + if compared.left_only or compared.right_only or compared.diff_files: |
| 18 | + return False |
| 19 | + for common_dir in compared.common_dirs: |
| 20 | + path1 = os.path.join(dir1, common_dir) |
| 21 | + path2 = os.path.join(dir2, common_dir) |
| 22 | + if not directory_compare(path1, path2, ignore): |
| 23 | + return False |
| 24 | + return True |
| 25 | + |
| 26 | +class BemanModule: |
| 27 | + def __init__(self, dirpath, remote, commit_hash): |
| 28 | + self.dirpath = dirpath |
| 29 | + self.remote = remote |
| 30 | + self.commit_hash = commit_hash |
| 31 | + |
| 32 | +def parse_beman_module_file(path): |
| 33 | + config = configparser.ConfigParser() |
| 34 | + read_result = config.read(path) |
| 35 | + def fail(): |
| 36 | + raise Exception(f'Failed to parse {path} as a .beman_module file') |
| 37 | + if not read_result: |
| 38 | + fail() |
| 39 | + if not 'beman_module' in config: |
| 40 | + fail() |
| 41 | + if not 'remote' in config['beman_module']: |
| 42 | + fail() |
| 43 | + if not 'commit_hash' in config['beman_module']: |
| 44 | + fail() |
| 45 | + return BemanModule( |
| 46 | + str(pathlib.Path(path).resolve().parent), |
| 47 | + config['beman_module']['remote'], config['beman_module']['commit_hash']) |
| 48 | + |
| 49 | +def get_beman_module(dir): |
| 50 | + beman_module_filepath = os.path.join(dir, '.beman_module') |
| 51 | + if os.path.isfile(beman_module_filepath): |
| 52 | + return parse_beman_module_file(beman_module_filepath) |
| 53 | + else: |
| 54 | + return None |
| 55 | + |
| 56 | +def find_beman_modules_in(dir): |
| 57 | + assert os.path.isdir(dir) |
| 58 | + result = [] |
| 59 | + for dirpath, _, filenames in os.walk(dir): |
| 60 | + if '.beman_module' in filenames: |
| 61 | + result.append(parse_beman_module_file(os.path.join(dirpath, '.beman_module'))) |
| 62 | + return sorted(result, key=lambda module: module.dirpath) |
| 63 | + |
| 64 | +def cwd_git_repository_path(): |
| 65 | + process = subprocess.run( |
| 66 | + ['git', 'rev-parse', '--show-toplevel'], capture_output=True, text=True, |
| 67 | + check=False) |
| 68 | + if process.returncode == 0: |
| 69 | + return process.stdout.strip() |
| 70 | + elif "fatal: not a git repository" in process.stderr: |
| 71 | + return None |
| 72 | + else: |
| 73 | + raise Exception("git rev-parse --show-toplevel failed") |
| 74 | + |
| 75 | +def clone_beman_module_into_tmpdir(beman_module, remote): |
| 76 | + tmpdir = tempfile.TemporaryDirectory() |
| 77 | + subprocess.run( |
| 78 | + ['git', 'clone', beman_module.remote, tmpdir.name], capture_output=True, |
| 79 | + check=True) |
| 80 | + if not remote: |
| 81 | + subprocess.run( |
| 82 | + ['git', '-C', tmpdir.name, 'reset', '--hard', beman_module.commit_hash], |
| 83 | + capture_output=True, check=True) |
| 84 | + return tmpdir |
| 85 | + |
| 86 | +def beman_module_status(beman_module): |
| 87 | + tmpdir = clone_beman_module_into_tmpdir(beman_module, False) |
| 88 | + if directory_compare(tmpdir.name, beman_module.dirpath, ['.beman_module', '.git']): |
| 89 | + status_character=' ' |
| 90 | + else: |
| 91 | + status_character='+' |
| 92 | + parent_repo_path = cwd_git_repository_path() |
| 93 | + if not parent_repo_path: |
| 94 | + raise Exception('this is not a git repository') |
| 95 | + relpath = pathlib.Path( |
| 96 | + beman_module.dirpath).relative_to(pathlib.Path(parent_repo_path)) |
| 97 | + return status_character + ' ' + beman_module.commit_hash + ' ' + str(relpath) |
| 98 | + |
| 99 | +def beman_module_update(beman_module, remote): |
| 100 | + tmpdir = clone_beman_module_into_tmpdir(beman_module, remote) |
| 101 | + shutil.rmtree(beman_module.dirpath) |
| 102 | + with open(os.path.join(tmpdir.name, '.beman_module'), 'w') as f: |
| 103 | + f.write('[beman_module]\n') |
| 104 | + f.write(f'remote={beman_module.remote}\n') |
| 105 | + f.write(f'commit_hash={beman_module.commit_hash}\n') |
| 106 | + shutil.rmtree(os.path.join(tmpdir.name, '.git')) |
| 107 | + shutil.copytree(tmpdir.name, beman_module.dirpath) |
| 108 | + |
| 109 | +def update_command(remote, path): |
| 110 | + if not path: |
| 111 | + parent_repo_path = cwd_git_repository_path() |
| 112 | + if not parent_repo_path: |
| 113 | + raise Exception('this is not a git repository') |
| 114 | + beman_modules = find_beman_modules_in(parent_repo_path) |
| 115 | + else: |
| 116 | + beman_module = get_beman_module(path) |
| 117 | + if not beman_module: |
| 118 | + raise Exception(f'{path} is not a beman_module') |
| 119 | + beman_modules = [beman_module] |
| 120 | + for beman_module in beman_modules: |
| 121 | + beman_module_update(beman_module, remote) |
| 122 | + |
| 123 | +def add_command(repository, path): |
| 124 | + tmpdir = tempfile.TemporaryDirectory() |
| 125 | + subprocess.run( |
| 126 | + ['git', 'clone', repository], capture_output=True, check=True, cwd=tmpdir.name) |
| 127 | + repository_name = os.listdir(tmpdir.name)[0] |
| 128 | + if not path: |
| 129 | + path = repository_name |
| 130 | + if os.path.exists(path): |
| 131 | + raise Exception(f'{path} exists') |
| 132 | + os.makedirs(path) |
| 133 | + tmpdir_repo = os.path.join(tmpdir.name, repository_name) |
| 134 | + sha_process = subprocess.run( |
| 135 | + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, |
| 136 | + cwd=tmpdir_repo) |
| 137 | + with open(os.path.join(tmpdir_repo, '.beman_module'), 'w') as f: |
| 138 | + f.write('[beman_module]\n') |
| 139 | + f.write(f'remote={repository}\n') |
| 140 | + f.write(f'commit_hash={sha_process.stdout.strip()}\n') |
| 141 | + shutil.rmtree(os.path.join(tmpdir_repo, '.git')) |
| 142 | + shutil.copytree(tmpdir_repo, path, dirs_exist_ok=True) |
| 143 | + |
| 144 | +def status_command(paths): |
| 145 | + if not paths: |
| 146 | + parent_repo_path = cwd_git_repository_path() |
| 147 | + if not parent_repo_path: |
| 148 | + raise Exception('this is not a git repository') |
| 149 | + beman_modules = find_beman_modules_in(parent_repo_path) |
| 150 | + else: |
| 151 | + beman_modules = [] |
| 152 | + for path in paths: |
| 153 | + beman_module = get_beman_module(path) |
| 154 | + if not beman_module: |
| 155 | + raise Exception(f'{path} is not a beman_module') |
| 156 | + beman_modules.append(beman_module) |
| 157 | + for beman_module in beman_modules: |
| 158 | + print(beman_module_status(beman_module)) |
| 159 | + |
| 160 | +def get_parser(): |
| 161 | + parser = argparse.ArgumentParser(description='Beman pseudo-submodule tool') |
| 162 | + subparsers = parser.add_subparsers(dest='command', help='available commands') |
| 163 | + parser_update = subparsers.add_parser('update', help='update beman_modules') |
| 164 | + parser_update.add_argument( |
| 165 | + '--remote', action='store_true', |
| 166 | + help='update a beman_module to its latest from upstream') |
| 167 | + parser_update.add_argument( |
| 168 | + 'beman_module_path', nargs='?', |
| 169 | + help='relative path to the beman_module to update') |
| 170 | + parser_add = subparsers.add_parser('add', help='add a new beman_module') |
| 171 | + parser_add.add_argument('repository', help='git repository to add') |
| 172 | + parser_add.add_argument( |
| 173 | + 'path', nargs='?', help='path where the repository will be added') |
| 174 | + parser_status = subparsers.add_parser( |
| 175 | + 'status', help='show the status of beman_modules') |
| 176 | + parser_status.add_argument('paths', nargs='*') |
| 177 | + return parser |
| 178 | + |
| 179 | +def parse_args(args): |
| 180 | + return get_parser().parse_args(args); |
| 181 | + |
| 182 | +def usage(): |
| 183 | + return get_parser().format_help() |
| 184 | + |
| 185 | +def run_command(args): |
| 186 | + if args.command == 'update': |
| 187 | + update_command(args.remote, args.beman_module_path) |
| 188 | + elif args.command == 'add': |
| 189 | + add_command(args.repository, args.path) |
| 190 | + elif args.command == 'status': |
| 191 | + status_command(args.paths) |
| 192 | + else: |
| 193 | + raise Exception(usage()) |
| 194 | + |
| 195 | +def check_for_git(path): |
| 196 | + env = os.environ.copy() |
| 197 | + if path is not None: |
| 198 | + env["PATH"] = path |
| 199 | + return shutil.which("git", path=env.get("PATH")) is not None |
| 200 | + |
| 201 | +def main(): |
| 202 | + try: |
| 203 | + if not check_for_git(None): |
| 204 | + raise Exception('git not found in PATH') |
| 205 | + args = parse_args(sys.argv[1:]) |
| 206 | + run_command(args) |
| 207 | + except Exception as e: |
| 208 | + print("Error:", e, file=sys.stderr) |
| 209 | + sys.exit(1) |
| 210 | + |
| 211 | +if __name__ == '__main__': |
| 212 | + main() |
0 commit comments