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)