From 3f187deeff065eadfad9503a6d89ae0bac03b1c2 Mon Sep 17 00:00:00 2001 From: Matt Thompson Date: Thu, 10 Apr 2025 10:11:35 -0400 Subject: [PATCH 1/9] Add README info for uv (#354) --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index f3fdf22f..d0ef5ed2 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,24 @@ To install `mepo` using `pip`, run the following command: pip install mepo ``` +### Using uv + +#### uv install + +You can install `mepo` using the `uv` package manager. To do so, run the following command: + +``` +uv install mepo +``` + +#### uvx + +If you'd like to run `mepo` without installing it, you can use `uvx` to run it directly: + +``` +uvx mepo +``` + ### Homebrew Using Homebrew, you can install `mepo` by installing from the gmao-si-team tap: From 14de417ebd5f2600f0f43474cdff3a85b6af43ee Mon Sep 17 00:00:00 2001 From: Matt Thompson Date: Thu, 24 Apr 2025 15:34:35 -0400 Subject: [PATCH 2/9] GitFlow: Merge main into develop after hotfix (#357) Co-authored-by: Purnendu Chakraborty Fixes #355. Allow for fixture without develop key (#356) Fixes #355. Allow for fixture without develop key Fix issue with checkout and submodules (#359) --- CHANGELOG.md | 12 ++++++++++++ pyproject.toml | 2 +- src/mepo/command/checkout-if-exists.py | 4 +++- src/mepo/command/checkout.py | 2 +- src/mepo/command/clone.py | 6 +++--- src/mepo/command/develop.py | 2 +- src/mepo/command/restore-state.py | 2 +- src/mepo/git.py | 6 ++++-- src/mepo/registry.py | 6 ++++-- tests/test_mepo_commands.py | 2 +- 10 files changed, 31 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76ba6c56..7be15c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +## [2.3.2] - 2025-04-24 + +### Fixed + +- Fixed `checkout` command to use `--recurse-submodules` if component has submodules + +## [2.3.1] - 2025-04-16 + +### Fixed + +- Fixed `clone` to allow for fixtures without a `develop:` key in `components.yaml` + ## [2.3.0] - 2025-01-12 ### Changed diff --git a/pyproject.toml b/pyproject.toml index d11854e5..fb7c7ec0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mepo" -version = "2.3.0" +version = "2.3.2" description = "A tool for managing (m)ultiple r(epo)s" authors = [{name="GMAO SI Team", email="siteam@gmao.gsfc.nasa.gov"}] dependencies = [ diff --git a/src/mepo/command/checkout-if-exists.py b/src/mepo/command/checkout-if-exists.py index bb596284..6a20db72 100644 --- a/src/mepo/command/checkout-if-exists.py +++ b/src/mepo/command/checkout-if-exists.py @@ -30,4 +30,6 @@ def run(args): colors.RESET + comp.name + colors.RESET, ) ) - git.checkout(ref_name, args.detach) + git.checkout( + ref_name, detach=args.detach, recurse=comp.recurse_submodules + ) diff --git a/src/mepo/command/checkout.py b/src/mepo/command/checkout.py index ae767737..b9547533 100644 --- a/src/mepo/command/checkout.py +++ b/src/mepo/command/checkout.py @@ -30,7 +30,7 @@ def run(args): colors.RESET + comp.name + colors.RESET, ) ) - git.checkout(branch, args.detach) + git.checkout(branch, detach=args.detach, recurse=comp.recurse_submodules) def _get_comps_to_checkout(specified_comps, allcomps): diff --git a/src/mepo/command/clone.py b/src/mepo/command/clone.py index 599692ea..fecab8da 100644 --- a/src/mepo/command/clone.py +++ b/src/mepo/command/clone.py @@ -68,7 +68,7 @@ def clone_fixture(url, branch=None, directory=None, partial=None): last_url_node = p.path.rsplit("/")[-1] directory = pathlib.Path(last_url_node).stem git = GitRepository(url, directory) - git.clone(branch, partial) + git.clone(branch, partial=partial) return directory @@ -105,7 +105,7 @@ def clone_components(allcomps, partial): version = comp.version.name version = version.replace("origin/", "") git = GitRepository(comp.remote, comp.local) - git.clone(version, recurse_submodules, partial) + git.clone(version, recurse=recurse_submodules, partial=partial) if comp.sparse: git.sparsify(comp.sparse) print_clone_info(comp.name, comp.version, max_namelen) @@ -123,4 +123,4 @@ def checkout_all_repos(allcomps, branch): branch_y = colors.YELLOW + branch + colors.RESET print(f"Checking out {branch_y} in {comp.name}") git = GitRepository(comp.remote, comp.local) - git.checkout(branch) + git.checkout(branch, recurse=comp.recurse_submodules) diff --git a/src/mepo/command/develop.py b/src/mepo/command/develop.py index e679955e..58d38287 100644 --- a/src/mepo/command/develop.py +++ b/src/mepo/command/develop.py @@ -20,5 +20,5 @@ def run(args): colors.RESET + comp.name + colors.RESET, ) ) - git.checkout(comp.develop) + git.checkout(comp.develop, recurse=comp.recurse_submodules) _ = git.pull() diff --git a/src/mepo/command/restore-state.py b/src/mepo/command/restore-state.py index 3d9fe86c..e520648b 100644 --- a/src/mepo/command/restore-state.py +++ b/src/mepo/command/restore-state.py @@ -41,4 +41,4 @@ def restore_state(allcomps, result): colors.RED + current_version + colors.RESET, ) ) - git.checkout(comp.version.name) + git.checkout(comp.version.name, recurse=comp.recurse_submodules) diff --git a/src/mepo/git.py b/src/mepo/git.py index deb223db..7a035e0f 100644 --- a/src/mepo/git.py +++ b/src/mepo/git.py @@ -60,10 +60,12 @@ def clone(self, version=None, recurse=None, partial=None): shellcmd.run(shlex.split(cmd)) if version is not None: - self.checkout(version) + self.checkout(version, recurse=recurse) - def checkout(self, version, detach=False): + def checkout(self, version, detach=False, recurse=None): cmd = self.__git + " checkout " + if recurse is not None: + cmd += " --recurse-submodules " cmd += "--quiet {}".format(version) shellcmd.run(shlex.split(cmd)) if detach: diff --git a/src/mepo/registry.py b/src/mepo/registry.py index e5eb9cf3..36351cf2 100644 --- a/src/mepo/registry.py +++ b/src/mepo/registry.py @@ -35,9 +35,11 @@ def __validate(self, d): num_fixtures = 0 for k, v in d.items(): if "fixture" in v: - # In case of a fixture, develop is the only additional key + # In case of a fixture, develop is the only additional allowed key num_fixtures += 1 - assert list(v.keys()) == ["fixture", "develop"] + required_ = ["fixture"] + optional_ = ["develop"] + assert list(v.keys()) in (required_, required_ + optional_) else: # For non-fixture, one and only one of branch/tag/hash allowed xsection = git_tag_types.intersection(set(v.keys())) diff --git a/tests/test_mepo_commands.py b/tests/test_mepo_commands.py index bdf4c712..f9b93794 100644 --- a/tests/test_mepo_commands.py +++ b/tests/test_mepo_commands.py @@ -337,7 +337,7 @@ def test_reset(self): self.__class__.__mepo_clone() def test_mepo_version(self): - self.assertEqual(get_mepo_version(), "2.3.0") + self.assertEqual(get_mepo_version(), "2.3.2") def tearDown(self): pass From 509d875a02e930db98b7924b61d94cd8cf446aae Mon Sep 17 00:00:00 2001 From: Purnendu Chakraborty Date: Wed, 30 Apr 2025 06:57:06 -0500 Subject: [PATCH 3/9] Fixes #358 - migrate from rye to uv (#361) * Fixes #355. Allow for fixture without develop key (#356) * Fixes #355. Allow for fixture without develop key * Fix per black * A small change to make it just a little easier to add required and optional keys for fixtures --------- Co-authored-by: Purnendu Chakraborty * Fix issue with checkout and submodules (#359) * Fix issue with checkout and submodules * Black formatter * First try at migrating from rye to uv * Updated CHANGELOG.md --------- Co-authored-by: Matt Thompson --- .github/workflows/run-formatter.yaml | 10 ++++++---- .github/workflows/run-linter.yaml | 10 ++++++---- .github/workflows/run-tests.yaml | 14 ++++++++------ CHANGELOG.md | 2 ++ pyproject.toml | 4 ++-- tests/output/output_branch_list.txt | 2 +- tests/test_mepo_commands.py | 2 +- 7 files changed, 26 insertions(+), 18 deletions(-) diff --git a/.github/workflows/run-formatter.yaml b/.github/workflows/run-formatter.yaml index a061e610..2502218d 100644 --- a/.github/workflows/run-formatter.yaml +++ b/.github/workflows/run-formatter.yaml @@ -11,11 +11,13 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Install rye - uses: eifinger/setup-rye@v4 + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "latest" - - name: Sync dependencies - run: rye sync + - name: Install Python and other dependencies + run: uv sync - name: Run black run: | diff --git a/.github/workflows/run-linter.yaml b/.github/workflows/run-linter.yaml index a3251c6c..88bf2234 100644 --- a/.github/workflows/run-linter.yaml +++ b/.github/workflows/run-linter.yaml @@ -10,11 +10,13 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Install rye - uses: eifinger/setup-rye@v4 + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "latest" - - name: Sync dependencies - run: rye sync + - name: Install Python and other dependencies + run: uv sync - name: Run pylint run: | diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index e5f8239e..66a217fd 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -16,15 +16,17 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Install rye - uses: eifinger/setup-rye@v4 + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "latest" - - name: Sync dependencies + - name: Install Python and other dependencies run: | - rye pin ${{ matrix.python-version }} - rye sync + uv python pin ${{ matrix.python-version }} + uv sync - name: Run tests run: | - rye test -v + uv run pytest -v timeout-minutes: 5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be15c08..9ce0a829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Migrated mepo management from rye to uv + ## [2.3.2] - 2025-04-24 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index fb7c7ec0..1f48f247 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mepo" -version = "2.3.2" +version = "2.4.0" description = "A tool for managing (m)ultiple r(epo)s" authors = [{name="GMAO SI Team", email="siteam@gmao.gsfc.nasa.gov"}] dependencies = [ @@ -15,7 +15,7 @@ requires-python = ">= 3.9" [project.scripts] mepo = "mepo.__main__:main" -[tool.rye] +[tool.uv] managed = true dev-dependencies = [ "black>=24.4.2", diff --git a/tests/output/output_branch_list.txt b/tests/output/output_branch_list.txt index 0fb80988..560c7b26 100644 --- a/tests/output/output_branch_list.txt +++ b/tests/output/output_branch_list.txt @@ -4,9 +4,9 @@ ecbuild | * (HEAD detached at geos/v1.3.0) | remotes/origin/develop | remotes/origin/feature/FindMKL-portability-improvement | remotes/origin/feature/ecbuild_use_package_quiet - | remotes/origin/feature/mathomp4/add-jemalloc | remotes/origin/feature/netcdf4-cmake | remotes/origin/geos/main | remotes/origin/master | remotes/origin/mepo-testing-do-not-delete + | remotes/origin/merge-develop-into-MKL-2025Apr28 | remotes/origin/release/stable diff --git a/tests/test_mepo_commands.py b/tests/test_mepo_commands.py index f9b93794..9baa0c10 100644 --- a/tests/test_mepo_commands.py +++ b/tests/test_mepo_commands.py @@ -337,7 +337,7 @@ def test_reset(self): self.__class__.__mepo_clone() def test_mepo_version(self): - self.assertEqual(get_mepo_version(), "2.3.2") + self.assertEqual(get_mepo_version(), "2.4.0") def tearDown(self): pass From 26f51be0f6d66337e2fdccfe7ec6500e23766bc2 Mon Sep 17 00:00:00 2001 From: Purnendu Chakraborty Date: Wed, 30 Apr 2025 08:29:08 -0500 Subject: [PATCH 4/9] Fixes #353 - status ahead behind (#360) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixes #355. Allow for fixture without develop key (#356) * Fixes #355. Allow for fixture without develop key * Fix per black * A small change to make it just a little easier to add required and optional keys for fixtures --------- Co-authored-by: Purnendu Chakraborty * Fix issue with checkout and submodules (#359) * Fix issue with checkout and submodules * Black formatter * Streamlined code for processing output of git status * Moved coloring of On branch feature/pchakrab/#353-status-ahead-behind Your branch is up to date with 'origin/feature/pchakrab/#353-status-ahead-behind'. Changes to be committed: (use "git restore --staged ..." to unstage) modified: command/status.py modified: git.py to command/status.py. Also added ahead/behind/stash info * Minor formatting * Updated CHANGELOG.md * Removed mepo.spec which is not being used --------- Co-authored-by: Matt Thompson --- CHANGELOG.md | 4 + mepo.spec | 51 -------- src/mepo/command/status.py | 73 ++++++++--- src/mepo/git.py | 213 +------------------------------- src/mepo/utilities/statcolor.py | 65 ++++++++++ 5 files changed, 132 insertions(+), 274 deletions(-) delete mode 100644 mepo.spec create mode 100644 src/mepo/utilities/statcolor.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ce0a829..1e46dece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added ahead/behind info to `mepo status` + ### Changed +- Moved coloring of `git status` output from `src/mepo/git.py` to `src/mepo/command/status/py` +- Streamlined output processing of `git status` - Migrated mepo management from rye to uv ## [2.3.2] - 2025-04-24 diff --git a/mepo.spec b/mepo.spec deleted file mode 100644 index ef59192d..00000000 --- a/mepo.spec +++ /dev/null @@ -1,51 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - -import os -import glob - -cmd_dir = os.path.join(SPECPATH, 'src/mepo/command') -cmd_list = [os.path.basename(x).split('.')[0] for x in glob.glob(os.path.join(cmd_dir, '*.py'))] -hidden_imports = [f'mepo.command.{x}' for x in cmd_list if '_' not in x] # exclude subcommands -print(f'hidden_imports: {hidden_imports}') - -a = Analysis( - ['src/mepo/__main__.py'], - pathex=[], - binaries=[], - datas=[], - hiddenimports=hidden_imports, - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - noarchive=False, - optimize=0, -) -pyz = PYZ(a.pure) - -exe = EXE( - pyz, - a.scripts, - [], - exclude_binaries=True, - name='mepo', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) -coll = COLLECT( - exe, - a.binaries, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='mepo', -) diff --git a/src/mepo/command/status.py b/src/mepo/command/status.py index 6b309116..0bdb8d4d 100644 --- a/src/mepo/command/status.py +++ b/src/mepo/command/status.py @@ -8,6 +8,7 @@ from ..git import GitRepository from ..utilities import colors from ..utilities import shellcmd +from ..utilities import statcolor from ..utilities.version import version_to_string from ..utilities.version import sanitize_version_string @@ -55,13 +56,9 @@ def check_component_status(comp, ignore_permissions): # This command is to try and work with git tag oddities curr_ver = sanitize_version_string(orig_ver, curr_ver, git) - # We also want to see if there are any stashes in the component - num_stashes = len(git.list_stash().splitlines()) - return ( curr_ver, internal_state_branch_name, - num_stashes, git.check_status(ignore_permissions, _ignore_submodules), ) @@ -75,7 +72,7 @@ def print_status(allcomps, result, max_width, nocolor=False, hashes=False): def print_component_status(comp, result, width, nocolor=False, hashes=False): """Print the status of a single component""" - current_version, internal_state_branch_name, num_stashes, output = result + current_version, internal_state_branch_name, output = result if hashes: comp_path = _get_relative_path(comp.local) comp_hash = shellcmd.run( @@ -94,12 +91,60 @@ def print_component_status(comp, result, width, nocolor=False, hashes=False): else: component_name = comp.name - # If there are stashes, we print the number of stashes in yellow - if num_stashes: - stash_str = colors.YELLOW + f"[stashes: {num_stashes}]" + colors.RESET - print(f"{component_name:<{width}} | {current_version} {stash_str}") - else: - print(f"{component_name:<{width}} | {current_version}") - if output: - for line in output.split("\n"): - print(" |", line.rstrip()) + ahead_behind_stash, changes = parse_output(output) + print(f"{component_name:<{width}} | {current_version}{ahead_behind_stash}") + for line in changes: + print(" |", line.rstrip()) + + +def parse_output(output): + headers = [] + changes = [] + for item in output.splitlines(): + if item.startswith("#"): + headers.append(item) + else: + changes.append(item) + ahead_behind_stash = parse_headers(headers) + changes = parse_changed_entries(changes) + return (ahead_behind_stash, changes) + + +def parse_headers(headers): + result = "" + for header in headers: + header = header.strip("# ").split() + if header[0] == "stash": + num_stashes = header[1] + result += statcolor.yellow(f" [stashes: {num_stashes}]") + elif header[0] == "branch.ab": + ahead = int(header[1].lstrip("+")) + if ahead > 0: + result += statcolor.yellow(f" [ahead: {ahead}]") + behind = int(header[2].lstrip("-")) + if behind > 0: + result += statcolor.yellow(f" [behind: {behind}]") + return result + + +def parse_changed_entries(changes): + max_len = 0 + if changes: + changed_files = [x.split()[-1] for x in changes] + max_len = len(max(changed_files, key=len)) + for idx, item in enumerate(changes): + type_ = item.split()[0] + short_ = item.split()[1] + if type_ == "1": + status_ = statcolor.get_ordinary_change_status(short_) + elif type_ == "2": + new_file_ = item.split()[-2] + status_ = statcolor.get_renamed_copied_status(short_, new_file_) + elif type_ == "?": + status_ = statcolor.red("untracked file") + else: + status_ = statcolor.cyan("unknown") + " (contact mepo maintainer)" + file_name = item.split()[-1] + status_string_ = f"{file_name:>{max_len}}: {status_}" + changes[idx] = status_string_ + return changes diff --git a/src/mepo/git.py b/src/mepo/git.py index 7a035e0f..460aa751 100644 --- a/src/mepo/git.py +++ b/src/mepo/git.py @@ -3,11 +3,7 @@ import shlex import subprocess as sp -from urllib.parse import urljoin - from .utilities import shellcmd -from .utilities import colors -from .utilities.exceptions import RepoAlreadyClonedError def get_editor(): @@ -36,7 +32,7 @@ class GitRepository: def __init__(self, remote_url, local_path_abs): self.__local_path_abs = local_path_abs self.__remote = remote_url - self.__git = 'git -C "{}"'.format(self.__local_path_abs) + self.__git = f"git -C {self.__local_path_abs}" def get_local_path(self): return self.__local_path_abs @@ -226,215 +222,14 @@ def verify_branch_or_tag(self, ref_name): return status, ref_type def check_status(self, ignore_permissions=False, ignore_submodules=False): - cmd = "git -C {}".format(self.__local_path_abs) + cmd = f"git -C {self.__local_path_abs}" if ignore_permissions: cmd += " -c core.fileMode=false" - cmd += " status --porcelain=v2" + cmd += " status --porcelain=v2 --branch --show-stash" if ignore_submodules: cmd += " --ignore-submodules=all" output = shellcmd.run(shlex.split(cmd), output=True) - if output.strip(): - output_list = output.splitlines() - - # Grab the file names first for pretty printing - file_name_list = [item.split()[-1] for item in output_list] - max_file_name_length = len(max(file_name_list, key=len)) - - verbose_output_list = [] - for item in output_list: - - index_field = item.split()[0] - if index_field == "2": - new_file_name = colors.YELLOW + item.split()[-2] + colors.RESET - - file_name = item.split()[-1] - - short_status = item.split()[1] - - if index_field == "?": - verbose_status = colors.RED + "untracked file" + colors.RESET - - elif short_status == ".D": - verbose_status = colors.RED + "deleted, not staged" + colors.RESET - elif short_status == ".M": - verbose_status = colors.RED + "modified, not staged" + colors.RESET - elif short_status == ".A": - verbose_status = colors.RED + "added, not staged" + colors.RESET - elif short_status == ".T": - verbose_status = ( - colors.RED + "typechange, not staged" + colors.RESET - ) - - elif short_status == "D.": - verbose_status = colors.GREEN + "deleted, staged" + colors.RESET - elif short_status == "M.": - verbose_status = colors.GREEN + "modified, staged" + colors.RESET - elif short_status == "A.": - verbose_status = colors.GREEN + "added, staged" + colors.RESET - elif short_status == "T.": - verbose_status = colors.GREEN + "typechange, staged" + colors.RESET - - elif short_status == "MM": - verbose_status = ( - colors.GREEN - + "modified, staged" - + colors.RESET - + " with " - + colors.RED - + "unstaged changes" - + colors.RESET - ) - elif short_status == "MD": - verbose_status = ( - colors.GREEN - + "modified, staged" - + colors.RESET - + " but " - + colors.RED - + "deleted, not staged" - + colors.RESET - ) - - elif short_status == "AM": - verbose_status = ( - colors.GREEN - + "added, staged" - + colors.RESET - + " with " - + colors.RED - + "unstaged changes" - + colors.RESET - ) - elif short_status == "AD": - verbose_status = ( - colors.GREEN - + "added, staged" - + colors.RESET - + " but " - + colors.RED - + "deleted, not staged" - + colors.RESET - ) - - elif short_status == "TM": - verbose_status = ( - colors.GREEN - + "typechange, staged" - + colors.RESET - + " with " - + colors.RED - + "unstaged changes" - + colors.RESET - ) - elif short_status == "TD": - verbose_status = ( - colors.GREEN - + "typechange, staged" - + colors.RESET - + " but " - + colors.RED - + "deleted, not staged" - + colors.RESET - ) - - elif short_status == "R.": - verbose_status = ( - colors.GREEN - + "renamed" - + colors.RESET - + " as " - + colors.YELLOW - + new_file_name - + colors.RESET - ) - elif short_status == "RM": - verbose_status = ( - colors.GREEN - + "renamed, staged" - + colors.RESET - + " as " - + colors.YELLOW - + new_file_name - + colors.RESET - + " with " - + colors.RED - + "unstaged changes" - + colors.RESET - ) - elif short_status == "RD": - verbose_status = ( - colors.GREEN - + "renamed, staged" - + colors.RESET - + " as " - + colors.YELLOW - + new_file_name - + colors.RESET - + " but " - + colors.RED - + "deleted, not staged" - + colors.RESET - ) - - elif short_status == "C.": - verbose_status = ( - colors.GREEN - + "copied" - + colors.RESET - + " as " - + colors.YELLOW - + new_file_name - + colors.RESET - ) - elif short_status == "CM": - verbose_status = ( - colors.GREEN - + "copied, staged" - + colors.RESET - + " as " - + colors.YELLOW - + new_file_name - + colors.RESET - + " with " - + colors.RED - + "unstaged changes" - + colors.RESET - ) - elif short_status == "CD": - verbose_status = ( - colors.GREEN - + "copied, staged" - + colors.RESET - + " as " - + colors.YELLOW - + new_file_name - + colors.RESET - + " but " - + colors.RED - + "deleted, not staged" - + colors.RESET - ) - - else: - verbose_status = ( - colors.CYAN - + "unknown" - + colors.RESET - + " (please contact mepo maintainer)" - ) - - verbose_status_string = ( - "{file_name:>{file_name_length}}: {verbose_status}".format( - file_name=file_name, - file_name_length=max_file_name_length, - verbose_status=verbose_status, - ) - ) - verbose_output_list.append(verbose_status_string) - - output = "\n".join(verbose_output_list) - - return output.rstrip() + return output.strip() def __get_modified_files(self, orig_ver, comp_type): if not orig_ver: diff --git a/src/mepo/utilities/statcolor.py b/src/mepo/utilities/statcolor.py new file mode 100644 index 00000000..2862ba54 --- /dev/null +++ b/src/mepo/utilities/statcolor.py @@ -0,0 +1,65 @@ +from . import colors + + +def red(string): + return colors.RED + string + colors.RESET + + +def blue(string): + return colors.BLUE + string + colors.RESET + + +def cyan(string): + return colors.CYAN + string + colors.RESET + + +def green(string): + return colors.GREEN + string + colors.RESET + + +def yellow(string): + return colors.YELLOW + string + colors.RESET + + +def get_ordinary_change_status(short_status): + unstaged_ = " with " + red("unstaged changes") + deleted_unstaged_ = " but " + red("deleted, not staged") + d = { + # unstaged changes + ".D": red("deleted, not staged"), + ".M": red("modified, not staged"), + ".A": red("added, not staged"), + ".T": red("typechange, not staged"), + # staged changes + "D.": green("deleted, staged"), + "M.": green("modified, staged"), + "A.": green("added, staged"), + "T.": green("typechange, staged"), + # modified staged ... + "MM": green("modified, staged") + unstaged_, + "MD": green("modified, staged") + deleted_unstaged_, + # added staged ... + "AM": green("added, staged") + unstaged_, + "AD": green("added, staged") + deleted_unstaged_, + # typechange staged ... + "TM": green("typechange, staged") + unstaged_, + "TD": green("typechange, staged") + deleted_unstaged_, + } + return d[short_status] + + +def get_renamed_copied_status(short_status, new_file_name): + new_file_name_ = " as " + yellow(new_file_name) + unstaged_ = " with " + red("unstaged changes") + deleted_unstaged_ = " but " + red("deleted, not staged") + d = { + # renamed + "R.": green("renamed") + new_file_name_, + "RM": green("renamed, staged") + new_file_name_ + unstaged_, + "RD": green("renamed, staged") + new_file_name_ + deleted_unstaged_, + # copied + "C.": green("copied") + new_file_name_, + "CM": green("copied, staged") + new_file_name_ + unstaged_, + "CD": green("copied, staged") + new_file_name_ + deleted_unstaged_, + } + return d[short_status] From e08ead3d01ed5d63d9810ed1fe303551c3a00a3b Mon Sep 17 00:00:00 2001 From: Purnendu Chakraborty Date: Thu, 1 May 2025 07:41:33 -0500 Subject: [PATCH 5/9] Better handling of input key for dictionary (#362) * Return unknown if dict key does not exist * Updated ecbuild's list of branches, for testing * Passing an extra argument to pytest to print the test timings --- .github/workflows/run-tests.yaml | 2 +- src/mepo/command/status.py | 2 +- src/mepo/utilities/statcolor.py | 7 +++++-- tests/output/output_branch_list.txt | 9 +++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 66a217fd..f267ec32 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -28,5 +28,5 @@ jobs: - name: Run tests run: | - uv run pytest -v + uv run pytest --durations=0 -v timeout-minutes: 5 diff --git a/src/mepo/command/status.py b/src/mepo/command/status.py index 0bdb8d4d..edc49a76 100644 --- a/src/mepo/command/status.py +++ b/src/mepo/command/status.py @@ -143,7 +143,7 @@ def parse_changed_entries(changes): elif type_ == "?": status_ = statcolor.red("untracked file") else: - status_ = statcolor.cyan("unknown") + " (contact mepo maintainer)" + status_ = statcolor.UNKNOWN file_name = item.split()[-1] status_string_ = f"{file_name:>{max_len}}: {status_}" changes[idx] = status_string_ diff --git a/src/mepo/utilities/statcolor.py b/src/mepo/utilities/statcolor.py index 2862ba54..a648bf22 100644 --- a/src/mepo/utilities/statcolor.py +++ b/src/mepo/utilities/statcolor.py @@ -21,6 +21,9 @@ def yellow(string): return colors.YELLOW + string + colors.RESET +UNKNOWN = cyan("unknown") + " (contact mepo maintainer)" + + def get_ordinary_change_status(short_status): unstaged_ = " with " + red("unstaged changes") deleted_unstaged_ = " but " + red("deleted, not staged") @@ -45,7 +48,7 @@ def get_ordinary_change_status(short_status): "TM": green("typechange, staged") + unstaged_, "TD": green("typechange, staged") + deleted_unstaged_, } - return d[short_status] + return d.get(short_status, UNKNOWN) def get_renamed_copied_status(short_status, new_file_name): @@ -62,4 +65,4 @@ def get_renamed_copied_status(short_status, new_file_name): "CM": green("copied, staged") + new_file_name_ + unstaged_, "CD": green("copied, staged") + new_file_name_ + deleted_unstaged_, } - return d[short_status] + return d.get(short_status, UNKNOWN) diff --git a/tests/output/output_branch_list.txt b/tests/output/output_branch_list.txt index 560c7b26..4e1b898b 100644 --- a/tests/output/output_branch_list.txt +++ b/tests/output/output_branch_list.txt @@ -1,12 +1,9 @@ ecbuild | * (HEAD detached at geos/v1.3.0) - | release/stable - | remotes/origin/HEAD -> origin/release/stable + | geos/develop + | remotes/origin/HEAD -> origin/geos/develop | remotes/origin/develop | remotes/origin/feature/FindMKL-portability-improvement - | remotes/origin/feature/ecbuild_use_package_quiet - | remotes/origin/feature/netcdf4-cmake + | remotes/origin/geos/develop | remotes/origin/geos/main | remotes/origin/master | remotes/origin/mepo-testing-do-not-delete - | remotes/origin/merge-develop-into-MKL-2025Apr28 - | remotes/origin/release/stable From 176cbdd289ba418fc86fd6e9cdc6cce0e44d3b05 Mon Sep 17 00:00:00 2001 From: Matt Thompson Date: Fri, 30 May 2025 11:37:16 -0400 Subject: [PATCH 6/9] Fixes #347. Add blobless option to components.yaml (#364) * Fixes #347. Add blobless option to components.yaml * Fix up test * Update src/mepo/command/clone.py Co-authored-by: pchakraborty --------- Co-authored-by: pchakraborty --- CHANGELOG.md | 1 + src/mepo/cmdline/parser.py | 3 ++- src/mepo/command/clone.py | 11 +++++++++-- src/mepo/component.py | 8 +++++++- tests/output/output_branch_list.txt | 1 + tests/test_component.py | 2 +- 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e46dece..3d462bbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added ahead/behind info to `mepo status` +- Added `blobless:` option to `components.yaml` to force blobless cloning for a repo ### Changed diff --git a/src/mepo/cmdline/parser.py b/src/mepo/cmdline/parser.py index 89a16e1f..f9171aac 100644 --- a/src/mepo/cmdline/parser.py +++ b/src/mepo/cmdline/parser.py @@ -21,7 +21,8 @@ def __init__(self, option_strings, dest, const=True, help=None): super().__init__(option_strings, dest, const, help=help) def __call__(self, parser, namespace, values, option_string=None): - import os, sys + import os + import sys import mepo print(os.path.dirname(mepo.__file__)) diff --git a/src/mepo/command/clone.py b/src/mepo/command/clone.py index fecab8da..4e057548 100644 --- a/src/mepo/command/clone.py +++ b/src/mepo/command/clone.py @@ -92,7 +92,7 @@ def get_registry(arg_registry): return registry -def clone_components(allcomps, partial): +def clone_components(allcomps, arg_partial): max_namelen = max([len(comp.name) for comp in allcomps]) for comp in allcomps: if comp.fixture: @@ -101,7 +101,14 @@ def clone_components(allcomps, partial): # According to Git, treeless clones do not interact well with # submodules. So if any comp has the recurse option set to True, # we do a non-partial clone - partial = None if partial == "treeless" and recurse_submodules else partial + partial = arg_partial + if arg_partial == "treeless" and recurse_submodules: + partial = None + + # The components.yaml can specify blobless as an option so that wins out + if comp.blobless: + partial = "blobless" + version = comp.version.name version = version.replace("origin/", "") git = GitRepository(comp.remote, comp.local) diff --git a/src/mepo/component.py b/src/mepo/component.py index 7d9753b8..d2d801cd 100644 --- a/src/mepo/component.py +++ b/src/mepo/component.py @@ -1,5 +1,4 @@ import os -import shlex from dataclasses import dataclass from urllib.parse import urljoin @@ -25,6 +24,7 @@ class MepoComponent(object): "recurse_submodules", "fixture", "ignore_submodules", + "blobless", ] def __init__( @@ -38,6 +38,7 @@ def __init__( recurse_submodules=None, fixture=None, ignore_submodules=None, + blobless=None, ): self.name = name self.local = local @@ -48,6 +49,7 @@ def __init__( self.recurse_submodules = recurse_submodules self.fixture = fixture self.ignore_submodules = ignore_submodules + self.blobless = blobless def __repr__(self): # Older mepo clones will not have ignore_submodules in comp, so @@ -66,6 +68,7 @@ def __repr__(self): f" develop: {self.develop}\n" f" recurse_submodules: {self.recurse_submodules}\n" f" fixture: {self.fixture}\n" + f" blobless: {self.blobless}\n" f" ignore_submodules: {_ignore_submodules}" ) @@ -131,6 +134,7 @@ def registry_to_component(self, comp_name, comp_details, comp_style): self.develop = comp_details.get("develop", None) self.recurse_submodules = comp_details.get("recurse_submodules", None) self.ignore_submodules = comp_details.get("ignore_submodules", None) + self.blobless = comp_details.get("blobless", None) # version self.__set_original_version(comp_details) @@ -165,6 +169,8 @@ def to_registry_format(self): details["recurse_submodules"] = self.recurse_submodules if self.ignore_submodules: details["ignore_submodules"] = self.ignore_submodules + if self.blobless: + details["blobless"] = self.blobless return {self.name: details} def deserialize(self, d): diff --git a/tests/output/output_branch_list.txt b/tests/output/output_branch_list.txt index 4e1b898b..fe56c461 100644 --- a/tests/output/output_branch_list.txt +++ b/tests/output/output_branch_list.txt @@ -3,6 +3,7 @@ ecbuild | * (HEAD detached at geos/v1.3.0) | remotes/origin/HEAD -> origin/geos/develop | remotes/origin/develop | remotes/origin/feature/FindMKL-portability-improvement + | remotes/origin/geos-develop-before-2025May13-merge | remotes/origin/geos/develop | remotes/origin/geos/main | remotes/origin/master diff --git a/tests/test_component.py b/tests/test_component.py index 28f74f25..1e0638ca 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -43,6 +43,7 @@ def get_fvdycore_serialized(): "recurse_submodules": None, "fixture": False, "ignore_submodules": None, + "blobless": None, } @@ -60,7 +61,6 @@ def test_stylize_local_path(): def test_MepoComponent(): registry = get_registry() - complist = list() for name, comp in registry.items(): if name == "fvdycore": fvdycore = MepoComponent().registry_to_component(name, comp, None) From 65a170467d87f195dd6e2a4b16ee5c4277d249fd Mon Sep 17 00:00:00 2001 From: pchakraborty Date: Fri, 30 May 2025 13:25:36 -0500 Subject: [PATCH 7/9] Updated ChangeLog for 2.4.0 release (#366) * Updated ChangeLog for 2.4.0 release * Updated tests - list of branches has changed --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d462bbe..b2649fa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +### Changed + +## [2.4.0] - 2025-05-30 + +### Fixed + +### Added + - Added ahead/behind info to `mepo status` - Added `blobless:` option to `components.yaml` to force blobless cloning for a repo From 4d0b134282de2d30964c62b9ae3a3fc19386e7c2 Mon Sep 17 00:00:00 2001 From: pchakraborty Date: Mon, 2 Jun 2025 12:31:40 -0500 Subject: [PATCH 8/9] Bugfix (#367) * Updated ChangeLog for 2.4.0 release * Updated tests - list of branches has changed * Fixed a bug that would cause a crash (key 'blobless' not found) when using an older mepo clone --- src/mepo/component.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/mepo/component.py b/src/mepo/component.py index d2d801cd..86607d5a 100644 --- a/src/mepo/component.py +++ b/src/mepo/component.py @@ -52,12 +52,14 @@ def __init__( self.blobless = blobless def __repr__(self): - # Older mepo clones will not have ignore_submodules in comp, so + # Older mepo clones may not have some keys comp, so # we need to handle this gracefully try: _ignore_submodules = self.ignore_submodules + _blobless = self.blobless except AttributeError: _ignore_submodules = None + _blobless = None return ( f"{self.name} -\n" @@ -68,7 +70,7 @@ def __repr__(self): f" develop: {self.develop}\n" f" recurse_submodules: {self.recurse_submodules}\n" f" fixture: {self.fixture}\n" - f" blobless: {self.blobless}\n" + f" blobless: {_blobless}\n" f" ignore_submodules: {_ignore_submodules}" ) @@ -175,7 +177,7 @@ def to_registry_format(self): def deserialize(self, d): for k in self.__slots__: - v = d[k] + v = d.get(k, None) # for older clones, some keys could be missing if k == "version": # list -> namedtuple v = MepoVersion(*v) # * for arg unpacking From 20d1137b08f7ae6c92ca4e7461c955ad8961da90 Mon Sep 17 00:00:00 2001 From: Matt Thompson Date: Mon, 2 Jun 2025 15:50:50 -0400 Subject: [PATCH 9/9] Add DeepSeek Badge (#365) Co-authored-by: pchakraborty --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0ef5ed2..2725e27e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# mepo [![Actions Status](https://github.com/pchakraborty/mepo/workflows/Unit%20testing%20of%20mepo/badge.svg)](https://github.com/pchakraborty/mepo/actions) [![DOI](https://zenodo.org/badge/215067850.svg)](https://zenodo.org/badge/latestdoi/215067850) [![Rye](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/rye/main/artwork/badge.json)](https://rye-up.com) +# mepo [![Actions Status](https://github.com/pchakraborty/mepo/workflows/Unit%20testing%20of%20mepo/badge.svg)](https://github.com/pchakraborty/mepo/actions) [![DOI](https://zenodo.org/badge/215067850.svg)](https://zenodo.org/badge/latestdoi/215067850) [![Rye](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/rye/main/artwork/badge.json)](https://rye-up.com) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/GEOS-ESM/mepo) `mepo` is a tool, written in Python3, to manage (m)ultiple git r(epo)sitories, by attempting to create an illusion of a 'single repository' for multi-repository projects. Please see the [Wiki](../../wiki) for examples of `mepo` workflows.