@@ -1,7 +1,13 @@ | |||
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 | |||
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 | |||
will automatically migrate to the new one. | |||
- Fixed a bug related to Python 3 compatibility. | |||
- Fixed unicode support. | |||
- Fixed Unicode support. | |||
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 | |||
of this software and associated documentation files (the "Software"), to deal | |||
@@ -1,9 +1,9 @@ | |||
__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 | |||
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 | |||
...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 | |||
@@ -51,10 +51,9 @@ Additionally, you can just type: | |||
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 | |||
@@ -82,6 +81,13 @@ Update all git repositories in your current directory: | |||
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` | |||
(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: | |||
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" | |||
__copyright__ = "Copyright (C) 2011-2017 Ben Kurtovic" | |||
__copyright__ = "Copyright (C) 2011-2018 Ben Kurtovic" | |||
__license__ = "MIT License" | |||
__version__ = "0.5.dev0" | |||
__email__ = "ben.kurtovic@gmail.com" |
@@ -1,6 +1,6 @@ | |||
# -*- 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. | |||
from __future__ import print_function | |||
@@ -39,12 +39,16 @@ def main(): | |||
group_u.add_argument( | |||
'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( | |||
'-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, | |||
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 | |||
remote tracked by the current branch instead of all remotes""") | |||
group_u.add_argument( | |||
@@ -90,7 +94,6 @@ def main(): | |||
color_init(autoreset=True) | |||
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() | |||
@@ -121,15 +124,15 @@ def main(): | |||
if args.command: | |||
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: | |||
run_command(get_bookmarks(args.bookmark_file), args.command) | |||
run_command(get_bookmarks(args.bookmark_file), args) | |||
else: | |||
if args.directories_to_update: | |||
update_directories(args.directories_to_update, update_args) | |||
update_directories(args.directories_to_update, args) | |||
acted = True | |||
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(): | |||
"""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) | |||
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. | |||
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 + ":") | |||
@@ -161,7 +161,7 @@ def _update_repository(repo, repo_name, current_only, fetch_only, prune): | |||
active = repo.active_branch | |||
except TypeError: # Happens when HEAD is detached | |||
active = None | |||
if current_only: | |||
if args.current_only: | |||
if not active: | |||
print(INDENT2, ERROR, | |||
"--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: | |||
print(INDENT2, ERROR, "no remotes configured to fetch.") | |||
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): | |||
_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.""" | |||
print(INDENT1, BOLD + repo_name + ":") | |||
cmd = shlex.split(command) | |||
cmd = shlex.split(args.command) | |||
try: | |||
out = repo.git.execute( | |||
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(): | |||
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. | |||
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. | |||
""" | |||
def _collect(paths, maxdepth=6): | |||
def _collect(paths, max_depth): | |||
"""Return all valid repo paths in the given paths, recursively.""" | |||
if maxdepth <= 0: | |||
if max_depth == 0: | |||
return [] | |||
valid = [] | |||
@@ -222,7 +222,7 @@ def _dispatch(base_path, callback, *args): | |||
if not os.path.isdir(path): | |||
continue | |||
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: | |||
continue | |||
return valid | |||
@@ -240,6 +240,10 @@ def _dispatch(base_path, callback, *args): | |||
return path.split(prefix + os.path.sep, 1)[1] | |||
base = os.path.expanduser(base_path) | |||
max_depth = args.max_depth | |||
if max_depth >= 0: | |||
max_depth += 1 | |||
try: | |||
Repo(base) | |||
valid = [base] | |||
@@ -248,12 +252,12 @@ def _dispatch(base_path, callback, *args): | |||
if not paths: | |||
print(ERROR, BOLD + base, "doesn't exist!") | |||
return | |||
valid = _collect(paths) | |||
valid = _collect(paths, max_depth) | |||
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!") | |||
return | |||
valid = _collect([base]) | |||
valid = _collect([base], max_depth) | |||
base = os.path.abspath(base) | |||
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] | |||
paths = [(_get_basename(base, path), path) for path in valid] | |||
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.""" | |||
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) | |||
_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.""" | |||
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.""" | |||
for path in paths: | |||
_dispatch(path, _run_command, command) | |||
_dispatch(path, _run_command, args) |
@@ -1,6 +1,6 @@ | |||
# -*- 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. | |||
import sys | |||
@@ -8,7 +8,7 @@ import sys | |||
from setuptools import setup, find_packages | |||
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__ | |||
@@ -23,18 +23,19 @@ setup( | |||
version = __version__, | |||
author = "Ben Kurtovic", | |||
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, | |||
license = "MIT License", | |||
keywords = "git repository pull update", | |||
url = "http://github.com/earwig/git-repo-updater", | |||
url = "https://github.com/earwig/git-repo-updater", | |||
classifiers = [ | |||
"Development Status :: 4 - Beta", | |||
"Environment :: Console", | |||
"Intended Audience :: Developers", | |||
"License :: OSI Approved :: MIT License", | |||
"Natural Language :: English", | |||
"Operating System :: MacOS :: MacOS X", | |||
"Operating System :: POSIX :: Linux", | |||
"Operating System :: POSIX", | |||
"Operating System :: Microsoft :: Windows", | |||
"Programming Language :: Python", | |||
"Programming Language :: Python :: 2.7", | |||
@@ -44,6 +45,7 @@ setup( | |||
"Programming Language :: Python :: 3.4", | |||
"Programming Language :: Python :: 3.5", | |||
"Programming Language :: Python :: 3.6", | |||
"Topic :: Software Development :: Version Control" | |||
"Programming Language :: Python :: 3.7", | |||
"Topic :: Software Development :: Version Control :: Git" | |||
] | |||
) |