瀏覽代碼

Merge branch 'develop'

tags/v0.4
Ben Kurtovic 9 年之前
父節點
當前提交
45d8e6c4cc
共有 8 個檔案被更改,包括 148 行新增167 行删除
  1. +37
    -0
      CHANGELOG
  2. +1
    -1
      LICENSE
  3. +8
    -8
      README.md
  4. +4
    -4
      gitup/__init__.py
  5. +9
    -5
      gitup/config.py
  6. +26
    -16
      gitup/script.py
  7. +54
    -130
      gitup/update.py
  8. +9
    -3
      setup.py

+ 37
- 0
CHANGELOG 查看文件

@@ -0,0 +1,37 @@
v0.3 (released June 7, 2015):

- Added support for Python 3.
- Fixed behavior on bare repositories.
- Made branch updating code safer in general: only fast-forwardable branches
tracking upstreams are updated. This deprecates `--merge` and `--rebase`.
- Added `--fetch-only` to disable branch updating entirely, if desired.
- Fixed trying to fetch remotes without configured refspecs.
- Miscellaneous fixes and tweaks.

v0.2.4 (released May 23, 2015):

- Follow the XDG Base Directory Specification for the config file.
- Added installation instructions for Homebrew.

v0.2.3 (released March 14, 2015):

- Added support for newer versions of GitPython.

v0.2.2 (released April 27, 2014):

- Fixed an error being raised when HEAD is detached.

v0.2.1 (released April 21, 2014):

- Fixed a bug when handling errors during a fetch.

v0.2 (released April 21, 2014):

- Rewrote backend to use GitPython instead of direct shell calls. Improved
stability and fixed various bugs.
- Use colorama for highlighting instead of ANSI escape codes.
- Added `--current-only`, `--merge`, and `--rebase` options.

v0.1 (released June 7, 2011):

- Initial release.

+ 1
- 1
LICENSE 查看文件

@@ -1,4 +1,4 @@
Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
Copyright (C) 2011-2015 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


+ 8
- 8
README.md 查看文件

@@ -6,13 +6,13 @@ 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 should work on OS X, Linux, and Windows. You should have the latest
version of git and at least Python 2.7 installed.
version of git and either Python 2.7 or Python 3 installed.

# Installation

