[Buildbot-devel] [PATCH] Better support for git

Haavard Skinnemoen hskinnemoen at atmel.no
Sat Oct 13 14:46:54 UTC 2007


This patch aims to improve the support for git in BuildBot. More
specifically:

  * Use git, not cogito
  * Support checking out or updating to a specific branch
  * Support checking out or updating to a specific revision (SHA1)
  * Implement parseGotRevision() on the slave side
  * Implement computeSourceRevision() on the master side
  * Implement GitExtractor class in tryclient
  * Add testcases
  * Add post-receive hook (contrib/git_buildbot.py)
  * Update documentation

Signed-off-by: Haavard Skinnemoen <hskinnemoen at atmel.com>
---
I've run the test cases and tried it out in practice, and it seems to
work nicely. Please let me know if you spot something stupid.

The tryclient stuff is probably not implemented in the best way
possible, but it should work if you're careful about keeping your
local repository in sync with the upstream repository.

Regards,
Håvard

 buildbot/scripts/tryclient.py |   28 +++++
 buildbot/slave/commands.py    |   81 ++++++++++---
 buildbot/steps/source.py      |   18 +++-
 buildbot/test/test_vc.py      |  147 ++++++++++++++++++++++-
 contrib/git_buildbot.py       |  254 +++++++++++++++++++++++++++++++++++++++++
 docs/buildbot.texinfo         |   93 +++++++++++++---
 6 files changed, 579 insertions(+), 42 deletions(-)
 create mode 100755 contrib/git_buildbot.py

diff --git a/buildbot/scripts/tryclient.py b/buildbot/scripts/tryclient.py
index 11f8e66..dc398f7 100644
--- a/buildbot/scripts/tryclient.py
+++ b/buildbot/scripts/tryclient.py
@@ -203,6 +203,32 @@ class DarcsExtractor(SourceStampExtractor):
         d.addCallback(self.readPatch, self.patchlevel)
         return d
 
+class GitExtractor(SourceStampExtractor):
+    patchlevel = 1
+    vcexe = "git"
+
+    def getBaseRevision(self):
+	d = self.dovc(["branch", "--no-color", "-v", "--no-abbrev"])
+	d.addCallback(self.parseStatus)
+	return d
+
+    def parseStatus(self, res):
+	# The current branch is marked by '*' at the start of the
+	# line, followed by the branch name and the SHA1.
+	#
+	# Branch names may contain pretty much anything but whitespace.
+	m = re.search(r'^\* (\S+)\s+([0-9a-f]{40})', res, re.MULTILINE)
+	if m:
+	    self.branch = m.group(1)
+	    self.baserev = m.group(2)
+	    return
+	raise IndexError("Could not find current GIT branch: %s" % res)
+
+    def getPatch(self, res):
+	d = self.dovc(["diff", self.baserev])
+	d.addCallback(self.readPatch, self.patchlevel)
+	return d
+
 def getSourceStamp(vctype, treetop, branch=None):
     if vctype == "cvs":
         e = CVSExtractor(treetop, branch)
@@ -218,6 +244,8 @@ def getSourceStamp(vctype, treetop, branch=None):
         e = MercurialExtractor(treetop, branch)
     elif vctype == "darcs":
         e = DarcsExtractor(treetop, branch)
+    elif vctype == "git":
+	e = GitExtractor(treetop, branch)
     else:
         raise KeyError("unknown vctype '%s'" % vctype)
     return e.get()
diff --git a/buildbot/slave/commands.py b/buildbot/slave/commands.py
index 4fd348e..5a3d636 100644
--- a/buildbot/slave/commands.py
+++ b/buildbot/slave/commands.py
@@ -15,7 +15,7 @@ from buildbot.slave.registry import registerSlaveCommand
 # this used to be a CVS $-style "Revision" auto-updated keyword, but since I
 # moved to Darcs as the primary repository, this is updated manually each
 # time this file is changed. The last cvs_ver that was here was 1.51 .
-command_version = "2.3"
+command_version = "2.4"
 
 # version history:
 #  >=1.17: commands are interruptable
