Ben Kurtovic преди 1 година
родител
ревизия
3e6dbf321a
променени са 7 файла, в които са добавени 174 реда и са изтрити 52 реда
  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 Целия файл

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

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


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

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

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
(if not a repo directly)""")
(if not a repo directly)""",
)
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(
'-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
(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(
'-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(
'-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(
'-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(
'-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(
'-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(
'-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(
'-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(
'-b', '--bookmark-file', nargs="?", metavar="path",
"-b",
"--bookmark-file",
nargs="?",
metavar="path",
help="use a specific bookmark config file (default: {0})".format(
get_default_config_path()))
get_default_config_path()
),
)

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(
'-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(
'-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(
'--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


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

run_tests()


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


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


+ 23
- 4
src/gitup/config.py Целия файл

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

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
RED = Fore.RED + Style.BRIGHT

INDENT1 = " " * 3


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


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


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


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


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


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


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


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


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


def clean_bookmarks(config_path=None):
"""Delete any bookmarks that don't exist."""
bookmarks = _load_config_file(config_path)
@@ -128,8 +144,11 @@ def clean_bookmarks(config_path=None):
print("You have no bookmarks to clean up.")
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:
print("All of your bookmarks are valid.")
return


+ 4
- 0
src/gitup/migrate.py Целия файл

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

__all__ = ["run_migrations"]


def _get_old_path():
"""Return the old default path to the configuration file."""
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")


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


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


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


+ 2
- 0
src/gitup/test/__init__.py Целия файл

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


def run_tests(args=None):
import pytest

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

+ 1
- 0
src/gitup/test/test_bookmarks.py Целия файл

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

from gitup import config


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


+ 3
- 2
src/gitup/test/test_cli.py Целия файл

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

from gitup import __version__


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


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

+ 39
- 14
src/gitup/update.py Целия файл

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


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

@@ -35,7 +36,7 @@ class _ProgressMonitor(RemoteProgress):
super(_ProgressMonitor, self).__init__()
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."""
if op_code & (self.COMPRESSING | self.RECEIVING):
cur_count = str(int(cur_count))
@@ -59,14 +60,17 @@ class _ProgressMonitor(RemoteProgress):

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

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

# 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

for remote in remotes:
@@ -92,19 +96,25 @@ def _fetch_remotes(remotes, prune):
return
except AssertionError: # Seems to be the result of a bug in GitPython
# 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
rlist = []
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:
desc = singular if len(names) == 1 else plural
colored = GREEN + desc + RESET
rlist.append("{0} ({1})".format(colored, ", ".join(names)))
print(":", (", ".join(rlist) if rlist else up_to_date) + ".")


def _update_branch(repo, branch, is_active=False):
"""Update a single branch."""
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.")
else:
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:
print(YELLOW + "skipped:", "not possible to fast-forward.")
else:
repo.git.branch(branch.name, upstream.name, force=True)
print(GREEN + "done", end=".\n")


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

@@ -172,8 +187,11 @@ def _update_repository(repo, repo_name, args):
active = None
if args.current_only:
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
ref = active.tracking_branch()
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):
_update_branch(repo, branch, branch == active)


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

cmd = shlex.split(args.command)
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:
print(INDENT2, ERROR, err)
return
@@ -207,6 +225,7 @@ def _run_command(repo, repo_name, args):
for line in out[1].splitlines() + out[2].splitlines():
print(INDENT2, line)


def _dispatch(base_path, callback, args):
"""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.
"""

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


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


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


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


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


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


Зареждане…
Отказ
Запис