Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ jobs:
publish:

runs-on: ubuntu-latest
environment: release
permissions:
id-token: write # mandatory for trusted publishing

steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.9'
python-version: "3.14"

- run: pip install -U pip setuptools wheel twine tox
- run: tox -e py,docs,style
- run: python setup.py sdist bdist_wheel --universal

- name: Publish sdist and wheel
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.MGIT_TOKEN }}
run: twine upload --non-interactive dist/*
- uses: astral-sh/setup-uv@v7
- run: uv venv
- run: uv pip install -U tox-uv
- run: .venv/bin/tox -e py,style
- run: uv build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
18 changes: 8 additions & 10 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,27 @@ jobs:

strategy:
matrix:
python-version: [3.6, 3.9]
python-version: ["3.10", "3.14"]

steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- run: pip install -U pip tox
- run: tox -e py
- uses: codecov/codecov-action@v1
with:
file: .tox/test-reports/coverage.xml
- uses: codecov/codecov-action@v5
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The codecov action configuration is missing the coverage file path. The previous configuration explicitly specified file: .tox/test-reports/coverage.xml, but this has been removed.

While codecov v4 can auto-discover coverage files, explicitly specifying the file path makes the configuration more robust and clear. Consider adding:

with:
  file: .tox/test-reports/coverage.xml
Suggested change
- uses: codecov/codecov-action@v5
- uses: codecov/codecov-action@v5
with:
file: .tox/test-reports/coverage.xml

Copilot uses AI. Check for mistakes.

linters:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.9'
python-version: "3.14"

- run: pip install -U pip tox
- run: tox -e docs,style
- run: tox -e style
3 changes: 2 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ You can also compile from source::

git clone https://github.com/zsimic/mgit.git
cd mgit
tox -e venv
uv venv
uv pip install -e .

.venv/bin/mgit --help

Expand Down
8 changes: 4 additions & 4 deletions docs/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ To get going locally, simply do this::
git clone https://github.com/zsimic/mgit.git
cd mgit

tox -e venv
uv venv
uv pip install -r tests/requirements.txt -e .

# You have a venv now in ./.venv, use it, open it with pycharm etc
source .venv/bin/activate
Expand All @@ -24,12 +25,11 @@ To get going locally, simply do this::
Running the tests
=================

To run the tests, simply run ``tox``, this will run tests against all python versions you have locally installed.
You can use pyenv_ for example to get python installations.
To run the tests, simply run ``tox``.

Run:

* ``tox -e py38`` (for example) to limit test run to only one python version.
* ``tox -e py314`` (for example) to limit test run to only one python version.

* ``tox -e style`` to run style checks only

Expand Down
96 changes: 96 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
[build-system]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"

[tool.setuptools_scm]
local_scheme = "dirty-tag"

[project]
name = "mgit"
authors = [
{name = "Zoran Simic", email = "zoran@simicweb.com"},
]
description = "Fetch collections of git projects"
readme = "README.rst"
requires-python = ">=3.10"
license = "MIT"
license-files = ["LICENSE.txt"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Operating System :: MacOS :: MacOS X",
"Operating System :: POSIX",
"Operating System :: Unix",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Utilities",
]
dependencies = [
"click~=8.3",
"runez~=5.3",
]
dynamic = ["version"]

[project.scripts]
mgit = "mgit.cli:main"

[project.urls]
Source = "https://github.com/zsimic/mgit"


[tool.ruff]
cache-dir = ".tox/.ruff_cache"
line-length = 140

[tool.ruff.lint]
#ignore = ["RUF021", "RUF022", "RUF023", "S101"]
extend-select = [
# "A", # flake8-builtins
# "ARG", # flake8-unused-arguments
# "B", # flake8-bugbear
"C4", # flake8-comprehensions
"C90", # mccabe
# "D", # pydocstyle
"DTZ", # flake8-datetimez
"E", # pycodestyle errors
"ERA", # eradicate
"EXE", # flake8-executable
"F", # pyflakes
"FLY", # flynt
"G", # flake8-logging-format
"I", # isort
"INT", # flake8-gettext
"PGH", # pygrep-hooks
"PIE", # flake8-pie
# "PT", # flake8-pytest
"PYI", # flake8-pyi
"Q", # flake8-quotes
"RSE", # flake8-raise
# "RET", # flake8-return
# "RUF", # ruff-specific
# "S", #flake8-bandit
# "SIM", # flake8-simplify
# "SLF", # flake8-self
"SLOT", # flake8-slots
"T10", # flake8-debugger
"TID", # flake8-tidy-imports
"TCH", # flake8-type-checking
"TD", # flake8-todos
# "TRY", # tryceratops
"W", # pycodestyle warnings
]

[tool.ruff.lint.isort]
order-by-type = false

[tool.ruff.lint.mccabe]
max-complexity = 20

[tool.ruff.lint.pydocstyle]
convention = "numpy"
3 changes: 0 additions & 3 deletions requirements.txt

This file was deleted.

35 changes: 0 additions & 35 deletions setup.py

This file was deleted.

47 changes: 23 additions & 24 deletions src/mgit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from mgit.git import GitDir, GitRunReport


LOG = logging.getLogger(__name__)


Expand Down Expand Up @@ -55,31 +54,31 @@ def print_modified(items, color1, color2=None):
for item in items:
state = item[0:2]
if color2:
state = "%s%s" % (color1(item[0]), color2(item[1]))
state = f"{color1(item[0])}{color2(item[1])}"

elif color1:
state = color1(state)

print(" %s %s" % (state, item[3:]))
print(f" {state} {item[3:]}")


class MgitPreferences:
"""Various prefs"""

name_size = None # How many chars to align names when displaying list of checkouts
align = True # Whether to align names or not
verbose = False # Show verbose output
all = False # Show all entries, including missing/invalid checkout folders
fetch = False # Auto-fetch before showing status
pull = False # Auto-pull before showing status
inspect_remotes = False # Inspect remote branches to report cleanable (slower)
name_size = None # How many chars to align names when displaying list of checkouts
align = True # Whether to align names or not
verbose = False # Show verbose output
all = False # Show all entries, including missing/invalid checkout folders
fetch = False # Auto-fetch before showing status
pull = False # Auto-pull before showing status
inspect_remotes = False # Inspect remote branches to report cleanable (slower)

def __init__(self, **kwargs):
self.update(**kwargs)

def __repr__(self):
result = [self._value_representation(k) for k in sorted(self.__dict__)]
return ' '.join(s for s in result if s is not None)
return " ".join(s for s in result if s is not None)

def _value_representation(self, name):
value = getattr(self, name, None)
Expand All @@ -92,7 +91,7 @@ def _value_representation(self, name):
if value is False:
return "!%s" % name

return "%s=%s" % (name, value)
return f"{name}={value}"

def set_short(self, value):
"""
Expand Down Expand Up @@ -133,7 +132,7 @@ def __init__(self, url):
self.name = url.repo or "unknown"

def __repr__(self):
return "%s/%s" % (self.type, self.name)
return f"{self.type}/{self.name}"

def __hash__(self):
return hash(str(self))
Expand Down Expand Up @@ -213,7 +212,7 @@ def name(self):
if not self.git.config.repo_name or not self.git.is_git_checkout or self.basename == self.git.config.repo_name:
return self.basename

return "%s (%s)" % (self.basename, self.git.config.repo_name)
return f"{self.basename} ({self.git.config.repo_name})"

@runez.cached_property
def origin_project(self):
Expand Down Expand Up @@ -301,13 +300,13 @@ def __init__(self, path, prefs=None):
:param str path: Path to folder
:param MgitPreferences|None prefs: Display prefs
"""
self.path = path # Path to folder to examine
self.prefs = prefs or MgitPreferences() # Preferences on how to output result
self.checkouts = [] # Actual git checkouts in 'path'
self.projects = collections.defaultdict(set) # Seen remotes
self.predominant = None # Predominant remote, if any
self.additional = None # Additional projects (sorted by checkouts, descending)
self.stash_projects = {} # Corresponding projects from stash, when applicable
self.path = path # Path to folder to examine
self.prefs = prefs or MgitPreferences() # Preferences on how to output result
self.checkouts = [] # Actual git checkouts in 'path'
self.projects = collections.defaultdict(set) # Seen remotes
self.predominant = None # Predominant remote, if any
self.additional = None # Additional projects (sorted by checkouts, descending)
self.stash_projects = {} # Corresponding projects from stash, when applicable
self.scan()

def __repr__(self):
Expand Down Expand Up @@ -375,16 +374,16 @@ def header(self):
result = "%s:" % runez.purple(runez.short(self.path))

if not self.projects:
return "%s %s" % (result, runez.orange("no git folders"))
return "{} {}".format(result, runez.orange("no git folders"))

if self.predominant:
result += runez.bold(" %s %s" % (len(self.projects[self.predominant]), self.predominant))
result += runez.bold(f" {len(self.projects[self.predominant])} {self.predominant}")

else:
result += runez.orange(" no predominant project")

if self.additional:
result += " (%s)" % runez.purple(", ".join("+%s %s" % (len(self.projects[project]), project) for project in self.additional))
result += " (%s)" % runez.purple(", ".join(f"+{len(self.projects[project])} {project}" for project in self.additional))

return result

Expand Down
7 changes: 3 additions & 4 deletions src/mgit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from mgit import get_target, GitCheckout
from mgit.git import GitRunReport


LOG = logging.getLogger(__name__)
VALID_CLEAN_ACTIONS = "show local remote all reset".split()

Expand Down Expand Up @@ -123,7 +122,7 @@ def clean_show(target):

else:
for branch in target.git.local_cleanable_branches:
print(" %s branch %s can be cleaned" % (runez.bold("local"), runez.bold(branch)))
print(" {} branch {} can be cleaned".format(runez.bold("local"), runez.bold(branch)))

if not target.git.remote_cleanable_branches:
print(" No remote branches can be cleaned")
Expand Down Expand Up @@ -175,7 +174,7 @@ def handle_single_clean(target, what):
print("%s cleaned" % runez.plural(cleaned, "remote branch"))

else:
print("%s/%s remote branches cleaned" % (cleaned, total))
print(f"{cleaned}/{total} remote branches cleaned")

target.git.reset_cached_properties()
if what == "all":
Expand Down Expand Up @@ -206,7 +205,7 @@ def handle_single_clean(target, what):
print(runez.bold("%s cleaned" % runez.plural(cleaned, "local branch")))

else:
print(runez.orange("%s/%s local branches cleaned" % (cleaned, total)))
print(runez.orange(f"{cleaned}/{total} local branches cleaned"))

target.git.reset_cached_properties()

Expand Down
Loading