@@ -35,6 +35,7 @@ command_version = "2.3"
 #          (release 0.7.4)
 #  >= 2.2: added monotone, uploadFile, and downloadFile (release 0.7.5)
 #  >= 2.3: added bzr
+#  >= 2.4: Git understands 'revision' and branches
 
 class CommandInterrupted(Exception):
     pass
@@ -1718,7 +1719,9 @@ class Git(SourceBase):
     """Git specific VC operation. In addition to the arguments
     handled by SourceBase, this command reads the following keys:
 
-    ['repourl'] (required): the Cogito repository string
+    ['repourl'] (required): the upstream GIT repository string
+    ['branch'] (optional): which version (i.e. branch or tag) to
+			   retrieve. Default: "master".
     """
 
     header = "git operation"
@@ -1726,31 +1729,75 @@ class Git(SourceBase):
     def setup(self, args):
         SourceBase.setup(self, args)
         self.repourl = args['repourl']
-        #self.sourcedata = "" # TODO
+	self.branch = args.get('branch')
+	if not self.branch:
+	    self.branch = "master"
+	self.sourcedata = "%s %s\n" % (self.repourl, self.branch)
+
+    def _fullSrcdir(self):
+	return os.path.join(self.builder.basedir, self.srcdir)
+
+    def _commitSpec(self):
+	if self.revision:
+	    return self.revision
+	return self.branch
 
     def sourcedirIsUpdateable(self):
-        if os.path.exists(os.path.join(self.builder.basedir,
-                                       self.srcdir, ".buildbot-patched")):
+        if os.path.exists(os.path.join(self._fullSrcdir(),
+					".buildbot-patched")):
             return False
-        return os.path.isdir(os.path.join(self.builder.basedir,
-                                          self.srcdir, ".git"))
+        return os.path.isdir(os.path.join(self._fullSrcdir(), ".git"))
+
+    def _didFetch(self, res):
+	if self.revision:
+	    head = self.revision
+	else:
+	    head = 'FETCH_HEAD'
+
+	command = ['git-reset', '--hard', head]
+	c = ShellCommand(self.builder, command, self._fullSrcdir(),
+		         sendRC=False, timeout=self.timeout)
+	self.command = c
+	return c.start()
 
     def doVCUpdate(self):
