diff --git a/.github/workflows/nos.yml b/.github/workflows/nos.yml index 52cf572..c82d49d 100644 --- a/.github/workflows/nos.yml +++ b/.github/workflows/nos.yml @@ -4,13 +4,14 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + timeout-minutes: 10 strategy: matrix: - os: [ubuntu-20.04, macos-latest, windows-latest] - python-version: [3.6, 3.7, 3.8, 3.9, "3.10", "3.11"] + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: diff --git a/gita/__main__.py b/gita/__main__.py index 7955133..865ea13 100644 --- a/gita/__main__.py +++ b/gita/__main__.py @@ -25,8 +25,9 @@ from itertools import chain from pathlib import Path import glob +from typing import Dict, Optional -from . import utils, info, common +from . import utils, info, common, io def _group_name(name: str, exclude_old_names=True) -> str: @@ -153,7 +154,7 @@ def f_clone(args: argparse.Namespace): if args.dry_run: if args.from_file: - for url, repo_name, abs_path in utils.parse_clone_config(args.clonee): + for url, repo_name, abs_path in io.parse_clone_config(args.clonee): print(f"git clone {url} {abs_path}") else: print(f"git clone {args.clonee}") @@ -173,28 +174,35 @@ def f_clone(args: argparse.Namespace): f_add(args) return + # TODO: add repos to group too + repos, groups = io.parse_clone_config(args.clonee) if args.preserve_path: utils.exec_async_tasks( - utils.run_async(repo_name, path, ["git", "clone", url, abs_path]) - for url, repo_name, abs_path in utils.parse_clone_config(args.clonee) + utils.run_async(repo_name, path, ["git", "clone", r["url"], r["path"]]) + for repo_name, r in repos.items() ) else: utils.exec_async_tasks( - utils.run_async(repo_name, path, ["git", "clone", url]) - for url, repo_name, _ in utils.parse_clone_config(args.clonee) + utils.run_async(repo_name, path, ["git", "clone", r["url"]]) + for repo_name, r in repos.items() ) def f_freeze(args): - repos = utils.get_repos() + """ + print repo and group information for future cloning + """ ctx = utils.get_context() if args.group is None and ctx: args.group = ctx.stem + repos = utils.get_repos() + group_name = args.group group_repos = None - if args.group: # only display repos in this group - group_repos = utils.get_groups()[args.group]["repos"] + if group_name: # only display repos in this group + group_repos = utils.get_groups()[group_name]["repos"] repos = {k: repos[k] for k in group_repos if k in repos} seen = {""} + # print(repos) for name, prop in repos.items(): path = prop["path"] url = "" @@ -212,7 +220,16 @@ def f_freeze(args): url = parts[1] if url not in seen: seen.add(url) - print(f"{url},{name},{path}") + # TODO: add another field to distinguish regular repo or worktree or submodule + print(f"{url},{name},{path},") + # group information: these lines don't have URL + if group_name: + group_path = utils.get_groups()[group_name]["path"] + print(f",{group_name},{group_path},{'|'.join(group_repos)}") + else: # show all groups + for gname, g in utils.get_groups().items(): + group_repos = "|".join(g["repos"]) + print(f",{gname},{g['path']},{group_repos}") def f_ll(args: argparse.Namespace): diff --git a/gita/io.py b/gita/io.py new file mode 100644 index 0000000..78665b7 --- /dev/null +++ b/gita/io.py @@ -0,0 +1,33 @@ +import os +import csv +from typing import Tuple + + +def parse_clone_config(fname: str) -> Tuple: + """ + Return the repo information (url, name, path, type) and group information + (, name, path, repos) saved in `fname`. + """ + repos = {} + groups = {} + if os.path.isfile(fname) and os.stat(fname).st_size > 0: + with open(fname) as f: + rows = csv.DictReader( + f, ["url", "name", "path", "type", "flags"], restval="" + ) # it's actually a reader + for r in rows: + if r["url"]: + repos[r["name"]] = { + "path": r["path"], + "type": r["type"], + "flags": r["flags"].split(), + "url": r["url"], + } + else: + groups[r["name"]] = { + "path": r["path"], + "repos": [ + repo for repo in r["type"].split("|") if repo in repos + ], + } + return repos, groups diff --git a/gita/utils.py b/gita/utils.py index 6746d7f..a623666 100644 --- a/gita/utils.py +++ b/gita/utils.py @@ -7,7 +7,7 @@ import subprocess from functools import lru_cache from pathlib import Path -from typing import List, Dict, Coroutine, Union, Iterator, Tuple +from typing import List, Dict, Coroutine, Union, Tuple from collections import Counter, defaultdict from concurrent.futures import ThreadPoolExecutor import multiprocessing @@ -367,15 +367,6 @@ def auto_group(repos: Dict[str, Dict[str, str]], paths: List[str]) -> Dict[str, return new_groups -def parse_clone_config(fname: str) -> Iterator[List[str]]: - """ - Return the url, name, and path of all repos in `fname`. - """ - with open(fname) as f: - for line in f: - yield line.strip().split(",") - - async def run_async(repo_name: str, path: str, cmds: List[str]) -> Union[None, str]: """ Run `cmds` asynchronously in `path` directory. Return the `path` if diff --git a/setup.py b/setup.py index 38fb607..b656964 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="gita", packages=["gita"], - version="0.16.6.5", + version="0.16.7", license="MIT", description="Manage multiple git repos with sanity", long_description=long_description, @@ -30,10 +30,11 @@ "Topic :: Software Development :: Version Control :: Git", "Topic :: Terminals", "Topic :: Utilities", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], include_package_data=True, ) diff --git a/tests/test_main.py b/tests/test_main.py index 5a15b3e..03b15d4 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -196,8 +196,11 @@ def test_clone_with_url(mock_run): @patch( - "gita.utils.parse_clone_config", - return_value=[["git@github.com:user/repo.git", "repo", "/a/repo"]], + "gita.io.parse_clone_config", + return_value=( + {"repo": {"url": "git@github.com:user/repo.git", "path": "/a/repo"}}, + {}, + ), ) @patch("gita.utils.run_async", new=async_mock()) @patch("subprocess.run") @@ -217,8 +220,11 @@ def test_clone_with_config_file(*_): @patch( - "gita.utils.parse_clone_config", - return_value=[["git@github.com:user/repo.git", "repo", "/a/repo"]], + "gita.io.parse_clone_config", + return_value=( + {"repo": {"url": "git@github.com:user/repo.git", "path": "/a/repo"}}, + {}, + ), ) @patch("gita.utils.run_async", new=async_mock()) @patch("subprocess.run")