From ee92fc6cbd71c640f037ea4bb0da47e3b9014aa4 Mon Sep 17 00:00:00 2001 From: David Aguilar Date: Fri, 9 May 2008 03:50:53 -0700 Subject: [PATCH 1/7] Initial checkin of python-git Signed-off-by: David Aguilar --- .gitignore | 4 + debian/changelog | 6 + debian/compat | 1 + debian/control | 12 + debian/copyright | 34 +++ debian/pyversions | 1 + debian/rules | 47 ++++ ez_setup.py | 272 ++++++++++++++++++++++ git/__init__.py | 667 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 15 ++ 10 files changed, 1059 insertions(+) create mode 100644 .gitignore create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/pyversions create mode 100755 debian/rules create mode 100644 ez_setup.py create mode 100644 git/__init__.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3643ad3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/debian/files +/debian/python-git* +/build +*.py[co] diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..206f058 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,6 @@ +python-git (1.0.0-1) unstable; urgency=low + + Initial version, imported the git module from the ugit project: + http://ugit.sf.net/ + + -- David Aguilar Thu, 08 May 2008 12:00:00 -0700 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7ed6ff8 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +5 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..deb9379 --- /dev/null +++ b/debian/control @@ -0,0 +1,12 @@ +Source: python-git +Section: python +Priority: optional +Maintainer: David Aguilar +Build-Depends-Indep: python-support (>= 0.6), debhelper(>= 5), python-all-dev (>= 2.4) +XB-Python-Version: ${python:Versions} +Package: python-git +Architecture: all +Provides: ${python:Provides} +Depends: ${python:Depends}, git-core (>= 1.5) +Description: python-git is a python interface to git. +Standards-Version: 3.7.2 diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..455a2e8 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,34 @@ +This package was debianized by David Aguilar on +Sun, 13 Apr 2008 18:28:41 -0700. + +It was downloaded from http://ugit.sf.net/ + +Upstream Author(s): + + David Aguilar + +Copyright: + + Copyright (C) 2008 David Aguilar + +License: + + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this package; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +On Debian systems, the complete text of the GNU General +Public License can be found in `/usr/share/common-licenses/GPL'. + +The Debian packaging is (C) 2008, David Aguilar and +is licensed under the GPL, see above. diff --git a/debian/pyversions b/debian/pyversions new file mode 100644 index 0000000..8b253bc --- /dev/null +++ b/debian/pyversions @@ -0,0 +1 @@ +2.4- diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..c9bdb3c --- /dev/null +++ b/debian/rules @@ -0,0 +1,47 @@ +#!/usr/bin/make -f +# -*- makefile -*- +#export DH_VERBOSE=1 + +build: build-stamp + +build-stamp: + dh_testdir + python setup.py build + touch build-stamp + +clean: + dh_testdir + dh_testroot + rm -f build-stamp + rm -rf build + -find . -name '*.py[co]' | xargs rm -f + dh_clean + +install: + dh_testdir + dh_testroot + dh_clean -k + python setup.py install \ + --root=$(CURDIR)/debian/python-git + +# Build architecture-independent files here. +binary-indep: build install + dh_testdir + dh_testroot +# dh_installchangelogs CHANGELOG +# dh_installdocs README +# dh_installexamples examples/* +# dh_installman +# use debian's python-support infrastructure + dh_pysupport + dh_fixperms + dh_installdeb + dh_gencontrol + dh_md5sums + dh_builddeb + +# Build architecture-dependent files here. +binary: binary-indep +# We have nothing to do by default. + +.PHONY: build clean binary-indep binary-arch binary install configure diff --git a/ez_setup.py b/ez_setup.py new file mode 100644 index 0000000..89cf056 --- /dev/null +++ b/ez_setup.py @@ -0,0 +1,272 @@ +#!python +"""Bootstrap setuptools installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import sys +DEFAULT_VERSION = "0.6c8" +DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] + +md5_data = { + 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', + 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', + 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', + 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', + 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', + 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', + 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', + 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', + 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', + 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', + 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', + 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', + 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', + 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', + 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', + 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', + 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', + 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', + 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', + 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', + 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', + 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', + 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', + 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', + 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', + 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', + 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', + 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', + 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', + 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', +} + +import sys, os + +def _validate_md5(egg_name, data): + if egg_name in md5_data: + from md5 import md5 + digest = md5(data).hexdigest() + if digest != md5_data[egg_name]: + print >>sys.stderr, ( + "md5 validation of %s failed! (Possible download problem?)" + % egg_name + ) + sys.exit(2) + return data + + +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + download_delay=15 +): + """Automatically find/download setuptools and make it available on sys.path + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end with + a '/'). `to_dir` is the directory where setuptools will be downloaded, if + it is not already available. If `download_delay` is specified, it should + be the number of seconds that will be paused before initiating a download, + should one be required. If an older version of setuptools is installed, + this routine will print a message to ``sys.stderr`` and raise SystemExit in + an attempt to abort the calling script. + """ + was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules + def do_download(): + egg = download_setuptools(version, download_base, to_dir, download_delay) + sys.path.insert(0, egg) + import setuptools; setuptools.bootstrap_install_from = egg + try: + import pkg_resources + except ImportError: + return do_download() + try: + pkg_resources.require("setuptools>="+version); return + except pkg_resources.VersionConflict, e: + if was_imported: + print >>sys.stderr, ( + "The required version of setuptools (>=%s) is not available, and\n" + "can't be installed while this script is running. Please install\n" + " a more recent version first, using 'easy_install -U setuptools'." + "\n\n(Currently using %r)" + ) % (version, e.args[0]) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return do_download() + except pkg_resources.DistributionNotFound: + return do_download() + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + delay = 15 +): + """Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download attempt. + """ + import urllib2, shutil + egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) + url = download_base + egg_name + saveto = os.path.join(to_dir, egg_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + from distutils import log + if delay: + log.warn(""" +--------------------------------------------------------------------------- +This script requires setuptools version %s to run (even to display +help). I will attempt to download it for you (from +%s), but +you may need to enable firewall access for this script first. +I will start the download in %d seconds. + +(Note: if this machine does not have network access, please obtain the file + + %s + +and place it in this directory before rerunning this script.) +---------------------------------------------------------------------------""", + version, download_base, delay, url + ); from time import sleep; sleep(delay) + log.warn("Downloading %s", url) + src = urllib2.urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = _validate_md5(egg_name, src.read()) + dst = open(saveto,"wb"); dst.write(data) + finally: + if src: src.close() + if dst: dst.close() + return os.path.realpath(saveto) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + try: + import setuptools + except ImportError: + egg = None + try: + egg = download_setuptools(version, delay=0) + sys.path.insert(0,egg) + from setuptools.command.easy_install import main + return main(list(argv)+[egg]) # we're done here + finally: + if egg and os.path.exists(egg): + os.unlink(egg) + else: + if setuptools.__version__ == '0.0.1': + print >>sys.stderr, ( + "You have an obsolete version of setuptools installed. Please\n" + "remove it from your system entirely before rerunning this script." + ) + sys.exit(2) + + req = "setuptools>="+version + import pkg_resources + try: + pkg_resources.require(req) + except pkg_resources.VersionConflict: + try: + from setuptools.command.easy_install import main + except ImportError: + from easy_install import main + main(list(argv)+[download_setuptools(delay=0)]) + sys.exit(0) # try to force an exit + else: + if argv: + from setuptools.command.easy_install import main + main(argv) + else: + print "Setuptools version",version,"or greater has been installed." + print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' + +def update_md5(filenames): + """Update our built-in md5 registry""" + + import re + from md5 import md5 + + for name in filenames: + base = os.path.basename(name) + f = open(name,'rb') + md5_data[base] = md5(f.read()).hexdigest() + f.close() + + data = [" %r: %r,\n" % it for it in md5_data.items()] + data.sort() + repl = "".join(data) + + import inspect + srcfile = inspect.getsourcefile(sys.modules[__name__]) + f = open(srcfile, 'rb'); src = f.read(); f.close() + + match = re.search("\nmd5_data = {\n([^}]+)}", src) + if not match: + print >>sys.stderr, "Internal error!" + sys.exit(2) + + src = src[:match.start(1)] + repl + src[match.end(1):] + f = open(srcfile,'w') + f.write(src) + f.close() + + +if __name__=='__main__': + if len(sys.argv)>2 and sys.argv[1]=='--md5update': + update_md5(sys.argv[2:]) + else: + main(sys.argv[1:]) + + + + + diff --git a/git/__init__.py b/git/__init__.py new file mode 100644 index 0000000..19b0568 --- /dev/null +++ b/git/__init__.py @@ -0,0 +1,667 @@ +"""python-git: Copyright (c) 2008 David Aguilar + +The git module is a wrapper around the git command line interface. +Any git command can be called by simply calling a function of +the same name as the git command. + +The command's output is returned in a string. + +Passing the optional with_status=True keyword +causes the function to returns a tuple of (status, output) instead. + +To specify a dashed option, such as "--index", pass the name +of the option as a keyword parameter set to true. + +To specify options with dashes in their name, use +underscores. For example, --patch-with-raw becomes patch_with_raw. +This applies to the command names as well. + + import git + + print git.rev_parse('HEAD') + print git.diff('--', 'foo', cached=True) + print git.commit('foo', F='COMMIT_MSG', s=True) + print git.log('HEAD^..HEAD', patch_with_raw=True, abbrev=4) + +Results in the following commands being executed: + + [ 'git', 'rev-parse', 'HEAD' ] + [ 'git', 'diff', '--cached', '--', 'foo' ] + [ 'git', 'commit', '-FCOMMIT_MSG', '-s', 'foo' ] + [ 'git', 'log', '--abbrev=4', '--patch-with-raw', 'HEAD^..HEAD' ] + +Example: + +>>> git.foo(with_status=True) +(1, "git: 'foo' is not a git-command. See 'git --help'.") + + +The strings returned by this module strip off trailing whitespace by +default since it simplifies command usage. To avoid that pass +raw=True into any git. function. + +All git functions specified in the split string list below are present +in the git.commands dictionary at import time. Any that are not +specified are added dynamically as modules import or use them. + +For example: + import git + print git.foo() + +Is perfectly valid but will raise an exception since git will return +exit status 1. To not raise exceptions, users can set +git.RAISE_EXCEPTIONS=False or pass allow_errors=True to an +individual command: + + print git.foo(allow_errors=True) + +would continue and print the standard git error message: + +git: 'foo' is not a git-command. See 'git --help'. + +Had git actually found a git-foo command, then its output would have +been returned instead (as expected). + +This is to allow seamless upgradeability with future as well as +seamless integration with custom, user-defined git commands. + +What this means is that the list of git commands included in this +file is not strictly required for the proper execution of this module. +We include the list, though, to provide convenience for users who +use the python console's tab completion facilities. + +""" + +import os +import re +import sys +import time +import types +import subprocess + +from cStringIO import StringIO + +DIFF_CONTEXT = 3 +RAISE_EXCEPTIONS = True + +def set_diff_context(ctxt): + global DIFF_CONTEXT + DIFF_CONTEXT = ctxt + +def get_tmp_filename(): + # Allow TMPDIR/TMP with a fallback to /tmp + env = os.environ + return os.path.join(env.get('TMP', env.get('TMPDIR', '/tmp')), + '.git.%s.%s' % ( os.getpid(), time.time())) + +def run_cmd(cmd, *args, **kwargs): + """ + Returns an array of strings from the command's output. + + DEFAULTS: + raw=False + + Passing raw=True prevents the output from being striped. + + with_status = False + + Passing with_status=True returns tuple(status,output) + instead of just the command's output. + + run_command("git foo", bar, buzz, + baz=value, bar=True, q=True, f='foo') + + Implies: + argv = ["git", "foo", + "-q", "-ffoo", + "--bar", "--baz=value", + "bar","buzz" ] + """ + + def pop_key(d, key): + val = d.get(key) + try: del d[key] + except: pass + return val + raw = pop_key(kwargs, 'raw') + allow_errors = pop_key(kwargs, 'allow_errors') + with_status = pop_key(kwargs,'with_status') + with_stderr = not pop_key(kwargs,'without_stderr') + cwd = os.getcwd() + + if not RAISE_EXCEPTIONS: + allow_errors = True + + kwarglist = [] + for k,v in kwargs.iteritems(): + if len(k) > 1: + k = k.replace('_','-') + if v is True: + kwarglist.append("--%s" % k) + elif v is not None and type(v) is not bool: + kwarglist.append("--%s=%s" % (k,v)) + else: + if v is True: + kwarglist.append("-%s" % k) + elif v is not None and type(v) is not bool: + kwarglist.append("-%s" % k) + kwarglist.append(str(v)) + # Handle cmd as either a string or an argv list + if type(cmd) is str: + # we only call run_cmd(str) with str='git command' + # or other simple commands + cmd = cmd.split(' ') + cmd += kwarglist + cmd += tuple(args) + else: + cmd = tuple(cmd + kwarglist + list(args)) + + stderr = None + if with_stderr: + stderr = subprocess.STDOUT + # start the process + proc = subprocess.Popen(cmd, cwd=cwd, + stdout=subprocess.PIPE, + stderr=stderr, + stdin=None) + # Wait for the process to return + output, err = proc.communicate() + err = proc.poll() + # conveniently strip off trailing newlines + if not raw: + output = output.rstrip() + + if with_status: + return (err, output) + else: + if err and not allow_errors: + raise RuntimeError("%s return exit status %d" + % ( str(cmd), err )) + return output + +# union of functions in this file and dynamic functions +# defined in the git command string list below +def git(*args,**kwargs): + """This is a convenience wrapper around run_cmd that + sets things up so that commands are run in the canonical + 'git command [options] [args]' form.""" + cmd = 'git %s' % args[0] + return run_cmd(cmd, *args[1:], **kwargs) + +class GitCommand(object): + """This class wraps this module so that arbitrary git commands + can be dynamically called at runtime.""" + def __init__(self, module): + self.module = module + self.commands = {} + # This creates git.foo() methods dynamically for each of the + # following names at import-time. + for cmd in """ + add + am + annotate + apply + archive + archive_recursive + bisect + blame + branch + bundle + checkout + checkout_index + cherry + cherry_pick + citool + clean + clone + commit + config + count_objects + describe + diff + fast_export + fetch + filter_branch + format_patch + fsck + gc + get_tar_commit_id + grep + gui + hard_repack + imap_send + init + instaweb + log + lost_found + ls_files + ls_remote + ls_tree + merge + mergetool + mv + name_rev + pull + push + read_tree + rebase + relink + remote + repack + request_pull + reset + revert + rev_list + rm + send_email + shortlog + show + show_branch + show_ref + stash + status + submodule + svn + tag + var + verify_pack + whatchanged + """.split(): getattr(self, cmd) + + def setup_commands(self): + # Import the functions from the module + for name, val in self.module.__dict__.iteritems(): + if type(val) is types.FunctionType: + setattr(self, name, val) + # Import dynamic functions and those from the module + # functions into self.commands + for name, val in self.__dict__.iteritems(): + if type(val) is types.FunctionType: + self.commands[name] = val + + def __getattr__(self, cmd): + if hasattr(self.module, cmd): + value = getattr(self.module, cmd) + setattr(self, cmd, value) + return value + def git_cmd(*args, **kwargs): + """Runs "git [options] [args]" + The output is returned as a string. + Pass with_stauts=True to merge stderr's into stdout. + Pass raw=True to avoid stripping git's output. + Finally, pass with_status=True to + return a (status, output) tuple.""" + return git(cmd.replace('_','-'), *args, **kwargs) + setattr(self, cmd, git_cmd) + return git_cmd + +# core git wrapper for use in this module +gitcmd = GitCommand(sys.modules[__name__]) +sys.modules[__name__] = gitcmd + +#+------------------------------------------------------------------------- +#+ A regex for matching the output of git(log|rev-list) --pretty=oneline +REV_LIST_REGEX = re.compile('([0-9a-f]+)\W(.*)') + +def abort_merge(): + # Reset the worktree + output = gitcmd.read_tree("HEAD", reset=True, u=True, v=True) + # remove MERGE_HEAD + merge_head = git_repo_path('MERGE_HEAD') + if os.path.exists(merge_head): + os.unlink(merge_head) + # remove MERGE_MESSAGE, etc. + merge_msg_path = get_merge_message_path() + while merge_msg_path is not None: + os.unlink(merge_msg_path) + merge_msg_path = get_merge_message_path() + +def add_or_remove(*to_process): + """Invokes 'git add' to index the filenames in to_process that exist + and 'git rm' for those that do not exist.""" + + if not to_process: + return 'No files to add or remove.' + + to_add = [] + to_remove = [] + + for filename in to_process: + if os.path.exists(filename): + to_add.append(filename) + + output = gitcmd.add(verbose=True, *to_add) + + if len(to_add) == len(to_process): + # to_process only contained unremoved files -- + # short-circuit the removal checks + return output + + # Process files to remote + for filename in to_process: + if not os.path.exists(filename): + to_remove.append(filename) + output + '\n\n' + gitcmd.rm(*to_remove) + +def branch_list(remote=False): + branches = map(lambda x: x.lstrip('* '), + gitcmd.branch(r=remote).splitlines()) + if remote: + remotes = [] + for branch in branches: + if branch.endswith('/HEAD'): + continue + remotes.append(branch) + return remotes + return branches + +def cherry_pick_list(revs, **kwargs): + """Cherry-picks each revision into the current branch. + Returns a list of command output strings (1 per cherry pick)""" + if not revs: + return [] + cherries = [] + for rev in revs: + cherries.append(gitcmd.cherry_pick(rev, **kwargs)) + return '\n'.join(cherries) + +def commit_with_msg(msg, amend=False): + """Creates a git commit.""" + + if not msg.endswith('\n'): + msg += '\n' + # Sure, this is a potential "security risk," but if someone + # is trying to intercept/re-write commit messages on your system, + # then you probably have bigger problems to worry about. + tmpfile = get_tmp_filename() + kwargs = { + 'F': tmpfile, + 'amend': amend, + } + # Create the commit message file + file = open(tmpfile, 'w') + file.write(msg) + file.close() + + # Run 'git commit' + output = gitcmd.commit(F=tmpfile, amend=amend) + os.unlink(tmpfile) + + return ('git commit -F %s --amend %s\n\n%s' + % ( tmpfile, amend, output )) + +def create_branch(name, base, track=False): + """Creates a branch starting from base. Pass track=True + to create a remote tracking branch.""" + return gitcmd.branch(name, base, track=track) + +def current_branch(): + """Parses 'git branch' to find the current branch.""" + + branches = gitcmd.branch().splitlines() + for branch in branches: + if branch.startswith('* '): + return branch.lstrip('* ') + return 'Detached HEAD' + +def diff_helper(commit=None, + filename=None, + color=False, + cached=True, + with_diff_header=False, + suppress_header=True, + reverse=False): + "Invokes git diff on a filepath." + + argv = [] + if commit: + argv.append('%s^..%s' % (commit, commit)) + + if filename: + argv.append('--') + if type(filename) is list: + argv.extend(filename) + else: + argv.append(filename) + + diff = gitcmd.diff( + R=reverse, + color=color, + cached=cached, + patch_with_raw=True, + unified=DIFF_CONTEXT, + *argv + ).splitlines() + + output = StringIO() + start = False + del_tag = 'deleted file mode ' + + headers = [] + deleted = cached and not os.path.exists(filename) + for line in diff: + if not start and '@@ ' in line and ' @@' in line: + start = True + if start or(deleted and del_tag in line): + output.write(line + '\n') + else: + if with_diff_header: + headers.append(line) + elif not suppress_header: + output.write(line + '\n') + result = output.getvalue() + output.close() + if with_diff_header: + return('\n'.join(headers), result) + else: + return result + +def diffstat(): + return gitcmd.diff( + 'HEAD^', + unified=DIFF_CONTEXT, + stat=True) + +def diffindex(): + return gitcmd.diff( + unified=DIFF_CONTEXT, + stat=True, + cached=True) + +def format_patch_helper(output='patches', *revs): + """writes patches named by revs to the output directory.""" + num_patches = 1 + output = [] + for idx, rev in enumerate(revs): + real_idx = idx + num_patches + revarg = '%s^..%s' % (rev,rev) + output.append( + gitcmd.format_patch( + revarg, + o=output, + start_number=real_idx, + n=len(revs) > 1, + thread=True, + patch_with_stat=True + ) + ) + num_patches += output[-1].count('\n') + return '\n'.join(output) + +def get_merge_message(): + return gitcmd.fmt_merge_msg('--file', git_repo_path('FETCH_HEAD')) + +def config_set(key=None, value=None, local=True): + if key and value is not None: + # git config category.key value + strval = str(value) + if type(value) is bool: + # git uses "true" and "false" + strval = strval.lower() + if local: + argv = [ key, strval ] + else: + argv = [ '--global', key, strval ] + return gitcmd.config(*argv) + else: + msg = "oops in git.config_set(key=%s,value=%s,local=%s" + raise Exception(msg % (key, value, local)) + +def config_dict(local=True): + if local: + argv = [ '--list' ] + else: + argv = ['--global', '--list' ] + return config_to_dict( + gitcmd.config(*argv).splitlines()) + +def config_to_dict(config_lines): + """parses the lines from git config --list into a dictionary""" + + newdict = {} + for line in config_lines: + k, v = line.split('=', 1) + k = k.replace('.','_') # git -> model + if v == 'true' or v == 'false': + v = bool(eval(v.title())) + try: + v = int(eval(v)) + except: + pass + newdict[k]=v + return newdict + +def log_helper(all=False): + """Returns a pair of parallel arrays listing the revision sha1's + and commit summaries.""" + revs = [] + summaries = [] + regex = REV_LIST_REGEX + output = gitcmd.log(pretty='oneline', all=all) + for line in output.splitlines(): + match = regex.match(line) + if match: + revs.append(match.group(1)) + summaries.append(match.group(2)) + return( revs, summaries ) + +def parse_ls_tree(rev): + """Returns a list of(mode, type, sha1, path) tuples.""" + lines = gitcmd.ls_tree(rev, r=True).splitlines() + output = [] + regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$') + for line in lines: + match = regex.match(line) + if match: + mode = match.group(1) + objtype = match.group(2) + sha1 = match.group(3) + filename = match.group(4) + output.append((mode, objtype, sha1, filename,) ) + return output + +def push_helper(remote, local_branch, remote_branch, ffwd=True, tags=False): + if ffwd: + branch_arg = '%s:%s' % ( local_branch, remote_branch ) + else: + branch_arg = '+%s:%s' % ( local_branch, remote_branch ) + return gitcmd.push(remote, branch_arg, with_status=True, tags=tags) + +def remote_url(name): + return gitcmd.config('remote.%s.url' % name, get=True) + +def rev_list_range(start, end): + range = '%s..%s' % ( start, end ) + raw_revs = gitcmd.rev_list(range, pretty='oneline') + return parse_rev_list(raw_revs) + +def git_repo_path(*subpaths): + paths = [ gitcmd.rev_parse(git_dir=True) ] + paths.extend(subpaths) + return os.path.realpath(os.path.join(*paths)) + +def get_merge_message_path(): + for file in ('MERGE_MSG', 'SQUASH_MSG'): + path = git_repo_path(file) + if os.path.exists(path): + return path + return None + +def reset_helper(*args, **kwargs): + return gitcmd.reset('--', *args, **kwargs) + +def parse_rev_list(raw_revs): + revs = [] + for line in raw_revs.splitlines(): + match = REV_LIST_REGEX.match(line) + if match: + rev_id = match.group(1) + summary = match.group(2) + revs.append((rev_id, summary,) ) + return revs + +def parse_status(): + """RETURNS: A tuple of staged, unstaged and untracked file lists. + """ + def eval_path(path): + """handles quoted paths.""" + if path.startswith('"') and path.endswith('"'): + return eval(path) + else: + return path + + MODIFIED_TAG = '# Changed but not updated:' + UNTRACKED_TAG = '# Untracked files:' + RGX_RENAMED = re.compile( + '(#\trenamed:\s+)' + '(.*?)\s->\s(.*)' + ) + RGX_MODIFIED = re.compile( + '(#\tmodified:\s+' + '|#\tnew file:\s+' + '|#\tdeleted:\s+)' + ) + staged = [] + unstaged = [] + untracked = [] + + STAGED_MODE = 0 + UNSTAGED_MODE = 1 + UNTRACKED_MODE = 2 + + current_dest = staged + mode = STAGED_MODE + + for status_line in gitcmd.status().splitlines(): + if status_line == MODIFIED_TAG: + mode = UNSTAGED_MODE + current_dest = unstaged + continue + elif status_line == UNTRACKED_TAG: + mode = UNTRACKED_MODE + current_dest = untracked + continue + # Staged/unstaged modified/renamed/deleted files + if mode is STAGED_MODE or mode is UNSTAGED_MODE: + match = RGX_MODIFIED.match(status_line) + if match: + tag = match.group(0) + filename = status_line.replace(tag, '') + current_dest.append(eval_path(filename)) + continue + match = RGX_RENAMED.match(status_line) + if match: + oldname = match.group(2) + newname = match.group(3) + current_dest.append(eval_path(oldname)) + current_dest.append(eval_path(newname)) + continue + # Untracked files + elif mode is UNTRACKED_MODE: + if status_line.startswith('#\t'): + current_dest.append(eval_path(status_line[2:])) + + return( staged, unstaged, untracked ) + +# Must be executed after all functions are defined +gitcmd.setup_commands() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c610fbd --- /dev/null +++ b/setup.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +from ez_setup import use_setuptools +use_setuptools() +from setuptools import setup + +setup (name = "python-git", + description="A python git interface", + version="1.0", + author="David Aguilar", + author_email="davvid@gmail.com", + url="http://ugit.sf.net/", + packages=['git'], + license="Python", + long_description="python-git is an interface to git, a distributed version control system.", +) -- 2.11.4.GIT From 80108e27c1124028a64bbe93d2fbd256ede44a95 Mon Sep 17 00:00:00 2001 From: David Aguilar Date: Fri, 9 May 2008 04:40:24 -0700 Subject: [PATCH 2/7] build: add a convenient Makefile The Makefile has the standard targets: clean all install uninstall Signed-off-by: David Aguilar --- Makefile | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d859ca1 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +#! /usr/bin/make -f + +prefix=/usr/local +PYTHON=python2.5 + +PYTHONPATH=$(prefix)/lib/$(PYTHON)/site-packages + +all: + $(PYTHON) setup.py build + +install: + mkdir -p $(prefix)/lib/$(PYTHON)/site-packages + $(PYTHON) setup.py install --prefix=$(prefix) + +uninstall: + rm -rf $(prefix) + +clean: + rm -rf dist build python_git.egg-info + find . -name '*.py[co]' | xargs rm -f + +release: + $(PYTHON) setup.py egg_info -RDb "" sdist bdist_egg register upload -- 2.11.4.GIT From 043147a410c13350a744f3877624529341357648 Mon Sep 17 00:00:00 2001 From: David Aguilar Date: Fri, 9 May 2008 04:41:05 -0700 Subject: [PATCH 3/7] setup.py: add automatic versioning We now pull our version numbers straight from git tags and commit histories. Signed-off-by: David Aguilar --- scripts/gitversion.sh | 13 +++++++++++++ setup.py | 44 +++++++++++++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 15 deletions(-) create mode 100755 scripts/gitversion.sh rewrite setup.py (87%) diff --git a/scripts/gitversion.sh b/scripts/gitversion.sh new file mode 100755 index 0000000..de899eb --- /dev/null +++ b/scripts/gitversion.sh @@ -0,0 +1,13 @@ +#!/bin/sh +VN=$(git describe HEAD 2>/dev/null) +VN=$(echo "$VN" | sed -e 's/-/./g') +LF=' +' +case "$VN" in +*$LF*) (exit 1) ;; +v[0-9]*) + test -z "$(git diff-index --name-only HEAD)" || + VN="$VN-dirty" ;; +esac +VN=$(expr "$VN" : v*'\(.*\)') +echo "$VN" diff --git a/setup.py b/setup.py dissimilarity index 87% index c610fbd..0da615b 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,29 @@ -#!/usr/bin/env python -from ez_setup import use_setuptools -use_setuptools() -from setuptools import setup - -setup (name = "python-git", - description="A python git interface", - version="1.0", - author="David Aguilar", - author_email="davvid@gmail.com", - url="http://ugit.sf.net/", - packages=['git'], - license="Python", - long_description="python-git is an interface to git, a distributed version control system.", -) +#!/usr/bin/env python +import ez_setup +ez_setup.use_setuptools() + +from setuptools import setup + +import os +from os.path import join + +# Release versioning +def get_version(): + """Runs version.sh and returns the output.""" + cmd = join(os.path.dirname(__file__), 'scripts', 'gitversion.sh') + pipe = os.popen(cmd) + version = pipe.read() + pipe.close() + return version.strip() + +setup( + name = "python-git", + description="A python git interface", + version=get_version(), + author="David Aguilar", + author_email="davvid@gmail.com", + url="http://ugit.sf.net/", + packages=['git'], + license="Python", + long_description="python-git is an interface to git, a distributed version control system.", +) -- 2.11.4.GIT From e49d49a6289c451f2d94aa9219e7b569e797196f Mon Sep 17 00:00:00 2001 From: David Aguilar Date: Fri, 9 May 2008 05:03:08 -0700 Subject: [PATCH 4/7] build: add readme, install and manifest for setuptools Short README.txt and INSTALL.txt files were added. A MANIFEST.in file was added to make proper source releases. Signed-off-by: David Aguilar --- INSTALL.txt | 10 +++++++++ MANIFEST.in | 10 +++++++++ README.txt | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 INSTALL.txt create mode 100644 MANIFEST.in create mode 100644 README.txt diff --git a/INSTALL.txt b/INSTALL.txt new file mode 100644 index 0000000..b9869aa --- /dev/null +++ b/INSTALL.txt @@ -0,0 +1,10 @@ + +python setup.py build +python setup.py --prefix=/usr install + + +ALTERNATIVELY: + +make +make prefix=/usr install + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..aad8705 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,10 @@ +include *.txt +include ez_setup.py +recursive-include *.py +prune .gitignore +include debian/rules +include debian/control +include debian/changelog +include debian/copyright +include debian/pyversions +include debian/compat diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..41f8741 --- /dev/null +++ b/README.txt @@ -0,0 +1,71 @@ +python-git: Copyright (c) 2008 David Aguilar + +The git module is a wrapper around the git command line interface. +Any git command can be called by simply calling a function of +the same name as the git command. + +The command's output is returned in a string. + +Passing the optional with_status=True keyword +causes the function to returns a tuple of (status, output) instead. + +To specify a dashed option, such as "--index", pass the name +of the option as a keyword parameter set to true. + +To specify options with dashes in their name, use +underscores. For example, --patch-with-raw becomes patch_with_raw. +This applies to the command names as well. + + import git + + print git.rev_parse('HEAD') + print git.diff('--', 'foo', cached=True) + print git.commit('foo', F='COMMIT_MSG', s=True) + print git.log('HEAD^..HEAD', patch_with_raw=True, abbrev=4) + +Results in the following commands being executed: + + [ 'git', 'rev-parse', 'HEAD' ] + [ 'git', 'diff', '--cached', '--', 'foo' ] + [ 'git', 'commit', '-FCOMMIT_MSG', '-s', 'foo' ] + [ 'git', 'log', '--abbrev=4', '--patch-with-raw', 'HEAD^..HEAD' ] + +Example: + +>>> git.foo(with_status=True) +(1, "git: 'foo' is not a git-command. See 'git --help'.") + + +The strings returned by this module strip off trailing whitespace by +default since it simplifies command usage. To avoid that pass +raw=True into any git. function. + +All git functions specified in the split string list below are present +in the git.commands dictionary at import time. Any that are not +specified are added dynamically as modules import or use them. + +For example: + import git + print git.foo() + +Is perfectly valid but will raise an exception since git will return +exit status 1. To not raise exceptions, users can set +git.RAISE_EXCEPTIONS=False or pass allow_errors=True to an +individual command: + + print git.foo(allow_errors=True) + +would continue and print the standard git error message: + +git: 'foo' is not a git-command. See 'git --help'. + +Had git actually found a git-foo command, then its output would have +been returned instead (as expected). + +This is to allow seamless upgradeability with future as well as +seamless integration with custom, user-defined git commands. + +What this means is that the list of git commands included in this +file is not strictly required for the proper execution of this module. +We include the list, though, to provide convenience for users who +use the python console's tab completion facilities. -- 2.11.4.GIT From 8ce26ea8ab5e736d4c3f5107bb07de271fcfd504 Mon Sep 17 00:00:00 2001 From: David Aguilar Date: Fri, 9 May 2008 05:09:53 -0700 Subject: [PATCH 5/7] updated .gitignores Signed-off-by: David Aguilar --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 3643ad3..b4fc1aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /debian/files /debian/python-git* /build +/dist +/python_git.egg-info *.py[co] -- 2.11.4.GIT From 41d790b9aab60fc8f580eec8ace8cdda04167ba4 Mon Sep 17 00:00:00 2001 From: David Aguilar Date: Sat, 17 May 2008 13:08:35 -0700 Subject: [PATCH 6/7] format_patch_helper: fix reuse of output variable The output variable was being reused due to a recent rename. This fixes that. Signed-off-by: David Aguilar --- git/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/git/__init__.py b/git/__init__.py index 19b0568..953ec62 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -471,11 +471,11 @@ def diffindex(): def format_patch_helper(output='patches', *revs): """writes patches named by revs to the output directory.""" num_patches = 1 - output = [] + lines = [] for idx, rev in enumerate(revs): real_idx = idx + num_patches revarg = '%s^..%s' % (rev,rev) - output.append( + lines.append( gitcmd.format_patch( revarg, o=output, @@ -485,8 +485,8 @@ def format_patch_helper(output='patches', *revs): patch_with_stat=True ) ) - num_patches += output[-1].count('\n') - return '\n'.join(output) + num_patches += lines[-1].count('\n') + return '\n'.join(lines) def get_merge_message(): return gitcmd.fmt_merge_msg('--file', git_repo_path('FETCH_HEAD')) -- 2.11.4.GIT From add56509794285066d5ac1f52ac72f8e077380e7 Mon Sep 17 00:00:00 2001 From: David Aguilar Date: Sat, 17 May 2008 13:26:05 -0700 Subject: [PATCH 7/7] format_patch_helper: make "output" a keyword arg This fixes git.format_patch_helper(output="foo", *revs) Signed-off-by: David Aguilar --- git/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/git/__init__.py b/git/__init__.py index 953ec62..f6a4d37 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -468,8 +468,9 @@ def diffindex(): stat=True, cached=True) -def format_patch_helper(output='patches', *revs): +def format_patch_helper(*revs, **kwargs): """writes patches named by revs to the output directory.""" + output = kwargs.get("output", "patches") num_patches = 1 lines = [] for idx, rev in enumerate(revs): -- 2.11.4.GIT