-        d = os.path.join(self.builder.basedir, self.srcdir)
-        command = ['cg-update']
-        c = ShellCommand(self.builder, command, d,
+	command = ['git-fetch', self.repourl, self.branch]
+	self.sendStatus({"header": "fetching branch %s from %s\n"
+					% (self.branch, self.repourl)})
+        c = ShellCommand(self.builder, command, self._fullSrcdir(),
                          sendRC=False, timeout=self.timeout)
         self.command = c
-        return c.start()
+	d = c.start()
+	d.addCallback(self._abandonOnFailure)
+	d.addCallback(self._didFetch)
+        return d
+
+    def _didInit(self, res):
+	return self.doVCUpdate()
 
     def doVCFull(self):
-        d = os.path.join(self.builder.basedir, self.srcdir)
-        os.mkdir(d)
-        command = ['cg-clone', '-s', self.repourl]
-        c = ShellCommand(self.builder, command, d,
+	os.mkdir(self._fullSrcdir())
+        c = ShellCommand(self.builder, ['git-init'], self._fullSrcdir(),
                          sendRC=False, timeout=self.timeout)
         self.command = c
-        return c.start()
+	d = c.start()
+	d.addCallback(self._abandonOnFailure)
+	d.addCallback(self._didInit)
+        return d
+
+    def parseGotRevision(self):
+	command = ['git-rev-parse', 'HEAD']
+	c = ShellCommand(self.builder, command, self._fullSrcdir(),
+		         sendRC=False, keepStdout=True)
+	c.usePTY = False
+	d = c.start()
+	def _parse(res):
+	    hash = c.stdout.strip()
+	    if len(hash) != 40:
+		return None
+	    return hash
+	d.addCallback(_parse)
+	return d
 
 registerSlaveCommand("git", Git, command_version)
 
@@ -2200,7 +2247,7 @@ class P4(SourceBase):
             return False
         # We assume our client spec is still around.
         # We just say we aren't updateable if the dir doesn't exist so we
-        # don't get ENOENT checking the sourcedata.
+        # don't get ENOENT checking the sURLourcedata.
         return os.path.isdir(os.path.join(self.builder.basedir,
                                           self.srcdir))
 
diff --git a/buildbot/steps/source.py b/buildbot/steps/source.py
index dac7dea..1059138 100644
--- a/buildbot/steps/source.py
+++ b/buildbot/steps/source.py
@@ -571,15 +571,25 @@ class Git(Source):
 
     name = "git"
 
-    def __init__(self, repourl, **kwargs):
+    def __init__(self, repourl, branch="master", **kwargs):
         """
         @type  repourl: string
         @param repourl: the URL which points at the git repository
+
+	@type  branch: string
+	@param branch: The branch or tag to check out by default. If
+		       a build specifies a different branch, it will
+		       be used instead of this.
         """
-        self.branch = None # TODO
         Source.__init__(self, **kwargs)
-        self.addFactoryArguments(repourl=repourl)
-        self.args['repourl'] = repourl
+        self.addFactoryArguments(repourl=repourl, branch=branch)
+	self.args.update({'repourl': repourl,
+	                  'branch': branch})
+
+    def computeSourceRevision(self, changes):
+	if not changes:
+	    return None
+	return changes[-1].revision
 
     def startVC(self, branch, revision, patch):
         self.args['branch'] = branch
diff --git a/buildbot/test/test_vc.py b/buildbot/test/test_vc.py
index 73eb979..0177feb 100644
--- a/buildbot/test/test_vc.py
+++ b/buildbot/test/test_vc.py
@@ -342,7 +342,8 @@ class BaseHelper:
         self.branch.append(rev)
         self.allrevs.append(rev)
 
-    def runCommand(self, basedir, command, failureIsOk=False, stdin=None):
+    def runCommand(self, basedir, command, failureIsOk=False,
+	           stdin=None, env=None):
         # all commands passed to do() should be strings or lists. If they are
         # strings, none of the arguments may have spaces. This makes the
         # commands less verbose at the expense of restricting what they can
@@ -355,8 +356,10 @@ class BaseHelper:
             print " in basedir %s" % basedir
             if stdin:
                 print " STDIN:\n", stdin, "\n--STDIN DONE"
-        env = os.environ.copy()
-        env['LC_ALL'] = "C"
+
+	if not env:
+	    env = os.environ.copy()
+	env['LC_ALL'] = "C"
         d = myGetProcessOutputAndValue(command[0], command[1:],
                                        env=env, path=basedir,
                                        stdin=stdin)
@@ -379,19 +382,19 @@ class BaseHelper:
         d.addCallback(check)
         return d
 
-    def do(self, basedir, command, failureIsOk=False, stdin=None):
+    def do(self, basedir, command, failureIsOk=False, stdin=None, env=None):
         d = self.runCommand(basedir, command, failureIsOk=failureIsOk,
-                            stdin=stdin)
+                            stdin=stdin, env=env)
         return waitForDeferred(d)
 
-    def dovc(self, basedir, command, failureIsOk=False, stdin=None):
+    def dovc(self, basedir, command, failureIsOk=False, stdin=None, env=None):
         """Like do(), but the VC binary will be prepended to COMMAND."""
         if isinstance(command, (str, unicode)):
             command = self.vcexe + " " + command
         else:
             # command is a list
             command = [self.vcexe] + command
-        return self.do(basedir, command, failureIsOk, stdin)
+        return self.do(basedir, command, failureIsOk, stdin, env)
 
 class VCBase(SignalMixin):
     metadir = None
@@ -2466,6 +2469,136 @@ class Mercurial(VCBase, unittest.TestCase):
 
 VCS.registerVC(Mercurial.vc_name, MercurialHelper())
 
+class GitHelper(BaseHelper):
+    branchname = "branch"
+    try_branchname = "branch"
+
+    def capable(self):
+	gitpaths = which('git')
+	if gitpaths:
+	    self.vcexe = gitpaths[0]
+	    return (True, None)
+	return (False, "GIT is not installed")
+
+    def createRepository(self):
+	self.createBasedir()
+	self.gitrepo = os.path.join(self.repbase,
+				   "GIT-Repository")
+	tmp = os.path.join(self.repbase, "gittmp")
+
+	env = os.environ.copy()
+	env['GIT_DIR'] = self.gitrepo
+	w = self.dovc(self.repbase, "init", env=env)
+	yield w; w.getResult()
+
+	self.populate(tmp)
+	w = self.dovc(tmp, "init")
+	yield w; w.getResult()
+	w = self.dovc(tmp, ["add", "."])
+	yield w; w.getResult()
+	w = self.dovc(tmp, ["commit", "-m", "initial_import"])
+	yield w; w.getResult()
+
+	w = self.dovc(tmp, ["checkout", "-b", self.branchname])
+	yield w; w.getResult()
+	self.populate_branch(tmp)
+	w = self.dovc(tmp, ["commit", "-a", "-m", "commit_on_branch"])
+	yield w; w.getResult()
+
+	w = self.dovc(tmp, ["rev-parse", "master", self.branchname])
+	yield w; out = w.getResult()
+	revs = out.splitlines()
+	self.addTrunkRev(revs[0])
+	self.addBranchRev(revs[1])
+
+	w = self.dovc(tmp, ["push", self.gitrepo, "master", self.branchname])
+	yield w; w.getResult()
+
+	rmdirRecursive(tmp)
+    createRepository = deferredGenerator(createRepository)
+
+    def vc_revise(self):
+	tmp = os.path.join(self.repbase, "gittmp")
+	rmdirRecursive(tmp)
+	log.msg("vc_revise" + self.gitrepo)
+	w = self.dovc(self.repbase, ["clone", self.gitrepo, "gittmp"])
+	yield w; w.getResult()
+
+	self.version += 1
+	version_c = VERSION_C % self.version
+	open(os.path.join(tmp, "version.c"), "w").write(version_c)
+
+	w = self.dovc(tmp, ["commit", "-m", "revised_to_%d" % self.version,
+	                    "version.c"])
+	yield w; w.getResult()
+	w = self.dovc(tmp, ["rev-parse", "master"])
+	yield w; out = w.getResult()
+	self.addTrunkRev(out.strip())
+
+	w = self.dovc(tmp, ["push", self.gitrepo, "master"])
+	yield w; out = w.getResult()
+	rmdirRecursive(tmp)
+    vc_revise = deferredGenerator(vc_revise)
+
+    def vc_try_checkout(self, workdir, rev, branch=None):
+        assert os.path.abspath(workdir) == workdir
+        if os.path.exists(workdir):
+            rmdirRecursive(workdir)
+
+	w = self.dovc(self.repbase, ["clone", self.gitrepo, workdir])
+	yield w; w.getResult()
+
+	if branch is not None:
+	    w = self.dovc(workdir, ["checkout", "-b", branch,
+	                            "origin/%s" % branch])
+	    yield w; w.getResult()
+
+	# Hmm...why do nobody else bother to check out the correct
+	# revision?
+	w = self.dovc(workdir, ["reset", "--hard", rev])
+	yield w; w.getResult()
+
+	try_c_filename = os.path.join(workdir, "subdir", "subdir.c")
+	open(try_c_filename, "w").write(TRY_C)
+    vc_try_checkout = deferredGenerator(vc_try_checkout)
+
+    def vc_try_finish(self, workdir):
+	rmdirRecursive(workdir)
+
+class Git(VCBase, unittest.TestCase):
+    vc_name = "git"
+
+    # No 'export' mode yet...
+    # metadir = ".git"
+    vctype = "source.Git"
+    vctype_try = "git"
+    has_got_revision = True
+
+    def testCheckout(self):
+	self.helper.vcargs = { 'repourl': self.helper.gitrepo }
+	d = self.do_vctest()
+	return d
+
+    def testPatch(self):
+	self.helper.vcargs = { 'repourl': self.helper.gitrepo,
+			       'branch': "master" }
+	d = self.do_patch()
+	return d
+
+    def testCheckoutBranch(self):
+	self.helper.vcargs = { 'repourl': self.helper.gitrepo,
+			       'branch': "master" }
+	d = self.do_branch()
+	return d
+
+    def testTry(self):
+	self.helper.vcargs = { 'repourl': self.helper.gitrepo,
+			       'branch': "master" }
+	d = self.do_getpatch()
+	return d
+
+VCS.registerVC(Git.vc_name, GitHelper())
+
 
 class Sources(unittest.TestCase):
     # TODO: this needs serious rethink
diff --git a/contrib/git_buildbot.py b/contrib/git_buildbot.py
new file mode 100755
index 0000000..513209c
--- /dev/null
+++ b/contrib/git_buildbot.py
@@ -0,0 +1,254 @@
+#! /usr/bin/env python
+
+# This script is meant to run from hooks/post-receive in the git
+# repository. It expects one line for each new revision on the form
+#   <oldrev> <newrev> <refname>
+#
+# For example:
+#   aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master
+#
+# Each of these changes will be passed to the buildbot server along
+# with any other change information we manage to extract from the
+# repository.
+#
+# Largely based on contrib/hooks/post-receive-email from git.
+
+import commands, logging, os, re, sys
+
+from twisted.spread import pb
+from twisted.cred import credentials
+from twisted.internet import reactor
+
+from buildbot.scripts import runner
+from optparse import OptionParser
+
+# Modify this to fit your setup
+
+master = "localhost:9989"
+
+# The GIT_DIR environment variable must have been set up so that any
+# git commands that are executed will operate on the repository we're
+# installed in.
+
+changes = []
+
+def connectFailed(error):
+    logging.error("Could not connect to %s: %s"
+	    % (master, error.getErrorMessage()))
+    return error
+
+def addChange(dummy, remote, changei):
+    logging.debug("addChange %s, %s" % (repr(remote), repr(changei)))
+    try:
+	c = changei.next()
+    except StopIteration:
+	remote.broker.transport.loseConnection()
+	return None
+
+    logging.info("New revision: %s" % c['revision'][:8])
+    for key, value in c.iteritems():
+	logging.debug("  %s: %s" % (key, value))
+
+    d = remote.callRemote('addChange', c)
+    d.addCallback(addChange, remote, changei)
+    return d
+
+def connected(remote):
+    return addChange(None, remote, changes.__iter__())
+
+def grab_commit_info(c, rev):
+    # Extract information about committer and files using git-show
+    f = os.popen("git-show --raw --pretty=full %s" % rev, 'r')
+
+    files = []
+
+    while True:
+	line = f.readline()
+	if not line:
+	    break
+
+	m = re.match(r"^:.*[MAD]\s+(.+)$", line)
+	if m:
+	    logging.debug("Got file: %s" % m.group(1))
+	    files.append(m.group(1))
+	    continue
+
+	m = re.match(r"^Commit:\s+(.+)$", line)
+	if m:
+	    logging.debug("Got committer: %s" % m.group(1))
+	    c['who'] = m.group(1)
+
+    c['files'] = files
+    status = f.close()
+    if status:
+	logging.warning("git-show exited with status %d" % status)
+
+def gen_changes(input, branch):
+    while True:
+	line = input.readline()
+	if not line:
+	    break
+
+	logging.debug("Change: %s" % line)
+
+	m = re.match(r"^([0-9a-f]+) (.*)$", line.strip())
+	c = { 'revision': m.group(1), 'comments': m.group(2),
+		'branch': branch }
+	grab_commit_info(c, m.group(1))
+	changes.append(c)
+
+def gen_create_branch_changes(newrev, refname, branch):
+    # A new branch has been created. Generate changes for everything
+    # up to `newrev' which does not exist in any branch but `refname'.
+    #
+    # Note that this may be inaccurate if two new branches are created
+    # at the same time, pointing to the same commit, or if there are
+    # commits that only exists in a common subset of the new branches.
+
+    logging.info("Branch `%s' created" % branch)
+
+    f = os.popen("git-rev-parse --not --branches"
+	    + "| grep -v $(git-rev-parse %s)" % refname
+	    + "| git-rev-list --reverse --pretty=oneline --stdin %s" % newrev,
+	    'r')
+
+    gen_changes(f, branch)
+
+    status = f.close()
+    if status:
+	logging.warning("git-rev-list exited with status %d" % status)
+
+def gen_update_branch_changes(oldrev, newrev, refname, branch):
+    # A branch has been updated. If it was a fast-forward update,
+    # generate Change events for everything between oldrev and newrev.
+    #
+    # In case of a forced update, first generate a "fake" Change event
+    # rewinding the branch to the common ancestor of oldrev and
+    # newrev. Then, generate Change events for each commit between the
+    # common ancestor and newrev.
+
+    logging.info("Branch `%s' updated %s .. %s"
+	    % (branch, oldrev[:8], newrev[:8]))
+
+    baserev = commands.getoutput("git-merge-base %s %s" % (oldrev, newrev))
+    logging.debug("oldrev=%s newrev=%s baserev=%s" % (oldrev, newrev, baserev))
+    if baserev != oldrev:
+	c = { 'revision': baserev, 'comments': "Rewind branch",
+		'branch': branch, 'who': "dummy" }
+
+	logging.info("Branch %s was rewound to %s" % (branch, baserev[:8]))
+	files = []
+	f = os.popen("git-diff --raw %s..%s" % (oldrev, baserev), 'r')
+	while True:
+	    line = f.readline()
+	    if not line:
+		break
+
+	    file = re.match(r"^:.*[MAD]\s*(.+)$", line).group(1)
+	    logging.debug("  Rewound file: %s" % file)
+	    files.append(file)
+
+	status = f.close()
+	if status:
+	    logging.warning("git-diff exited with status %d" % status)
+
+	if files:
+	    c['files'] = files
+	    changes.append(c)
+
+    if newrev != baserev:
+	# Not a pure rewind
+	f = os.popen("git-rev-list --reverse --pretty=oneline %s..%s"
+		% (baserev, newrev), 'r')
+	gen_changes(f, branch)
+
+	status = f.close()
+	if status:
+	    logging.warning("git-rev-list exited with status %d" % status)
+
+def cleanup(res):
+    reactor.stop()
+
+def process_changes():
+    # Read branch updates from stdin and generate Change events
+    while True:
+	line = sys.stdin.readline()
+	if not line:
+	    break
+
+	[oldrev, newrev, refname] = line.split(None, 2)
+
+	# We only care about regular heads, i.e. branches
+	m = re.match(r"^refs\/heads\/(.+)$", refname)
+	if not m:
+	    logging.info("Ignoring refname `%s': Not a branch" % refname)
+	    continue
+
+	branch = m.group(1)
+
+	# Find out if the branch was created, deleted or updated. Branches
+	# being deleted aren't really interesting.
+	if re.match(r"^0*$", newrev):
+	    logging.info("Branch `%s' deleted, ignoring" % branch)
+	    continue
+	elif re.match(r"^0*$", oldrev):
+	    gen_create_branch_changes(newrev, refname, branch)
+	else:
+	    gen_update_branch_changes(oldrev, newrev, refname, branch)
+
+    # Submit the changes, if any
+    if not changes:
+	logging.warning("No changes found")
+	return
+
+    host, port = master.split(':')
+    port = int(port)
+
+    f = pb.PBClientFactory()
+    d = f.login(credentials.UsernamePassword("change", "changepw"))
+    reactor.connectTCP(host, port, f)
+
+    d.addErrback(connectFailed)
+    d.addCallback(connected)
+    d.addBoth(cleanup)
+
+    reactor.run()
+
+def parse_options():
+    parser = OptionParser()
+    parser.add_option("-l", "--logfile", action="store", type="string",
+	    help="Log to the specified file")
+    parser.add_option("-v", "--verbose", action="count",
+	    help="Be more verbose. Ignored if -l is not specified.")
+    options, args = parser.parse_args()
+    return options
+
+# Log errors and critical messages to stderr. Optionally log
+# information to a file as well (we'll set that up later.)
+stderr = logging.StreamHandler(sys.stderr)
+fmt = logging.Formatter("git_buildbot: %(levelname)s: %(message)s")
+stderr.setLevel(logging.ERROR)
+stderr.setFormatter(fmt)
+logging.getLogger().addHandler(stderr)
+logging.getLogger().setLevel(logging.DEBUG)
+
+try:
+    options = parse_options()
+    level = logging.WARNING
+    if options.verbose:
+	level -= 10 * options.verbose
+	if level < 0:
+	    level = 0
+
+    if options.logfile:
+	logfile = logging.FileHandler(options.logfile)
+	logfile.setLevel(level)
+	fmt = logging.Formatter("%(asctime)s %(levelname)s: %(message)s")
+	logfile.setFormatter(fmt)
+	logging.getLogger().addHandler(logfile)
+
+    process_changes()
+except:
+    logging.exception("Unhandled exception")
+    sys.exit(1)
+
diff --git a/docs/buildbot.texinfo b/docs/buildbot.texinfo
index 37c8411..e48a098 100644
--- a/docs/buildbot.texinfo
+++ b/docs/buildbot.texinfo
@@ -1268,9 +1268,9 @@ Arch/Baz/Bazaar, or a labeled tag used in CVS)@footnote{many VC
 systems provide more complexity than this: in particular the local
 views that P4 and ClearCase can assemble out of various source
 directories are more complex than we're prepared to take advantage of
-here}. The SHA1 revision ID used by Monotone and Mercurial is also a
-kind of revision stamp, in that it specifies a unique copy of the
-source tree, as does a Darcs ``context'' file.
+here}. The SHA1 revision ID used by Monotone, Mercurial and Git is
+also a kind of revision stamp, in that it specifies a unique copy of
+the source tree, as does a Darcs ``context'' file.
 
 When we aren't intending to make any changes to the sources we check out
 (at least not any that need to be committed back upstream), there are two
