瀏覽代碼

Merge branch 'develop'

tags/v0.3
Ben Kurtovic 10 年之前
父節點
當前提交
fa847d4fbf
共有 9 個文件被更改,包括 556 次插入352 次删除
  1. +7
    -1
      .gitignore
  2. +1
    -1
      LICENSE
  3. +29
    -16
      README.md
  4. +0
    -303
      gitup.py
  5. +14
    -0
      gitup/__init__.py
  6. +104
    -0
      gitup/config.py
  7. +92
    -0
      gitup/script.py
  8. +282
    -0
      gitup/update.py
  9. +27
    -31
      setup.py

+ 7
- 1
.gitignore 查看文件

@@ -1 +1,7 @@
build/
*.pyc
*.egg
*.egg-info
.DS_Store
__pycache__
build
dist

+ 1
- 1
LICENSE 查看文件

@@ -1,4 +1,4 @@
Copyright (c) 2011 by Ben Kurtovic <ben.kurtovic@verizon.net>
Copyright (C) 2011-2014 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


+ 29
- 16
README.md 查看文件

@@ -1,12 +1,12 @@
__gitup__ (the _git-repo-updater_)

gitup is a tool designed to pull to a large number of git repositories at once.
It is smart enough to ignore repos with dirty working directories, and provides
a (hopefully) great way to get everything up-to-date for those short periods of
internet access between long periods of none.
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 works on both OS X and Linux. You should have the latest version of git
and at least Python 2.7 installed.
gitup should work on OS X, Linux, and Windows. You should have the latest
version of git and at least Python 2.7 installed.

# Installation

@@ -25,6 +25,12 @@ Then, to install for everyone:

Finally, simply delete the `git-repo-updater` directory, and you're done!

