@@ -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" | |||||
] | ] | ||||
) | ) |