# 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 # =True => -- # ='' => --= # Special keyword arguments: # _quiet: Discard all output even if an error occurs # _interactive: Don't capture stdout and stderr # _input=: Feed to stdinin of the command # _outfile= 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.(...) 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 RuntimeException("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 RuntimeException("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