Skip to content

Commit

Permalink
Add a --depth argument to control recursion depth. Cleanup.
Browse files Browse the repository at this point in the history
  • Loading branch information
earwig committed Feb 18, 2018
1 parent a4810c3 commit cc0254d
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 55 deletions.
12 changes: 9 additions & 3 deletions CHANGELOG
@@ -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).

Expand All @@ -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):

Expand Down
2 changes: 1 addition & 1 deletion LICENSE
@@ -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
Expand Down
25 changes: 14 additions & 11 deletions README.md
@@ -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.
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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`).
2 changes: 1 addition & 1 deletion gitup/__init__.py
Expand Up @@ -8,7 +8,7 @@
"""

__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"
19 changes: 11 additions & 8 deletions gitup/script.py
@@ -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
Expand Down Expand Up @@ -39,11 +39,15 @@ 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""")
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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."""
Expand Down
54 changes: 29 additions & 25 deletions gitup/update.py
Expand Up @@ -145,23 +145,23 @@ 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 + ":")

try:
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.")
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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 = []
Expand All @@ -222,7 +222,7 @@ def _collect(paths, maxdepth=6):
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
Expand All @@ -240,6 +240,10 @@ def _get_basename(base, path):
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]
Expand All @@ -248,12 +252,12 @@ def _get_basename(base, path):
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"
Expand All @@ -262,23 +266,23 @@ def _get_basename(base, path):
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)
14 changes: 8 additions & 6 deletions setup.py
@@ -1,14 +1,14 @@
# -*- 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

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__

Expand All @@ -23,18 +23,19 @@
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",
Expand All @@ -44,6 +45,7 @@
"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"
]
)

0 comments on commit cc0254d

Please sign in to comment.