__Note:__ If you are using Windows, you may wish to add a macro so you can
invoke gitup in any directory. Note that `C:\python27\` refers to the
directory where Python is installed:

DOSKEY gitup=c:\python27\python.exe c:\python27\Scripts\gitup $*

# Usage

There are two ways to update repos: you can pass them as command arguments,
@@ -34,28 +40,27 @@ For example:

gitup ~/repos/foo ~/repos/bar ~/repos/baz

...will automatically pull to the `foo`, `bar`, and `baz` git repositories if
their working directories are clean (to avoid merge conflicts). Additionally,
you can just type:
will automatically pull to the `foo`, `bar`, and `baz` git repositories.
Additionally, you can just type:

gitup ~/repos

...to automatically update all git repositories in that directory.
to automatically update all git repositories in that directory.

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

gitup --add ~/repos/foo ~/repos/bar ~/repos/baz
gitup --add ~/repos

Then, to update (pull to) all of your bookmarks, just run gitup without args:
Then, to update all of your bookmarks, just run gitup without args:

gitup

Deleting a bookmark is as easy as adding one:
Delete a bookmark:

gitup --delete ~/repos

Want to view your current bookmarks? Simple:
View your current bookmarks:

gitup --list

@@ -66,13 +71,21 @@ You can mix and match bookmarks and command arguments:
gitup # update 'foo' and 'bar' only
gitup ~/repos/baz --update # update all three!

Want to update all git repositories in your current directory?
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.

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.

For a 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).
Finally, all paths can be either absolute (e.g. `/path/to/repo`) or relative
(e.g. `../my/repo`).

+ 0
- 303
gitup.py 查看文件

@@ -1,303 +0,0 @@
#! /usr/bin/python
# -*- coding: utf-8 -*-

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

import argparse
import ConfigParser as configparser
import os
import re
import shlex
import subprocess

__author__ = "Ben Kurtovic"
__copyright__ = "Copyright (c) 2011 by Ben Kurtovic"
__license__ = "MIT License"
__version__ = "0.1"
__email__ = "ben.kurtovic@verizon.net"

config_filename = os.path.join(os.path.expanduser("~"), ".gitup")

ansi = { # ANSI escape codes to make terminal output colorful
"reset": "\x1b[0m",
"bold": "\x1b[1m",
"red": "\x1b[1m\x1b[31m",
"green": "\x1b[1m\x1b[32m",
"yellow": "\x1b[1m\x1b[33m",
"blue": "\x1b[1m\x1b[34m",
}

def out(indent, msg):
"""Print a message at a given indentation level."""
width = 4 # amount to indent at each level
if indent == 0:
spacing = "\n"
else:
spacing = " " * width * indent
msg = re.sub("\s+", " ", msg) # collapse multiple spaces into one
print spacing + msg

def exec_shell(command):
"""Execute a shell command and get the output."""
command = shlex.split(command)
result = subprocess.check_output(command, stderr=subprocess.STDOUT)
if result:
result = result[:-1] # strip newline if command returned anything
return result

def directory_is_git_repo(directory_path):
"""Check if a directory is a git repository."""
if os.path.isdir(directory_path):
git_subfolder = os.path.join(directory_path, ".git")
if os.path.isdir(git_subfolder): # check for path/to/repository/.git
return True
return False

def update_repository(repo_path, repo_name):
"""Update a single git repository by pulling from the remote."""
out(1, "{}{}{}:".format(ansi['bold'], repo_name, ansi['reset']))
os.chdir(repo_path) # cd into our folder so git commands target the correct
# repo
try:
dry_fetch = exec_shell("git fetch --dry-run") # check if there is
# anything to pull, but
# don't do it yet
except subprocess.CalledProcessError:
out(2, """{}Error:{} cannot fetch; do you have a remote repository
configured correctly?""".format(ansi['red'], ansi['reset']))
return
try:
last = exec_shell("git log -n 1 --pretty=\"%ar\"") # last commit time
except subprocess.CalledProcessError:
last = "never" # couldn't get a log, so no commits
if not dry_fetch: # no new changes to pull
out(2, "{}No new changes.{} Last commit was {}.".format(ansi['blue'],
ansi['reset'], last))
else: # stuff has happened!
out(2, "There are new changes upstream...")
status = exec_shell("git status")
if status.endswith("nothing to commit (working directory clean)"):
out(2, "{}Pulling new changes...{}".format(ansi['green'],
ansi['reset']))
result = exec_shell("git pull")
out(2, "The following changes have been made since {}:".format(
last))
print result
else:
out(2, """{}Warning:{} You have uncommitted changes in this
repository!""".format(ansi['red'], ansi['reset']))
out(2, "Ignoring.")

def update_directory(dir_path, dir_name, is_bookmark=False):
"""First, make sure the specified object is actually a directory, then
determine whether the directory is a git repo on its own or a directory
of git repositories. If the former, update the single repository; if the
latter, update all repositories contained within."""
if is_bookmark:
dir_source = "Bookmark" # where did we get this directory from?
else:
dir_source = "Directory"
try:
os.listdir(dir_path) # test if we can access this directory
except OSError:
out(0, "{}Error:{} cannot enter {} '{}{}{}'; does it exist?".format(
ansi['red'], ansi['reset'], dir_source.lower(), ansi['bold'], dir_path,
ansi['reset']))
return
if not os.path.isdir(dir_path):
if os.path.exists(dir_path):
error_message = "is not a directory"
else:
error_message = "does not exist"
out(0, "{}Error{}: {} '{}{}{}' {}!".format(ansi['red'], ansi['reset'],
dir_source, ansi['bold'], dir_path, ansi['reset'],
error_message))
return
if directory_is_git_repo(dir_path):
out(0, "{} '{}{}{}' is a git repository:".format(dir_source,
ansi['bold'], dir_path, ansi['reset']))
update_repository(dir_path, dir_name)
else:
repositories = []
dir_contents = os.listdir(dir_path) # get potential repos in directory
for item in dir_contents:
repo_path = os.path.join(dir_path, item)
repo_name = os.path.join(dir_name, item)
if directory_is_git_repo(repo_path): # filter out non-repositories
repositories.append((repo_path, repo_name))
repo_count = len(repositories)
if repo_count == 1:
pluralize = "repository"
else:
pluralize = "repositories"
out(0, "{} '{}{}{}' contains {} git {}:".format(dir_source,
ansi['bold'], dir_path, ansi['reset'], repo_count, pluralize))

for repo_path, repo_name in repositories:
update_repository(repo_path, repo_name)

def update_directories(paths):
"""Update a list of directories supplied by command arguments."""
for path in paths:
path = os.path.abspath(path) # convert relative to absolute path
path_name = os.path.split(path)[1] # directory name; "x" in /path/to/x/
update_directory(path, path_name, is_bookmark=False)

def update_bookmarks():
"""Loop through and update all bookmarks."""
try:
bookmarks = load_config_file().items("bookmarks")
except configparser.NoSectionError:
bookmarks = []
if bookmarks:
for bookmark_path, bookmark_name in bookmarks:
update_directory(bookmark_path, bookmark_name, is_bookmark=True)
else:
out(0, """You don't have any bookmarks configured! Get help with
'gitup -h'.""")

def load_config_file():
"""Read the file storing our config options from config_filename and return
the resulting SafeConfigParser() object."""
config = configparser.SafeConfigParser()
config.optionxform = str # don't lowercase option names, because we are
# storing paths there
config.read(config_filename)
return config

def save_config_file(config):
"""Save our config changes to the config file specified by
config_filename."""
with open(config_filename, "wb") as config_file:
config.write(config_file)

def add_bookmarks(paths):
"""Add a list of paths as bookmarks to the config file."""
config = load_config_file()
if not config.has_section("bookmarks"):
config.add_section("bookmarks")
out(0, "{}Added bookmarks:{}".format(ansi['yellow'], ansi['reset']))
for path in paths:
path = os.path.abspath(path) # convert relative to absolute path
if config.has_option("bookmarks", path):
out(1, "'{}' is already bookmarked.".format(path))
else:
path_name = os.path.split(path)[1]
config.set("bookmarks", path, path_name)
out(1, "{}{}{}".format(ansi['bold'], path, ansi['reset']))
save_config_file(config)

def delete_bookmarks(paths):
"""Remove a list of paths from the bookmark config file."""
config = load_config_file()
if config.has_section("bookmarks"):
out(0, "{}Deleted bookmarks:{}".format(ansi['yellow'], ansi['reset']))
for path in paths:
path = os.path.abspath(path) # convert relative to absolute path
config_was_changed = config.remove_option("bookmarks", path)
if config_was_changed:
out(1, "{}{}{}".format(ansi['bold'], path, ansi['reset']))
else:
out(1, "'{}' is not bookmarked.".format(path))
save_config_file(config)
else:
out(0, "There are no bookmarks to delete!")

def list_bookmarks():
"""Print all of our current bookmarks."""
config = load_config_file()
try:
bookmarks = config.items("bookmarks")
except configparser.NoSectionError:
bookmarks = []
if bookmarks:
out(0, "{}Current bookmarks:{}".format(ansi['yellow'], ansi['reset']))
for bookmark_path, bookmark_name in bookmarks:
out(1, bookmark_path)
else:
out(0, "You have no bookmarks to display.")

def main():
"""Parse arguments and then call the appropriate function(s)."""
parser = argparse.ArgumentParser(description="""Easily pull to multiple git
repositories at once.""", epilog="""Both relative and absolute
paths are accepted by all arguments. Questions? Comments? Email the
author at {}.""".format(__email__), 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")
group_u.add_argument('directories_to_update', nargs="*", metavar="path",
help="""update all repositories in this directory (or the directory
itself, if it is a repo)""")
group_u.add_argument('-u', '--update', action="store_true", help="""update
all bookmarks (default behavior when called without arguments)""")
group_b.add_argument('-a', '--add', dest="bookmarks_to_add", nargs="+",
metavar="path", help="add directory(s) as bookmarks")
group_b.add_argument('-d', '--delete', dest="bookmarks_to_del", nargs="+",
metavar="path",
help="delete bookmark(s) (leaves actual directories alone)")
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__)
args = parser.parse_args()
print "{}gitup{}: the git-repo-updater".format(ansi['bold'], ansi['reset'])
if args.bookmarks_to_add:
add_bookmarks(args.bookmarks_to_add)

if args.bookmarks_to_del:
delete_bookmarks(args.bookmarks_to_del)
if args.list_bookmarks:
list_bookmarks()
if args.directories_to_update:
update_directories(args.directories_to_update)
if args.update:
update_bookmarks()
if not any(vars(args).values()): # if they did not tell us to do anything,
update_bookmarks() # automatically update bookmarks

if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
out(0, "Stopped by user.")

+ 14
- 0
gitup/__init__.py 查看文件

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

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

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

+ 104
- 0
gitup/config.py 查看文件

@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
# See the LICENSE file for details.

from __future__ import print_function

import ConfigParser as configparser
import os

from colorama import Fore, Style

__all__ = ["get_bookmarks", "add_bookmarks", "delete_bookmarks",
"list_bookmarks"]

CONFIG_FILENAME = os.path.join(os.path.expanduser("~"), ".gitup")

YELLOW = Fore.YELLOW + Style.BRIGHT
RED = Fore.RED + Style.BRIGHT

INDENT1 = " " * 3

def _load_config_file():
"""Read the config file and return a SafeConfigParser() object."""
config = configparser.SafeConfigParser()
# Don't lowercase option names, because we are storing paths there:
config.optionxform = str
config.read(CONFIG_FILENAME)
return config

def _save_config_file(config):
"""Save config changes to the config file specified by CONFIG_FILENAME."""
with open(CONFIG_FILENAME, "wb") as config_file:
config.write(config_file)

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")
except configparser.NoSectionError:
return []

def add_bookmarks(paths):
"""Add a list of paths as bookmarks to the config file."""
config = _load_config_file()
if not config.has_section("bookmarks"):
config.add_section("bookmarks")

added, exists = [], []
for path in paths:
path = os.path.abspath(path)
if config.has_option("bookmarks", path):
exists.append(path)
else:
path_name = os.path.split(path)[1]
config.set("bookmarks", path, path_name)
added.append(path)
_save_config_file(config)

if added:
print(YELLOW + "Added bookmarks:")
for path in added:
print(INDENT1, path)
if exists:
print(RED + "Already bookmarked:")
for path in exists:
print(INDENT1, path)

def delete_bookmarks(paths):
"""Remove a list of paths from the bookmark config file."""
config = _load_config_file()

deleted, notmarked = [], []
if config.has_section("bookmarks"):
for path in paths:
path = os.path.abspath(path)
config_was_changed = config.remove_option("bookmarks", path)
if config_was_changed:
deleted.append(path)
else:
notmarked.append(path)
_save_config_file(config)
else:
notmarked = [os.path.abspath(path) for path in paths]

if deleted:
print(YELLOW + "Deleted bookmarks:")
for path in deleted:
print(INDENT1, path)
if notmarked:
print(RED + "Not bookmarked:")
for path in notmarked:
print(INDENT1, path)

def list_bookmarks():
"""Print all of our current bookmarks."""
bookmarks = get_bookmarks()
if bookmarks:
print(YELLOW + "Current bookmarks:")
for bookmark_path, _ in bookmarks:
print(INDENT1, bookmark_path)
else:
print("You have no bookmarks to display.")

+ 92
- 0
gitup/script.py 查看文件

@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
# See the LICENSE file for details.

from __future__ import print_function

import argparse

from colorama import init as color_init, Style

from . import __version__, __email__
from .config import (get_bookmarks, add_bookmarks, delete_bookmarks,
list_bookmarks)
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.""",
epilog="""
Both relative and absolute paths are accepted by all arguments.
Questions? Comments? Email the author at {0}.""".format(__email__),
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",
help="""update all repositories in this directory (or the directory
itself, if it is a repo)""")
group_u.add_argument(
'-u', '--update', action="store_true", help="""update all bookmarks
(default behavior when called without arguments)""")
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_b.add_argument(
'-a', '--add', dest="bookmarks_to_add", nargs="+", metavar="path",
help="add directory(s) as bookmarks")
group_b.add_argument(
'-d', '--delete', dest="bookmarks_to_del", nargs="+", metavar="path",
help="delete bookmark(s) (leaves actual directories alone)")
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__)

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

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

acted = False
if args.bookmarks_to_add:
add_bookmarks(args.bookmarks_to_add)
acted = True
if args.bookmarks_to_del:
delete_bookmarks(args.bookmarks_to_del)
acted = True
if args.list_bookmarks:
list_bookmarks()
acted = True
if args.directories_to_update:
update_directories(args.directories_to_update, update_args)
acted = True
if args.update or not acted:
update_bookmarks(get_bookmarks(), update_args)

def run():
"""Thin wrapper for main() that catches KeyboardInterrupts."""
try:
main()
except KeyboardInterrupt:
print("Stopped by user.")

+ 282
- 0
gitup/update.py 查看文件

@@ -0,0 +1,282 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
# See the LICENSE file for details.

from __future__ import print_function

import os

from colorama import Fore, Style
from git import RemoteReference as RemoteRef, Repo, exc
from git.util import RemoteProgress

__all__ = ["update_bookmarks", "update_directories"]

BOLD = Style.BRIGHT
BLUE = Fore.BLUE + BOLD
GREEN = Fore.GREEN + BOLD
RED = Fore.RED + BOLD
YELLOW = Fore.YELLOW + BOLD
RESET = Style.RESET_ALL

INDENT1 = " " * 3
INDENT2 = " " * 7
ERROR = RED + "Error:" + RESET

class _ProgressMonitor(RemoteProgress):
"""Displays relevant output during the fetching process."""

def __init__(self):
super(_ProgressMonitor, self).__init__()
self._started = False

def update(self, op_code, cur_count, max_count=None, message=''):
"""Called whenever progress changes. Overrides default behavior."""
if op_code & (self.COMPRESSING | self.RECEIVING):
if op_code & self.BEGIN:
print("\b, " if self._started else " (", end="")
if not self._started:
self._started = True
if op_code & self.END:
end = ")"
else:
end = "\b" * (1 + len(cur_count) + len(max_count))
print("{0}/{1}".format(cur_count, max_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):
"""Return the local name of a remote or tag reference."""
return ref.remote_head if isinstance(ref, RemoteRef) else ref.name

info = [("NEW_HEAD", "new branch", "new branches"),
("NEW_TAG", "new tag", "new tags"),
("FAST_FORWARD", "branch update", "branch updates")]
up_to_date = BLUE + "up to date" + RESET

for remote in remotes:
print(INDENT2, "Fetching", BOLD + remote.name, end="")
try:
results = remote.fetch(progress=_ProgressMonitor())
except exc.GitCommandError as err:
msg = err.command[0].replace("Error when fetching: ", "")
if not msg.endswith("."):
msg += "."
print(RED + "error:", msg)
return
except AssertionError: # Seems to be the result of a bug in GitPython
# This happens when git initiates an auto-gc during fetch:
print(RED + "error:", "something went wrong in GitPython,",
"but the fetch might have been successful.")
rlist = []
for attr, singular, plural in info:
names = [_get_name(res.ref)
for res in results if res.flags & getattr(res, attr)]
if names:
desc = singular if len(names) == 1 else plural
colored = GREEN + desc + RESET
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):
"""Update a single branch."""
print(INDENT2, "Updating", BOLD + branch.name, end=": ")
upstream = branch.tracking_branch()
if not upstream:
print(YELLOW + "skipped:", "no upstream is tracked.")
return

try:
branch.commit, upstream.commit
except ValueError:
print(YELLOW + "skipped:", "branch has no revisions.")
return
if _is_up_to_date(repo, branch, upstream):
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."""
_update_branch(repo, active, merge, rebase)
branches = set(repo.heads) - {active}
if branches:
stasher = _Stasher(repo)
try:
for branch in sorted(branches, key=lambda b: b.name):
_update_branch(repo, branch, merge, rebase, stasher)
finally:
active.checkout()
stasher.restore()

def _update_repository(repo, current_only=False, rebase=False, merge=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.
"""
print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":")

active = repo.active_branch
if current_only:
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.")
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):
"""Update all subdirectories that are git repos in a given directory."""
repos = []
for item in os.listdir(path):
try:
repo = Repo(os.path.join(path, item))
except (exc.InvalidGitRepositoryError, exc.NoSuchPathError):
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))
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):
"""Update a particular directory.

Determine whether the directory is a git repo on its own, a directory of
git repositories, or something invalid. If the first, update the single
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!")
except exc.InvalidGitRepositoryError:
if os.path.isdir(path):
_update_subdirectories(path, long_name, update_args)
else:
print(ERROR, long_name, "isn't a repository!")
else:
long_name = (dir_type.capitalize() + ' "' + BOLD + repo.working_dir +
RESET + '"')
print(long_name, "is a git repository:")
_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)
else:
print("You don't have any bookmarks configured! Get help with 'gitup -h'.")

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)

+ 27
- 31
setup.py 查看文件

@@ -1,35 +1,35 @@
from distutils.core import setup
import os
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
# See the LICENSE file 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/>.")

remove_py_extension = True # install script as "gitup" instead of "gitup.py"

if os.path.exists("gitup"):
remove_py_extension = False
else:
os.rename("gitup.py", "gitup")

desc = "Easily pull to multiple git repositories at once."
from gitup import __version__

with open('README.md') as file:
long_desc = file.read()
with open('README.md') as fp:
long_desc = fp.read()

try:
setup(
name = "gitup",
version = "0.1",
scripts = ['gitup'],
author = "Ben Kurtovic",
author_email = "ben.kurtovic@verizon.net",
description = desc,
long_description = long_desc,
license = "MIT License",
keywords = "git repository pull update",
url = "http://github.com/earwig/git-repo-updater",
classifiers = ["Environment :: Console",
setup(
name = "gitup",
packages = find_packages(),
entry_points = {"console_scripts": ["gitup = gitup.script:run"]},
install_requires = ["GitPython >= 0.3.2.RC1", "colorama >= 0.2.7"],
version = __version__,
author = "Ben Kurtovic",
author_email = "ben.kurtovic@gmail.com",
description = "Easily pull to 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",
classifiers = [
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
@@ -38,9 +38,5 @@ try:
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
"Topic :: Software Development :: Version Control"
]
)
finally:
if remove_py_extension:
os.rename("gitup", "gitup.py") # restore file location
]
)

Loading…
取消
儲存