Переглянути джерело

Merge branch 'feature/improvements' into develop

tags/v0.2
Ben Kurtovic 10 роки тому
джерело
коміт
9f8072c673
9 змінених файлів з 383 додано та 349 видалено
  1. +7
    -3
      .gitignore
  2. +1
    -1
      LICENSE
  3. +2
    -2
      README.md
  4. +0
    -312
      gitup.py
  5. +14
    -0
      gitup/__init__.py
  6. +104
    -0
      gitup/config.py
  7. +78
    -0
      gitup/script.py
  8. +150
    -0
      gitup/update.py
  9. +27
    -31
      setup.py

+ 7
- 3
.gitignore Переглянути файл

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

+ 1
- 1
LICENSE Переглянути файл

@@ -1,4 +1,4 @@
Copyright (c) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>
Copyright (C) 2011-2014 Ben Kurtovic <ben.kurtovic@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal


+ 2
- 2
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`).

+ 0
- 312
gitup.py Переглянути файл

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

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

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

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

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 "{}{}{}".format(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("\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) + ":")

os.chdir(repo_path) # cd into our folder so git commands target the correct
# repo

try:
dry_fetch = exec_shell("git fetch --dry-run") # check if there is
# anything to pull, but
# don't do it yet
except subprocess.CalledProcessError:
out(2, 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!")
out(2, "Ignoring.")

def update_directory(dir_path, dir_name, is_bookmark=False):
"""First, make sure the specified object is actually a directory, then
determine whether the directory is a git repo on its own or a directory
of git repositories. If the former, update the single repository; if the
latter, update all repositories contained within."""
if is_bookmark:
dir_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)

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

def update_bookmarks():
"""Loop through and update all bookmarks."""
try:
bookmarks = load_config_file().items("bookmarks")
except configparser.NoSectionError:
bookmarks = []

if bookmarks:
for bookmark_path, bookmark_name in bookmarks:
update_directory(bookmark_path, bookmark_name, is_bookmark=True)
else:
out(0, "You don't have any bookmarks configured! Get help with " +
"'gitup -h'.")

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

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

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

out(0, 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, "'{}' 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, "'{}' 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.")

def main():
"""Parse arguments and then call the appropriate function(s)."""
parser = argparse.ArgumentParser(description="""Easily pull to multiple git
repositories at once.""", epilog="""Both relative and absolute
paths are accepted by all arguments. Questions? Comments? Email the
author at {}.""".format(__email__), add_help=False)

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

group_u.add_argument('directories_to_update', nargs="*", metavar="path",
help="""update all repositories in this directory (or the directory
itself, if it is a repo)""")

group_u.add_argument('-u', '--update', action="store_true", help="""update
all bookmarks (default behavior when called without arguments)""")

group_b.add_argument('-a', '--add', dest="bookmarks_to_add", nargs="+",
metavar="path", help="add directory(s) as bookmarks")

group_b.add_argument('-d', '--delete', dest="bookmarks_to_del", nargs="+",
metavar="path",
help="delete bookmark(s) (leaves actual directories alone)")

group_b.add_argument('-l', '--list', dest="list_bookmarks",
action="store_true", help="list current bookmarks")

group_m.add_argument('-h', '--help', action="help",
help="show this help message and exit")

group_m.add_argument('-v', '--version', action="version",
version="gitup version "+__version__)

args = parser.parse_args()

print 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 __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
out(0, "Stopped by user.")

+ 14
- 0
gitup/__init__.py Переглянути файл

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

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

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

+ 104
- 0
gitup/config.py Переглянути файл

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

from __future__ import print_function

import ConfigParser as configparser
import os

from colorama import Fore, Style

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

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

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

INDENT1 = " " * 3

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

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

def get_bookmarks():
"""Get a list of all bookmarks, or an empty list if there are none."""
config = _load_config_file()
try:
return config.items("bookmarks")
except configparser.NoSectionError:
return []

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

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

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

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

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

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

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

+ 78
- 0
gitup/script.py Переглянути файл

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

from __future__ import print_function

import argparse

from colorama import init as color_init, Style

from . import __version__, __email__
from .config import (get_bookmarks, add_bookmarks, delete_bookmarks,
list_bookmarks)
from .update import update_bookmarks, update_directories

def main():
"""Parse arguments and then call the appropriate function(s)."""
parser = argparse.ArgumentParser(
description="""Easily 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__)

color_init(autoreset=True)
args = parser.parse_args()

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

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(get_bookmarks())

# If they did not tell us to do anything, automatically update bookmarks:
if not any(vars(args).values()):
update_bookmarks(get_bookmarks())

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

+ 150
- 0
gitup/update.py Переглянути файл

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

from __future__ import print_function

import os
import shlex
import subprocess

from colorama import Fore, Style

__all__ = ["update_bookmarks", "update_directories"]

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."""
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."""
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")
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

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_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 = 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!")
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)

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)

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:
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)

+ 27
- 31
setup.py Переглянути файл

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

import sys

from setuptools import setup, find_packages

if sys.hexversion < 0x02070000:
exit("Please upgrade to Python 2.7 or greater: <http://python.org/>.")

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

if os.path.exists("gitup"):
remove_py_extension = False
else:
os.rename("gitup.py", "gitup")
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", "colorama >= 0.2.7"],
version = __version__,
author = "Ben Kurtovic",
author_email = "ben.kurtovic@gmail.com",
description = "Easily pull to multiple git repositories at once.",
long_description = long_desc,
license = "MIT License",
keywords = "git repository pull update",
url = "http://github.com/earwig/git-repo-updater",
classifiers = [
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
@@ -38,9 +38,5 @@ try:
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
"Topic :: Software Development :: Version Control"
]
)

finally:
if remove_py_extension:
os.rename("gitup", "gitup.py") # restore file location
]
)

Завантаження…
Відмінити
Зберегти