Sfoglia il codice sorgente

Run ruff

pull/74/head
Ben Kurtovic 1 anno fa
parent
commit
3e6dbf321a
7 ha cambiato i file con 174 aggiunte e 52 eliminazioni
  1. +102
    -32
      src/gitup/cli.py
  2. +23
    -4
      src/gitup/config.py
  3. +4
    -0
      src/gitup/migrate.py
  4. +2
    -0
      src/gitup/test/__init__.py
  5. +1
    -0
      src/gitup/test/test_bookmarks.py
  6. +3
    -2
      src/gitup/test/test_cli.py
  7. +39
    -14
      src/gitup/update.py

+ 102
- 32
src/gitup/cli.py Vedi File

@@ -10,8 +10,14 @@ import platform
from colorama import init as color_init, Style from colorama import init as color_init, Style


from gitup import __version__ from gitup import __version__
from gitup.config import (get_default_config_path, get_bookmarks, add_bookmarks,
delete_bookmarks, list_bookmarks, clean_bookmarks)
from gitup.config import (
get_default_config_path,
get_bookmarks,
add_bookmarks,
delete_bookmarks,
list_bookmarks,
clean_bookmarks,
)
from gitup.update import update_bookmarks, update_directories, run_command from gitup.update import update_bookmarks, update_directories, run_command




@@ -23,7 +29,8 @@ def _build_parser():
Both relative and absolute paths are accepted by all arguments. Both relative and absolute paths are accepted by all arguments.
Direct bug reports and feature requests to Direct bug reports and feature requests to
https://github.com/earwig/git-repo-updater.""", https://github.com/earwig/git-repo-updater.""",
add_help=False)
add_help=False,
)


group_u = parser.add_argument_group("updating repositories") group_u = parser.add_argument_group("updating repositories")
group_b = parser.add_argument_group("bookmarking") group_b = parser.add_argument_group("bookmarking")
@@ -31,63 +38,125 @@ def _build_parser():
group_m = parser.add_argument_group("miscellaneous") group_m = parser.add_argument_group("miscellaneous")


