From bc53d760b75d89299c879e08f65c4fd99ad5506e Mon Sep 17 00:00:00 2001 From: warner Date: Thu, 22 Nov 2007 07:03:37 +0100 Subject: [PATCH] improve Git support, plus docs and tests. Thanks to Haavard Skinnemoen for the amazing patch. Closes #130. --- ChangeLog | 15 ++ buildbot/scripts/tryclient.py | 28 ++++ buildbot/slave/commands.py | 83 ++++++++--- buildbot/steps/source.py | 18 ++- buildbot/test/test_vc.py | 145 ++++++++++++++++++- contrib/git_buildbot.py | 254 ++++++++++++++++++++++++++++++++++ docs/buildbot.texinfo | 94 +++++++++++-- 7 files changed, 595 insertions(+), 42 deletions(-) create mode 100644 contrib/git_buildbot.py diff --git a/ChangeLog b/ChangeLog index d1d0a01fbfb..9e1ad07d6cf 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,20 @@ 2007-11-21 Brian Warner + * lots: improve Git support, AMAZING patch from Haavard + Skinnemoen, complete with unit tests and docs, thanks! Closes #130. + * buildbot/scripts/tryclient.py (GitExtractor): add 'try' support + * buildbot/slave/commands.py (Git): accept branch and revisions, + and use 'git' instead of the 'cogito' wrapper + * buildbot/steps/source.py (Git.__init__): same + * buildbot/test/test_vc.py (BaseHelper.runCommand): offer control + over env= + (BaseHelper.do): same + (BaseHelper.dovc): same + (Git): unit tests for Git support. Wow! + * docs/buildbot.texinfo (Git): docs for Git support. Double Wow! + * contrib/git_buildbot.py: commit-hook script for git + + * buildbot/changes/p4poller.py (P4Source._finished): don't let a failure cause us to stop polling. Thanks to John Backstrand for the patch. Closes #135. diff --git a/buildbot/scripts/tryclient.py b/buildbot/scripts/tryclient.py index 11f8e66d312..459cfff2778 100644 --- a/buildbot/scripts/tryclient.py +++ b/buildbot/scripts/tryclient.py @@ -203,6 +203,32 @@ def getPatch(self, res): 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 4fd348e247f..5302f4ccf33 100644 --- a/buildbot/slave/commands.py +++ b/buildbot/slave/commands.py @@ -15,7 +15,7 @@ # 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 @@ # (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 @@ -707,7 +708,7 @@ def setup(self, args): self.rc = 0 def start(self): - if self.debug: + if self.debug: log.msg('SlaveFileUploadCommand started') # Open file @@ -819,7 +820,7 @@ def setup(self, args): self.rc = 0 def start(self): - if self.debug: + if self.debug: log.msg('SlaveFileDownloadCommand starting') # Open file @@ -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 doVCUpdate(self): - d = os.path.join(self.builder.basedir, self.srcdir) - command = ['cg-update'] - c = ShellCommand(self.builder, command, d, + 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): + 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 + 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) diff --git a/buildbot/steps/source.py b/buildbot/steps/source.py index dac7deac34e..c53e51440bb 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 73eb9791824..ca05e51d409 100644 --- a/buildbot/test/test_vc.py +++ b/buildbot/test/test_vc.py @@ -342,7 +342,8 @@ def addBranchRev(self, rev): 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,7 +356,9 @@ def runCommand(self, basedir, command, failureIsOk=False, stdin=None): print " in basedir %s" % basedir if stdin: print " STDIN:\n", stdin, "\n--STDIN DONE" - env = os.environ.copy() + + if not env: + env = os.environ.copy() env['LC_ALL'] = "C" d = myGetProcessOutputAndValue(command[0], command[1:], env=env, path=basedir, @@ -379,19 +382,19 @@ def check((out, err, code)): 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 @@ def testTry(self): 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 100644 index 00000000000..036110bce2d --- /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 +# +# +# 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 37c8411fd00..9197787125a 100644 --- a/docs/buildbot.texinfo +++ b/docs/buildbot.texinfo @@ -196,6 +196,7 @@ Source Checkout * Bazaar:: * Bzr:: * P4:: +* Git:: Simple ShellCommand Subclasses @@ -1268,9 +1269,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 +1469,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}. +@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 +1537,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 +@item Git +@code{revision} is a short string (a SHA1 hash), the output of e.g. +@code{git rev-parse} @end table @heading Branches @@ -1534,14 +1548,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 +1569,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'] +@item Git +branch='warner-newfeature', files=['src/foo.c'] @end table @heading Links @@ -2716,6 +2733,12 @@ hook) @code{contrib/arch_buildbot.py} run in a commit hook) @end itemize +@item Git +@itemize @bullet +@item pb.PBChangeSource (listening for connections from +@code{contrib/git_buildbot.py} run in the post-receive hook) +@end itemize + @end table All VC systems can be driven by a PBChangeSource and the @@ -2965,6 +2988,12 @@ http://opensource.perlig.de/en/svnmailer/ http://www.selenic.com/mercurial/wiki/index.cgi/NotifyExtension @end table +@item Git +@table @samp +@item post-receive-email +http://git.kernel.org/?p=git/git.git;a=blob;f=contrib/hooks/post-receive-email;hb=HEAD +@end table + @end table @@ -3801,6 +3830,7 @@ arguments are described on the following pages. * Bazaar:: * Bzr:: * P4:: +* Git:: @end menu @node CVS, SVN, Source Checkout, Source Checkout @@ -4134,7 +4164,7 @@ will be passed to the @code{bzr checkout} command. -@node P4, , Bzr, Source Checkout +@node P4, Git, Bzr, Source Checkout @subsubsection P4 @cindex Perforce Update @@ -4175,6 +4205,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 +@node Git, , P4, Source Checkout +@subsubsection Git + +@cindex Git Checkout +@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: + +@table @code +@item repourl +(required): the URL of the upstream Git repository. + +@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. +@end table + @node ShellCommand, Simple ShellCommand Subclasses, Source Checkout, Build Steps @subsection ShellCommand @@ -6914,7 +6967,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 +7046,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. -@c TODO: monotone, git +@item Git +@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. +@c TODO: I'm not sure if this actually works the way it's intended +@c since the extracted base revision might not actually exist in the +@c upstream repository. Perhaps we need to add a --remote option to +@c specify the remote tracking branch to generate a diff against. + +@c TODO: monotone @end table @heading waiting for results