diff --git a/CHANGELOG b/CHANGELOG index 729b082..67ff9e3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ v0.4 (unreleased): - Added a `--bookmark-file` flag to support multiple bookmark config files. - Added a `--cleanup` flag to remove old bookmarks that don't exist. - Added an `--exec` flag to run a shell command on all of your repos. +- Added support for shell glob patterns and tilde expansion in bookmark files. - Cleaned up the bookmark file format, fixing a related Windows bug. The script will automatically migrate to the new one. - Fixed a bug related to Python 3 compatibility. diff --git a/gitup/config.py b/gitup/config.py index 2227141..7eeea7b 100644 --- a/gitup/config.py +++ b/gitup/config.py @@ -5,6 +5,7 @@ from __future__ import print_function +from glob import glob import os from colorama import Fore, Style @@ -48,6 +49,12 @@ 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") @@ -60,9 +67,10 @@ def get_bookmarks(config_path=None): def add_bookmarks(paths, config_path=None): """Add a list of paths as bookmarks to the config file.""" config = _load_config_file(config_path) + paths = [_normalize_path(path) for path in paths] + added, exists = [], [] for path in paths: - path = os.path.normcase(os.path.abspath(path)) if path in config: exists.append(path) else: @@ -82,11 +90,11 @@ def add_bookmarks(paths, config_path=None): def delete_bookmarks(paths, config_path=None): """Remove a list of paths from the bookmark config file.""" config = _load_config_file(config_path) + paths = [_normalize_path(path) for path in paths] deleted, notmarked = [], [] if config: for path in paths: - path = os.path.normcase(os.path.abspath(path)) if path in config: config.remove(path) deleted.append(path) @@ -94,7 +102,7 @@ def delete_bookmarks(paths, config_path=None): notmarked.append(path) _save_config_file(config, config_path) else: - notmarked = [os.path.abspath(path) for path in paths] + notmarked = paths if deleted: print(YELLOW + "Deleted bookmarks:") @@ -122,7 +130,8 @@ 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)] + 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 diff --git a/gitup/update.py b/gitup/update.py index 139312f..0b15431 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -5,6 +5,7 @@ from __future__ import print_function +from glob import glob import os import shlex @@ -192,18 +193,20 @@ def _run_command(repo, command): for line in out[1].splitlines() + out[2].splitlines(): print(INDENT2, line) -def _dispatch_to_subdirs(path, callback, *args): - """Apply the callback to all git repo subdirectories in the directory.""" +def _dispatch_multi(base, paths, callback, *args): + """Apply the callback to all git repos in the list of paths.""" repos = [] - for item in os.listdir(path): + for path in paths: try: - repo = Repo(os.path.join(path, item)) + repo = Repo(path) except (exc.InvalidGitRepositoryError, exc.NoSuchPathError): continue repos.append(repo) + base = os.path.abspath(base) suffix = "" if len(repos) == 1 else "s" - print(BOLD + path, "({0} repo{1}):".format(len(repos), suffix)) + print(BOLD + base, "({0} repo{1}):".format(len(repos), suffix)) + for repo in sorted(repos, key=lambda r: os.path.split(r.working_dir)[1]): callback(repo, *args) @@ -211,19 +214,25 @@ def _dispatch(path, callback, *args): """Apply a callback function on each valid repo in the given path. Determine whether the directory is a git repo on its own, a directory of - git repositories, or something invalid. If the first, apply the callback on - it; if the second, apply the callback on all repositories contained within; - if the third, print an error. + git repositories, a shell glob pattern, or something invalid. If the first, + apply the callback on it; if the second or third, apply the callback on all + repositories contained within; if the last, print an error. The given args are passed directly to the callback function after the repo. """ + path = os.path.expanduser(path) try: repo = Repo(path) except exc.NoSuchPathError: - print(ERROR, BOLD + path, "doesn't exist!") + paths = glob(path) + if paths: + _dispatch_multi(path, paths, callback, *args) + else: + print(ERROR, BOLD + path, "doesn't exist!") except exc.InvalidGitRepositoryError: if os.path.isdir(path): - _dispatch_to_subdirs(path, callback, *args) + paths = [os.path.join(path, item) for item in os.listdir(path)] + _dispatch_multi(path, paths, callback, *args) else: print(ERROR, BOLD + path, "isn't a repository!") else: @@ -232,20 +241,19 @@ def _dispatch(path, callback, *args): def update_bookmarks(bookmarks, update_args): """Loop through and update all bookmarks.""" - if bookmarks: - for path in bookmarks: - _dispatch(path, _update_repository, *update_args) - else: + if not bookmarks: print("You don't have any bookmarks configured! Get help with 'gitup -h'.") + return + + for path in bookmarks: + _dispatch(path, _update_repository, *update_args) def update_directories(paths, update_args): """Update a list of directories supplied by command arguments.""" for path in paths: - full_path = os.path.abspath(path) - _dispatch(full_path, _update_repository, *update_args) + _dispatch(path, _update_repository, *update_args) def run_command(paths, command): """Run an arbitrary shell command on all repos.""" for path in paths: - full_path = os.path.abspath(path) - _dispatch(full_path, _run_command, command) + _dispatch(path, _run_command, command)