| @@ -1,7 +1,13 @@ | |||||
| v0.5 (unreleased): | v0.5 (unreleased): | ||||
| - Improved repository detection when passed a directory that contains repos: | |||||
| will now traverse through subdirectories automatically. | |||||
| - Added a `--depth` flag to control recursion depth when searching for | |||||
| repositories inside of subdirectories. For example: | |||||
| - `--depth 0` will never recurse into subdirectories; the provided paths must | |||||
| be repositories by themselves. | |||||
| - `--depth 1` will descend one level to look for repositories. This is the | |||||
| old behavior. | |||||
| - `--depth 3` will look three levels deep. This is the new default. | |||||
| - `--depth -1` will recurse indefinitely. This is not recommended. | |||||
| - Fixed an error when updating branches if the upstream is completely unrelated | - Fixed an error when updating branches if the upstream is completely unrelated | ||||
| from the local branch (no common ancestor). | from the local branch (no common ancestor). | ||||
| @@ -20,7 +26,7 @@ v0.4 (released January 17, 2017): | |||||
| - Cleaned up the bookmark file format, fixing a related Windows bug. The script | - Cleaned up the bookmark file format, fixing a related Windows bug. The script | ||||
| will automatically migrate to the new one. | will automatically migrate to the new one. | ||||
| - Fixed a bug related to Python 3 compatibility. | - Fixed a bug related to Python 3 compatibility. | ||||
| - Fixed unicode support. | |||||
| - Fixed Unicode support. | |||||
| v0.3 (released June 7, 2015): | v0.3 (released June 7, 2015): | ||||
| @@ -1,4 +1,4 @@ | |||||
| Copyright (C) 2011-2017 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| Copyright (C) 2011-2018 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | of this software and associated documentation files (the "Software"), to deal | ||||
| @@ -1,9 +1,9 @@ | |||||
| __gitup__ (the _git-repo-updater_) | __gitup__ (the _git-repo-updater_) | ||||
| gitup is a tool designed to update a large number of git repositories at once. | |||||
| It is smart enough to handle multiple remotes, branches, dirty working | |||||
| directories, and more, hopefully providing a great way to get everything | |||||
| up-to-date for short periods of internet access between long periods of none. | |||||
| gitup is a tool for updating multiple git repositories at once. It is smart | |||||
| enough to handle several remotes, dirty working directories, diverged local | |||||
| branches, detached HEADs, and more. It was originally created to manage a large | |||||
| collection of projects and deal with sporadic internet access. | |||||
| gitup should work on OS X, Linux, and Windows. You should have the latest | gitup should work on OS X, Linux, and Windows. You should have the latest | ||||
| version of git and either Python 2.7 or Python 3 installed. | version of git and either Python 2.7 or Python 3 installed. | ||||
| @@ -25,7 +25,7 @@ Then, to install for everyone: | |||||
| sudo python setup.py install | sudo python setup.py install | ||||
| ...or for just yourself (make sure you have `~/.local/bin` in your PATH): | |||||
| or for just yourself (make sure you have `~/.local/bin` in your PATH): | |||||
| python setup.py install --user | python setup.py install --user | ||||
| @@ -51,10 +51,9 @@ Additionally, you can just type: | |||||
| gitup ~/repos | gitup ~/repos | ||||
| to automatically update all git repositories in that directory and its | |||||
| subdirectories. | |||||
| to automatically update all git repositories in that directory. | |||||
| To add a bookmark (or bookmarks), either of these will work: | |||||
| To add bookmarks, either of these will work: | |||||
| gitup --add ~/repos/foo ~/repos/bar ~/repos/baz | gitup --add ~/repos/foo ~/repos/bar ~/repos/baz | ||||
| gitup --add ~/repos | gitup --add ~/repos | ||||
| @@ -82,6 +81,13 @@ Update all git repositories in your current directory: | |||||
| gitup . | gitup . | ||||
| You can control how deep gitup will look for repositories in a given directory, | |||||
| if that directory is not a git repo by itself, with the `--depth` (or `-t`) | |||||
| option. `--depth 0` will disable recursion entirely, meaning the provided paths | |||||
| must be repos by themselves. `--depth 1` will descend one level (this is the | |||||
| old behavior from pre-0.5 gitup). `--depth -1` will recurse indefinitely, | |||||
| which is not recommended. The default is `--depth 3`. | |||||
| By default, gitup will fetch all remotes in a repository. Pass `--current-only` | By default, gitup will fetch all remotes in a repository. Pass `--current-only` | ||||
| (or `-c`) to make it fetch only the remote tracked by the current branch. | (or `-c`) to make it fetch only the remote tracked by the current branch. | ||||
| @@ -97,6 +103,3 @@ upstream. Pass `--prune` (or `-p`) to delete them, or set `fetch.prune` or | |||||
| For a full list of all command arguments and abbreviations: | For a full list of all command arguments and abbreviations: | ||||
| gitup --help | gitup --help | ||||
| Finally, all paths can be either absolute (e.g. `/path/to/repo`) or relative | |||||
| (e.g. `../my/repo`). | |||||
| @@ -8,7 +8,7 @@ gitup: the git repository updater | |||||
| """ | """ | ||||
| __author__ = "Ben Kurtovic" | __author__ = "Ben Kurtovic" | ||||
| __copyright__ = "Copyright (C) 2011-2017 Ben Kurtovic" | |||||
| __copyright__ = "Copyright (C) 2011-2018 Ben Kurtovic" | |||||
| __license__ = "MIT License" | __license__ = "MIT License" | ||||
| __version__ = "0.5.dev0" | __version__ = "0.5.dev0" | ||||
| __email__ = "ben.kurtovic@gmail.com" | __email__ = "ben.kurtovic@gmail.com" | ||||
| @@ -1,6 +1,6 @@ | |||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
| # | # | ||||
| # Copyright (C) 2011-2016 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. | ||||
| from __future__ import print_function | from __future__ import print_function | ||||
| @@ -39,12 +39,16 @@ def main(): | |||||
| group_u.add_argument( | group_u.add_argument( | ||||
| 'directories_to_update', nargs="*", metavar="path", type=_decode, | 'directories_to_update', nargs="*", metavar="path", type=_decode, | ||||
| help="""update all repositories in this directory (or the directory | |||||
| itself, if it is a repo)""") | |||||
| help="""update this repository, or all repositories it contains | |||||
| (if not a repo directly)""") | |||||
| group_u.add_argument( | group_u.add_argument( | ||||
| '-u', '--update', action="store_true", help="""update all bookmarks | '-u', '--update', action="store_true", help="""update all bookmarks | ||||
| (default behavior when called without arguments)""") | (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, | |||||
| help="""max recursion depth when searching for repos in subdirectories | |||||
| (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 | '-c', '--current-only', action="store_true", help="""only fetch the | ||||
| remote tracked by the current branch instead of all remotes""") | remote tracked by the current branch instead of all remotes""") | ||||
| group_u.add_argument( | group_u.add_argument( | ||||
| @@ -90,7 +94,6 @@ def main(): | |||||
| color_init(autoreset=True) | color_init(autoreset=True) | ||||
| args = parser.parse_args() | args = parser.parse_args() | ||||
| update_args = args.current_only, args.fetch_only, args.prune | |||||
| print(Style.BRIGHT + "gitup" + Style.RESET_ALL + ": the git-repo-updater") | print(Style.BRIGHT + "gitup" + Style.RESET_ALL + ": the git-repo-updater") | ||||
| print() | print() | ||||
| @@ -121,15 +124,15 @@ def main(): | |||||
| if args.command: | if args.command: | ||||
| if args.directories_to_update: | if args.directories_to_update: | ||||
| run_command(args.directories_to_update, args.command) | |||||
| run_command(args.directories_to_update, args) | |||||
| if args.update or not args.directories_to_update: | if args.update or not args.directories_to_update: | ||||
| run_command(get_bookmarks(args.bookmark_file), args.command) | |||||
| run_command(get_bookmarks(args.bookmark_file), args) | |||||
| else: | else: | ||||
| if args.directories_to_update: | if args.directories_to_update: | ||||
| update_directories(args.directories_to_update, update_args) | |||||
| update_directories(args.directories_to_update, args) | |||||
| acted = True | acted = True | ||||
| if args.update or not acted: | if args.update or not acted: | ||||
| update_bookmarks(get_bookmarks(args.bookmark_file), update_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.""" | ||||
| @@ -145,15 +145,15 @@ def _update_branch(repo, branch, is_active=False): | |||||
| 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, current_only, fetch_only, prune): | |||||
| 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. | ||||
| The specific actions depend on the arguments given. We will fetch all | The specific actions depend on the arguments given. We will fetch all | ||||
| remotes if *current_only* is ``False``, or only the remote tracked by the | |||||
| current branch if ``True``. If *fetch_only* is ``False``, we will also | |||||
| update all fast-forwardable branches that are tracking valid upstreams. | |||||
| If *prune* is ``True``, remote-tracking branches that no longer exist on | |||||
| their remote after fetching will be deleted. | |||||
| remotes if *args.current_only* is ``False``, or only the remote tracked by | |||||
| the current branch if ``True``. If *args.fetch_only* is ``False``, we will | |||||
| also update all fast-forwardable branches that are tracking valid | |||||
| upstreams. If *args.prune* is ``True``, remote-tracking branches that no | |||||
| longer exist on their remote after fetching will be deleted. | |||||
| """ | """ | ||||
| print(INDENT1, BOLD + repo_name + ":") | print(INDENT1, BOLD + repo_name + ":") | ||||
| @@ -161,7 +161,7 @@ def _update_repository(repo, repo_name, current_only, fetch_only, prune): | |||||
| active = repo.active_branch | active = repo.active_branch | ||||
| except TypeError: # Happens when HEAD is detached | except TypeError: # Happens when HEAD is detached | ||||
| active = None | active = None | ||||
| if current_only: | |||||
| if args.current_only: | |||||
| if not active: | if not active: | ||||
| print(INDENT2, ERROR, | print(INDENT2, ERROR, | ||||
| "--current-only doesn't make sense with a detached HEAD.") | "--current-only doesn't make sense with a detached HEAD.") | ||||
| @@ -177,17 +177,17 @@ def _update_repository(repo, repo_name, current_only, fetch_only, prune): | |||||
| if not remotes: | if not remotes: | ||||
| print(INDENT2, ERROR, "no remotes configured to fetch.") | print(INDENT2, ERROR, "no remotes configured to fetch.") | ||||
| return | return | ||||
| _fetch_remotes(remotes, prune) | |||||
| _fetch_remotes(remotes, args.prune) | |||||
| if not fetch_only: | |||||
| if not args.fetch_only: | |||||
| 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, command): | |||||
| 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(command) | |||||
| cmd = shlex.split(args.command) | |||||
| try: | try: | ||||
| out = repo.git.execute( | out = repo.git.execute( | ||||
| cmd, with_extended_output=True, with_exceptions=False) | cmd, with_extended_output=True, with_exceptions=False) | ||||
| @@ -198,7 +198,7 @@ def _run_command(repo, repo_name, command): | |||||
| 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. | ||||
| Determine whether the directory is a git repo on its own, a directory of | Determine whether the directory is a git repo on its own, a directory of | ||||
| @@ -208,9 +208,9 @@ 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, maxdepth=6): | |||||
| 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 maxdepth <= 0: | |||||
| if max_depth == 0: | |||||
| return [] | return [] | ||||
| valid = [] | valid = [] | ||||
| @@ -222,7 +222,7 @@ def _dispatch(base_path, callback, *args): | |||||
| if not os.path.isdir(path): | if not os.path.isdir(path): | ||||
| continue | continue | ||||
| children = [os.path.join(path, it) for it in os.listdir(path)] | children = [os.path.join(path, it) for it in os.listdir(path)] | ||||
| valid += _collect(children, maxdepth=maxdepth-1) | |||||
| valid += _collect(children, max_depth - 1) | |||||
| except exc.NoSuchPathError: | except exc.NoSuchPathError: | ||||
| continue | continue | ||||
| return valid | return valid | ||||
| @@ -240,6 +240,10 @@ def _dispatch(base_path, callback, *args): | |||||
| return path.split(prefix + os.path.sep, 1)[1] | return path.split(prefix + os.path.sep, 1)[1] | ||||
| base = os.path.expanduser(base_path) | base = os.path.expanduser(base_path) | ||||
| max_depth = args.max_depth | |||||
| if max_depth >= 0: | |||||
| max_depth += 1 | |||||
| try: | try: | ||||
| Repo(base) | Repo(base) | ||||
| valid = [base] | valid = [base] | ||||
| @@ -248,12 +252,12 @@ def _dispatch(base_path, callback, *args): | |||||
| if not paths: | if not paths: | ||||
| print(ERROR, BOLD + base, "doesn't exist!") | print(ERROR, BOLD + base, "doesn't exist!") | ||||
| return | return | ||||
| valid = _collect(paths) | |||||
| valid = _collect(paths, max_depth) | |||||
| except exc.InvalidGitRepositoryError: | except exc.InvalidGitRepositoryError: | ||||
| if not os.path.isdir(base): | |||||
| if not os.path.isdir(base) or args.max_depth == 0: | |||||
| print(ERROR, BOLD + base, "isn't a repository!") | print(ERROR, BOLD + base, "isn't a repository!") | ||||
| return | return | ||||
| valid = _collect([base]) | |||||
| valid = _collect([base], max_depth) | |||||
| base = os.path.abspath(base) | base = os.path.abspath(base) | ||||
| suffix = "" if len(valid) == 1 else "s" | suffix = "" if len(valid) == 1 else "s" | ||||
| @@ -262,23 +266,23 @@ def _dispatch(base_path, callback, *args): | |||||
| valid = [os.path.abspath(path) for path in valid] | valid = [os.path.abspath(path) for path in valid] | ||||
| paths = [(_get_basename(base, path), path) for path in valid] | paths = [(_get_basename(base, path), path) for path in valid] | ||||
| for name, path in sorted(paths): | for name, path in sorted(paths): | ||||
| callback(Repo(path), name, *args) | |||||
| callback(Repo(path), name, args) | |||||
| def update_bookmarks(bookmarks, update_args): | |||||
| def update_bookmarks(bookmarks, args): | |||||
| """Loop through and update all bookmarks.""" | """Loop through and update all bookmarks.""" | ||||
| if not bookmarks: | if not bookmarks: | ||||
| print("You don't have any bookmarks configured! Get help with 'gitup -h'.") | print("You don't have any bookmarks configured! Get help with 'gitup -h'.") | ||||
| return | return | ||||
| for path in bookmarks: | for path in bookmarks: | ||||
| _dispatch(path, _update_repository, *update_args) | |||||
| _dispatch(path, _update_repository, args) | |||||
| def update_directories(paths, update_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, *update_args) | |||||
| _dispatch(path, _update_repository, args) | |||||
| def run_command(paths, command): | |||||
| 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: | ||||
| _dispatch(path, _run_command, command) | |||||
| _dispatch(path, _run_command, args) | |||||
| @@ -1,6 +1,6 @@ | |||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
| # | # | ||||
| # Copyright (C) 2011-2016 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. | ||||
| import sys | import sys | ||||
| @@ -8,7 +8,7 @@ import sys | |||||
| from setuptools import setup, find_packages | from setuptools import setup, find_packages | ||||
| if sys.hexversion < 0x02070000: | if sys.hexversion < 0x02070000: | ||||
| exit("Please upgrade to Python 2.7 or greater: <http://python.org/>.") | |||||
| exit("Please upgrade to Python 2.7 or greater: <https://www.python.org/>.") | |||||
| from gitup import __version__ | from gitup import __version__ | ||||
| @@ -23,18 +23,19 @@ setup( | |||||
| version = __version__, | version = __version__, | ||||
| author = "Ben Kurtovic", | author = "Ben Kurtovic", | ||||
| author_email = "ben.kurtovic@gmail.com", | author_email = "ben.kurtovic@gmail.com", | ||||
| description = "Easily pull to multiple git repositories at once", | |||||
| description = "Easily update multiple git repositories at once", | |||||
| long_description = long_desc, | long_description = long_desc, | ||||
| license = "MIT License", | license = "MIT License", | ||||
| keywords = "git repository pull update", | keywords = "git repository pull update", | ||||
| url = "http://github.com/earwig/git-repo-updater", | |||||
| url = "https://github.com/earwig/git-repo-updater", | |||||
| classifiers = [ | classifiers = [ | ||||
| "Development Status :: 4 - Beta", | |||||
| "Environment :: Console", | "Environment :: Console", | ||||
| "Intended Audience :: Developers", | "Intended Audience :: Developers", | ||||
| "License :: OSI Approved :: MIT License", | "License :: OSI Approved :: MIT License", | ||||
| "Natural Language :: English", | "Natural Language :: English", | ||||
| "Operating System :: MacOS :: MacOS X", | "Operating System :: MacOS :: MacOS X", | ||||
| "Operating System :: POSIX :: Linux", | |||||
| "Operating System :: POSIX", | |||||
| "Operating System :: Microsoft :: Windows", | "Operating System :: Microsoft :: Windows", | ||||
| "Programming Language :: Python", | "Programming Language :: Python", | ||||
| "Programming Language :: Python :: 2.7", | "Programming Language :: Python :: 2.7", | ||||
| @@ -44,6 +45,7 @@ setup( | |||||
| "Programming Language :: Python :: 3.4", | "Programming Language :: Python :: 3.4", | ||||
| "Programming Language :: Python :: 3.5", | "Programming Language :: Python :: 3.5", | ||||
| "Programming Language :: Python :: 3.6", | "Programming Language :: Python :: 3.6", | ||||
| "Topic :: Software Development :: Version Control" | |||||
| "Programming Language :: Python :: 3.7", | |||||
| "Topic :: Software Development :: Version Control :: Git" | |||||
| ] | ] | ||||
| ) | ) | ||||