|
# This file is part of Buildbot. Buildbot 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, version 2. # # 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, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # Copyright Buildbot Team Members
''' Helper function that the repository URL based on the parameter the source step took and the Change 'repository' property '''
return str(s.repository) else: # that's the backward compatibility case else:
"""I do CVS checkout/update operations.
Note: if you are doing anonymous/pserver CVS operations, you will need to manually do a 'cvs login' on each buildslave before the slave has any hope of success. XXX: fix then, take a cvs password as an argument and figure out how to do a 'cvs login' on each build """
#progressMetrics = ('output',) # # additional things to track: update gives one stderr line per directory # (starting with 'cvs server: Updating ') (and is fairly stable if files # is empty), export gives one line per directory (starting with 'cvs # export: Updating ') and another line per file (starting with U). Would # be nice to track these, requires grepping LogFile data for lines, # parsing each line. Might be handy to have a hook in LogFile that gets # called with each complete line.
global_options=[], branch=None, checkoutDelay=None, checkout_options=[], export_options=[], extra_options=[], login=None, **kwargs):
""" @type cvsroot: string @param cvsroot: CVS Repository from which the source tree should be obtained. '/home/warner/Repository' for local or NFS-reachable repositories, ':pserver:anon@foo.com:/cvs' for anonymous CVS, 'user@host.com:/cvs' for non-anonymous CVS or CVS over ssh. Lots of possibilities, check the CVS documentation for more.
@type cvsmodule: string @param cvsmodule: subdirectory of CVS repository that should be retrieved
@type login: string or None @param login: if not None, a string which will be provided as a password to the 'cvs login' command, used when a :pserver: method is used to access the repository. This login is only needed once, but must be run each time (just before the CVS operation) because there is no way for the buildslave to tell whether it was previously performed or not.
@type branch: string @param branch: the default branch name, will be used in a '-r' argument to specify which branch of the source tree should be used for this checkout. Defaults to None, which means to use 'HEAD'.
@type checkoutDelay: int or None @param checkoutDelay: if not None, the number of seconds to put between the last known Change and the timestamp given to the -D argument. This defaults to exactly half of the parent Build's .treeStableTimer, but it could be set to something else if your CVS change notification has particularly weird latency characteristics.
@type global_options: list of strings @param global_options: these arguments are inserted in the cvs command line, before the 'checkout'/'update' command word. See 'cvs --help-options' for a list of what may be accepted here. ['-r'] will make the checked out files read only. ['-r', '-R'] will also assume the repository is read-only (I assume this means it won't use locks to insure atomic access to the ,v files).
@type checkout_options: list of strings @param checkout_options: these arguments are inserted in the cvs command line, after 'checkout' but before branch or revision specifiers.
@type export_options: list of strings @param export_options: these arguments are inserted in the cvs command line, after 'export' but before branch or revision specifiers.
@type extra_options: list of strings @param extra_options: these arguments are inserted in the cvs command line, after 'checkout' or 'export' but before branch or revision specifiers. """
'global_options': global_options, 'checkout_options':checkout_options, 'export_options':export_options, 'extra_options':extra_options, 'login': login, })
if not changes: return None lastChange = max([c.when for c in changes]) if self.checkoutDelay is not None: when = lastChange + self.checkoutDelay else: lastSubmit = max([br.submittedAt for br in self.build.requests]) when = (lastChange + lastSubmit) / 2 return formatdate(when)
if self.slaveVersionIsOlderThan("cvs", "1.39"): # the slave doesn't know to avoid re-using the same sourcedir # when the branch changes. We have no way of knowing which branch # the last build used, so if we're using a non-default branch and # either 'update' or 'copy' modes, it is safer to refuse to # build, and tell the user they need to upgrade the buildslave. if (branch != self.branch and self.args['mode'] in ("update", "copy")): m = ("This buildslave (%s) does not know about multiple " "branches, and using mode=%s would probably build the " "wrong tree. " "Refusing to build. Please upgrade the buildslave to " "buildbot-0.7.0 or newer." % (self.build.slavename, self.args['mode'])) log.msg(m) raise BuildSlaveTooOldError(m)
if self.slaveVersionIsOlderThan("cvs", "2.10"): if self.args['extra_options'] or self.args['export_options']: m = ("This buildslave (%s) does not support export_options " "or extra_options arguments to the CVS step." % (self.build.slavename)) log.msg(m) raise BuildSlaveTooOldError(m) # the unwanted args are empty, and will probably be ignored by # the slave, but delete them just to be safe del self.args['export_options'] del self.args['extra_options']
if branch is None: branch = "HEAD" self.args['cvsroot'] = self.cvsroot self.args['branch'] = branch self.args['revision'] = revision self.args['patch'] = patch
if self.args['branch'] == "HEAD" and self.args['revision']: # special case. 'cvs update -r HEAD -D today' gives no files # TODO: figure out why, see if it applies to -r BRANCH self.args['branch'] = None
# deal with old slaves warnings = [] slavever = self.slaveVersion("cvs", "old")
if slavever == "old": # 0.5.0 if self.args['mode'] == "export": self.args['export'] = 1 elif self.args['mode'] == "clobber": self.args['clobber'] = 1 elif self.args['mode'] == "copy": self.args['copydir'] = "source" self.args['tag'] = self.args['branch'] assert not self.args['patch'] # 0.5.0 slave can't do patch
cmd = RemoteCommand("cvs", self.args) self.startCommand(cmd, warnings)
"""I perform Subversion checkout/update operations."""
directory=None, username=None, password=None, extra_args=None, keep_on_purge=None, ignore_ignores=None, always_purge=None, depth=None, **kwargs): """ @type svnurl: string @param svnurl: the URL which points to the Subversion server, combining the access method (HTTP, ssh, local file), the repository host/port, the repository path, the sub-tree within the repository, and the branch to check out. Use exactly one of C{svnurl} and C{baseURL}.
@param baseURL: if branches are enabled, this is the base URL to which a branch name will be appended. It should probably end in a slash. Use exactly one of C{svnurl} and C{baseURL}.
@param defaultBranch: if branches are enabled, this is the branch to use if the Build does not specify one explicitly. It will simply be appended to C{baseURL} and the result handed to the SVN command.
@type username: string @param username: username to pass to svn's --username
@type password: string @param password: password to pass to svn's --password """
if not 'workdir' in kwargs and directory is not None: # deal with old configs warn("Please use workdir=, not directory=", DeprecationWarning) kwargs['workdir'] = directory
self.svnurl = svnurl and _ComputeRepositoryURL(self, svnurl) self.baseURL = _ComputeRepositoryURL(self, baseURL) self.branch = defaultBranch self.username = username self.password = password self.extra_args = extra_args self.keep_on_purge = keep_on_purge self.ignore_ignores = ignore_ignores self.always_purge = always_purge self.depth = depth
Source.__init__(self, **kwargs)
if svnurl and baseURL: raise ValueError("you must use either svnurl OR baseURL")
if not changes or None in [c.revision for c in changes]: return None lastChange = max([int(c.revision) for c in changes]) return lastChange
''' Handle compatibility between old slaves/svn clients '''
slavever = self.slaveVersion("svn", "old")
if not slavever: m = "slave does not have the 'svn' command" raise BuildSlaveTooOldError(m)
if self.slaveVersionIsOlderThan("svn", "1.39"): # the slave doesn't know to avoid re-using the same sourcedir # when the branch changes. We have no way of knowing which branch # the last build used, so if we're using a non-default branch and # either 'update' or 'copy' modes, it is safer to refuse to # build, and tell the user they need to upgrade the buildslave. if (self.args['branch'] != self.branch and self.args['mode'] in ("update", "copy")): m = ("This buildslave (%s) does not know about multiple " "branches, and using mode=%s would probably build the " "wrong tree. " "Refusing to build. Please upgrade the buildslave to " "buildbot-0.7.0 or newer." % (self.build.slavename, self.args['mode'])) raise BuildSlaveTooOldError(m)
if (self.depth is not None) and self.slaveVersionIsOlderThan("svn","2.9"): m = ("This buildslave (%s) does not support svn depth " "arguments. Refusing to build. " "Please upgrade the buildslave." % (self.build.slavename)) raise BuildSlaveTooOldError(m)
if (self.username is not None or self.password is not None) \ and self.slaveVersionIsOlderThan("svn", "2.8"): m = ("This buildslave (%s) does not support svn usernames " "and passwords. " "Refusing to build. Please upgrade the buildslave to " "buildbot-0.7.10 or newer." % (self.build.slavename,)) raise BuildSlaveTooOldError(m)
''' Compute the svn url that will be passed to the svn remote command ''' if self.svnurl: return self.svnurl else: if branch is None: m = ("The SVN source step belonging to builder '%s' does not know " "which branch to work with. This means that the change source " "did not specify a branch and that defaultBranch is None." \ % self.build.builder.name) raise RuntimeError(m)
computed = self.baseURL
if self.branch_placeholder in self.baseURL: return computed.replace(self.branch_placeholder, branch) else: return computed + branch
warnings = []
self.checkCompatibility()
self.args['svnurl'] = self.getSvnUrl(branch) self.args['revision'] = revision self.args['patch'] = patch self.args['always_purge'] = self.always_purge
#Set up depth if specified if self.depth is not None: self.args['depth'] = self.depth
if self.username is not None: self.args['username'] = self.username if self.password is not None: self.args['password'] = self.password
if self.extra_args is not None: self.args['extra_args'] = self.extra_args
revstuff = [] #revstuff.append(self.args['svnurl']) if self.args['svnurl'].find('trunk') == -1: revstuff.append("[branch]") if revision is not None: revstuff.append("r%s" % revision) if patch is not None: revstuff.append("[patch]") self.description.extend(revstuff) self.descriptionDone.extend(revstuff)
cmd = RemoteCommand("svn", self.args) self.startCommand(cmd, warnings)
"""Check out a source tree from a Darcs repository at 'repourl'.
Darcs has no concept of file modes. This means the eXecute-bit will be cleared on all source files. As a result, you may need to invoke configuration scripts with something like:
C{s(step.Configure, command=['/bin/sh', './configure'])} """
**kwargs): """ @type repourl: string @param repourl: the URL which points at the Darcs repository. This is used as the default branch. Using C{repourl} does not enable builds of alternate branches: use C{baseURL} to enable this. Use either C{repourl} or C{baseURL}, not both.
@param baseURL: if branches are enabled, this is the base URL to which a branch name will be appended. It should probably end in a slash. Use exactly one of C{repourl} and C{baseURL}.
@param defaultBranch: if branches are enabled, this is the branch to use if the Build does not specify one explicitly. It will simply be appended to C{baseURL} and the result handed to the 'darcs pull' command. """ self.repourl = _ComputeRepositoryURL(self, repourl) self.baseURL = _ComputeRepositoryURL(self, baseURL) self.branch = defaultBranch Source.__init__(self, **kwargs) assert self.args['mode'] != "export", \ "Darcs does not have an 'export' mode" if repourl and baseURL: raise ValueError("you must provide exactly one of repourl and" " baseURL")
slavever = self.slaveVersion("darcs") if not slavever: m = "slave is too old, does not know about darcs" raise BuildSlaveTooOldError(m)
if self.slaveVersionIsOlderThan("darcs", "1.39"): if revision: # TODO: revisit this once we implement computeSourceRevision m = "0.6.6 slaves can't handle args['revision']" raise BuildSlaveTooOldError(m)
# the slave doesn't know to avoid re-using the same sourcedir # when the branch changes. We have no way of knowing which branch # the last build used, so if we're using a non-default branch and # either 'update' or 'copy' modes, it is safer to refuse to # build, and tell the user they need to upgrade the buildslave. if (branch != self.branch and self.args['mode'] in ("update", "copy")): m = ("This buildslave (%s) does not know about multiple " "branches, and using mode=%s would probably build the " "wrong tree. " "Refusing to build. Please upgrade the buildslave to " "buildbot-0.7.0 or newer." % (self.build.slavename, self.args['mode'])) raise BuildSlaveTooOldError(m)
if self.repourl: assert not branch # we need baseURL= to use branches self.args['repourl'] = self.repourl else: self.args['repourl'] = self.baseURL + branch self.args['revision'] = revision self.args['patch'] = patch
revstuff = [] if branch is not None and branch != self.branch: revstuff.append("[branch]") self.description.extend(revstuff) self.descriptionDone.extend(revstuff)
cmd = RemoteCommand("darcs", self.args) self.startCommand(cmd)
"""Check out a source tree from a git repository 'repourl'."""
branch="master", submodules=False, ignore_ignores=None, reference=None, shallow=False, progress=False, **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.
@type submodules: boolean @param submodules: Whether or not to update (and initialize) git submodules.
@type reference: string @param reference: The path to a reference repository to obtain objects from, if any.
@type shallow: boolean @param shallow: Use a shallow or clone, if possible
@type progress: boolean @param progress: Pass the --progress option when fetching. This can solve long fetches getting killed due to lack of output, but requires Git 1.7.2+. """ 'ignore_ignores': ignore_ignores, 'reference': reference, 'shallow': shallow, 'progress': progress, })
if not changes: return None return changes[-1].revision
self.args['branch'] = branch self.args['repourl'] = self.repourl self.args['revision'] = revision self.args['patch'] = patch
# check if there is any patchset we should fetch from Gerrit if self.build.hasProperty("event.patchSet.ref"): # GerritChangeSource self.args['gerrit_branch'] = self.build.getProperty("event.patchSet.ref") self.setProperty("gerrit_branch", self.args['gerrit_branch']) else: try: # forced build change = self.build.getProperty("gerrit_change", '').split('/') if len(change) == 2: self.args['gerrit_branch'] = "refs/changes/%2.2d/%d/%d" \ % (int(change[0]) % 100, int(change[0]), int(change[1])) self.setProperty("gerrit_branch", self.args['gerrit_branch']) except: pass
slavever = self.slaveVersion("git") if not slavever: raise BuildSlaveTooOldError("slave is too old, does not know " "about git") cmd = RemoteCommand("git", self.args) self.startCommand(cmd)
"""Check out a source tree from a repo repository described by manifest."""
manifest_url=None, manifest_branch="master", manifest_file="default.xml", tarball=None, **kwargs): """ @type manifest_url: string @param manifest_url: The URL which points at the repo manifests repository.
@type manifest_branch: string @param manifest_branch: The manifest branch to check out by default.
@type manifest_file: string @param manifest_file: The manifest to use for sync.
""" 'manifest_file': manifest_file, 'tarball': tarball, 'manifest_override_url': None })
if not changes: return None return changes[-1].revision
""" lets try to be nice in the format we want can support several instances of "repo download proj number/patch" (direct copy paste from gerrit web site) or several instances of "proj number/patch" (simpler version) This feature allows integrator to build with several pending interdependant changes. returns list of repo downloads sent to the buildslave """ return []
"""taken the changesource and forcebuild property, build the repo download command to send to the slave making this a defereable allow config to tweak this in order to e.g. manage dependancies """ downloads = self.build.getProperty("repo_downloads", [])
# download patches based on GerritChangeSource events for change in self.build.allChanges(): if (change.properties.has_key("event.type") and change.properties["event.type"] == "patchset-created"): downloads.append("%s %s/%s"% (change.properties["event.change.project"], change.properties["event.change.number"], change.properties["event.patchSet.number"]))
# download patches based on web site forced build properties: # "repo_d", "repo_d0", .., "repo_d9" # "repo_download", "repo_download0", .., "repo_download9" for propName in ["repo_d"] + ["repo_d%d" % i for i in xrange(0,10)] + \ ["repo_download"] + ["repo_download%d" % i for i in xrange(0,10)]: s = self.build.getProperty(propName) if s is not None: downloads.extend(self.parseDownloadProperty(s))
if downloads: self.args["repo_downloads"] = downloads self.setProperty("repo_downloads", downloads) return defer.succeed(None)
self.args['manifest_url'] = self.manifest_url
# manifest override self.args['manifest_override_url'] = None try: self.args['manifest_override_url'] = self.build.getProperty("manifest_override_url") except KeyError: pass # only master has access to properties, so we must implement this here. d = self.buildDownloadList() d.addCallback(self.continueStartVC, branch, revision, patch) d.addErrback(self.failed)
slavever = self.slaveVersion("repo") if not slavever: raise BuildSlaveTooOldError("slave is too old, does not know " "about repo") cmd = RemoteCommand("repo", self.args) self.startCommand(cmd)
repo_downloaded = [] if cmd.updates.has_key("repo_downloaded"): repo_downloaded = cmd.updates["repo_downloaded"][-1] if repo_downloaded: self.setProperty("repo_downloaded", str(repo_downloaded), "Source") else: repo_downloaded = [] orig_downloads = self.getProperty("repo_downloads") or [] if len(orig_downloads) != len(repo_downloaded): self.step_status.setText(["repo download issues"])
"""Check out a source tree from a bzr (Bazaar) repository at 'repourl'.
"""
forceSharedRepo=None, **kwargs): """ @type repourl: string @param repourl: the URL which points at the bzr repository. This is used as the default branch. Using C{repourl} does not enable builds of alternate branches: use C{baseURL} to enable this. Use either C{repourl} or C{baseURL}, not both.
@param baseURL: if branches are enabled, this is the base URL to which a branch name will be appended. It should probably end in a slash. Use exactly one of C{repourl} and C{baseURL}.
@param defaultBranch: if branches are enabled, this is the branch to use if the Build does not specify one explicitly. It will simply be appended to C{baseURL} and the result handed to the 'bzr checkout pull' command.
@param forceSharedRepo: Boolean, defaults to False. If set to True, the working directory will be made into a bzr shared repository if it is not already. Shared repository greatly reduces the amount of history data that needs to be downloaded if not using update/copy mode, or if using update/copy mode with multiple branches. """ self.repourl = _ComputeRepositoryURL(self, repourl) self.baseURL = _ComputeRepositoryURL(self, baseURL) self.branch = defaultBranch Source.__init__(self, **kwargs) self.args.update({'forceSharedRepo': forceSharedRepo}) if repourl and baseURL: raise ValueError("you must provide exactly one of repourl and" " baseURL")
if not changes: return None lastChange = max([int(c.revision) for c in changes]) return lastChange
slavever = self.slaveVersion("bzr") if not slavever: m = "slave is too old, does not know about bzr" raise BuildSlaveTooOldError(m)
if self.repourl: assert not branch # we need baseURL= to use branches self.args['repourl'] = self.repourl else: self.args['repourl'] = self.baseURL + branch self.args['revision'] = revision self.args['patch'] = patch
revstuff = [] if branch is not None and branch != self.branch: revstuff.append("[" + branch + "]") if revision is not None: revstuff.append("r%s" % revision) self.description.extend(revstuff) self.descriptionDone.extend(revstuff)
cmd = RemoteCommand("bzr", self.args) self.startCommand(cmd)
"""Check out a source tree from a mercurial repository 'repourl'."""
branchType='dirname', clobberOnBranchChange=True, **kwargs): """ @type repourl: string @param repourl: the URL which points at the Mercurial repository. This uses the 'default' branch unless defaultBranch is specified below and the C{branchType} is set to 'inrepo'. It is an error to specify a branch without setting the C{branchType} to 'inrepo'.
@param baseURL: if 'dirname' branches are enabled, this is the base URL to which a branch name will be appended. It should probably end in a slash. Use exactly one of C{repourl} and C{baseURL}.
@param defaultBranch: if branches are enabled, this is the branch to use if the Build does not specify one explicitly. For 'dirname' branches, It will simply be appended to C{baseURL} and the result handed to the 'hg update' command. For 'inrepo' branches, this specifies the named revision to which the tree will update after a clone.
@param branchType: either 'dirname' or 'inrepo' depending on whether the branch name should be appended to the C{baseURL} or the branch is a mercurial named branch and can be found within the C{repourl}
@param clobberOnBranchChange: boolean, defaults to True. If set and using inrepos branches, clobber the tree at each branch change. Otherwise, just update to the branch. """ self.repourl = _ComputeRepositoryURL(self, repourl) self.baseURL = _ComputeRepositoryURL(self, baseURL) self.branch = defaultBranch self.branchType = branchType self.clobberOnBranchChange = clobberOnBranchChange Source.__init__(self, **kwargs) if repourl and baseURL: raise ValueError("you must provide exactly one of repourl and" " baseURL")
slavever = self.slaveVersion("hg") if not slavever: raise BuildSlaveTooOldError("slave is too old, does not know " "about hg")
if self.repourl: # we need baseURL= to use dirname branches assert self.branchType == 'inrepo' or not branch self.args['repourl'] = self.repourl if branch: self.args['branch'] = branch else: self.args['repourl'] = self.baseURL + (branch or '') self.args['revision'] = revision self.args['patch'] = patch self.args['clobberOnBranchChange'] = self.clobberOnBranchChange self.args['branchType'] = self.branchType
revstuff = [] if branch is not None and branch != self.branch: revstuff.append("[branch]") self.description.extend(revstuff) self.descriptionDone.extend(revstuff)
cmd = RemoteCommand("hg", self.args) self.startCommand(cmd)
if not changes: return None # without knowing the revision ancestry graph, we can't sort the # changes at all. So for now, assume they were given to us in sorted # order, and just pay attention to the last one. See ticket #103 for # more details. if len(changes) > 1: log.msg("Mercurial.computeSourceRevision: warning: " "there are %d changes here, assuming the last one is " "the most recent" % len(changes)) return changes[-1].revision
""" P4 is a class for accessing perforce revision control"""
p4passwd=None, p4extra_views=[], p4line_end='local', p4client='buildbot_%(slave)s_%(builder)s', **kwargs): """ @type p4base: string @param p4base: A view into a perforce depot, typically "//depot/proj/"
@type defaultBranch: string @param defaultBranch: Identify a branch to build by default. Perforce is a view based branching system. So, the branch is normally the name after the base. For example, branch=1.0 is view=//depot/proj/1.0/... branch=1.1 is view=//depot/proj/1.1/...
@type p4port: string @param p4port: Specify the perforce server to connection in the format <host>:<port>. Example "perforce.example.com:1666"
@type p4user: string @param p4user: The perforce user to run the command as.
@type p4passwd: string @param p4passwd: The password for the perforce user.
@type p4extra_views: list of tuples @param p4extra_views: Extra views to be added to the client that is being used.
@type p4line_end: string @param p4line_end: value of the LineEnd client specification property
@type p4client: string @param p4client: The perforce client to use for this buildslave. """
self.p4base = _ComputeRepositoryURL(self, p4base) self.branch = defaultBranch Source.__init__(self, **kwargs) self.args['p4port'] = p4port self.args['p4user'] = p4user self.args['p4passwd'] = p4passwd self.args['p4extra_views'] = p4extra_views self.args['p4line_end'] = p4line_end self.p4client = p4client
Source.setBuild(self, build) self.args['p4client'] = self.p4client % { 'slave': build.slavename, 'builder': build.builder.name, }
if not changes: return None lastChange = max([int(c.revision) for c in changes]) return lastChange
slavever = self.slaveVersion("p4") assert slavever, "slave is too old, does not know about p4" args = dict(self.args) args['p4base'] = self.p4base args['branch'] = branch or self.branch args['revision'] = revision args['patch'] = patch cmd = RemoteCommand("p4", args) self.startCommand(cmd)
"""Check out a source tree from a monotone repository 'repourl'."""
""" @type repourl: string @param repourl: the URI which points at the monotone 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.
@type progress: boolean @param progress: Pass the --ticker=dot option when pulling. This can solve long fetches getting killed due to lack of output. """ Source.__init__(self, **kwargs) self.repourl = _ComputeRepositoryURL(self, repourl) if (not repourl): raise ValueError("you must provide a repository uri in 'repourl'") if (not branch): raise ValueError("you must provide a default branch in 'branch'") self.args.update({'branch': branch, 'progress': progress, })
slavever = self.slaveVersion("mtn") if not slavever: raise BuildSlaveTooOldError("slave is too old, does not know " "about mtn")
self.args['repourl'] = self.repourl if branch: self.args['branch'] = branch self.args['revision'] = revision self.args['patch'] = patch
cmd = RemoteCommand("mtn", self.args) self.startCommand(cmd)
if not changes: return None # without knowing the revision ancestry graph, we can't sort the # changes at all. So for now, assume they were given to us in sorted # order, and just pay attention to the last one. See ticket #103 for # more details. if len(changes) > 1: log.msg("Monotone.computeSourceRevision: warning: " "there are %d changes here, assuming the last one is " "the most recent" % len(changes)) return changes[-1].revision |