| @@ -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: | ||||
| @@ -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 | ||||
| @@ -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() | ||||
| @@ -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) | ||||
| @@ -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) | ||||
| @@ -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 | ||||
| @@ -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: | ||||