With [Homebrew](http://brew.sh/):

brew install pr0d1r2/contrib/gitup && brew link gitup
brew install gitup

## From source

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

By default, gitup will fetch all remotes in a repository. Pass `--current-only`
(or `-c`) to make it only fetch the remote tracked by the current branch.
(or `-c`) to make it fetch _only_ the remote tracked by the current branch.

gitup will _merge_ upstream branches by default unless `pull.rebase` or
`branch.<name>.rebase` is specified in git's config. Pass `--rebase` or `-r` to
make it always _rebase_ (like doing `git pull --rebase=preserve`). Pass
`--merge` or `-m` to make it always merge.
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
(e.g. dirty working directory or a merge/rebase is required). Pass
`--fetch-only` (or `-f`) to only fetch remotes.

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

gitup --help



+ 4
- 4
gitup/__init__.py 查看文件

@@ -1,14 +1,14 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
# See the LICENSE file for details.
# Copyright (C) 2011-2015 Ben Kurtovic <ben.kurtovic@gmail.com>
# Released under the terms of the MIT License. See LICENSE for details.

"""
gitup: the git repository updater
"""

__author__ = "Ben Kurtovic"
__copyright__ = "Copyright (C) 2011-2014 Ben Kurtovic"
__copyright__ = "Copyright (C) 2011-2015 Ben Kurtovic"
__license__ = "MIT License"
__version__ = "0.2.4"
__version__ = "0.3"
__email__ = "ben.kurtovic@gmail.com"

+ 9
- 5
gitup/config.py 查看文件

@@ -1,13 +1,17 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
# See the LICENSE file for details.
# Copyright (C) 2011-2015 Ben Kurtovic <ben.kurtovic@gmail.com>
# Released under the terms of the MIT License. See LICENSE for details.

from __future__ import print_function

import ConfigParser as configparser
import os

try:
import configparser
except ImportError: # Python 2
import ConfigParser as configparser

from colorama import Fore, Style

__all__ = ["get_bookmarks", "add_bookmarks", "delete_bookmarks",
@@ -58,7 +62,7 @@ def get_bookmarks():
"""Get a list of all bookmarks, or an empty list if there are none."""
config = _load_config_file()
try:
return config.items("bookmarks")
return [path for path, _ in config.items("bookmarks")]
except configparser.NoSectionError:
return []

@@ -119,7 +123,7 @@ def list_bookmarks():
bookmarks = get_bookmarks()
if bookmarks:
print(YELLOW + "Current bookmarks:")
for bookmark_path, _ in bookmarks:
for bookmark_path in bookmarks:
print(INDENT1, bookmark_path)
else:
print("You have no bookmarks to display.")

+ 26
- 16
gitup/script.py 查看文件

@@ -1,15 +1,15 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
# See the LICENSE file for details.
# Copyright (C) 2011-2015 Ben Kurtovic <ben.kurtovic@gmail.com>
# Released under the terms of the MIT License. See LICENSE for details.

from __future__ import print_function

import argparse

from colorama import init as color_init, Style
from colorama import init as color_init, Fore, Style

from . import __version__, __email__
from . import __version__
from .config import (get_bookmarks, add_bookmarks, delete_bookmarks,
list_bookmarks)
from .update import update_bookmarks, update_directories
@@ -17,16 +17,16 @@ from .update import update_bookmarks, update_directories
def main():
"""Parse arguments and then call the appropriate function(s)."""
parser = argparse.ArgumentParser(
description="""Easily update multiple git repositories at once.""",
description="Easily update multiple git repositories at once.",
epilog="""
Both relative and absolute paths are accepted by all arguments.
Questions? Comments? Email the author at {0}.""".format(__email__),
Direct bug reports and feature requests to:
https://github.com/earwig/git-repo-updater.""",
add_help=False)

group_u = parser.add_argument_group("updating repositories")
group_b = parser.add_argument_group("bookmarking")
group_m = parser.add_argument_group("miscellaneous")
rebase_or_merge = group_u.add_mutually_exclusive_group()

group_u.add_argument(
'directories_to_update', nargs="*", metavar="path",
@@ -38,13 +38,9 @@ def main():
group_u.add_argument(
'-c', '--current-only', action="store_true", help="""only fetch the
remote tracked by the current branch instead of all remotes""")
rebase_or_merge.add_argument(
'-r', '--rebase', action="store_true", help="""always rebase upstream
branches instead of following `pull.rebase` and `branch.<name>.rebase`
in git config (like `git pull --rebase=preserve`)""")
rebase_or_merge.add_argument(
'-m', '--merge', action="store_true", help="""like --rebase, but merge
instead""")
group_u.add_argument(
'-f', '--fetch-only', action="store_true",
help="only fetch remotes, don't try to fast-forward any branches")

group_b.add_argument(
'-a', '--add', dest="bookmarks_to_add", nargs="+", metavar="path",
@@ -55,19 +51,33 @@ def main():
group_b.add_argument(
'-l', '--list', dest="list_bookmarks", action="store_true",
help="list current bookmarks")

group_m.add_argument(
'-h', '--help', action="help", help="show this help message and exit")
group_m.add_argument(
'-v', '--version', action="version",
version="gitup version " + __version__)
version="gitup " + __version__)

# TODO: deprecated arguments, for removal in v1.0:
parser.add_argument(
'-m', '--merge', action="store_true", help=argparse.SUPPRESS)
parser.add_argument(
'-r', '--rebase', action="store_true", help=argparse.SUPPRESS)

color_init(autoreset=True)
args = parser.parse_args()
update_args = args.current_only, args.rebase, args.merge
update_args = args.current_only, args.fetch_only

print(Style.BRIGHT + "gitup" + Style.RESET_ALL + ": the git-repo-updater")
print()

# TODO: remove in v1.0
if args.merge or args.rebase:
print(Style.BRIGHT + Fore.YELLOW + "Warning:", "--merge and --rebase "
"are deprecated. Branches are only updated if they\ntrack an "
"upstream branch and can be safely fast-forwarded. Use "
"--fetch-only to\navoid updating any branches.\n")

acted = False
if args.bookmarks_to_add:
add_bookmarks(args.bookmarks_to_add)


+ 54
- 130
gitup/update.py 查看文件

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
# See the LICENSE file for details.
# Copyright (C) 2011-2015 Ben Kurtovic <ben.kurtovic@gmail.com>
# Released under the terms of the MIT License. See LICENSE for details.

from __future__ import print_function

@@ -53,34 +53,6 @@ class _ProgressMonitor(RemoteProgress):
print(str(cur_count), end=end)


class _Stasher(object):
"""Manages the stash state of a given repository."""

def __init__(self, repo):
self._repo = repo
self._clean = self._stashed = False

def clean(self):
"""Ensure the working directory is clean, so we can do checkouts."""
if not self._clean:
res = self._repo.git.stash("--all")
self._clean = True
if res != "No local changes to save":
self._stashed = True

def restore(self):
"""Restore the pre-stash state."""
if self._stashed:
self._repo.git.stash("pop", "--index")


def _read_config(repo, attr):
"""Read an attribute from git config."""
try:
return repo.git.config("--get", attr)
except exc.GitCommandError:
return None

def _fetch_remotes(remotes):
"""Fetch a list of remotes, displaying progress info along the way."""
def _get_name(ref):
@@ -94,6 +66,11 @@ def _fetch_remotes(remotes):

for remote in remotes:
print(INDENT2, "Fetching", BOLD + remote.name, end="")

if not remote.config_reader.has_option("fetch"):
print(":", YELLOW + "skipped:", "no configured refspec.")
continue

try:
results = remote.fetch(progress=_ProgressMonitor())
except exc.GitCommandError as err:
@@ -117,56 +94,7 @@ def _fetch_remotes(remotes):
rlist.append("{0} ({1})".format(colored, ", ".join(names)))
print(":", (", ".join(rlist) if rlist else up_to_date) + ".")

def _is_up_to_date(repo, branch, upstream):
"""Return whether *branch* is up-to-date with its *upstream*."""
base = repo.git.merge_base(branch.commit, upstream.commit)
return repo.commit(base) == upstream.commit

def _rebase(repo, name):
"""Rebase the current HEAD of *repo* onto the branch *name*."""
print(GREEN + "rebasing...", end="")
try:
res = repo.git.rebase(name, "--preserve-merges")
except exc.GitCommandError as err:
msg = err.stderr.replace("\n", " ").strip()
if not msg.endswith("."):
msg += "."
if "unstaged changes" in msg:
print(RED + " error:", "unstaged changes.")
elif "uncommitted changes" in msg:
print(RED + " error:", "uncommitted changes.")
else:
try:
repo.git.rebase("--abort")
except exc.GitCommandError:
pass
print(RED + " error:", msg if msg else "rebase conflict.",
"Aborted.")
else:
print("\b" * 6 + " " * 6 + "\b" * 6 + GREEN + "ed", end=".\n")

def _merge(repo, name):
"""Merge the branch *name* into the current HEAD of *repo*."""
print(GREEN + "merging...", end="")
try:
repo.git.merge(name)
except exc.GitCommandError as err:
msg = err.stderr.replace("\n", " ").strip()
if not msg.endswith("."):
msg += "."
if "local changes" in msg and "would be overwritten" in msg:
print(RED + " error:", "uncommitted changes.")
else:
try:
repo.git.merge("--abort")
except exc.GitCommandError:
pass
print(RED + " error:", msg if msg else "merge conflict.",
"Aborted.")
else:
print("\b" * 6 + " " * 6 + "\b" * 6 + GREEN + "ed", end=".\n")

def _update_branch(repo, branch, merge, rebase, stasher=None):
def _update_branch(repo, branch, is_active=False):
"""Update a single branch."""
print(INDENT2, "Updating", BOLD + branch.name, end=": ")
upstream = branch.tracking_branch()
@@ -179,43 +107,39 @@ def _update_branch(repo, branch, merge, rebase, stasher=None):
except ValueError:
print(YELLOW + "skipped:", "branch has no revisions.")
return
if _is_up_to_date(repo, branch, upstream):

base = repo.git.merge_base(branch.commit, upstream.commit)
if repo.commit(base) == upstream.commit:
print(BLUE + "up to date", end=".\n")
return

if stasher:
stasher.clean()
branch.checkout()
config_attr = "branch.{0}.rebase".format(branch.name)
if not merge and (rebase or _read_config(repo, config_attr)):
_rebase(repo, upstream.name)
else:
_merge(repo, upstream.name)

def _update_branches(repo, active, merge, rebase):
"""Update a list of branches."""
if active:
_update_branch(repo, active, merge, rebase)
branches = set(repo.heads) - {active}
if branches:
stasher = _Stasher(repo)
if is_active:
try:
for branch in sorted(branches, key=lambda b: b.name):
_update_branch(repo, branch, merge, rebase, stasher)
finally:
if active:
active.checkout()
stasher.restore()
repo.git.merge(upstream.name, ff_only=True)
print(GREEN + "done", end=".\n")
except exc.GitCommandError as err:
msg = err.stderr
if "local changes" in msg and "would be overwritten" in msg:
print(YELLOW + "skipped:", "uncommitted changes.")
else:
print(YELLOW + "skipped:", "not possible to fast-forward.")
else:
status = repo.git.merge_base(
branch.commit, upstream.commit, is_ancestor=True,
with_extended_output=True, with_exceptions=False)[0]
if status != 0:
print(YELLOW + "skipped:", "not possible to fast-forward.")
else:
repo.git.branch(branch.name, upstream.name, force=True)
print(GREEN + "done", end=".\n")

def _update_repository(repo, current_only=False, rebase=False, merge=False):
def _update_repository(repo, current_only=False, fetch_only=False):
"""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``. By default, we will merge unless
``pull.rebase`` or ``branch.<name>.rebase`` is set in config; *rebase* will
cause us to always rebase with ``--preserve-merges``, and *merge* will
cause us to always merge.
current branch if ``True``. If *fetch_only* is ``False``, we will also
update all fast-forwardable branches that are tracking valid upstreams.
"""
print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":")

@@ -224,22 +148,28 @@ def _update_repository(repo, current_only=False, rebase=False, merge=False):
except TypeError: # Happens when HEAD is detached
active = None
if current_only:
ref = active.tracking_branch() if active else None
if not active:
print(INDENT2, ERROR,
"--current-only doesn't make sense with a detached HEAD.")
return
ref = active.tracking_branch()
if not ref:
print(INDENT2, ERROR, "no remote tracked by current branch.")
return
remotes = [repo.remotes[ref.remote_name]]
else:
remotes = repo.remotes

if not remotes:
print(INDENT2, ERROR, "no remotes configured to pull from.")
print(INDENT2, ERROR, "no remotes configured to fetch.")
return
rebase = rebase or _read_config(repo, "pull.rebase")

_fetch_remotes(remotes)
_update_branches(repo, active, merge, rebase)

def _update_subdirectories(path, long_name, update_args):
if not fetch_only:
for branch in sorted(repo.heads, key=lambda b: b.name):
_update_branch(repo, branch, branch == active)

def _update_subdirectories(path, update_args):
"""Update all subdirectories that are git repos in a given directory."""
repos = []
for item in os.listdir(path):
@@ -249,13 +179,12 @@ def _update_subdirectories(path, long_name, update_args):
continue
repos.append(repo)

suffix = "ies" if len(repos) != 1 else "y"
print(long_name[0].upper() + long_name[1:],
"contains {0} git repositor{1}:".format(len(repos), suffix))
suffix = "" if len(repos) == 1 else "s"
print(BOLD + path, "({0} repo{1}):".format(len(repos), suffix))
for repo in sorted(repos, key=lambda r: os.path.split(r.working_dir)[1]):
_update_repository(repo, *update_args)

def _update_directory(path, update_args, is_bookmark=False):
def _update_directory(path, update_args):
"""Update a particular directory.

Determine whether the directory is a git repo on its own, a directory of
@@ -263,29 +192,24 @@ def _update_directory(path, update_args, is_bookmark=False):
repository; if the second, update all repositories contained within; if the
third, print an error.
"""
dir_type = "bookmark" if is_bookmark else "directory"
long_name = dir_type + ' "' + BOLD + path + RESET + '"'

try:
repo = Repo(path)
except exc.NoSuchPathError:
print(ERROR, long_name, "doesn't exist!")
print(ERROR, BOLD + path, "doesn't exist!")
except exc.InvalidGitRepositoryError:
if os.path.isdir(path):
_update_subdirectories(path, long_name, update_args)
_update_subdirectories(path, update_args)
else:
print(ERROR, long_name, "isn't a repository!")
print(ERROR, BOLD + path, "isn't a repository!")
else:
long_name = (dir_type.capitalize() + ' "' + BOLD + repo.working_dir +
RESET + '"')
print(long_name, "is a git repository:")
print(BOLD + repo.working_dir, "(1 repo):")
_update_repository(repo, *update_args)

def update_bookmarks(bookmarks, update_args):
"""Loop through and update all bookmarks."""
if bookmarks:
for path, name in bookmarks:
_update_directory(path, update_args, is_bookmark=True)
for path in bookmarks:
_update_directory(path, update_args)
else:
print("You don't have any bookmarks configured! Get help with 'gitup -h'.")

@@ -293,4 +217,4 @@ def update_directories(paths, update_args):
"""Update a list of directories supplied by command arguments."""
for path in paths:
full_path = os.path.abspath(path)
_update_directory(full_path, update_args, is_bookmark=False)
_update_directory(full_path, update_args)

+ 9
- 3
setup.py 查看文件

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
# See the LICENSE file for details.
# Copyright (C) 2011-2015 Ben Kurtovic <ben.kurtovic@gmail.com>
# Released under the terms of the MIT License. See LICENSE for details.

import sys

@@ -23,7 +23,7 @@ setup(
version = __version__,
author = "Ben Kurtovic",
author_email = "ben.kurtovic@gmail.com",
description = "Easily pull to multiple git repositories at once.",
description = "Easily pull to multiple git repositories at once",
long_description = long_desc,
license = "MIT License",
keywords = "git repository pull update",
@@ -35,8 +35,14 @@ setup(
"Natural Language :: English",
"Operating System :: MacOS :: MacOS X",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Topic :: Software Development :: Version Control"
]
)

Loading…
取消
儲存