From 80d9bd8e37e0ddf07571f7df4c2e799d3faff351 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Tue, 7 Jun 2011 18:27:13 -0400 Subject: [PATCH 01/34] alphabetically sort each repo when doing a directory of repos --- gitup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gitup.py b/gitup.py index 4820480..2386e74 100644 --- a/gitup.py +++ b/gitup.py @@ -150,6 +150,7 @@ def update_directory(dir_path, dir_name, is_bookmark=False): out(0, "{} '{}{}{}' contains {} git {}:".format(dir_source, ansi['bold'], dir_path, ansi['reset'], repo_count, pluralize)) + repositories.sort() # go alphabetically instead of randomly for repo_path, repo_name in repositories: update_repository(repo_path, repo_name) From 235e790cbd4cdb67e6835117a3ee95275f22e516 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Tue, 7 Jun 2011 18:49:05 -0400 Subject: [PATCH 02/34] adding style(text, effect) function --- gitup.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/gitup.py b/gitup.py index 2386e74..0c6b166 100644 --- a/gitup.py +++ b/gitup.py @@ -20,15 +20,6 @@ __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 @@ -39,6 +30,22 @@ def out(indent, msg): msg = re.sub("\s+", " ", msg) # collapse multiple spaces into one print spacing + msg +def style(text, effect): + """Give a text string a certain effect, such as boldness, or a color.""" + ansi = { # ANSI escape codes to make terminal output fancy + "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", + } + + try: # pad text with effect, unless effect does not exist + return "{}{}{}".format(ansi[effect], text, ansi['reset']) + except KeyError: + return text + def exec_shell(command): """Execute a shell command and get the output.""" command = shlex.split(command) From 1e8695bceea1841fedb75789f4476eb310c40877 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Tue, 7 Jun 2011 19:12:47 -0400 Subject: [PATCH 03/34] converting old formatting to style(), hopefully this makes things neater --- gitup.py | 71 ++++++++++++++++++++++++++++++++++------------------------------ 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/gitup.py b/gitup.py index 0c6b166..0065b86 100644 --- a/gitup.py +++ b/gitup.py @@ -64,7 +64,8 @@ def directory_is_git_repo(directory_path): 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'])) + bold_name = style(repo_name, "bold") + out(1, "{}:".format(bold_name)) os.chdir(repo_path) # cd into our folder so git commands target the correct # repo @@ -74,34 +75,36 @@ def update_repository(repo_path, repo_name): # 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'])) + error_msg = style("Error:", "red") + out(2, """{} cannot fetch; do you have a remote repository configured + correctly?""".format(error_msg)) return try: - last = exec_shell("git log -n 1 --pretty=\"%ar\"") # last commit time + last_commit = exec_shell("git log -n 1 --pretty=\"%ar\"") except subprocess.CalledProcessError: - last = "never" # couldn't get a log, so no commits + last_commit = "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)) + nochanges_msg = style("No new changes.", "blue") + out(2, "{} Last commit was {}.".format(nochanges_msg, last_commit)) 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'])) + + out(2, style("Pulling new changes...", "green")) result = exec_shell("git pull") out(2, "The following changes have been made since {}:".format( - last)) + last_commit)) print result else: - out(2, """{}Warning:{} You have uncommitted changes in this - repository!""".format(ansi['red'], ansi['reset'])) + warning_msg = style("Warning:", "red") + out(2, "{} You have uncommitted changes in this repository!".format( + warning_msg)) out(2, "Ignoring.") def update_directory(dir_path, dir_name, is_bookmark=False): @@ -110,16 +113,18 @@ def update_directory(dir_path, dir_name, is_bookmark=False): 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? + dir_type = "bookmark" # where did we get this directory from? else: - dir_source = "Directory" + dir_type = "directory" + + error = style("Error:", "red") + bold_path_name = style(dir_path, "bold") 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'])) + out(0, "{} cannot enter {} '{}'; does it exist?".format(error, + dir_type, bold_path_name)) return if not os.path.isdir(dir_path): @@ -128,14 +133,13 @@ def update_directory(dir_path, dir_name, is_bookmark=False): else: error_message = "does not exist" - out(0, "{}Error{}: {} '{}{}{}' {}!".format(ansi['red'], ansi['reset'], - dir_source, ansi['bold'], dir_path, ansi['reset'], + out(0, "{} {} '{}' {}!".format(error, dir_type, bold_path_name, 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'])) + out(0, "{} '{}' is a git repository:".format(dir_type.capitalize(), + bold_path_name)) update_repository(dir_path, dir_name) else: @@ -148,14 +152,14 @@ def update_directory(dir_path, dir_name, is_bookmark=False): 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" + num_of_repos = len(repositories) + if num_of_repos == 1: + pluralized = "repository" else: - pluralize = "repositories" + pluralized = "repositories" - out(0, "{} '{}{}{}' contains {} git {}:".format(dir_source, - ansi['bold'], dir_path, ansi['reset'], repo_count, pluralize)) + out(0, "{} '{}' contains {} git {}:".format(dir_type.capitalize(), + bold_path_name, num_of_repos, pluralized)) repositories.sort() # go alphabetically instead of randomly for repo_path, repo_name in repositories: @@ -203,7 +207,8 @@ def add_bookmarks(paths): if not config.has_section("bookmarks"): config.add_section("bookmarks") - out(0, "{}Added bookmarks:{}".format(ansi['yellow'], ansi['reset'])) + out(0, style("Added bookmarks:", "yellow")) + for path in paths: path = os.path.abspath(path) # convert relative to absolute path if config.has_option("bookmarks", path): @@ -211,7 +216,7 @@ def add_bookmarks(paths): else: path_name = os.path.split(path)[1] config.set("bookmarks", path, path_name) - out(1, "{}{}{}".format(ansi['bold'], path, ansi['reset'])) + out(1, style(path, "bold")) save_config_file(config) @@ -220,12 +225,12 @@ def delete_bookmarks(paths): config = load_config_file() if config.has_section("bookmarks"): - out(0, "{}Deleted bookmarks:{}".format(ansi['yellow'], ansi['reset'])) + out(0, style("Deleted bookmarks:", "yellow")) 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'])) + out(1, style(path, "bold")) else: out(1, "'{}' is not bookmarked.".format(path)) save_config_file(config) @@ -242,7 +247,7 @@ def list_bookmarks(): bookmarks = [] if bookmarks: - out(0, "{}Current bookmarks:{}".format(ansi['yellow'], ansi['reset'])) + out(0, style("Current bookmarks:", "yellow")) for bookmark_path, bookmark_name in bookmarks: out(1, bookmark_path) else: @@ -284,7 +289,7 @@ def main(): args = parser.parse_args() - print "{}gitup{}: the git-repo-updater".format(ansi['bold'], ansi['reset']) + print "{}: the git-repo-updater".format(style("gitup", "bold")) if args.bookmarks_to_add: add_bookmarks(args.bookmarks_to_add) From 96078165a1a26f3f003ef84622cd45305ecffe6b Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Tue, 7 Jun 2011 19:14:00 -0400 Subject: [PATCH 04/34] whitespace paranoia --- gitup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gitup.py b/gitup.py index 0065b86..b6a296a 100644 --- a/gitup.py +++ b/gitup.py @@ -94,7 +94,6 @@ def update_repository(repo_path, repo_name): status = exec_shell("git status") if status.endswith("nothing to commit (working directory clean)"): - out(2, style("Pulling new changes...", "green")) result = exec_shell("git pull") out(2, "The following changes have been made since {}:".format( From faf962749136cc46d689cb7dd9e740672746f32f Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Tue, 7 Jun 2011 20:13:48 -0400 Subject: [PATCH 05/34] made per-style functions (bold(), red(), blue()...) and (tried to) clean up styling more --- gitup.py | 89 +++++++++++++++++++++++++++++++--------------------------------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/gitup.py b/gitup.py index b6a296a..4675dd0 100644 --- a/gitup.py +++ b/gitup.py @@ -20,17 +20,14 @@ __email__ = "ben.kurtovic@verizon.net" config_filename = os.path.join(os.path.expanduser("~"), ".gitup") -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 +# Text formatting functions +bold = lambda t: style_text(t, "bold") +red = lambda t: style_text(t, "red") +green = lambda t: style_text(t, "green") +yellow = lambda t: style_text(t, "yellow") +blue = lambda t: style_text(t, "blue") -def style(text, effect): +def style_text(text, effect): """Give a text string a certain effect, such as boldness, or a color.""" ansi = { # ANSI escape codes to make terminal output fancy "reset": "\x1b[0m", @@ -46,6 +43,16 @@ def style(text, effect): except KeyError: return text +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) @@ -64,8 +71,7 @@ def directory_is_git_repo(directory_path): def update_repository(repo_path, repo_name): """Update a single git repository by pulling from the remote.""" - bold_name = style(repo_name, "bold") - out(1, "{}:".format(bold_name)) + out(1, bold(repo_name) + ":") os.chdir(repo_path) # cd into our folder so git commands target the correct # repo @@ -75,9 +81,8 @@ def update_repository(repo_path, repo_name): # anything to pull, but # don't do it yet except subprocess.CalledProcessError: - error_msg = style("Error:", "red") - out(2, """{} cannot fetch; do you have a remote repository configured - correctly?""".format(error_msg)) + out(2, red("Error: ") + "cannot fetch; do you have a remote " + + "repository configured correctly?") return try: @@ -86,24 +91,23 @@ def update_repository(repo_path, repo_name): last_commit = "never" # couldn't get a log, so no commits if not dry_fetch: # no new changes to pull - nochanges_msg = style("No new changes.", "blue") - out(2, "{} Last commit was {}.".format(nochanges_msg, last_commit)) + out(2, blue("No new changes.") + " Last commit was {}.".format( + last_commit)) 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, style("Pulling new changes...", "green")) + out(2, green("Pulling new changes...")) result = exec_shell("git pull") out(2, "The following changes have been made since {}:".format( last_commit)) print result else: - warning_msg = style("Warning:", "red") - out(2, "{} You have uncommitted changes in this repository!".format( - warning_msg)) + out(2, red("Warning: ") + "you have uncommitted changes in this " + + "repository!") out(2, "Ignoring.") def update_directory(dir_path, dir_name, is_bookmark=False): @@ -116,29 +120,24 @@ def update_directory(dir_path, dir_name, is_bookmark=False): else: dir_type = "directory" - error = style("Error:", "red") - bold_path_name = style(dir_path, "bold") + dir_long_name = "{} '{}'".format(dir_type, bold(dir_path)) try: os.listdir(dir_path) # test if we can access this directory except OSError: - out(0, "{} cannot enter {} '{}'; does it exist?".format(error, - dir_type, bold_path_name)) + out(0, red("Error: ") + "cannot enter {}; does it exist?".format( + dir_long_name)) return if not os.path.isdir(dir_path): if os.path.exists(dir_path): - error_message = "is not a directory" + out(0, red("Error: ") + dir_long_name + " is not a directory!") else: - error_message = "does not exist" - - out(0, "{} {} '{}' {}!".format(error, dir_type, bold_path_name, - error_message)) + out(0, red("Error: ") + dir_long_name + " does not exist!") return if directory_is_git_repo(dir_path): - out(0, "{} '{}' is a git repository:".format(dir_type.capitalize(), - bold_path_name)) + out(0, dir_long_name.capitalize() + " is a git repository:") update_repository(dir_path, dir_name) else: @@ -153,13 +152,11 @@ def update_directory(dir_path, dir_name, is_bookmark=False): num_of_repos = len(repositories) if num_of_repos == 1: - pluralized = "repository" + out(0, dir_long_name.capitalize() + " contains 1 git repository:") else: - pluralized = "repositories" - - out(0, "{} '{}' contains {} git {}:".format(dir_type.capitalize(), - bold_path_name, num_of_repos, pluralized)) - + out(0, dir_long_name.capitalize() + + " contains {} 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) @@ -182,8 +179,8 @@ def update_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'.""") + 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 @@ -206,7 +203,7 @@ def add_bookmarks(paths): if not config.has_section("bookmarks"): config.add_section("bookmarks") - out(0, style("Added bookmarks:", "yellow")) + out(0, yellow("Added bookmarks:")) for path in paths: path = os.path.abspath(path) # convert relative to absolute path @@ -215,7 +212,7 @@ def add_bookmarks(paths): else: path_name = os.path.split(path)[1] config.set("bookmarks", path, path_name) - out(1, style(path, "bold")) + out(1, bold(path)) save_config_file(config) @@ -224,12 +221,12 @@ def delete_bookmarks(paths): config = load_config_file() if config.has_section("bookmarks"): - out(0, style("Deleted bookmarks:", "yellow")) + out(0, yellow("Deleted bookmarks:")) 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, style(path, "bold")) + out(1, bold(path)) else: out(1, "'{}' is not bookmarked.".format(path)) save_config_file(config) @@ -246,7 +243,7 @@ def list_bookmarks(): bookmarks = [] if bookmarks: - out(0, style("Current bookmarks:", "yellow")) + out(0, yellow("Current bookmarks:")) for bookmark_path, bookmark_name in bookmarks: out(1, bookmark_path) else: @@ -288,7 +285,7 @@ def main(): args = parser.parse_args() - print "{}: the git-repo-updater".format(style("gitup", "bold")) + print bold("gitup") + ": the git-repo-updater" if args.bookmarks_to_add: add_bookmarks(args.bookmarks_to_add) From ce50561a81a551ef58c15e36bd9b680d554c1f7d Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sat, 7 Jul 2012 00:14:36 -0400 Subject: [PATCH 06/34] Update copyright for 2012. --- LICENSE | 2 +- gitup.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 6572aa7..14ecd56 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011 by Ben Kurtovic +Copyright (c) 2011-2012 Ben Kurtovic Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/gitup.py b/gitup.py index 4675dd0..694b584 100644 --- a/gitup.py +++ b/gitup.py @@ -13,7 +13,7 @@ import shlex import subprocess __author__ = "Ben Kurtovic" -__copyright__ = "Copyright (c) 2011 by Ben Kurtovic" +__copyright__ = "Copyright (c) 2011-2012 Ben Kurtovic" __license__ = "MIT License" __version__ = "0.1" __email__ = "ben.kurtovic@verizon.net" diff --git a/setup.py b/setup.py index 0052eff..250316e 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from distutils.core import setup +from setuptools import setup import os import sys From 8a4c14c00bd2f140fc0f6a9bdd990f3f16f1d98e Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Fri, 12 Apr 2013 17:46:59 -0300 Subject: [PATCH 07/34] Update README.md work on Windows (closes #3). --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0dd6ea1..a04641b 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ 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 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, From 2d8872dbcd7521e9c4d9e38a12446f01635abb89 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Mon, 21 Oct 2013 20:48:25 -0400 Subject: [PATCH 08/34] Email address update. --- LICENSE | 2 +- gitup.py | 78 ++++++++++++++++++++++++++++++++-------------------------------- setup.py | 4 ++-- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/LICENSE b/LICENSE index 14ecd56..ddae58f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2012 Ben Kurtovic +Copyright (c) 2011-2012 Ben Kurtovic Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/gitup.py b/gitup.py index 694b584..62bfcea 100644 --- a/gitup.py +++ b/gitup.py @@ -16,7 +16,7 @@ __author__ = "Ben Kurtovic" __copyright__ = "Copyright (c) 2011-2012 Ben Kurtovic" __license__ = "MIT License" __version__ = "0.1" -__email__ = "ben.kurtovic@verizon.net" +__email__ = "ben.kurtovic@gmail.com" config_filename = os.path.join(os.path.expanduser("~"), ".gitup") @@ -37,7 +37,7 @@ def style_text(text, effect): "yellow": "\x1b[1m\x1b[33m", "blue": "\x1b[1m\x1b[34m", } - + try: # pad text with effect, unless effect does not exist return "{}{}{}".format(ansi[effect], text, ansi['reset']) except KeyError: @@ -72,10 +72,10 @@ def directory_is_git_repo(directory_path): def update_repository(repo_path, repo_name): """Update a single git repository by pulling from the remote.""" out(1, bold(repo_name) + ":") - + 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 @@ -84,27 +84,27 @@ def update_repository(repo_path, repo_name): out(2, red("Error: ") + "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 - + if not dry_fetch: # no new changes to pull out(2, blue("No new changes.") + " Last commit was {}.".format( last_commit)) - + 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, green("Pulling new changes...")) result = exec_shell("git pull") out(2, "The following changes have been made since {}:".format( last_commit)) print result - + else: out(2, red("Warning: ") + "you have uncommitted changes in this " + "repository!") @@ -119,44 +119,44 @@ def update_directory(dir_path, dir_name, is_bookmark=False): dir_type = "bookmark" # where did we get this directory from? else: dir_type = "directory" - + dir_long_name = "{} '{}'".format(dir_type, bold(dir_path)) - + try: os.listdir(dir_path) # test if we can access this directory except OSError: out(0, red("Error: ") + "cannot enter {}; does it exist?".format( dir_long_name)) return - + if not os.path.isdir(dir_path): if os.path.exists(dir_path): out(0, red("Error: ") + dir_long_name + " is not a directory!") else: out(0, red("Error: ") + dir_long_name + " does not exist!") return - + if directory_is_git_repo(dir_path): out(0, dir_long_name.capitalize() + " is a git repository:") 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)) - + num_of_repos = len(repositories) if num_of_repos == 1: out(0, dir_long_name.capitalize() + " contains 1 git repository:") else: out(0, dir_long_name.capitalize() + " contains {} 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) @@ -174,7 +174,7 @@ def update_bookmarks(): 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) @@ -202,9 +202,9 @@ def add_bookmarks(paths): config = load_config_file() if not config.has_section("bookmarks"): config.add_section("bookmarks") - + out(0, yellow("Added bookmarks:")) - + for path in paths: path = os.path.abspath(path) # convert relative to absolute path if config.has_option("bookmarks", path): @@ -213,13 +213,13 @@ def add_bookmarks(paths): path_name = os.path.split(path)[1] config.set("bookmarks", path, path_name) out(1, bold(path)) - + 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, yellow("Deleted bookmarks:")) for path in paths: @@ -230,7 +230,7 @@ def delete_bookmarks(paths): else: out(1, "'{}' is not bookmarked.".format(path)) save_config_file(config) - + else: out(0, "There are no bookmarks to delete!") @@ -241,7 +241,7 @@ def list_bookmarks(): bookmarks = config.items("bookmarks") except configparser.NoSectionError: bookmarks = [] - + if bookmarks: out(0, yellow("Current bookmarks:")) for bookmark_path, bookmark_name in bookmarks: @@ -255,53 +255,53 @@ def main(): 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 bold("gitup") + ": the git-repo-updater" - + 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 diff --git a/setup.py b/setup.py index 250316e..c571036 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ try: version = "0.1", scripts = ['gitup'], author = "Ben Kurtovic", - author_email = "ben.kurtovic@verizon.net", + author_email = "ben.kurtovic@gmail.com", description = desc, long_description = long_desc, license = "MIT License", @@ -40,7 +40,7 @@ try: "Topic :: Software Development :: Version Control" ] ) - + finally: if remove_py_extension: os.rename("gitup", "gitup.py") # restore file location From 290d091e905f18d2316855f365c348d93cf9e177 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Thu, 20 Feb 2014 00:53:59 -0500 Subject: [PATCH 09/34] Version bump, doc updates, gitignore update. --- .gitignore | 2 ++ LICENSE | 2 +- README.md | 4 ++-- gitup.py | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 567609b..25aacff 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ build/ +dist/ +*.egg-info/ diff --git a/LICENSE b/LICENSE index ddae58f..7b13d64 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2012 Ben Kurtovic +Copyright (c) 2011-2014 Ben Kurtovic Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a04641b..1c88284 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,13 @@ For example: gitup ~/repos/foo ~/repos/bar ~/repos/baz -...will automatically pull to the `foo`, `bar`, and `baz` git repositories if +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: 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: diff --git a/gitup.py b/gitup.py index 62bfcea..208c022 100644 --- a/gitup.py +++ b/gitup.py @@ -13,9 +13,9 @@ import shlex import subprocess __author__ = "Ben Kurtovic" -__copyright__ = "Copyright (c) 2011-2012 Ben Kurtovic" +__copyright__ = "Copyright (c) 2011-2014 Ben Kurtovic" __license__ = "MIT License" -__version__ = "0.1" +__version__ = "0.2.dev" __email__ = "ben.kurtovic@gmail.com" config_filename = os.path.join(os.path.expanduser("~"), ".gitup") From 91344c10a768c7fa530a9ef26012e14fa8dc4941 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Thu, 20 Feb 2014 00:55:16 -0500 Subject: [PATCH 10/34] Quick fix for #4 until I implement GitPython. --- gitup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitup.py b/gitup.py index 208c022..cf6c524 100644 --- a/gitup.py +++ b/gitup.py @@ -98,7 +98,7 @@ def update_repository(repo_path, repo_name): out(2, "There are new changes upstream...") status = exec_shell("git status") - if status.endswith("nothing to commit (working directory clean)"): + if status.endswith("nothing to commit, working directory clean"): out(2, green("Pulling new changes...")) result = exec_shell("git pull") out(2, "The following changes have been made since {}:".format( From e74944165ab2b0cdd7a516ad4703e0a921ab3ad9 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Thu, 20 Feb 2014 03:41:58 -0500 Subject: [PATCH 11/34] Starting work on the new package structure. --- gitup/__init__.py | 16 ++++ gitup.py => gitup/script.py | 196 +++++++++++++++++++++----------------------- setup.py | 58 ++++++------- 3 files changed, 138 insertions(+), 132 deletions(-) create mode 100644 gitup/__init__.py rename gitup.py => gitup/script.py (55%) diff --git a/gitup/__init__.py b/gitup/__init__.py new file mode 100644 index 0000000..2d8d6d9 --- /dev/null +++ b/gitup/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2011-2014 Ben Kurtovic +# 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.dev" +__email__ = "ben.kurtovic@gmail.com" + +from . import script diff --git a/gitup.py b/gitup/script.py similarity index 55% rename from gitup.py rename to gitup/script.py index cf6c524..31e4a64 100644 --- a/gitup.py +++ b/gitup/script.py @@ -1,9 +1,9 @@ -#! /usr/bin/python -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- +# +# Copyright (C) 2011-2014 Ben Kurtovic +# See the LICENSE file for details. -""" -gitup: the git repository updater -""" +from __future__ import print_function import argparse import ConfigParser as configparser @@ -12,24 +12,20 @@ import re import shlex import subprocess -__author__ = "Ben Kurtovic" -__copyright__ = "Copyright (c) 2011-2014 Ben Kurtovic" -__license__ = "MIT License" -__version__ = "0.2.dev" -__email__ = "ben.kurtovic@gmail.com" +from . import __version__, __email__ config_filename = os.path.join(os.path.expanduser("~"), ".gitup") -# Text formatting functions -bold = lambda t: style_text(t, "bold") -red = lambda t: style_text(t, "red") -green = lambda t: style_text(t, "green") -yellow = lambda t: style_text(t, "yellow") -blue = lambda t: style_text(t, "blue") +# Text formatting functions: +bold = lambda t: _style_text(t, "bold") +red = lambda t: _style_text(t, "red") +green = lambda t: _style_text(t, "green") +yellow = lambda t: _style_text(t, "yellow") +blue = lambda t: _style_text(t, "blue") -def style_text(text, effect): +def _style_text(text, effect): """Give a text string a certain effect, such as boldness, or a color.""" - ansi = { # ANSI escape codes to make terminal output fancy + ansi = { # ANSI escape codes to make terminal output fancy "reset": "\x1b[0m", "bold": "\x1b[1m", "red": "\x1b[1m\x1b[31m", @@ -38,34 +34,34 @@ def style_text(text, effect): "blue": "\x1b[1m\x1b[34m", } - try: # pad text with effect, unless effect does not exist - return "{}{}{}".format(ansi[effect], text, ansi['reset']) + try: # Pad text with effect, unless effect does not exist + return ansi[effect] + text + ansi["reset"] except KeyError: return text def out(indent, msg): """Print a message at a given indentation level.""" - width = 4 # amount to indent at each 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 + msg = re.sub(r"\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 + 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 + if os.path.isdir(git_subfolder): # Check for path/to/repository/.git return True return False @@ -73,60 +69,62 @@ def update_repository(repo_path, repo_name): """Update a single git repository by pulling from the remote.""" out(1, bold(repo_name) + ":") - os.chdir(repo_path) # cd into our folder so git commands target the correct - # repo + # cd into our folder so git commands target the correct repo: + os.chdir(repo_path) try: - dry_fetch = exec_shell("git fetch --dry-run") # check if there is - # anything to pull, but - # don't do it yet + # Check if there is anything to pull, but don't do it yet: + dry_fetch = exec_shell("git fetch --dry-run") except subprocess.CalledProcessError: - out(2, red("Error: ") + "cannot fetch; do you have a remote " + - "repository configured correctly?") + out(2, red("Error: ") + "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 + last_commit = "never" # Couldn't get a log, so no commits - if not dry_fetch: # no new changes to pull - out(2, blue("No new changes.") + " Last commit was {}.".format( - last_commit)) + if not dry_fetch: # No new changes to pull + out(2, blue("No new changes.") + + " Last commit was {0}.".format(last_commit)) - else: # stuff has happened! + 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, green("Pulling new changes...")) result = exec_shell("git pull") - out(2, "The following changes have been made since {}:".format( + out(2, "The following changes have been made since {0}:".format( last_commit)) - print result + print(result) else: - out(2, red("Warning: ") + "you have uncommitted changes in this " + - "repository!") + out(2, red("Warning: ") + + "you have uncommitted changes in this repository!") out(2, "Ignoring.") def update_directory(dir_path, dir_name, is_bookmark=False): - """First, make sure the specified object is actually a directory, then + """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.""" + latter, update all repositories contained within. + """ if is_bookmark: - dir_type = "bookmark" # where did we get this directory from? + dir_type = "bookmark" # Where did we get this directory from? else: dir_type = "directory" - dir_long_name = "{} '{}'".format(dir_type, bold(dir_path)) + dir_long_name = "{0} '{1}'".format(dir_type, bold(dir_path)) try: - os.listdir(dir_path) # test if we can access this directory + os.listdir(dir_path) # Test if we can access this directory except OSError: - out(0, red("Error: ") + "cannot enter {}; does it exist?".format( - dir_long_name)) + out(0, red("Error: ") + + "cannot enter {0}; does it exist?".format(dir_long_name)) return if not os.path.isdir(dir_path): @@ -143,11 +141,11 @@ def update_directory(dir_path, dir_name, is_bookmark=False): else: repositories = [] - dir_contents = os.listdir(dir_path) # get potential repos in directory + 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 + if directory_is_git_repo(repo_path): # Filter out non-repositories repositories.append((repo_path, repo_name)) num_of_repos = len(repositories) @@ -155,17 +153,17 @@ def update_directory(dir_path, dir_name, is_bookmark=False): out(0, dir_long_name.capitalize() + " contains 1 git repository:") else: out(0, dir_long_name.capitalize() + - " contains {} git repositories:".format(num_of_repos)) + " contains {0} git repositories:".format(num_of_repos)) - repositories.sort() # go alphabetically instead of randomly + repositories.sort() # Go alphabetically instead of randomly 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/ + 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) def update_bookmarks(): @@ -179,21 +177,19 @@ def update_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'.") + 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.""" + """Read the config file and return a SafeConfigParser() object.""" config = configparser.SafeConfigParser() - config.optionxform = str # don't lowercase option names, because we are - # storing paths there + # 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 our config changes to the config file specified by - config_filename.""" + """Save config changes to the config file specified by config_filename.""" with open(config_filename, "wb") as config_file: config.write(config_file) @@ -206,9 +202,9 @@ def add_bookmarks(paths): out(0, yellow("Added bookmarks:")) for path in paths: - path = os.path.abspath(path) # convert relative to absolute path + path = os.path.abspath(path) # Convert relative to absolute path if config.has_option("bookmarks", path): - out(1, "'{}' is already bookmarked.".format(path)) + out(1, "'{0}' is already bookmarked.".format(path)) else: path_name = os.path.split(path)[1] config.set("bookmarks", path, path_name) @@ -223,12 +219,12 @@ def delete_bookmarks(paths): if config.has_section("bookmarks"): out(0, yellow("Deleted bookmarks:")) for path in paths: - path = os.path.abspath(path) # convert relative to absolute path + path = os.path.abspath(path) # Convert relative to absolute path config_was_changed = config.remove_option("bookmarks", path) if config_was_changed: out(1, bold(path)) else: - out(1, "'{}' is not bookmarked.".format(path)) + out(1, "'{0}' is not bookmarked.".format(path)) save_config_file(config) else: @@ -251,61 +247,59 @@ def list_bookmarks(): 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) + 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 {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") - 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__) + 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 bold("gitup") + ": the git-repo-updater" + print(bold("gitup") + ": the git-repo-updater") 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 they did not tell us to do anything, automatically update bookmarks: + if not any(vars(args).values()): + update_bookmarks() -if __name__ == "__main__": +def run(): + """Thin wrapper for main() that catches KeyboardInterrupts.""" try: main() except KeyboardInterrupt: diff --git a/setup.py b/setup.py index c571036..3d7b348 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,35 @@ -from setuptools import setup -import os +# -*- coding: utf-8 -*- +# +# Copyright (C) 2011-2014 Ben Kurtovic +# 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: .") -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") +from gitup import __version__ -desc = "Easily pull to multiple git repositories at once." +with open('README.md') as fp: + long_desc = fp.read() -with open('README.md') as file: - long_desc = file.read() - -try: - setup( - name = "gitup", - version = "0.1", - scripts = ['gitup'], - author = "Ben Kurtovic", - author_email = "ben.kurtovic@gmail.com", - 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"], + 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 + ] +) From e9e2adec63656f1170e9a5af3c38154e60bea052 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Mon, 24 Feb 2014 14:15:37 -0500 Subject: [PATCH 12/34] Update gitignore, README. --- .gitignore | 10 +++++++--- README.md | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 25aacff..cc17441 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ -build/ -dist/ -*.egg-info/ +*.pyc +*.egg +*.egg-info +.DS_Store +__pycache__ +build +dist diff --git a/README.md b/README.md index 1c88284..3709582 100644 --- a/README.md +++ b/README.md @@ -80,5 +80,5 @@ 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`). From 910b53aeba521f7b4fef41b0bd40f6a621a45942 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Mon, 24 Feb 2014 14:43:28 -0500 Subject: [PATCH 13/34] Split code into multiple files. --- gitup/__init__.py | 2 +- gitup/config.py | 82 ++++++++++++++++++ gitup/output.py | 41 +++++++++ gitup/script.py | 244 ++---------------------------------------------------- gitup/update.py | 138 ++++++++++++++++++++++++++++++ 5 files changed, 268 insertions(+), 239 deletions(-) create mode 100644 gitup/config.py create mode 100644 gitup/output.py create mode 100644 gitup/update.py diff --git a/gitup/__init__.py b/gitup/__init__.py index 2d8d6d9..9bebff1 100644 --- a/gitup/__init__.py +++ b/gitup/__init__.py @@ -13,4 +13,4 @@ __license__ = "MIT License" __version__ = "0.2.dev" __email__ = "ben.kurtovic@gmail.com" -from . import script +from . import script, update diff --git a/gitup/config.py b/gitup/config.py new file mode 100644 index 0000000..72c1cbf --- /dev/null +++ b/gitup/config.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2011-2014 Ben Kurtovic +# See the LICENSE file for details. + +import ConfigParser as configparser +import os + +from .output import out, bold, yellow + +__all__ = ["get_bookmarks", "add_bookmarks", "delete_bookmarks", + "list_bookmarks"] + +_config_filename = os.path.join(os.path.expanduser("~"), ".gitup") + +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") + + out(0, yellow("Added bookmarks:")) + + for path in paths: + path = os.path.abspath(path) # Convert relative to absolute path + if config.has_option("bookmarks", path): + out(1, "'{0}' is already bookmarked.".format(path)) + else: + path_name = os.path.split(path)[1] + config.set("bookmarks", path, path_name) + out(1, bold(path)) + + _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, yellow("Deleted bookmarks:")) + 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, bold(path)) + else: + out(1, "'{0}' 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.""" + bookmarks = get_bookmarks() + if bookmarks: + out(0, yellow("Current bookmarks:")) + for bookmark_path, bookmark_name in bookmarks: + out(1, bookmark_path) + else: + out(0, "You have no bookmarks to display.") diff --git a/gitup/output.py b/gitup/output.py new file mode 100644 index 0000000..a92d86c --- /dev/null +++ b/gitup/output.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2011-2014 Ben Kurtovic +# See the LICENSE file for details. + +import re + +__all__ = ["out", "bold", "red", "green", "yellow", "blue"] + +# Text formatting functions: +bold = lambda t: _style_text(t, "bold") +red = lambda t: _style_text(t, "red") +green = lambda t: _style_text(t, "green") +yellow = lambda t: _style_text(t, "yellow") +blue = lambda t: _style_text(t, "blue") + +def _style_text(text, effect): + """Give a text string a certain effect, such as boldness, or a color.""" + ansi = { # ANSI escape codes to make terminal output fancy + "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", + } + + try: # Pad text with effect, unless effect does not exist + return ansi[effect] + text + ansi["reset"] + except KeyError: + return text + +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(r"\s+", " ", msg) # Collapse multiple spaces into one + print(spacing + msg) diff --git a/gitup/script.py b/gitup/script.py index 31e4a64..ea616cd 100644 --- a/gitup/script.py +++ b/gitup/script.py @@ -6,244 +6,12 @@ from __future__ import print_function import argparse -import ConfigParser as configparser -import os -import re -import shlex -import subprocess from . import __version__, __email__ - -config_filename = os.path.join(os.path.expanduser("~"), ".gitup") - -# Text formatting functions: -bold = lambda t: _style_text(t, "bold") -red = lambda t: _style_text(t, "red") -green = lambda t: _style_text(t, "green") -yellow = lambda t: _style_text(t, "yellow") -blue = lambda t: _style_text(t, "blue") - -def _style_text(text, effect): - """Give a text string a certain effect, such as boldness, or a color.""" - ansi = { # ANSI escape codes to make terminal output fancy - "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", - } - - try: # Pad text with effect, unless effect does not exist - return ansi[effect] + text + ansi["reset"] - except KeyError: - return text - -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(r"\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, bold(repo_name) + ":") - - # cd into our folder so git commands target the correct repo: - os.chdir(repo_path) - - 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: - out(2, red("Error: ") + "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 - - if not dry_fetch: # No new changes to pull - out(2, blue("No new changes.") + - " Last commit was {0}.".format(last_commit)) - - 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, green("Pulling new changes...")) - result = exec_shell("git pull") - out(2, "The following changes have been made since {0}:".format( - last_commit)) - print(result) - - else: - out(2, red("Warning: ") + - "you have uncommitted changes in this repository!") - out(2, "Ignoring.") - -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. - """ - if is_bookmark: - dir_type = "bookmark" # Where did we get this directory from? - else: - dir_type = "directory" - - dir_long_name = "{0} '{1}'".format(dir_type, bold(dir_path)) - - try: - os.listdir(dir_path) # Test if we can access this directory - except OSError: - out(0, red("Error: ") + - "cannot enter {0}; does it exist?".format(dir_long_name)) - return - - if not os.path.isdir(dir_path): - if os.path.exists(dir_path): - out(0, red("Error: ") + dir_long_name + " is not a directory!") - else: - out(0, red("Error: ") + dir_long_name + " does not exist!") - return - - if directory_is_git_repo(dir_path): - out(0, dir_long_name.capitalize() + " is a git repository:") - 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)) - - num_of_repos = len(repositories) - if num_of_repos == 1: - out(0, dir_long_name.capitalize() + " contains 1 git repository:") - else: - out(0, 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) - -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) - -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 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 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, yellow("Added bookmarks:")) - - for path in paths: - path = os.path.abspath(path) # Convert relative to absolute path - if config.has_option("bookmarks", path): - out(1, "'{0}' is already bookmarked.".format(path)) - else: - path_name = os.path.split(path)[1] - config.set("bookmarks", path, path_name) - out(1, bold(path)) - - 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, yellow("Deleted bookmarks:")) - 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, bold(path)) - else: - out(1, "'{0}' 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, yellow("Current bookmarks:")) - for bookmark_path, bookmark_name in bookmarks: - out(1, bookmark_path) - else: - out(0, "You have no bookmarks to display.") +from .config import (get_bookmarks, add_bookmarks, delete_bookmarks, + list_bookmarks) +from .output import out, bold +from .update import update_bookmarks, update_directories def main(): """Parse arguments and then call the appropriate function(s).""" @@ -292,11 +60,11 @@ def main(): if args.directories_to_update: update_directories(args.directories_to_update) if args.update: - update_bookmarks() + update_bookmarks(get_bookmarks()) # If they did not tell us to do anything, automatically update bookmarks: if not any(vars(args).values()): - update_bookmarks() + update_bookmarks(get_bookmarks()) def run(): """Thin wrapper for main() that catches KeyboardInterrupts.""" diff --git a/gitup/update.py b/gitup/update.py new file mode 100644 index 0000000..0245159 --- /dev/null +++ b/gitup/update.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2011-2014 Ben Kurtovic +# See the LICENSE file for details. + +import os +import shlex +import subprocess + +from .output import out, bold, red, green, blue + +__all__ = ["update_bookmarks", "update_directories"] + +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, bold(repo_name) + ":") + + # cd into our folder so git commands target the correct repo: + os.chdir(repo_path) + + 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: + out(2, red("Error: ") + "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 + + if not dry_fetch: # No new changes to pull + out(2, blue("No new changes.") + + " Last commit was {0}.".format(last_commit)) + + 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, green("Pulling new changes...")) + result = _exec_shell("git pull") + out(2, "The following changes have been made since {0}:".format( + last_commit)) + print(result) + + else: + out(2, red("Warning: ") + + "you have uncommitted changes in this repository!") + out(2, "Ignoring.") + +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. + """ + if is_bookmark: + dir_type = "bookmark" # Where did we get this directory from? + else: + dir_type = "directory" + + dir_long_name = "{0} '{1}'".format(dir_type, bold(dir_path)) + + try: + os.listdir(dir_path) # Test if we can access this directory + except OSError: + out(0, red("Error: ") + + "cannot enter {0}; does it exist?".format(dir_long_name)) + return + + if not os.path.isdir(dir_path): + if os.path.exists(dir_path): + out(0, red("Error: ") + dir_long_name + " is not a directory!") + else: + out(0, red("Error: ") + dir_long_name + " does not exist!") + return + + if _directory_is_git_repo(dir_path): + out(0, dir_long_name.capitalize() + " is a git repository:") + _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)) + + num_of_repos = len(repositories) + if num_of_repos == 1: + out(0, dir_long_name.capitalize() + " contains 1 git repository:") + else: + out(0, 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) + +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) + else: + out(0, "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) From beac864d29a4b883e73c6e0bb0eae7a98886924a Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 2 Mar 2014 00:53:34 -0500 Subject: [PATCH 14/34] Clean up output.py. --- gitup/output.py | 26 +++++--------------------- gitup/update.py | 2 +- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/gitup/output.py b/gitup/output.py index a92d86c..7b160ea 100644 --- a/gitup/output.py +++ b/gitup/output.py @@ -8,27 +8,11 @@ import re __all__ = ["out", "bold", "red", "green", "yellow", "blue"] # Text formatting functions: -bold = lambda t: _style_text(t, "bold") -red = lambda t: _style_text(t, "red") -green = lambda t: _style_text(t, "green") -yellow = lambda t: _style_text(t, "yellow") -blue = lambda t: _style_text(t, "blue") - -def _style_text(text, effect): - """Give a text string a certain effect, such as boldness, or a color.""" - ansi = { # ANSI escape codes to make terminal output fancy - "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", - } - - try: # Pad text with effect, unless effect does not exist - return ansi[effect] + text + ansi["reset"] - except KeyError: - return text +bold = lambda t: "\x1b[1m" + t + "\x1b[0m" +red = lambda t: "\x1b[1m\x1b[31m" + t + "\x1b[0m" +green = lambda t: "\x1b[1m\x1b[32m" + t + "\x1b[0m" +yellow = lambda t: "\x1b[1m\x1b[33m" + t + "\x1b[0m" +blue = lambda t: "\x1b[1m\x1b[34m" + t + "\x1b[0m" def out(indent, msg): """Print a message at a given indentation level.""" diff --git a/gitup/update.py b/gitup/update.py index 0245159..480a2e9 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -107,7 +107,7 @@ def _update_directory(dir_path, dir_name, is_bookmark=False): 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 + if _directory_is_git_repo(repo_path): # Filter out non-repos repositories.append((repo_path, repo_name)) num_of_repos = len(repositories) From 25127f4e944edc6fd012899bbd0a7be6cc6d05d3 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 23 Mar 2014 04:43:36 -0400 Subject: [PATCH 15/34] Finish conversion from output.py to colorama. --- LICENSE | 2 +- gitup/__init__.py | 2 +- gitup/config.py | 64 ++++++++++++++++++++++++++++++--------------- gitup/output.py | 25 ------------------ gitup/script.py | 10 ++++--- gitup/update.py | 78 ++++++++++++++++++++++++++++++++----------------------- setup.py | 2 +- 7 files changed, 98 insertions(+), 85 deletions(-) delete mode 100644 gitup/output.py diff --git a/LICENSE b/LICENSE index 7b13d64..b755367 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2014 Ben Kurtovic +Copyright (C) 2011-2014 Ben Kurtovic Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/gitup/__init__.py b/gitup/__init__.py index 9bebff1..7482a7c 100644 --- a/gitup/__init__.py +++ b/gitup/__init__.py @@ -13,4 +13,4 @@ __license__ = "MIT License" __version__ = "0.2.dev" __email__ = "ben.kurtovic@gmail.com" -from . import script, update +from . import config, script, update diff --git a/gitup/config.py b/gitup/config.py index 72c1cbf..5571cfe 100644 --- a/gitup/config.py +++ b/gitup/config.py @@ -3,27 +3,34 @@ # Copyright (C) 2011-2014 Ben Kurtovic # See the LICENSE file for details. +from __future__ import print_function + import ConfigParser as configparser import os -from .output import out, bold, yellow +from colorama import Fore, Style __all__ = ["get_bookmarks", "add_bookmarks", "delete_bookmarks", "list_bookmarks"] -_config_filename = os.path.join(os.path.expanduser("~"), ".gitup") +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) + 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: + """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(): @@ -40,43 +47,58 @@ def add_bookmarks(paths): if not config.has_section("bookmarks"): config.add_section("bookmarks") - out(0, yellow("Added bookmarks:")) - + added, exists = [], [] for path in paths: - path = os.path.abspath(path) # Convert relative to absolute path + path = os.path.abspath(path) if config.has_option("bookmarks", path): - out(1, "'{0}' is already bookmarked.".format(path)) + exists.append(path) else: path_name = os.path.split(path)[1] config.set("bookmarks", path, path_name) - out(1, bold(path)) - + 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"): - out(0, yellow("Deleted bookmarks:")) for path in paths: - path = os.path.abspath(path) # Convert relative to absolute path + path = os.path.abspath(path) config_was_changed = config.remove_option("bookmarks", path) if config_was_changed: - out(1, bold(path)) + deleted.append(path) else: - out(1, "'{0}' is not bookmarked.".format(path)) + notmarked.append(path) _save_config_file(config) - else: - out(0, "There are no bookmarks to delete!") + 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: - out(0, yellow("Current bookmarks:")) - for bookmark_path, bookmark_name in bookmarks: - out(1, bookmark_path) + print(YELLOW + "Current bookmarks:") + for bookmark_path, _ in bookmarks: + print(INDENT1, bookmark_path) else: - out(0, "You have no bookmarks to display.") + print("You have no bookmarks to display.") diff --git a/gitup/output.py b/gitup/output.py deleted file mode 100644 index 7b160ea..0000000 --- a/gitup/output.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2011-2014 Ben Kurtovic -# See the LICENSE file for details. - -import re - -__all__ = ["out", "bold", "red", "green", "yellow", "blue"] - -# Text formatting functions: -bold = lambda t: "\x1b[1m" + t + "\x1b[0m" -red = lambda t: "\x1b[1m\x1b[31m" + t + "\x1b[0m" -green = lambda t: "\x1b[1m\x1b[32m" + t + "\x1b[0m" -yellow = lambda t: "\x1b[1m\x1b[33m" + t + "\x1b[0m" -blue = lambda t: "\x1b[1m\x1b[34m" + t + "\x1b[0m" - -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(r"\s+", " ", msg) # Collapse multiple spaces into one - print(spacing + msg) diff --git a/gitup/script.py b/gitup/script.py index ea616cd..8105d8a 100644 --- a/gitup/script.py +++ b/gitup/script.py @@ -7,10 +7,11 @@ 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 .output import out, bold from .update import update_bookmarks, update_directories def main(): @@ -48,8 +49,11 @@ def main(): '-v', '--version', action="version", version="gitup version " + __version__) + color_init(autoreset=True) args = parser.parse_args() - print(bold("gitup") + ": the git-repo-updater") + + print(Style.BRIGHT + "gitup" + Style.RESET_ALL + ": the git-repo-updater") + print() if args.bookmarks_to_add: add_bookmarks(args.bookmarks_to_add) @@ -71,4 +75,4 @@ def run(): try: main() except KeyboardInterrupt: - out(0, "Stopped by user.") + print("Stopped by user.") diff --git a/gitup/update.py b/gitup/update.py index 480a2e9..de2d01b 100644 --- a/gitup/update.py +++ b/gitup/update.py @@ -3,21 +3,24 @@ # Copyright (C) 2011-2014 Ben Kurtovic # See the LICENSE file for details. +from __future__ import print_function + import os import shlex import subprocess -from .output import out, bold, red, green, blue +from colorama import Fore, Style __all__ = ["update_bookmarks", "update_directories"] -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 +BOLD = Style.BRIGHT +RED = Fore.RED + BOLD +GREEN = Fore.GREEN + BOLD +BLUE = Fore.BLUE + BOLD +RESET = Style.RESET_ALL + +INDENT1 = " " * 3 +INDENT2 = " " * 7 def _directory_is_git_repo(directory_path): """Check if a directory is a git repository.""" @@ -29,17 +32,25 @@ def _directory_is_git_repo(directory_path): def _update_repository(repo_path, repo_name): """Update a single git repository by pulling from the remote.""" - out(1, bold(repo_name) + ":") + 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) + 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") except subprocess.CalledProcessError: - out(2, red("Error: ") + "cannot fetch; do you have a remote " \ - "repository configured correctly?") + print(INDENT2, RED + "Error:" + RESET, "cannot fetch;", + "do you have a remote repository configured correctly?") return try: @@ -48,24 +59,27 @@ def _update_repository(repo_path, repo_name): last_commit = "never" # Couldn't get a log, so no commits if not dry_fetch: # No new changes to pull - out(2, blue("No new changes.") + - " Last commit was {0}.".format(last_commit)) + print(INDENT2, BLUE + "No new changes." + RESET, + "Last commit was {0}.".format(last_commit)) else: # Stuff has happened! - out(2, "There are new changes upstream...") + print(INDENT2, "There are new changes upstream...") status = _exec_shell("git status") if status.endswith("nothing to commit, working directory clean"): - out(2, green("Pulling new changes...")) + print(INDENT2, GREEN + "Pulling new changes...") result = _exec_shell("git pull") - out(2, "The following changes have been made since {0}:".format( - last_commit)) + 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: - out(2, red("Warning: ") + - "you have uncommitted changes in this repository!") - out(2, "Ignoring.") + print(INDENT2, RED + "Warning:" + RESET, + "you have uncommitted changes in this repository!") + print(INDENT2, "Ignoring.") def _update_directory(dir_path, dir_name, is_bookmark=False): """Update a particular directory. @@ -79,25 +93,24 @@ def _update_directory(dir_path, dir_name, is_bookmark=False): dir_type = "bookmark" # Where did we get this directory from? else: dir_type = "directory" - - dir_long_name = "{0} '{1}'".format(dir_type, bold(dir_path)) + dir_long_name = dir_type + ' "' + BOLD + dir_path + RESET + '"' try: os.listdir(dir_path) # Test if we can access this directory except OSError: - out(0, red("Error: ") + - "cannot enter {0}; does it exist?".format(dir_long_name)) + 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): - out(0, red("Error: ") + dir_long_name + " is not a directory!") + print(RED + "Error:" + RESET, dir_long_name, "is not a directory!") else: - out(0, red("Error: ") + dir_long_name + " does not exist!") + print(RED + "Error:" + RESET, dir_long_name, "does not exist!") return if _directory_is_git_repo(dir_path): - out(0, dir_long_name.capitalize() + " is a git repository:") + print(dir_long_name.capitalize(), "is a git repository:") _update_repository(dir_path, dir_name) else: @@ -112,10 +125,10 @@ def _update_directory(dir_path, dir_name, is_bookmark=False): num_of_repos = len(repositories) if num_of_repos == 1: - out(0, dir_long_name.capitalize() + " contains 1 git repository:") + print(dir_long_name.capitalize(), "contains 1 git repository:") else: - out(0, dir_long_name.capitalize() + - " contains {0} git repositories:".format(num_of_repos)) + 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: @@ -127,8 +140,7 @@ def update_bookmarks(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'.") + 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.""" diff --git a/setup.py b/setup.py index 3d7b348..4a907ad 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name = "gitup", packages = find_packages(), entry_points = {"console_scripts": ["gitup = gitup.script:run"]}, - install_requires = ["GitPython >= 0.3.2.RC1"], + install_requires = ["GitPython >= 0.3.2.RC1", "colorama >= 0.2.7"], version = __version__, author = "Ben Kurtovic", author_email = "ben.kurtovic@gmail.com", From 30d01fd200d30a709099eaf17e7391e8c53e66b0 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 23 Mar 2014 04:45:19 -0400 Subject: [PATCH 16/34] Remove relative imports from __init__ so __version__ can be read without deps. --- gitup/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/gitup/__init__.py b/gitup/__init__.py index 7482a7c..f82076a 100644 --- a/gitup/__init__.py +++ b/gitup/__init__.py @@ -12,5 +12,3 @@ __copyright__ = "Copyright (C) 2011-2014 Ben Kurtovic" __license__ = "MIT License" __version__ = "0.2.dev" __email__ = "ben.kurtovic@gmail.com" - -from . import config, script, update From 48e82888f5f1eb5afb26f3899e2b0c81f592dfd0 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Wed, 26 Mar 2014 10:42:29 -0400 Subject: [PATCH 17/34] 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 18/34] 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 19/34] 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 20/34] 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 21/34] 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 22/34] 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 23/34] 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 24/34] 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 25/34] 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 26/34] 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 27/34] 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 28/34] 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 29/34] 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 30/34] 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 31/34] 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 32/34] 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 33/34] 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") From 845156c4a21c3559c42fa8e2813350397095ef3f Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Mon, 21 Apr 2014 01:32:58 -0400 Subject: [PATCH 34/34] release/0.2 --- gitup/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitup/__init__.py b/gitup/__init__.py index f82076a..a361042 100644 --- a/gitup/__init__.py +++ b/gitup/__init__.py @@ -10,5 +10,5 @@ gitup: the git repository updater __author__ = "Ben Kurtovic" __copyright__ = "Copyright (C) 2011-2014 Ben Kurtovic" __license__ = "MIT License" -__version__ = "0.2.dev" +__version__ = "0.2" __email__ = "ben.kurtovic@gmail.com"