Browse Source

Traverse into subdirectories when searching for repos (fixes #42).

tags/v0.5
Ben Kurtovic 6 years ago
parent
commit
a4810c3c5c
4 changed files with 78 additions and 45 deletions
  1. +7
    -0
      CHANGELOG
  2. +5
    -4
      README.md
  3. +2
    -2
      gitup/__init__.py
  4. +64
    -39
      gitup/update.py

+ 7
- 0
CHANGELOG View File

@@ -1,3 +1,10 @@
v0.5 (unreleased):

- Improved repository detection when passed a directory that contains repos:
will now traverse through subdirectories automatically.
- Fixed an error when updating branches if the upstream is completely unrelated
from the local branch (no common ancestor).

v0.4.1 (released December 13, 2017): v0.4.1 (released December 13, 2017):


- Bump dependencies to deal with newer versions of Git. - Bump dependencies to deal with newer versions of Git.


+ 5
- 4
README.md View File

@@ -51,7 +51,8 @@ Additionally, you can just type:


gitup ~/repos gitup ~/repos


to automatically update all git repositories in that directory.
to automatically update all git repositories in that directory and its
subdirectories.


To add a bookmark (or bookmarks), either of these will work: To add a bookmark (or bookmarks), either of these will work:


@@ -82,16 +83,16 @@ Update all git repositories in your current directory:
gitup . gitup .


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.


Also by default, gitup will try to fast-forward all branches that have Also by default, gitup will try to fast-forward all branches that have
upstreams configured. It will always skip branches where this is not possible upstreams configured. It will always skip branches where this is not possible
(e.g. dirty working directory or a merge/rebase is required). Pass (e.g. dirty working directory or a merge/rebase is required). Pass
`--fetch-only` (or `-f`) to only fetch remotes.
`--fetch-only` (or `-f`) to skip this step and only fetch remotes.


After fetching, gitup will _keep_ remote-tracking branches that no longer exist After fetching, gitup will _keep_ remote-tracking branches that no longer exist
upstream. Pass `--prune` (or `-p`) to delete them, or set `fetch.prune` or upstream. Pass `--prune` (or `-p`) to delete them, or set `fetch.prune` or
`remote.<name>.prune` in git config to do this by default.
`remote.<name>.prune` in your git config to do this by default.


For a full list of all command arguments and abbreviations: For a full list of all command arguments and abbreviations:




+ 2
- 2
gitup/__init__.py View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (C) 2011-2017 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.


""" """
@@ -10,5 +10,5 @@ gitup: the git repository updater
__author__ = "Ben Kurtovic" __author__ = "Ben Kurtovic"
__copyright__ = "Copyright (C) 2011-2017 Ben Kurtovic" __copyright__ = "Copyright (C) 2011-2017 Ben Kurtovic"
__license__ = "MIT License" __license__ = "MIT License"
__version__ = "0.4.1"
__version__ = "0.5.dev0"
__email__ = "ben.kurtovic@gmail.com" __email__ = "ben.kurtovic@gmail.com"

+ 64
- 39
gitup/update.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
@@ -115,7 +115,12 @@ def _update_branch(repo, branch, is_active=False):
print(YELLOW + "skipped:", "upstream does not exist.") print(YELLOW + "skipped:", "upstream does not exist.")
return return


base = repo.git.merge_base(branch.commit, upstream.commit)
try:
base = repo.git.merge_base(branch.commit, upstream.commit)
except exc.GitCommandError as err:
print(YELLOW + "skipped:", "can't find merge base with upstream.")
return

if repo.commit(base) == upstream.commit: if repo.commit(base) == upstream.commit:
print(BLUE + "up to date", end=".\n") print(BLUE + "up to date", end=".\n")
return return
@@ -140,7 +145,7 @@ 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, current_only, fetch_only, prune):
def _update_repository(repo, repo_name, current_only, fetch_only, prune):
"""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
@@ -150,7 +155,7 @@ def _update_repository(repo, current_only, fetch_only, prune):
If *prune* is ``True``, remote-tracking branches that no longer exist on If *prune* is ``True``, remote-tracking branches that no longer exist on
their remote after fetching will be deleted. their remote after fetching will be deleted.
""" """
print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":")
print(INDENT1, BOLD + repo_name + ":")


try: try:
active = repo.active_branch active = repo.active_branch
@@ -178,9 +183,9 @@ def _update_repository(repo, current_only, fetch_only, prune):
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, command):
def _run_command(repo, repo_name, command):
"""Run an arbitrary shell command on the given repository.""" """Run an arbitrary shell command on the given repository."""
print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":")
print(INDENT1, BOLD + repo_name + ":")


cmd = shlex.split(command) cmd = shlex.split(command)
try: try:
@@ -193,24 +198,7 @@ def _run_command(repo, 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_multi(base, paths, callback, *args):
"""Apply the callback to all git repos in the list of paths."""
valid = []
for path in paths:
try:
Repo(path)
except (exc.InvalidGitRepositoryError, exc.NoSuchPathError):
continue
valid.append(path)

base = os.path.abspath(base)
suffix = "" if len(valid) == 1 else "s"
print(BOLD + base, "({0} repo{1}):".format(len(valid), suffix))

for path in sorted(valid, key=os.path.basename):
callback(Repo(path), *args)

def _dispatch(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
@@ -220,24 +208,61 @@ def _dispatch(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.
""" """
path = os.path.expanduser(path)
def _collect(paths, maxdepth=6):
"""Return all valid repo paths in the given paths, recursively."""
if maxdepth <= 0:
return []

valid = []
for path in paths:
try:
Repo(path)
valid.append(path)
except exc.InvalidGitRepositoryError:
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)
except exc.NoSuchPathError:
continue
return valid

def _get_basename(base, path):
"""Return a reasonable name for a repo path in the given base."""
if path.startswith(base + os.path.sep):
return path.split(base + os.path.sep, 1)[1]
prefix = os.path.commonprefix([base, path])
while not base.startswith(prefix + os.path.sep):
old = prefix
prefix = os.path.split(prefix)[0]
if prefix == old:
break # Prevent infinite loop, but should be almost impossible
return path.split(prefix + os.path.sep, 1)[1]

base = os.path.expanduser(base_path)
try: try:
repo = Repo(path)
Repo(base)
valid = [base]
except exc.NoSuchPathError: except exc.NoSuchPathError:
paths = glob(path)
if paths:
_dispatch_multi(path, paths, callback, *args)
else:
print(ERROR, BOLD + path, "doesn't exist!")
paths = glob(base)
if not paths:
print(ERROR, BOLD + base, "doesn't exist!")
return
valid = _collect(paths)
except exc.InvalidGitRepositoryError: except exc.InvalidGitRepositoryError:
if os.path.isdir(path):
paths = [os.path.join(path, item) for item in os.listdir(path)]
_dispatch_multi(path, paths, callback, *args)
else:
print(ERROR, BOLD + path, "isn't a repository!")
else:
print(BOLD + repo.working_dir, "(1 repo):")
callback(repo, *args)
if not os.path.isdir(base):
print(ERROR, BOLD + base, "isn't a repository!")
return
valid = _collect([base])

base = os.path.abspath(base)
suffix = "" if len(valid) == 1 else "s"
print(BOLD + base, "({0} repo{1}):".format(len(valid), suffix))

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)


def update_bookmarks(bookmarks, update_args): def update_bookmarks(bookmarks, update_args):
"""Loop through and update all bookmarks.""" """Loop through and update all bookmarks."""


Loading…
Cancel
Save