From 48e82888f5f1eb5afb26f3899e2b0c81f592dfd0 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Wed, 26 Mar 2014 10:42:29 -0400 Subject: [PATCH 01/17] Some work on GitPython integration. --- gitup/update.py | 100 ++++++++++++++++++++------------------------------------ 1 file changed, 35 insertions(+), 65 deletions(-) diff --git a/gitup/update.py b/gitup/update.py index de2d01b..4a36915 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -6,10 +6,9 @@ from __future__ import print_function import os -import shlex -import subprocess from colorama import Fore, Style +from git import Repo, exc __all__ = ["update_bookmarks", "update_directories"] @@ -22,29 +21,10 @@ RESET = Style.RESET_ALL INDENT1 = " " * 3 INDENT2 = " " * 7 -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): +def _update_repository(repo, repo_name): """Update a single git repository by pulling from the remote.""" - 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 - print(INDENT1, BOLD + repo_name + ":") - # cd into our folder so git commands target the correct repo: - os.chdir(repo_path) # TODO: remove this when using gitpython - try: # Check if there is anything to pull, but don't do it yet: dry_fetch = _exec_shell("git fetch --dry-run") @@ -81,58 +61,48 @@ def _update_repository(repo_path, repo_name): "you have uncommitted changes in this repository!") print(INDENT2, "Ignoring.") +def _update_subdirectories(dir_path, dir_name, dir_long_name): + """Update all subdirectories that are git repos in a given directory.""" + repos = [] + for item in os.listdir(dir_path): + try: + repo = Repo(os.path.join(dir_path, item)) + except (exc.InvalidGitRepositoryError, exc.NoSuchPathError): + continue + repos.append((repo, os.path.join(dir_name, item))) + + if len(repos) == 1: + print(dir_long_name.capitalize(), "contains 1 git repository:") + else: + print(dir_long_name.capitalize(), + "contains {0} git repositories:".format(len(repos))) + + for repo_path, repo_name in sorted(repos): + _update_repository(repo_path, repo_name) + def _update_directory(dir_path, dir_name, is_bookmark=False): """Update a particular directory. - 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. + 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. """ - if is_bookmark: - dir_type = "bookmark" # Where did we get this directory from? - else: - dir_type = "directory" + dir_type = "bookmark" if is_bookmark else "directory" dir_long_name = dir_type + ' "' + BOLD + dir_path + RESET + '"' try: - os.listdir(dir_path) # Test if we can access this directory - except OSError: - print(RED + "Error:" + RESET, - "cannot enter {0}; does it exist?".format(dir_long_name)) - return - - if not os.path.isdir(dir_path): - if os.path.exists(dir_path): - print(RED + "Error:" + RESET, dir_long_name, "is not a directory!") + repo = Repo(dir_path) + except exc.NoSuchPathError: + print(RED + "Error:" + RESET, dir_long_name, "doesn't exist!") + except exc.InvalidGitRepositoryError: + if os.path.isdir(dir_path): + _update_subdirectories(dir_path, dir_name, dir_long_name) else: - print(RED + "Error:" + RESET, dir_long_name, "does not exist!") - return - - if _directory_is_git_repo(dir_path): - print(dir_long_name.capitalize(), "is a git repository:") - _update_repository(dir_path, dir_name) - + print(RED + "Error:" + RESET, dir_long_name, "isn't a repository!") 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-repos - repositories.append((repo_path, repo_name)) - - num_of_repos = len(repositories) - if num_of_repos == 1: - print(dir_long_name.capitalize(), "contains 1 git repository:") - else: - print(dir_long_name.capitalize(), - "contains {0} git repositories:".format(num_of_repos)) - - repositories.sort() # Go alphabetically instead of randomly - for repo_path, repo_name in repositories: - _update_repository(repo_path, repo_name) + print(dir_long_name.capitalize(), "is a git repository:") + _update_repository(repo, dir_name) def update_bookmarks(bookmarks): """Loop through and update all bookmarks.""" From 4c878c2a63537cdacd9519852fc87b403f81a531 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Wed, 26 Mar 2014 10:59:42 -0400 Subject: [PATCH 02/17] More cleanup. --- gitup/update.py | 53 +++++++++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/gitup/update.py b/gitup/update.py index 4a36915..e077f72 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -21,9 +21,9 @@ RESET = Style.RESET_ALL INDENT1 = " " * 3 INDENT2 = " " * 7 -def _update_repository(repo, repo_name): +def _update_repository(repo): """Update a single git repository by pulling from the remote.""" - print(INDENT1, BOLD + repo_name + ":") + print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":") try: # Check if there is anything to pull, but don't do it yet: @@ -61,26 +61,23 @@ def _update_repository(repo, repo_name): "you have uncommitted changes in this repository!") print(INDENT2, "Ignoring.") -def _update_subdirectories(dir_path, dir_name, dir_long_name): +def _update_subdirectories(path, long_name): """Update all subdirectories that are git repos in a given directory.""" repos = [] - for item in os.listdir(dir_path): + for item in os.listdir(path): try: - repo = Repo(os.path.join(dir_path, item)) + repo = Repo(os.path.join(path, item)) except (exc.InvalidGitRepositoryError, exc.NoSuchPathError): continue - repos.append((repo, os.path.join(dir_name, item))) + repos.append(repo) - if len(repos) == 1: - print(dir_long_name.capitalize(), "contains 1 git repository:") - else: - print(dir_long_name.capitalize(), - "contains {0} git repositories:".format(len(repos))) - - for repo_path, repo_name in sorted(repos): - _update_repository(repo_path, repo_name) + 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): + _update_repository(repo) -def _update_directory(dir_path, dir_name, is_bookmark=False): +def _update_directory(path, is_bookmark=False): """Update a particular directory. Determine whether the directory is a git repo on its own, a directory of @@ -89,32 +86,32 @@ def _update_directory(dir_path, dir_name, is_bookmark=False): third, print an error. """ dir_type = "bookmark" if is_bookmark else "directory" - dir_long_name = dir_type + ' "' + BOLD + dir_path + RESET + '"' + long_name = dir_type + ' "' + BOLD + path + RESET + '"' try: - repo = Repo(dir_path) + repo = Repo(path) except exc.NoSuchPathError: - print(RED + "Error:" + RESET, dir_long_name, "doesn't exist!") + print(RED + "Error:" + RESET, long_name, "doesn't exist!") except exc.InvalidGitRepositoryError: - if os.path.isdir(dir_path): - _update_subdirectories(dir_path, dir_name, dir_long_name) + if os.path.isdir(path): + _update_subdirectories(path, long_name) else: - print(RED + "Error:" + RESET, dir_long_name, "isn't a repository!") + print(RED + "Error:" + RESET, long_name, "isn't a repository!") else: - print(dir_long_name.capitalize(), "is a git repository:") - _update_repository(repo, dir_name) + long_name = (dir_type.capitalize() + ' "' + BOLD + repo.working_dir + + RESET + '"') + print(long_name, "is a git repository:") + _update_repository(repo) def update_bookmarks(bookmarks): """Loop through and update all bookmarks.""" if bookmarks: - for bookmark_path, bookmark_name in bookmarks: - _update_directory(bookmark_path, bookmark_name, is_bookmark=True) + for path, name in bookmarks: + _update_directory(path, is_bookmark=True) else: print("You don't have any bookmarks configured! Get help with 'gitup -h'.") 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] # Dir name ("x" in /path/to/x/) - _update_directory(path, path_name, is_bookmark=False) + _update_directory(os.path.abspath(path), is_bookmark=False) From af992c69483f96d1a4020ecccc9896e585561018 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Fri, 28 Mar 2014 20:13:16 -0400 Subject: [PATCH 03/17] Commit some temp code. --- gitup/update.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/gitup/update.py b/gitup/update.py index e077f72..6346bc4 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -22,9 +22,35 @@ INDENT1 = " " * 3 INDENT2 = " " * 7 def _update_repository(repo): - """Update a single git repository by pulling from the remote.""" + """Update a single git repository by fetching remotes and rebasing/merging. + + The specific actions depends on ... + """ print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":") + ref = repo.head.ref.tracking_branch() + if ref: + remote = repo.remotes[ref.remote_name] + else: + ### + + remote.fetch() + + if not repo.remotes: + print(INDENT2, RED + "Error:" + RESET, "no remotes configured.") + return + try: + repo = repo.remotes.origin + except AttributeError: + if len(repo.remotes) == 1: + repo = repo.remotes[0] + else: + print(INDENT2, RED + "Error:" + RESET, "ambiguous remotes:", + ", ".join(remote.name for remote in repo.remotes)) + + + ##################################### + try: # Check if there is anything to pull, but don't do it yet: dry_fetch = _exec_shell("git fetch --dry-run") @@ -74,7 +100,7 @@ def _update_subdirectories(path, long_name): 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): + for repo in sorted(repos, key=lambda r: os.path.split(r.working_dir)[1]): _update_repository(repo) def _update_directory(path, is_bookmark=False): From 48ebdf02649e3c99eeb5861bbc5011d6039d43c6 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sat, 29 Mar 2014 15:00:31 -0400 Subject: [PATCH 04/17] Add some new arguments. --- gitup/script.py | 35 ++++++++++++++++++++++++++--------- gitup/update.py | 10 ++++++---- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/gitup/script.py b/gitup/script.py index 8105d8a..72f3d9b 100644 --- a/gitup/script.py +++ b/gitup/script.py @@ -17,7 +17,7 @@ from .update import update_bookmarks, update_directories def main(): """Parse arguments and then call the appropriate function(s).""" parser = argparse.ArgumentParser( - description="""Easily pull to 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__), @@ -26,6 +26,7 @@ def main(): 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", @@ -34,6 +35,20 @@ def main(): 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 pull 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..rebase` + in git config (like `git pull --rebase`)""") + rebase_or_merge.add_argument( + '-m', '--merge', action="store_true", help="""like --rebase, but merge + instead""") + group_u.add_argument( + '-v', '--verbose', action="store_true", help="""show more detailed + information while updating""") + group_b.add_argument( '-a', '--add', dest="bookmarks_to_add", nargs="+", metavar="path", help="add directory(s) as bookmarks") @@ -46,29 +61,31 @@ def main(): group_m.add_argument( '-h', '--help', action="help", help="show this help message and exit") group_m.add_argument( - '-v', '--version', action="version", + '-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, args.verbose 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) - if args.update: - update_bookmarks(get_bookmarks()) - - # If they did not tell us to do anything, automatically update bookmarks: - if not any(vars(args).values()): - update_bookmarks(get_bookmarks()) + 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.""" diff --git a/gitup/update.py b/gitup/update.py index 6346bc4..2620016 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -21,7 +21,7 @@ RESET = Style.RESET_ALL INDENT1 = " " * 3 INDENT2 = " " * 7 -def _update_repository(repo): +def _update_repository(repo, rebase=True): """Update a single git repository by fetching remotes and rebasing/merging. The specific actions depends on ... @@ -31,7 +31,7 @@ def _update_repository(repo): ref = repo.head.ref.tracking_branch() if ref: remote = repo.remotes[ref.remote_name] - else: + # else: ### remote.fetch() @@ -129,7 +129,8 @@ def _update_directory(path, is_bookmark=False): print(long_name, "is a git repository:") _update_repository(repo) -def update_bookmarks(bookmarks): +def update_bookmarks(bookmarks, current_only=False, rebase=False, merge=False, + verbose=False): """Loop through and update all bookmarks.""" if bookmarks: for path, name in bookmarks: @@ -137,7 +138,8 @@ def update_bookmarks(bookmarks): else: print("You don't have any bookmarks configured! Get help with 'gitup -h'.") -def update_directories(paths): +def update_directories(paths, current_only=False, rebase=False, merge=False, + verbose=False): """Update a list of directories supplied by command arguments.""" for path in paths: _update_directory(os.path.abspath(path), is_bookmark=False) From 8b0793436973d47dfe445a0aa610bbe630a25b98 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 30 Mar 2014 00:38:10 -0400 Subject: [PATCH 05/17] More work on GitPython integration. --- gitup/script.py | 6 ++-- gitup/update.py | 107 +++++++++++++++++++++++++++++++++++--------------------- 2 files changed, 70 insertions(+), 43 deletions(-) diff --git a/gitup/script.py b/gitup/script.py index 72f3d9b..8ed971a 100644 --- a/gitup/script.py +++ b/gitup/script.py @@ -41,7 +41,7 @@ def main(): rebase_or_merge.add_argument( '-r', '--rebase', action="store_true", help="""always rebase upstream branches instead of following `pull.rebase` and `branch..rebase` - in git config (like `git pull --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""") @@ -82,10 +82,10 @@ def main(): list_bookmarks() acted = True if args.directories_to_update: - update_directories(args.directories_to_update, *update_args) + update_directories(args.directories_to_update, update_args) acted = True if args.update or not acted: - update_bookmarks(get_bookmarks(), *update_args) + update_bookmarks(get_bookmarks(), update_args) def run(): """Thin wrapper for main() that catches KeyboardInterrupts.""" diff --git a/gitup/update.py b/gitup/update.py index 2620016..b896061 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -20,46 +20,74 @@ RESET = Style.RESET_ALL INDENT1 = " " * 3 INDENT2 = " " * 7 +ERROR = RED + "Error:" + RESET -def _update_repository(repo, rebase=True): +def _read_config(repo, attr): + """Read an attribute from git config.""" + try: + return repo.git.config("--get", attr) + except exc.GitCommandError: + return None + +def _update_repository(repo, current_only=False, rebase=False, merge=False, + verbose=False): """Update a single git repository by fetching remotes and rebasing/merging. - The specific actions depends on ... + 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..rebase`` is set in config; *rebase* will + cause us to always rebase with ``--preserve-merges``, and *merge* will + cause us to always merge. If *verbose* is set, additional information is + printed out for the user. """ + def _update_branch(branch): + """Update a single branch.""" + print(INDENT2, "Updating branch:", branch, end=" ") + upstream = branch.tracking_branch() + if not upstream: + print("Branch is not tracking any remote.") + continue + c_attr = "branch.{0}.rebase".format(branch.name) + if not merge and (rebase or repo_rebase or _read_config(repo, c_attr)): + ### TODO: rebase + else: + ### TODO: merge + print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":") - ref = repo.head.ref.tracking_branch() - if ref: - remote = repo.remotes[ref.remote_name] - # else: - ### + 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 - remote.fetch() + for remote in remotes: + print(INDENT2, "Fetching remote:", remote.name) + remote.fetch() # TODO: show progress - if not repo.remotes: - print(INDENT2, RED + "Error:" + RESET, "no remotes configured.") - return - try: - repo = repo.remotes.origin - except AttributeError: - if len(repo.remotes) == 1: - repo = repo.remotes[0] - else: - print(INDENT2, RED + "Error:" + RESET, "ambiguous remotes:", - ", ".join(remote.name for remote in repo.remotes)) + repo_rebase = _read_config(repo, "pull.rebase") + _update_branch(active) + branches = set(repo.heads) - {active} + if branches: + stashed = repo.git.stash("--all") != "No local changes to save" + try: + for branch in sorted(branches, key=lambda b: b.name): + branch.checkout() + _update_branch(branch) + finally: + active.checkout() + if stashed: + repo.git.stash("pop") ##################################### try: - # Check if there is anything to pull, but don't do it yet: - dry_fetch = _exec_shell("git fetch --dry-run") - except subprocess.CalledProcessError: - print(INDENT2, RED + "Error:" + RESET, "cannot fetch;", - "do you have a remote repository configured correctly?") - return - - try: last_commit = _exec_shell("git log -n 1 --pretty=\"%ar\"") except subprocess.CalledProcessError: last_commit = "never" # Couldn't get a log, so no commits @@ -87,7 +115,7 @@ def _update_repository(repo, rebase=True): "you have uncommitted changes in this repository!") print(INDENT2, "Ignoring.") -def _update_subdirectories(path, long_name): +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): @@ -101,9 +129,9 @@ def _update_subdirectories(path, long_name): 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_repository(repo, *update_args) -def _update_directory(path, is_bookmark=False): +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 @@ -117,29 +145,28 @@ def _update_directory(path, is_bookmark=False): try: repo = Repo(path) except exc.NoSuchPathError: - print(RED + "Error:" + RESET, long_name, "doesn't exist!") + print(ERROR, long_name, "doesn't exist!") except exc.InvalidGitRepositoryError: if os.path.isdir(path): - _update_subdirectories(path, long_name) + _update_subdirectories(path, long_name, update_args) else: - print(RED + "Error:" + RESET, long_name, "isn't a repository!") + 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_repository(repo, *update_args) -def update_bookmarks(bookmarks, current_only=False, rebase=False, merge=False, - verbose=False): +def update_bookmarks(bookmarks, update_args): """Loop through and update all bookmarks.""" if bookmarks: for path, name in bookmarks: - _update_directory(path, is_bookmark=True) + _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, current_only=False, rebase=False, merge=False, - verbose=False): +def update_directories(paths, update_args): """Update a list of directories supplied by command arguments.""" for path in paths: - _update_directory(os.path.abspath(path), is_bookmark=False) + full_path = os.path.abspath(path) + _update_directory(full_path, update_args, is_bookmark=False) From 9bbe6a32b77be5fab541e474a936aa10be8506ff Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Thu, 3 Apr 2014 14:45:11 -0400 Subject: [PATCH 06/17] More stuff. --- gitup/update.py | 63 ++++++++++++++++++++++----------------------------------- 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/gitup/update.py b/gitup/update.py index b896061..dd6a4e8 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -43,16 +43,28 @@ def _update_repository(repo, current_only=False, rebase=False, merge=False, """ def _update_branch(branch): """Update a single branch.""" - print(INDENT2, "Updating branch:", branch, end=" ") + print(INDENT2, "Updating", branch, end="...") upstream = branch.tracking_branch() if not upstream: - print("Branch is not tracking any remote.") - continue + print(" skipped; no upstream is tracked.") + return + if branch.commit == upstream.commit: + print(" up to date.") + return + branch.checkout() c_attr = "branch.{0}.rebase".format(branch.name) if not merge and (rebase or repo_rebase or _read_config(repo, c_attr)): - ### TODO: rebase + print(" rebasing...", end="") + try: + res = repo.git.rebase(upstream.name) + except exc.GitCommandError as err: + print(err) + ### TODO: ... + else: + print(" done.") else: - ### TODO: merge + repo.git.merge(upstream.name) + ### TODO: etc print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":") @@ -65,56 +77,29 @@ def _update_repository(repo, current_only=False, rebase=False, merge=False, remotes = [repo.remotes[ref.remote_name]] else: remotes = repo.remotes + if not remotes: + print(INDENT2, ERROR, "no remotes configured to pull from.") + return for remote in remotes: - print(INDENT2, "Fetching remote:", remote.name) - remote.fetch() # TODO: show progress + print(INDENT2, "Fetching", remote.name, end="...") + remote.fetch() ### TODO: show progress + print(" done.") repo_rebase = _read_config(repo, "pull.rebase") _update_branch(active) branches = set(repo.heads) - {active} if branches: - stashed = repo.git.stash("--all") != "No local changes to save" + stashed = repo.git.stash("--all") != "No local changes to save" ### TODO: don't do this unless actually necessary try: for branch in sorted(branches, key=lambda b: b.name): - branch.checkout() _update_branch(branch) finally: active.checkout() if stashed: repo.git.stash("pop") - ##################################### - - try: - last_commit = _exec_shell("git log -n 1 --pretty=\"%ar\"") - except subprocess.CalledProcessError: - last_commit = "never" # Couldn't get a log, so no commits - - if not dry_fetch: # No new changes to pull - print(INDENT2, BLUE + "No new changes." + RESET, - "Last commit was {0}.".format(last_commit)) - - else: # Stuff has happened! - print(INDENT2, "There are new changes upstream...") - status = _exec_shell("git status") - - if status.endswith("nothing to commit, working directory clean"): - print(INDENT2, GREEN + "Pulling new changes...") - result = _exec_shell("git pull") - if last_commit == "never": - print(INDENT2, "The following changes have been made:") - else: - print(INDENT2, "The following changes have been made since", - last_commit + ":") - print(result) - - else: - print(INDENT2, RED + "Warning:" + RESET, - "you have uncommitted changes in this repository!") - print(INDENT2, "Ignoring.") - def _update_subdirectories(path, long_name, update_args): """Update all subdirectories that are git repos in a given directory.""" repos = [] From 5468be61e3c6aa4163e27f138926e1e1c63297f9 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Fri, 4 Apr 2014 11:43:47 -0400 Subject: [PATCH 07/17] More work on rebasing/merging. --- gitup/update.py | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/gitup/update.py b/gitup/update.py index dd6a4e8..1f2ee08 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -29,6 +29,36 @@ def _read_config(repo, attr): except exc.GitCommandError: return None +def _rebase(repo, name): + """Rebase the current HEAD of *repo* onto the branch *name*.""" + print(" rebasing...", end="") + try: + res = repo.git.rebase(name) + except exc.GitCommandError as err: + if "unstaged changes" in err.stderr: + print(" error:", "unstaged changes.") + elif "uncommitted changes" in err.stderr: + print(" error:", "uncommitted changes.") + else: + try: + repo.git.rebase("--abort") + except exc.GitCommandError: + pass + print(" error:", err.stderr.replace("\n", " ")) + else: + print(" done.") + +def _merge(repo, name): + """Merge the branch *name* into the current HEAD of *repo*.""" + print(" merging...", end="") + try: + repo.git.merge(name) + except exc.GitCommandError as err: + print(err) + ### TODO: etc + else: + print(" done.") + def _update_repository(repo, current_only=False, rebase=False, merge=False, verbose=False): """Update a single git repository by fetching remotes and rebasing/merging. @@ -46,25 +76,17 @@ def _update_repository(repo, current_only=False, rebase=False, merge=False, print(INDENT2, "Updating", branch, end="...") upstream = branch.tracking_branch() if not upstream: - print(" skipped; no upstream is tracked.") + print(" skipped: no upstream is tracked.") return - if branch.commit == upstream.commit: + if branch.commit == upstream.commit: ### TODO: a better check is possible print(" up to date.") return branch.checkout() c_attr = "branch.{0}.rebase".format(branch.name) if not merge and (rebase or repo_rebase or _read_config(repo, c_attr)): - print(" rebasing...", end="") - try: - res = repo.git.rebase(upstream.name) - except exc.GitCommandError as err: - print(err) - ### TODO: ... - else: - print(" done.") + _rebase(repo, upstream.name) else: - repo.git.merge(upstream.name) - ### TODO: etc + _merge(repo, upstream.name) print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":") From 134c4451fcf796e7caab4a6efa14a8ea9a5372a0 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Fri, 4 Apr 2014 13:12:47 -0400 Subject: [PATCH 08/17] More work on some pending issues. --- gitup/update.py | 79 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/gitup/update.py b/gitup/update.py index 1f2ee08..fb8e81f 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -22,6 +22,27 @@ INDENT1 = " " * 3 INDENT2 = " " * 7 ERROR = RED + "Error:" + RESET +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: @@ -29,6 +50,12 @@ def _read_config(repo, attr): except exc.GitCommandError: return None +def _fetch_remote(remote): + """Fetch a given remote, and display progress info along the way.""" + print(INDENT2, "Fetching", remote.name, end="...") + remote.fetch() ### TODO: show progress + print(" done.") + def _rebase(repo, name): """Rebase the current HEAD of *repo* onto the branch *name*.""" print(" rebasing...", end="") @@ -59,6 +86,25 @@ def _merge(repo, name): else: print(" done.") +def _update_branch(repo, branch, merge, rebase, stasher=None): + """Update a single branch.""" + print(INDENT2, "Updating", branch, end="...") + upstream = branch.tracking_branch() + if not upstream: + print(" skipped: no upstream is tracked.") + return + if branch.commit == upstream.commit: ### TODO: a better check is possible + print(" up to date.") + 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_repository(repo, current_only=False, rebase=False, merge=False, verbose=False): """Update a single git repository by fetching remotes and rebasing/merging. @@ -71,23 +117,6 @@ def _update_repository(repo, current_only=False, rebase=False, merge=False, cause us to always merge. If *verbose* is set, additional information is printed out for the user. """ - def _update_branch(branch): - """Update a single branch.""" - print(INDENT2, "Updating", branch, end="...") - upstream = branch.tracking_branch() - if not upstream: - print(" skipped: no upstream is tracked.") - return - if branch.commit == upstream.commit: ### TODO: a better check is possible - print(" up to date.") - return - branch.checkout() - c_attr = "branch.{0}.rebase".format(branch.name) - if not merge and (rebase or repo_rebase or _read_config(repo, c_attr)): - _rebase(repo, upstream.name) - else: - _merge(repo, upstream.name) - print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":") active = repo.active_branch @@ -104,23 +133,19 @@ def _update_repository(repo, current_only=False, rebase=False, merge=False, return for remote in remotes: - print(INDENT2, "Fetching", remote.name, end="...") - remote.fetch() ### TODO: show progress - print(" done.") - - repo_rebase = _read_config(repo, "pull.rebase") + _fetch_remote(remote) - _update_branch(active) + rebase = rebase or _read_config(repo, "pull.rebase") + _update_branch(repo, active, merge, rebase) branches = set(repo.heads) - {active} if branches: - stashed = repo.git.stash("--all") != "No local changes to save" ### TODO: don't do this unless actually necessary + stasher = _Stasher(repo) try: for branch in sorted(branches, key=lambda b: b.name): - _update_branch(branch) + _update_branch(repo, branch, merge, rebase, stasher) finally: active.checkout() - if stashed: - repo.git.stash("pop") + stasher.restore() def _update_subdirectories(path, long_name, update_args): """Update all subdirectories that are git repos in a given directory.""" From 79536846277117644a569705af9d54b1d7e3963b Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Mon, 7 Apr 2014 10:59:38 -0400 Subject: [PATCH 09/17] Flesh out error handling for merge/rebase. --- gitup/update.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/gitup/update.py b/gitup/update.py index fb8e81f..a36a9cb 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -62,16 +62,17 @@ def _rebase(repo, name): try: res = repo.git.rebase(name) except exc.GitCommandError as err: - if "unstaged changes" in err.stderr: + msg = err.stderr.replace("\n", " ").strip() + if "unstaged changes" in msg: print(" error:", "unstaged changes.") - elif "uncommitted changes" in err.stderr: + elif "uncommitted changes" in msg: print(" error:", "uncommitted changes.") else: try: repo.git.rebase("--abort") except exc.GitCommandError: pass - print(" error:", err.stderr.replace("\n", " ")) + print(" error:", msg if msg else "rebase conflict") else: print(" done.") @@ -81,8 +82,15 @@ def _merge(repo, name): try: repo.git.merge(name) except exc.GitCommandError as err: - print(err) - ### TODO: etc + msg = err.stderr.replace("\n", " ").strip() + if "local changes" in msg and "would be overwritten" in msg: + print(" error:", "uncommitted changes.") + else: + try: + repo.git.merge("--abort") + except exc.GitCommandError: + pass + print(" error:", msg if msg else "merge conflict") else: print(" done.") From fbf29b46889cc672f126c56ba97f4222100bf8bc Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Mon, 7 Apr 2014 13:03:29 -0400 Subject: [PATCH 10/17] Proper up-to-date-check; fix when branches are empty. --- gitup/update.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/gitup/update.py b/gitup/update.py index a36a9cb..1484bff 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -56,6 +56,11 @@ def _fetch_remote(remote): remote.fetch() ### TODO: show progress print(" done.") +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(" rebasing...", end="") @@ -101,7 +106,12 @@ def _update_branch(repo, branch, merge, rebase, stasher=None): if not upstream: print(" skipped: no upstream is tracked.") return - if branch.commit == upstream.commit: ### TODO: a better check is possible + try: + branch.commit, upstream.commit + except ValueError: + print(" skipped: branch contains no revisions.") + return + if _is_up_to_date(repo, branch, upstream): print(" up to date.") return if stasher: From 976c7ccbc39e65bb60d7550c42114bb064f7e356 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Fri, 11 Apr 2014 14:45:51 -0400 Subject: [PATCH 11/17] Mostly finish new remote fetching. --- gitup/update.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/gitup/update.py b/gitup/update.py index 1484bff..3059610 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -9,6 +9,7 @@ import os from colorama import Fore, Style from git import Repo, exc +from git.util import RemoteProgress __all__ = ["update_bookmarks", "update_directories"] @@ -22,6 +23,30 @@ INDENT1 = " " * 3 INDENT2 = " " * 7 ERROR = RED + "Error:" + RESET +class _ProgressMonitor(RemoteProgress): + """Displays relevant output during the fetching process.""" + + def __init__(self, verbose): + super(_ProgressMonitor, self).__init__() + self._verbose = verbose + + def update(self, op_code, cur_count, max_count=None, message=''): + """Called whenever progress changes. Overrides default behavior.""" + if self._verbose: + if op_code & self.COUNTING: + print(" ({0})".format(cur_count), end="") + elif op_code & (self.COMPRESSING | self.RECEIVING): + if op_code & self.BEGIN: + print("\b, ", end="") + 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) + elif op_code & self.BEGIN: + print(".", end="") + + class _Stasher(object): """Manages the stash state of a given repository.""" @@ -50,11 +75,32 @@ def _read_config(repo, attr): except exc.GitCommandError: return None -def _fetch_remote(remote): - """Fetch a given remote, and display progress info along the way.""" - print(INDENT2, "Fetching", remote.name, end="...") - remote.fetch() ### TODO: show progress - print(" done.") +def _format_fetch_result(results): + """Format and print the results of a verbose fetch.""" + info = [("NEW_HEAD", "new branches"), ("NEW_TAG", "new tags"), + ("FAST_FORWARD", "updates"), ("ERROR", "errors")] + rlist = [] + for attr, desc in info: + names = [res.name for res in results if res.flags & getattr(res, attr)] + if names: + rlist.append("{0} ({1})".format(desc, ", ".join(names))) + print(":", (", ".join(rlist) if rlist else "up to date") + ".") + +def _fetch_remotes(remotes, verbose): + """Fetch a list of remotes, displaying progress info along the way.""" + if verbose: + for remote in remotes: + print(INDENT2, "Fetching", remote.name, end="") + result = remote.fetch(progress=_ProgressMonitor(True)) + _format_fetch_result(result) + else: + print(INDENT2, "Fetching:", end=" ") + for i, remote in enumerate(remotes): + print(remote.name, end="") + remote.fetch(progress=_ProgressMonitor(False)) + if i < len(remotes) - 1: + print(", ", end="") + print(".") def _is_up_to_date(repo, branch, upstream): """Return whether *branch* is up-to-date with its *upstream*.""" @@ -106,6 +152,7 @@ def _update_branch(repo, branch, merge, rebase, stasher=None): if not upstream: print(" skipped: no upstream is tracked.") return + try: branch.commit, upstream.commit except ValueError: @@ -114,6 +161,7 @@ def _update_branch(repo, branch, merge, rebase, stasher=None): if _is_up_to_date(repo, branch, upstream): print(" up to date.") return + if stasher: stasher.clean() branch.checkout() @@ -150,8 +198,7 @@ def _update_repository(repo, current_only=False, rebase=False, merge=False, print(INDENT2, ERROR, "no remotes configured to pull from.") return - for remote in remotes: - _fetch_remote(remote) + _fetch_remotes(remotes, verbose) rebase = rebase or _read_config(repo, "pull.rebase") _update_branch(repo, active, merge, rebase) From b247d90f000f9cf1f30bb4843fee5eecd830423b Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 20 Apr 2014 23:28:22 -0400 Subject: [PATCH 12/17] Remove mostly useless verbose option; clean up output. --- gitup/script.py | 7 ++--- gitup/update.py | 85 ++++++++++++++++++++++++++------------------------------- 2 files changed, 40 insertions(+), 52 deletions(-) diff --git a/gitup/script.py b/gitup/script.py index 8ed971a..61f9d44 100644 --- a/gitup/script.py +++ b/gitup/script.py @@ -45,9 +45,6 @@ def main(): rebase_or_merge.add_argument( '-m', '--merge', action="store_true", help="""like --rebase, but merge instead""") - group_u.add_argument( - '-v', '--verbose', action="store_true", help="""show more detailed - information while updating""") group_b.add_argument( '-a', '--add', dest="bookmarks_to_add", nargs="+", metavar="path", @@ -61,12 +58,12 @@ def main(): group_m.add_argument( '-h', '--help', action="help", help="show this help message and exit") group_m.add_argument( - '-V', '--version', action="version", + '-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, args.verbose + update_args = args.current_only, args.rebase, args.merge print(Style.BRIGHT + "gitup" + Style.RESET_ALL + ": the git-repo-updater") print() diff --git a/gitup/update.py b/gitup/update.py index 3059610..64b19e7 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -8,7 +8,7 @@ from __future__ import print_function import os from colorama import Fore, Style -from git import Repo, exc +from git import RemoteReference as RemoteRef, Repo, exc from git.util import RemoteProgress __all__ = ["update_bookmarks", "update_directories"] @@ -26,25 +26,21 @@ ERROR = RED + "Error:" + RESET class _ProgressMonitor(RemoteProgress): """Displays relevant output during the fetching process.""" - def __init__(self, verbose): + def __init__(self): super(_ProgressMonitor, self).__init__() - self._verbose = verbose def update(self, op_code, cur_count, max_count=None, message=''): """Called whenever progress changes. Overrides default behavior.""" - if self._verbose: - if op_code & self.COUNTING: - print(" ({0})".format(cur_count), end="") - elif op_code & (self.COMPRESSING | self.RECEIVING): - if op_code & self.BEGIN: - print("\b, ", end="") - 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) - elif op_code & self.BEGIN: - print(".", end="") + if op_code & self.COUNTING: + print(" ({0})".format(cur_count), end="") + elif op_code & (self.COMPRESSING | self.RECEIVING): + if op_code & self.BEGIN: + print("\b, ", end="") + 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): @@ -75,32 +71,30 @@ def _read_config(repo, attr): except exc.GitCommandError: return None -def _format_fetch_result(results): - """Format and print the results of a verbose fetch.""" - info = [("NEW_HEAD", "new branches"), ("NEW_TAG", "new tags"), - ("FAST_FORWARD", "updates"), ("ERROR", "errors")] - rlist = [] - for attr, desc in info: - names = [res.name for res in results if res.flags & getattr(res, attr)] - if names: - rlist.append("{0} ({1})".format(desc, ", ".join(names))) - print(":", (", ".join(rlist) if rlist else "up to date") + ".") - -def _fetch_remotes(remotes, verbose): +def _fetch_remotes(remotes): """Fetch a list of remotes, displaying progress info along the way.""" - if verbose: - for remote in remotes: - print(INDENT2, "Fetching", remote.name, end="") - result = remote.fetch(progress=_ProgressMonitor(True)) - _format_fetch_result(result) - else: - print(INDENT2, "Fetching:", end=" ") - for i, remote in enumerate(remotes): - print(remote.name, end="") - remote.fetch(progress=_ProgressMonitor(False)) - if i < len(remotes) - 1: - print(", ", end="") - print(".") + 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"), + ("ERROR", "error", "errors")] + up_to_date = BLUE + "up to date" + RESET + + for remote in remotes: + print(INDENT2, "Fetching", BOLD + remote.name, end="") + results = remote.fetch(progress=_ProgressMonitor()) + 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*.""" @@ -171,8 +165,7 @@ def _update_branch(repo, branch, merge, rebase, stasher=None): else: _merge(repo, upstream.name) -def _update_repository(repo, current_only=False, rebase=False, merge=False, - verbose=False): +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 @@ -180,8 +173,7 @@ def _update_repository(repo, current_only=False, rebase=False, merge=False, current branch if ``True``. By default, we will merge unless ``pull.rebase`` or ``branch..rebase`` is set in config; *rebase* will cause us to always rebase with ``--preserve-merges``, and *merge* will - cause us to always merge. If *verbose* is set, additional information is - printed out for the user. + cause us to always merge. """ print(INDENT1, BOLD + os.path.split(repo.working_dir)[1] + ":") @@ -197,8 +189,7 @@ def _update_repository(repo, current_only=False, rebase=False, merge=False, if not remotes: print(INDENT2, ERROR, "no remotes configured to pull from.") return - - _fetch_remotes(remotes, verbose) + _fetch_remotes(remotes) rebase = rebase or _read_config(repo, "pull.rebase") _update_branch(repo, active, merge, rebase) From 168856be5602e4142a38c673b52e746015ac9ff3 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 20 Apr 2014 23:40:51 -0400 Subject: [PATCH 13/17] Refactor out _update_branches(); remove COUNTING from fetch progress. --- gitup/update.py | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/gitup/update.py b/gitup/update.py index 64b19e7..04aad0a 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -28,14 +28,15 @@ class _ProgressMonitor(RemoteProgress): 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.COUNTING: - print(" ({0})".format(cur_count), end="") - elif op_code & (self.COMPRESSING | self.RECEIVING): + if op_code & (self.COMPRESSING | self.RECEIVING): if op_code & self.BEGIN: - print("\b, ", end="") + print("\b, " if self._started else " (", end="") + if not self._started: + self._started = True if op_code & self.END: end = ")" else: @@ -79,8 +80,7 @@ def _fetch_remotes(remotes): info = [("NEW_HEAD", "new branch", "new branches"), ("NEW_TAG", "new tag", "new tags"), - ("FAST_FORWARD", "branch update", "branch updates"), - ("ERROR", "error", "errors")] + ("FAST_FORWARD", "branch update", "branch updates")] up_to_date = BLUE + "up to date" + RESET for remote in remotes: @@ -165,6 +165,19 @@ def _update_branch(repo, branch, merge, rebase, stasher=None): 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. @@ -189,19 +202,10 @@ def _update_repository(repo, current_only=False, rebase=False, merge=False): if not remotes: print(INDENT2, ERROR, "no remotes configured to pull from.") return - _fetch_remotes(remotes) - rebase = rebase or _read_config(repo, "pull.rebase") - _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() + + _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.""" From 9a1473ef8bcb23e86bf033d4157568113399b8bd Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Mon, 21 Apr 2014 00:31:01 -0400 Subject: [PATCH 14/17] Colorize and merge branch update lines into one. --- gitup/update.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/gitup/update.py b/gitup/update.py index 04aad0a..94d74e5 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -14,9 +14,10 @@ from git.util import RemoteProgress __all__ = ["update_bookmarks", "update_directories"] BOLD = Style.BRIGHT -RED = Fore.RED + BOLD -GREEN = Fore.GREEN + BOLD BLUE = Fore.BLUE + BOLD +GREEN = Fore.GREEN + BOLD +RED = Fore.RED + BOLD +YELLOW = Fore.YELLOW + BOLD RESET = Style.RESET_ALL INDENT1 = " " * 3 @@ -103,57 +104,57 @@ def _is_up_to_date(repo, branch, upstream): def _rebase(repo, name): """Rebase the current HEAD of *repo* onto the branch *name*.""" - print(" rebasing...", end="") + print(GREEN + "rebasing...", end="") try: res = repo.git.rebase(name) except exc.GitCommandError as err: msg = err.stderr.replace("\n", " ").strip() if "unstaged changes" in msg: - print(" error:", "unstaged changes.") + print(RED, "error:" + RESET, "unstaged changes", end=")") elif "uncommitted changes" in msg: - print(" error:", "uncommitted changes.") + print(RED, "error:" + RESET, "uncommitted changes", end=")") else: try: repo.git.rebase("--abort") except exc.GitCommandError: pass - print(" error:", msg if msg else "rebase conflict") + print(RED, "error:" + RESET, msg if msg else "conflict", end=")") else: - print(" done.") + print("\b" * 6 + " " * 6 + "\b" * 6 + GREEN + "ed", end=")") def _merge(repo, name): """Merge the branch *name* into the current HEAD of *repo*.""" - print(" merging...", end="") + print(GREEN + "merging...", end="") try: repo.git.merge(name) except exc.GitCommandError as err: msg = err.stderr.replace("\n", " ").strip() if "local changes" in msg and "would be overwritten" in msg: - print(" error:", "uncommitted changes.") + print(RED, "error:" + RESET, "uncommitted changes", end=")") else: try: repo.git.merge("--abort") except exc.GitCommandError: pass - print(" error:", msg if msg else "merge conflict") + print(RED, "error:" + RESET, msg if msg else "conflict", end=")") else: - print(" done.") + print("\b" * 6 + " " * 6 + "\b" * 6 + GREEN + "ed", end=")") def _update_branch(repo, branch, merge, rebase, stasher=None): """Update a single branch.""" - print(INDENT2, "Updating", branch, end="...") + print(BOLD + branch.name, end=" (") upstream = branch.tracking_branch() if not upstream: - print(" skipped: no upstream is tracked.") + print(YELLOW + "skipped:" + RESET, "no upstream is tracked", end=")") return try: branch.commit, upstream.commit except ValueError: - print(" skipped: branch contains no revisions.") + print(YELLOW + "skipped:" + RESET, "branch has no revisions", end=")") return if _is_up_to_date(repo, branch, upstream): - print(" up to date.") + print(BLUE + "up to date", end=")") return if stasher: @@ -167,16 +168,19 @@ def _update_branch(repo, branch, merge, rebase, stasher=None): def _update_branches(repo, active, merge, rebase): """Update a list of branches.""" + print(INDENT2, "Updating: ", end="") _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): + print(", ", end="") _update_branch(repo, branch, merge, rebase, stasher) finally: active.checkout() stasher.restore() + print(".") def _update_repository(repo, current_only=False, rebase=False, merge=False): """Update a single git repository by fetching remotes and rebasing/merging. From 2fa50a416cf4bcf22708134b91b74b17e94fcaba Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Mon, 21 Apr 2014 00:46:21 -0400 Subject: [PATCH 15/17] Handle some GitPython exceptions. --- gitup/update.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/gitup/update.py b/gitup/update.py index 94d74e5..4cc7292 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -85,8 +85,19 @@ def _fetch_remotes(remotes): up_to_date = BLUE + "up to date" + RESET for remote in remotes: - print(INDENT2, "Fetching", BOLD + remote.name, end="") - results = remote.fetch(progress=_ProgressMonitor()) + 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) @@ -95,7 +106,7 @@ def _fetch_remotes(remotes): 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) + ".") + 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*.""" @@ -110,15 +121,15 @@ def _rebase(repo, name): except exc.GitCommandError as err: msg = err.stderr.replace("\n", " ").strip() if "unstaged changes" in msg: - print(RED, "error:" + RESET, "unstaged changes", end=")") + print(RED + " error:", "unstaged changes", end=")") elif "uncommitted changes" in msg: - print(RED, "error:" + RESET, "uncommitted changes", end=")") + print(RED + " error:", "uncommitted changes", end=")") else: try: repo.git.rebase("--abort") except exc.GitCommandError: pass - print(RED, "error:" + RESET, msg if msg else "conflict", end=")") + print(RED + " error:", msg if msg else "rebase conflict", end=")") else: print("\b" * 6 + " " * 6 + "\b" * 6 + GREEN + "ed", end=")") @@ -130,13 +141,13 @@ def _merge(repo, name): except exc.GitCommandError as err: msg = err.stderr.replace("\n", " ").strip() if "local changes" in msg and "would be overwritten" in msg: - print(RED, "error:" + RESET, "uncommitted changes", end=")") + print(RED + " error:", "uncommitted changes", end=")") else: try: repo.git.merge("--abort") except exc.GitCommandError: pass - print(RED, "error:" + RESET, msg if msg else "conflict", end=")") + print(RED + " error:", msg if msg else "merge conflict", end=")") else: print("\b" * 6 + " " * 6 + "\b" * 6 + GREEN + "ed", end=")") @@ -145,13 +156,13 @@ def _update_branch(repo, branch, merge, rebase, stasher=None): print(BOLD + branch.name, end=" (") upstream = branch.tracking_branch() if not upstream: - print(YELLOW + "skipped:" + RESET, "no upstream is tracked", end=")") + print(YELLOW + "skipped:", "no upstream is tracked", end=")") return try: branch.commit, upstream.commit except ValueError: - print(YELLOW + "skipped:" + RESET, "branch has no revisions", end=")") + print(YELLOW + "skipped:", "branch has no revisions", end=")") return if _is_up_to_date(repo, branch, upstream): print(BLUE + "up to date", end=")") From 6c8e1be0fbc613c06accf22c1f664f9973931d6c Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Mon, 21 Apr 2014 01:03:13 -0400 Subject: [PATCH 16/17] Some fixes; go back to one-line-per-branch system. --- gitup/update.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/gitup/update.py b/gitup/update.py index 4cc7292..f851b40 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -85,7 +85,7 @@ def _fetch_remotes(remotes): up_to_date = BLUE + "up to date" + RESET for remote in remotes: - print(INDENT2, "Fetching", BOLD + remote.name, end=": ") + print(INDENT2, "Fetching", BOLD + remote.name, end="") try: results = remote.fetch(progress=_ProgressMonitor()) except exc.GitCommandError as err: @@ -106,7 +106,7 @@ def _fetch_remotes(remotes): 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) + ".") + 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*.""" @@ -121,17 +121,17 @@ def _rebase(repo, name): except exc.GitCommandError as err: msg = err.stderr.replace("\n", " ").strip() if "unstaged changes" in msg: - print(RED + " error:", "unstaged changes", end=")") + print(RED + " error:", "unstaged changes.") elif "uncommitted changes" in msg: - print(RED + " error:", "uncommitted changes", end=")") + print(RED + " error:", "uncommitted changes.") else: try: repo.git.rebase("--abort") except exc.GitCommandError: pass - print(RED + " error:", msg if msg else "rebase conflict", end=")") + print(RED + " error:", msg if msg else "rebase conflict.") else: - print("\b" * 6 + " " * 6 + "\b" * 6 + GREEN + "ed", end=")") + print("\b" * 6 + " " * 6 + "\b" * 6 + GREEN + "ed", end=".\n") def _merge(repo, name): """Merge the branch *name* into the current HEAD of *repo*.""" @@ -141,31 +141,31 @@ def _merge(repo, name): except exc.GitCommandError as err: msg = err.stderr.replace("\n", " ").strip() if "local changes" in msg and "would be overwritten" in msg: - print(RED + " error:", "uncommitted changes", end=")") + print(RED + " error:", "uncommitted changes.") else: try: repo.git.merge("--abort") except exc.GitCommandError: pass - print(RED + " error:", msg if msg else "merge conflict", end=")") + print(RED + " error:", msg if msg else "merge conflict.") else: - print("\b" * 6 + " " * 6 + "\b" * 6 + GREEN + "ed", end=")") + print("\b" * 6 + " " * 6 + "\b" * 6 + GREEN + "ed", end=".\n") def _update_branch(repo, branch, merge, rebase, stasher=None): """Update a single branch.""" - print(BOLD + branch.name, end=" (") + print(INDENT2, "Updating", BOLD + branch.name, end=": ") upstream = branch.tracking_branch() if not upstream: - print(YELLOW + "skipped:", "no upstream is tracked", end=")") + print(YELLOW + "skipped:", "no upstream is tracked.") return try: branch.commit, upstream.commit except ValueError: - print(YELLOW + "skipped:", "branch has no revisions", end=")") + print(YELLOW + "skipped:", "branch has no revisions.") return if _is_up_to_date(repo, branch, upstream): - print(BLUE + "up to date", end=")") + print(BLUE + "up to date", end=".\n") return if stasher: @@ -179,19 +179,16 @@ def _update_branch(repo, branch, merge, rebase, stasher=None): def _update_branches(repo, active, merge, rebase): """Update a list of branches.""" - print(INDENT2, "Updating: ", end="") _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): - print(", ", end="") _update_branch(repo, branch, merge, rebase, stasher) finally: active.checkout() stasher.restore() - print(".") def _update_repository(repo, current_only=False, rebase=False, merge=False): """Update a single git repository by fetching remotes and rebasing/merging. From 2be8edfa9fe6bea7a2780fe86c96c25f82f13009 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Mon, 21 Apr 2014 01:21:49 -0400 Subject: [PATCH 17/17] Update README; --preserve-merges as promised. --- README.md | 29 ++++++++++++++++++----------- gitup/script.py | 2 +- gitup/update.py | 12 +++++++++--- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 3709582..a6f6cdf 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ __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 should work on OS X, Linux, and Windows. You should have the latest version of git and at least Python 2.7 installed. @@ -40,9 +40,8 @@ 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 @@ -53,15 +52,15 @@ 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 @@ -72,10 +71,18 @@ 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..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 diff --git a/gitup/script.py b/gitup/script.py index 61f9d44..a29982f 100644 --- a/gitup/script.py +++ b/gitup/script.py @@ -36,7 +36,7 @@ def main(): '-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 pull the + '-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 diff --git a/gitup/update.py b/gitup/update.py index f851b40..9ef7d42 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -117,9 +117,11 @@ def _rebase(repo, name): """Rebase the current HEAD of *repo* onto the branch *name*.""" print(GREEN + "rebasing...", end="") try: - res = repo.git.rebase(name) + 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: @@ -129,7 +131,8 @@ def _rebase(repo, name): repo.git.rebase("--abort") except exc.GitCommandError: pass - print(RED + " error:", msg if msg else "rebase conflict.") + print(RED + " error:", msg if msg else "rebase conflict.", + "Aborted.") else: print("\b" * 6 + " " * 6 + "\b" * 6 + GREEN + "ed", end=".\n") @@ -140,6 +143,8 @@ def _merge(repo, name): 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: @@ -147,7 +152,8 @@ def _merge(repo, name): repo.git.merge("--abort") except exc.GitCommandError: pass - print(RED + " error:", msg if msg else "merge conflict.") + print(RED + " error:", msg if msg else "merge conflict.", + "Aborted.") else: print("\b" * 6 + " " * 6 + "\b" * 6 + GREEN + "ed", end=".\n")