git-bin/git.py

212 lines
6.2 KiB
Python

# Utility functions for git
#
# Copyright (C) 2008 Owen Taylor
# Copyright (C) 2009 Red Hat, Inc
#
# This program 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 program 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 program; if not, If not, see
# http://www.gnu.org/licenses/.
#
# (These are adapted from git-bz)
import os
import re
from subprocess import Popen, PIPE
import sys
from util import die
# Clone of subprocess.CalledProcessError (not in Python 2.4)
class CalledProcessError(Exception):
def __init__(self, returncode, cmd):
self.returncode = returncode
self.cmd = cmd
def __str__(self):
return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
NULL_REVISION = "0000000000000000000000000000000000000000"
# Run a git command
# Non-keyword arguments are passed verbatim as command line arguments
# Keyword arguments are turned into command line options
# <name>=True => --<name>
# <name>='<str>' => --<name>=<str>
# Special keyword arguments:
# _quiet: Discard all output even if an error occurs
# _interactive: Don't capture stdout and stderr
# _input=<str>: Feed <str> to stdinin of the command
# _outfile=<file): Use <file> as the output file descriptor
# _split_lines: Return an array with one string per returned line
#
def git_run(command, *args, **kwargs):
to_run = ['git', command.replace("_", "-")]
interactive = False
quiet = False
input = None
interactive = False
outfile = None
do_split_lines = False
for (k,v) in kwargs.iteritems():
if k == '_quiet':
quiet = True
elif k == '_interactive':
interactive = True
elif k == '_input':
input = v
elif k == '_outfile':
outfile = v
elif k == '_split_lines':
do_split_lines = True
elif v is True:
if len(k) == 1:
to_run.append("-" + k)
else:
to_run.append("--" + k.replace("_", "-"))
else:
to_run.append("--" + k.replace("_", "-") + "=" + v)
to_run.extend(args)
if outfile:
stdout = outfile
else:
if interactive:
stdout = None
else:
stdout = PIPE
if interactive:
stderr = None
else:
stderr = PIPE
if input != None:
stdin = PIPE
else:
stdin = None
process = Popen(to_run,
stdout=stdout, stderr=stderr, stdin=stdin)
output, error = process.communicate(input)
if process.returncode != 0:
if not quiet and not interactive:
print >>sys.stderr, error,
print output,
raise CalledProcessError(process.returncode, " ".join(to_run))
if interactive or outfile:
return None
else:
if do_split_lines:
return output.strip().splitlines()
else:
return output.strip()
# Wrapper to allow us to do git.<command>(...) instead of git_run()
class Git:
def __getattr__(self, command):
def f(*args, **kwargs):
return git_run(command, *args, **kwargs)
return f
git = Git()
class GitCommit:
def __init__(self, id, subject):
self.id = id
self.subject = subject
# Takes argument like 'git.rev_list()' and returns a list of commit objects
def rev_list_commits(*args, **kwargs):
kwargs_copy = dict(kwargs)
kwargs_copy['pretty'] = 'format:%s'
kwargs_copy['_split_lines'] = True
lines = git.rev_list(*args, **kwargs_copy)
if (len(lines) % 2 != 0):
raise RuntimeError("git rev-list didn't return an even number of lines")
result = []
for i in xrange(0, len(lines), 2):
m = re.match("commit\s+([A-Fa-f0-9]+)", lines[i])
if not m:
raise RuntimeError("Can't parse commit it '%s'" % lines[i])
commit_id = m.group(1)
subject = lines[i + 1]
result.append(GitCommit(commit_id, subject))
return result
# Loads a single commit object by ID
def load_commit(commit_id):
return rev_list_commits(commit_id + "^!")[0]
# Return True if the commit has multiple parents
def commit_is_merge(commit):
if isinstance(commit, basestring):
commit = load_commit(commit)
parent_count = 0
for line in git.cat_file("commit", commit.id, _split_lines=True):
if line == "":
break
if line.startswith("parent "):
parent_count += 1
return parent_count > 1
# Return a short one-line summary of the commit
def commit_oneline(commit):
if isinstance(commit, basestring):
commit = load_commit(commit)
return commit.id[0:7]+"... " + commit.subject[0:59]
# Return the directory name with .git stripped as a short identifier
# for the module
def get_module_name():
try:
git_dir = git.rev_parse(git_dir=True, _quiet=True)
except CalledProcessError:
die("GIT_DIR not set")
# Use the directory name with .git stripped as a short identifier
absdir = os.path.abspath(git_dir)
if absdir.endswith(os.sep + '.git'):
absdir = os.path.dirname(absdir)
projectshort = os.path.basename(absdir)
if projectshort.endswith(".git"):
projectshort = projectshort[:-4]
return projectshort
# Return the project description or '' if it is 'Unnamed repository;'
def get_project_description():
try:
git_dir = git.rev_parse(git_dir=True, _quiet=True)
except CalledProcessError:
die("GIT_DIR not set")
projectdesc = ''
description = os.path.join(git_dir, 'description')
if os.path.exists(description):
try:
projectdesc = open(description).read().strip()
except:
pass
if projectdesc.startswith('Unnamed repository;'):
projectdesc = ''
return projectdesc