Browse Source

Add a --depth argument to control recursion depth. Cleanup.

tags/v0.5
Ben Kurtovic 6 years ago
parent
commit
cc0254d60d
7 changed files with 73 additions and 55 deletions
  1. +9
    -3
      CHANGELOG
  2. +1
    -1
      LICENSE
  3. +14
    -11
      README.md
  4. +1
    -1
      gitup/__init__.py
  5. +11
    -8
      gitup/script.py
  6. +29
    -25
      gitup/update.py
  7. +8
    -6
      setup.py

+ 9
- 3
CHANGELOG View File

@@ -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
- 1
LICENSE View File

@@ -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


+ 14
- 11
README.md View File

@@ -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`).

+ 1
- 1
gitup/__init__.py View File

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

+ 11
- 8
gitup/script.py View File

@@ -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."""


+ 29
- 25
gitup/update.py View File

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

+ 8
- 6
setup.py View File

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

Loading…
Cancel
Save