@@ -1468,6 +1468,16 @@ each branch is stored in a separate repository. The @code{repourl},
 same way as with Darcs. The ``revision'', however, is the hash
 identifier returned by @command{hg identify}.
 
+ at uref{http://git.or.cz/, Git} also follows a decentralized model, and
+each repository can have several branches and tags. The source Step is
+configured with a static @code{repourl} which specifies the location
+of the repository. In addition, an optional @code{branch} parameter
+can be specified to check out code from a specific branch instead of
+the default ``master'' branch. The ``revision'' is specified as a SHA1
+hash as returned by e.g. @command{git rev-parse}. No attempt is made
+to ensure that the specified revision is actually a subset of the
+specified branch.
+
 
 @node Attributes of Changes,  , How Different VC Systems Specify Sources, Version Control Systems
 @subsection Attributes of Changes
@@ -1526,6 +1536,9 @@ consumed by the @code{computeSourceRevision} method in the appropriate
 @code{revision} is the full revision ID (ending in --patch-%d)
 @item P4
 @code{revision} is an int, the transaction number
+ at item Git
+ at code{revision} is a short string (a SHA1 hash), the output of e.g.
+ at code{git rev-parse}
 @end table
 
 @heading Branches
@@ -1534,14 +1547,15 @@ The Change might also have a @code{branch} attribute. This indicates
 that all of the Change's files are in the same named branch. The
 Schedulers get to decide whether the branch should be built or not.
 
-For VC systems like CVS, Arch, and Monotone, the @code{branch} name is
-unrelated to the filename. (that is, the branch name and the filename
-inhabit unrelated namespaces). For SVN, branches are expressed as
-subdirectories of the repository, so the file's ``svnurl'' is a
-combination of some base URL, the branch name, and the filename within
-the branch. (In a sense, the branch name and the filename inhabit the
-same namespace). Darcs branches are subdirectories of a base URL just
-like SVN. Mercurial branches are the same as Darcs.
+For VC systems like CVS, Arch, Monotone, and Git, the @code{branch}
+name is unrelated to the filename. (that is, the branch name and the
+filename inhabit unrelated namespaces). For SVN, branches are
+expressed as subdirectories of the repository, so the file's
+``svnurl'' is a combination of some base URL, the branch name, and the
+filename within the branch. (In a sense, the branch name and the
+filename inhabit the same namespace). Darcs branches are
+subdirectories of a base URL just like SVN. Mercurial branches are the
+same as Darcs.
 
 @table @samp
 @item CVS
@@ -1554,6 +1568,8 @@ branch='warner-newfeature', files=['src/foo.c']
 branch='warner-newfeature', files=['src/foo.c']
 @item Arch/Bazaar
 branch='buildbot--usebranches--0', files=['buildbot/master.py']
+ at item Git
+branch='warner-newfeature', files=['src/foo.c']
 @end table
 
 @heading Links
@@ -2716,6 +2732,12 @@ hook)
 @code{contrib/arch_buildbot.py} run in a commit hook)
 @end itemize
 