group_u.add_argument( group_u.add_argument(
'directories_to_update', nargs="*", metavar="path",
"directories_to_update",
nargs="*",
metavar="path",
help="""update this repository, or all repositories it contains help="""update this repository, or all repositories it contains
(if not a repo directly)""")
(if not a repo directly)""",
)
group_u.add_argument( group_u.add_argument(
'-u', '--update', action="store_true", help="""update all bookmarks
(default behavior when called without arguments)""")
"-u",
"--update",
action="store_true",
help="""update all bookmarks
(default behavior when called without arguments)""",
)
group_u.add_argument( group_u.add_argument(
'-t', '--depth', dest="max_depth", metavar="n", type=int, default=3,
"-t",
"--depth",
dest="max_depth",
metavar="n",
type=int,
default=3,
help="""max recursion depth when searching for repos in subdirectories help="""max recursion depth when searching for repos in subdirectories
(default: 3; use 0 for no recursion, or -1 for unlimited)""")
(default: 3; use 0 for no recursion, or -1 for unlimited)""",
)
group_u.add_argument( group_u.add_argument(
'-c', '--current-only', action="store_true", help="""only fetch the
remote tracked by the current branch instead of all remotes""")
"-c",
"--current-only",
action="store_true",
help="""only fetch the
remote tracked by the current branch instead of all remotes""",
)
group_u.add_argument( group_u.add_argument(
'-f', '--fetch-only', action="store_true",
help="only fetch remotes, don't try to fast-forward any branches")
"-f",
"--fetch-only",
action="store_true",
help="only fetch remotes, don't try to fast-forward any branches",
)
group_u.add_argument( group_u.add_argument(
'-p', '--prune', action="store_true", help="""after fetching, delete
remote-tracking branches that no longer exist on their remote""")
"-p",
"--prune",
action="store_true",
help="""after fetching, delete
remote-tracking branches that no longer exist on their remote""",
)


group_b.add_argument( group_b.add_argument(
'-a', '--add', dest="bookmarks_to_add", nargs="+", metavar="path", help="add directory(s) as bookmarks")
"-a",
"--add",
dest="bookmarks_to_add",
nargs="+",
metavar="path",
help="add directory(s) as bookmarks",
)
group_b.add_argument( group_b.add_argument(
'-d', '--delete', dest="bookmarks_to_del", nargs="+", metavar="path",
help="delete bookmark(s) (leaves actual directories alone)")
"-d",
"--delete",
dest="bookmarks_to_del",
nargs="+",
metavar="path",
help="delete bookmark(s) (leaves actual directories alone)",
)
group_b.add_argument( group_b.add_argument(
'-l', '--list', dest="list_bookmarks", action="store_true",
help="list current bookmarks")
"-l",
"--list",
dest="list_bookmarks",
action="store_true",
help="list current bookmarks",
)
group_b.add_argument( group_b.add_argument(
'-n', '--clean', '--cleanup', dest="clean_bookmarks",
action="store_true", help="delete any bookmarks that don't exist")
"-n",
"--clean",
"--cleanup",
dest="clean_bookmarks",
action="store_true",
help="delete any bookmarks that don't exist",
)
group_b.add_argument( group_b.add_argument(
'-b', '--bookmark-file', nargs="?", metavar="path",
"-b",
"--bookmark-file",
nargs="?",
metavar="path",
help="use a specific bookmark config file (default: {0})".format( help="use a specific bookmark config file (default: {0})".format(
get_default_config_path()))
get_default_config_path()
),
)


group_a.add_argument( group_a.add_argument(
'-e', '--exec', '--batch', dest="command", metavar="command",
help="run a shell command on all repos")
"-e",
"--exec",
"--batch",
dest="command",
metavar="command",
help="run a shell command on all repos",
)


group_m.add_argument( group_m.add_argument(
'-h', '--help', action="help", help="show this help message and exit")
"-h", "--help", action="help", help="show this help message and exit"
)
group_m.add_argument( group_m.add_argument(
'-v', '--version', action="version",
version="gitup {0} (Python {1})".format(
__version__, platform.python_version()))
"-v",
"--version",
action="version",
version="gitup {0} (Python {1})".format(__version__, platform.python_version()),
)
group_m.add_argument( group_m.add_argument(
'--selftest', action="store_true",
help="run integrated test suite and exit (pytest must be available)")
"--selftest",
action="store_true",
help="run integrated test suite and exit (pytest must be available)",
)


return parser return parser



def _selftest(): def _selftest():
"""Run the integrated test suite with pytest.""" """Run the integrated test suite with pytest."""
from .test import run_tests from .test import run_tests

run_tests() run_tests()



def main(): def main():
"""Parse arguments and then call the appropriate function(s).""" """Parse arguments and then call the appropriate function(s)."""
parser = _build_parser() parser = _build_parser()
@@ -130,6 +199,7 @@ def main():
if args.update or not acted: if args.update or not acted:
update_bookmarks(get_bookmarks(args.bookmark_file), args) update_bookmarks(get_bookmarks(args.bookmark_file), args)



def run(): def run():
"""Thin wrapper for main() that catches KeyboardInterrupts.""" """Thin wrapper for main() that catches KeyboardInterrupts."""
try: try:


+ 23
- 4
src/gitup/config.py Vedi File

@@ -10,20 +10,28 @@ from colorama import Fore, Style


from gitup.migrate import run_migrations from gitup.migrate import run_migrations


__all__ = ["get_default_config_path", "get_bookmarks", "add_bookmarks",
"delete_bookmarks", "list_bookmarks", "clean_bookmarks"]
__all__ = [
"get_default_config_path",
"get_bookmarks",
"add_bookmarks",
"delete_bookmarks",
"list_bookmarks",
"clean_bookmarks",
]


YELLOW = Fore.YELLOW + Style.BRIGHT YELLOW = Fore.YELLOW + Style.BRIGHT
RED = Fore.RED + Style.BRIGHT RED = Fore.RED + Style.BRIGHT


INDENT1 = " " * 3 INDENT1 = " " * 3



def _ensure_dirs(path): def _ensure_dirs(path):
"""Ensure the directories within the given pathname exist.""" """Ensure the directories within the given pathname exist."""
dirname = os.path.dirname(path) dirname = os.path.dirname(path)
if dirname and not os.path.exists(dirname): # Race condition, meh... if dirname and not os.path.exists(dirname): # Race condition, meh...
os.makedirs(dirname) os.makedirs(dirname)



def _load_config_file(config_path=None): def _load_config_file(config_path=None):
"""Read the config file and return a list of bookmarks.""" """Read the config file and return a list of bookmarks."""
run_migrations() run_migrations()
@@ -37,6 +45,7 @@ def _load_config_file(config_path=None):
paths = [path.decode("utf8").strip() for path in paths] paths = [path.decode("utf8").strip() for path in paths]
return [path for path in paths if path] return [path for path in paths if path]



def _save_config_file(bookmarks, config_path=None): def _save_config_file(bookmarks, config_path=None):
"""Save the bookmarks list to the given config file.""" """Save the bookmarks list to the given config file."""
run_migrations() run_migrations()
@@ -47,21 +56,25 @@ def _save_config_file(bookmarks, config_path=None):
with open(cfg_path, "wb") as config_file: with open(cfg_path, "wb") as config_file:
config_file.write(dump) config_file.write(dump)



def _normalize_path(path): def _normalize_path(path):
"""Normalize the given path.""" """Normalize the given path."""
if path.startswith("~"): if path.startswith("~"):
return os.path.normcase(os.path.normpath(path)) return os.path.normcase(os.path.normpath(path))
return os.path.normcase(os.path.abspath(path)) return os.path.normcase(os.path.abspath(path))



def get_default_config_path(): def get_default_config_path():
"""Return the default path to the configuration file.""" """Return the default path to the configuration file."""
xdg_cfg = os.environ.get("XDG_CONFIG_HOME") or os.path.join("~", ".config") xdg_cfg = os.environ.get("XDG_CONFIG_HOME") or os.path.join("~", ".config")
return os.path.join(os.path.expanduser(xdg_cfg), "gitup", "bookmarks") return os.path.join(os.path.expanduser(xdg_cfg), "gitup", "bookmarks")



def get_bookmarks(config_path=None): def get_bookmarks(config_path=None):
"""Get a list of all bookmarks, or an empty list if there are none.""" """Get a list of all bookmarks, or an empty list if there are none."""
return _load_config_file(config_path) return _load_config_file(config_path)



def add_bookmarks(paths, config_path=None): def add_bookmarks(paths, config_path=None):
"""Add a list of paths as bookmarks to the config file.""" """Add a list of paths as bookmarks to the config file."""
config = _load_config_file(config_path) config = _load_config_file(config_path)
@@ -85,6 +98,7 @@ def add_bookmarks(paths, config_path=None):
for path in exists: for path in exists:
print(INDENT1, path) print(INDENT1, path)



def delete_bookmarks(paths, config_path=None): def delete_bookmarks(paths, config_path=None):
"""Remove a list of paths from the bookmark config file.""" """Remove a list of paths from the bookmark config file."""
config = _load_config_file(config_path) config = _load_config_file(config_path)
@@ -111,6 +125,7 @@ def delete_bookmarks(paths, config_path=None):
for path in notmarked: for path in notmarked:
print(INDENT1, path) print(INDENT1, path)



def list_bookmarks(config_path=None): def list_bookmarks(config_path=None):
"""Print all of our current bookmarks.""" """Print all of our current bookmarks."""
bookmarks = _load_config_file(config_path) bookmarks = _load_config_file(config_path)
@@ -121,6 +136,7 @@ def list_bookmarks(config_path=None):
else: else:
print("You have no bookmarks to display.") print("You have no bookmarks to display.")



def clean_bookmarks(config_path=None): def clean_bookmarks(config_path=None):
"""Delete any bookmarks that don't exist.""" """Delete any bookmarks that don't exist."""
bookmarks = _load_config_file(config_path) bookmarks = _load_config_file(config_path)
@@ -128,8 +144,11 @@ def clean_bookmarks(config_path=None):
print("You have no bookmarks to clean up.") print("You have no bookmarks to clean up.")
return return


delete = [path for path in bookmarks
if not (os.path.isdir(path) or glob(os.path.expanduser(path)))]
delete = [
path
for path in bookmarks
if not (os.path.isdir(path) or glob(os.path.expanduser(path)))
]
if not delete: if not delete:
print("All of your bookmarks are valid.") print("All of your bookmarks are valid.")
return return


+ 4
- 0
src/gitup/migrate.py Vedi File

@@ -9,11 +9,13 @@ from configparser import ConfigParser, NoSectionError


__all__ = ["run_migrations"] __all__ = ["run_migrations"]



def _get_old_path(): def _get_old_path():
"""Return the old default path to the configuration file.""" """Return the old default path to the configuration file."""
xdg_cfg = os.environ.get("XDG_CONFIG_HOME") or os.path.join("~", ".config") xdg_cfg = os.environ.get("XDG_CONFIG_HOME") or os.path.join("~", ".config")
return os.path.join(os.path.expanduser(xdg_cfg), "gitup", "config.ini") return os.path.join(os.path.expanduser(xdg_cfg), "gitup", "config.ini")



def _migrate_old_path(): def _migrate_old_path():
"""Migrate the old config location (~/.gitup) to the new one.""" """Migrate the old config location (~/.gitup) to the new one."""
old_path = os.path.expanduser(os.path.join("~", ".gitup")) old_path = os.path.expanduser(os.path.join("~", ".gitup"))
@@ -26,6 +28,7 @@ def _migrate_old_path():
os.makedirs(temp_dir) os.makedirs(temp_dir)
os.rename(old_path, temp_path) os.rename(old_path, temp_path)



def _migrate_old_format(): def _migrate_old_format():
"""Migrate the old config file format (.INI) to our custom list format.""" """Migrate the old config file format (.INI) to our custom list format."""
old_path = _get_old_path() old_path = _get_old_path()
@@ -47,6 +50,7 @@ def _migrate_old_format():
with open(new_path, "wb") as handle: with open(new_path, "wb") as handle:
handle.write(b"\n".join(bookmarks)) handle.write(b"\n".join(bookmarks))



def run_migrations(): def run_migrations():
"""Run any necessary migrations to ensure the config file is up-to-date.""" """Run any necessary migrations to ensure the config file is up-to-date."""
_migrate_old_path() _migrate_old_path()


+ 2
- 0
src/gitup/test/__init__.py Vedi File

@@ -3,8 +3,10 @@
# Copyright (C) 2011-2018 Ben Kurtovic <ben.kurtovic@gmail.com> # Copyright (C) 2011-2018 Ben Kurtovic <ben.kurtovic@gmail.com>
# Released under the terms of the MIT License. See LICENSE for details. # Released under the terms of the MIT License. See LICENSE for details.



def run_tests(args=None): def run_tests(args=None):
import pytest import pytest

if args is None: if args is None:
args = ["-v", "-rxw"] args = ["-v", "-rxw"]
return pytest.main(args) return pytest.main(args)

+ 1
- 0
src/gitup/test/test_bookmarks.py Vedi File

@@ -5,6 +5,7 @@


from gitup import config from gitup import config



def test_empty_list(tmpdir, capsys): def test_empty_list(tmpdir, capsys):
config_path = tmpdir / "config" config_path = tmpdir / "config"
config.list_bookmarks(config_path) config.list_bookmarks(config_path)


+ 3
- 2
src/gitup/test/test_cli.py Vedi File

@@ -9,14 +9,15 @@ import sys


from gitup import __version__ from gitup import __version__



def run_cli(*args): def run_cli(*args):
cmd = [sys.executable, "-m", "gitup"] + list(args) cmd = [sys.executable, "-m", "gitup"] + list(args)
output = subprocess.check_output(cmd) output = subprocess.check_output(cmd)
return output.strip().decode("utf8") return output.strip().decode("utf8")



def test_cli_version(): def test_cli_version():
"""make sure we're using the right version of gitup""" """make sure we're using the right version of gitup"""
output = run_cli("-v") output = run_cli("-v")
expected = "gitup {} (Python {})".format(
__version__, platform.python_version())
expected = "gitup {} (Python {})".format(__version__, platform.python_version())
assert output == expected assert output == expected

+ 39
- 14
src/gitup/update.py Vedi File

@@ -28,6 +28,7 @@ INDENT1 = " " * 3
INDENT2 = " " * 7 INDENT2 = " " * 7
ERROR = RED + "Error:" + RESET ERROR = RED + "Error:" + RESET



class _ProgressMonitor(RemoteProgress): class _ProgressMonitor(RemoteProgress):
"""Displays relevant output during the fetching process.""" """Displays relevant output during the fetching process."""


@@ -35,7 +36,7 @@ class _ProgressMonitor(RemoteProgress):
super(_ProgressMonitor, self).__init__() super(_ProgressMonitor, self).__init__()
self._started = False self._started = False


def update(self, op_code, cur_count, max_count=None, message=''):
def update(self, op_code, cur_count, max_count=None, message=""):
"""Called whenever progress changes. Overrides default behavior.""" """Called whenever progress changes. Overrides default behavior."""
if op_code & (self.COMPRESSING | self.RECEIVING): if op_code & (self.COMPRESSING | self.RECEIVING):
cur_count = str(int(cur_count)) cur_count = str(int(cur_count))
@@ -59,14 +60,17 @@ class _ProgressMonitor(RemoteProgress):


def _fetch_remotes(remotes, prune): def _fetch_remotes(remotes, prune):
"""Fetch a list of remotes, displaying progress info along the way.""" """Fetch a list of remotes, displaying progress info along the way."""

def _get_name(ref): def _get_name(ref):
"""Return the local name of a remote or tag reference.""" """Return the local name of a remote or tag reference."""
return ref.remote_head if isinstance(ref, RemoteRef) else ref.name return ref.remote_head if isinstance(ref, RemoteRef) else ref.name


# TODO: missing branch deleted (via --prune): # TODO: missing branch deleted (via --prune):
info = [("NEW_HEAD", "new branch", "new branches"),
("NEW_TAG", "new tag", "new tags"),
("FAST_FORWARD", "branch update", "branch updates")]
info = [
("NEW_HEAD", "new branch", "new branches"),
("NEW_TAG", "new tag", "new tags"),
("FAST_FORWARD", "branch update", "branch updates"),
]
up_to_date = BLUE + "up to date" + RESET up_to_date = BLUE + "up to date" + RESET


for remote in remotes: for remote in remotes:
@@ -92,19 +96,25 @@ def _fetch_remotes(remotes, prune):
return return
except AssertionError: # Seems to be the result of a bug in GitPython except AssertionError: # Seems to be the result of a bug in GitPython
# This happens when git initiates an auto-gc during fetch: # This happens when git initiates an auto-gc during fetch:
print(":", RED + "error:", "something went wrong in GitPython,",
"but the fetch might have been successful.")
print(
":",
RED + "error:",
"something went wrong in GitPython,",
"but the fetch might have been successful.",
)
return return
rlist = [] rlist = []
for attr, singular, plural in info: for attr, singular, plural in info:
names = [_get_name(res.ref)
for res in results if res.flags & getattr(res, attr)]
names = [
_get_name(res.ref) for res in results if res.flags & getattr(res, attr)
]
if names: if names:
desc = singular if len(names) == 1 else plural desc = singular if len(names) == 1 else plural
colored = GREEN + desc + RESET colored = GREEN + desc + RESET
rlist.append("{0} ({1})".format(colored, ", ".join(names))) rlist.append("{0} ({1})".format(colored, ", ".join(names)))
print(":", (", ".join(rlist) if rlist else up_to_date) + ".") print(":", (", ".join(rlist) if rlist else up_to_date) + ".")



def _update_branch(repo, branch, is_active=False): def _update_branch(repo, branch, is_active=False):
"""Update a single branch.""" """Update a single branch."""
print(INDENT2, "Updating", BOLD + branch.name, end=": ") print(INDENT2, "Updating", BOLD + branch.name, end=": ")
@@ -146,14 +156,19 @@ def _update_branch(repo, branch, is_active=False):
print(YELLOW + "skipped:", "not possible to fast-forward.") print(YELLOW + "skipped:", "not possible to fast-forward.")
else: else:
status = repo.git.merge_base( status = repo.git.merge_base(
branch.commit, upstream.commit, is_ancestor=True,
with_extended_output=True, with_exceptions=False)[0]
branch.commit,
upstream.commit,
is_ancestor=True,
with_extended_output=True,
with_exceptions=False,
)[0]
if status != 0: if status != 0:
print(YELLOW + "skipped:", "not possible to fast-forward.") print(YELLOW + "skipped:", "not possible to fast-forward.")
else: else:
repo.git.branch(branch.name, upstream.name, force=True) repo.git.branch(branch.name, upstream.name, force=True)
print(GREEN + "done", end=".\n") print(GREEN + "done", end=".\n")



def _update_repository(repo, repo_name, args): def _update_repository(repo, repo_name, args):
"""Update a single git repository by fetching remotes and rebasing/merging. """Update a single git repository by fetching remotes and rebasing/merging.


@@ -172,8 +187,11 @@ def _update_repository(repo, repo_name, args):
active = None active = None
if args.current_only: if args.current_only:
if not active: if not active:
print(INDENT2, ERROR,
"--current-only doesn't make sense with a detached HEAD.")
print(
INDENT2,
ERROR,
"--current-only doesn't make sense with a detached HEAD.",
)
return return
ref = active.tracking_branch() ref = active.tracking_branch()
if not ref: if not ref:
@@ -192,14 +210,14 @@ def _update_repository(repo, repo_name, args):
for branch in sorted(repo.heads, key=lambda b: b.name): for branch in sorted(repo.heads, key=lambda b: b.name):
_update_branch(repo, branch, branch == active) _update_branch(repo, branch, branch == active)



def _run_command(repo, repo_name, args): def _run_command(repo, repo_name, args):
"""Run an arbitrary shell command on the given repository.""" """Run an arbitrary shell command on the given repository."""
print(INDENT1, BOLD + repo_name + ":") print(INDENT1, BOLD + repo_name + ":")


cmd = shlex.split(args.command) cmd = shlex.split(args.command)
try: try:
out = repo.git.execute(
cmd, with_extended_output=True, with_exceptions=False)
out = repo.git.execute(cmd, with_extended_output=True, with_exceptions=False)
except exc.GitCommandNotFound as err: except exc.GitCommandNotFound as err:
print(INDENT2, ERROR, err) print(INDENT2, ERROR, err)
return return
@@ -207,6 +225,7 @@ def _run_command(repo, repo_name, args):
for line in out[1].splitlines() + out[2].splitlines(): for line in out[1].splitlines() + out[2].splitlines():
print(INDENT2, line) print(INDENT2, line)



def _dispatch(base_path, callback, args): def _dispatch(base_path, callback, args):
"""Apply a callback function on each valid repo in the given path. """Apply a callback function on each valid repo in the given path.


@@ -217,6 +236,7 @@ def _dispatch(base_path, callback, args):


The given args are passed directly to the callback function after the repo. The given args are passed directly to the callback function after the repo.
""" """

def _collect(paths, max_depth): def _collect(paths, max_depth):
"""Return all valid repo paths in the given paths, recursively.""" """Return all valid repo paths in the given paths, recursively."""
if max_depth == 0: if max_depth == 0:
@@ -282,14 +302,17 @@ def _dispatch(base_path, callback, args):
for name, path in sorted(paths): for name, path in sorted(paths):
callback(Repo(path), name, args) callback(Repo(path), name, args)



def is_comment(path): def is_comment(path):
"""Does the line start with a # symbol?""" """Does the line start with a # symbol?"""
return path.lstrip().startswith("#") return path.lstrip().startswith("#")



def get_comment(path): def get_comment(path):
"""Return the string minus the comment symbol.""" """Return the string minus the comment symbol."""
return path.lstrip().lstrip("#").strip() return path.lstrip().lstrip("#").strip()



def update_bookmarks(bookmarks, args): def update_bookmarks(bookmarks, args):
"""Loop through and update all bookmarks.""" """Loop through and update all bookmarks."""
if not bookmarks: if not bookmarks:
@@ -299,11 +322,13 @@ def update_bookmarks(bookmarks, args):
for path in bookmarks: for path in bookmarks:
_dispatch(path, _update_repository, args) _dispatch(path, _update_repository, args)



def update_directories(paths, args): def update_directories(paths, args):
"""Update a list of directories supplied by command arguments.""" """Update a list of directories supplied by command arguments."""
for path in paths: for path in paths:
_dispatch(path, _update_repository, args) _dispatch(path, _update_repository, args)



def run_command(paths, args): def run_command(paths, args):
"""Run an arbitrary shell command on all repos.""" """Run an arbitrary shell command on all repos."""
for path in paths: for path in paths:


Caricamento…
Annulla
Salva