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


+ 14
- 11
README.md View File

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

+ 1
- 1
gitup/__init__.py View File

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

+ 11
- 8
gitup/script.py View File

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


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

+ 8
- 6
setup.py View File

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

Loading…
Cancel
Save