+ at item Git
+ at itemize @bullet
+ at item pb.PBChangeSource (listening for connections from
+ at code{contrib/git_buildbot.py} run in the post-receive hook)
+ at end itemize
+
 @end table
 
 All VC systems can be driven by a PBChangeSource and the
@@ -2965,6 +2987,12 @@ http://opensource.perlig.de/en/svnmailer/
 http://www.selenic.com/mercurial/wiki/index.cgi/NotifyExtension
 @end table
 
+ at item Git
+ at table @samp
+ at item post-receive-email
+http://git.kernel.org/?p=git/git.git;a=blob;f=contrib/hooks/post-receive-email;hb=HEAD
+ at end table
+
 @end table
 
 
@@ -3801,6 +3829,7 @@ arguments are described on the following pages.
 * Bazaar::                      
 * Bzr::                         
 * P4::                          
+* Git::
 @end menu
 
 @node CVS, SVN, Source Checkout, Source Checkout
@@ -4134,7 +4163,7 @@ will be passed to the @code{bzr checkout} command.
 
 
 
- at node P4,  , Bzr, Source Checkout
+ at node P4, Git, Bzr, Source Checkout
 @subsubsection P4
 
 @cindex Perforce Update
@@ -4175,6 +4204,29 @@ to replace %(slave)s with the slave name and %(builder)s with the
 builder name. The default is "buildbot_%(slave)s_%(build)s".
 @end table
 
+ at node Git, , P4, Source Checkout
+ at subsubsection Git
+
+ at cindex Git Checkout
+ at bsindex buildbot.steps.source.Git
+
+
+The @code{Git} build step clones or updates a @uref{http://git.or.cz/,
+Git} repository and checks out the specified branch or revision.
+
+The Git step takes the following arguments:
+
+ at table @code
+ at item repourl
+(required): the URL of the upstream Git repository.
+
+ at item branch
+(optional): this specifies the name of the branch to use when a Build
+does not provide one of its own. If this this parameter is not
+specified, and the Build does not provide a branch, the ``master''
+branch will be used.
+ at end table
+
 @node ShellCommand, Simple ShellCommand Subclasses, Source Checkout, Build Steps
 @subsection ShellCommand
 
@@ -6914,7 +6966,7 @@ for each tree you use, so it may be more convenient to use the
 @code{try_topfile} approach instead.
 
 Other VC systems which work on full projects instead of individual
-directories (tla, baz, darcs, monotone, mercurial) do not require
+directories (tla, baz, darcs, monotone, mercurial, git) do not require
 @command{try} to know the top directory, so the @option{--try-topfile}
 and @option{--try-topdir} arguments will be ignored.
 @c is this true? I think I currently require topdirs all the time.
@@ -6993,7 +7045,20 @@ revision. For @command{try} to work, your working directory must only
 have patches that are available from the same remotely-available
 repository that the build process' @code{step.Mercurial} will use.
 
- at c TODO: monotone, git
+ at item Git
+ at code{git branch -v} lists all the branches available in the local
+repository along with the revision ID it points to and a short summary
+of the last commit. The line containing the currently checked out
+branch begins with '* ' (star and space) while all the others start
+with '  ' (two spaces). @command{try} scans for this line and extracts
+the branch name and revision from it. Then it generates a diff against
+the base revision.
+ at c TODO: I'm not sure if this actually works the way it's intended
+ at c since the extracted base revision might not actually exist in the
+ at c upstream repository. Perhaps we need to add a --remote option to
+ at c specify the remote tracking branch to generate a diff against.
+
+ at c TODO: monotone
 @end table
 
 @heading waiting for results
-- 
1.5.2.5





More information about the devel mailing list