PageRenderTime 27ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/master/buildbot/steps/source/mercurial.py

https://gitlab.com/murder187ss/buildbot
Python | 387 lines | 343 code | 20 blank | 24 comment | 23 complexity | 0e49d38b6d38d19e7e52752d2c6cd308 MD5 | raw file
  1. # This file is part of Buildbot. Buildbot is free software: you can
  2. # redistribute it and/or modify it under the terms of the GNU General Public
  3. # License as published by the Free Software Foundation, version 2.
  4. #
  5. # This program is distributed in the hope that it will be useful, but WITHOUT
  6. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  7. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
  8. # details.
  9. #
  10. # You should have received a copy of the GNU General Public License along with
  11. # this program; if not, write to the Free Software Foundation, Inc., 51
  12. # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  13. #
  14. # Copyright Buildbot Team Members
  15. """
  16. Source step code for mercurial
  17. """
  18. from twisted.internet import defer
  19. from twisted.internet import reactor
  20. from twisted.python import log
  21. from buildbot.config import ConfigErrors
  22. from buildbot.interfaces import WorkerTooOldError
  23. from buildbot.process import buildstep
  24. from buildbot.process import remotecommand
  25. from buildbot.process.results import SUCCESS
  26. from buildbot.steps.source.base import Source
  27. class Mercurial(Source):
  28. """ Class for Mercurial with all the smarts """
  29. name = "hg"
  30. renderables = ["repourl"]
  31. possible_methods = (None, 'clean', 'fresh', 'clobber')
  32. possible_branchTypes = ('inrepo', 'dirname')
  33. def __init__(self, repourl=None, mode='incremental',
  34. method=None, defaultBranch=None, branchType='dirname',
  35. clobberOnBranchChange=True, **kwargs):
  36. """
  37. @type repourl: string
  38. @param repourl: the URL which points at the Mercurial repository.
  39. if 'dirname' branches are enabled, this is the base URL
  40. to which a branch name will be appended. It should
  41. probably end in a slash.
  42. @param defaultBranch: if branches are enabled, this is the branch
  43. to use if the Build does not specify one
  44. explicitly.
  45. For 'dirname' branches, It will simply be
  46. appended to C{repourl} and the result handed to
  47. the 'hg update' command.
  48. For 'inrepo' branches, this specifies the named
  49. revision to which the tree will update after a
  50. clone.
  51. @param branchType: either 'dirname' or 'inrepo' depending on whether
  52. the branch name should be appended to the C{repourl}
  53. or the branch is a mercurial named branch and can be
  54. found within the C{repourl}
  55. @param clobberOnBranchChange: boolean, defaults to True. If set and
  56. using inrepos branches, clobber the tree
  57. at each branch change. Otherwise, just
  58. update to the branch.
  59. """
  60. self.repourl = repourl
  61. self.defaultBranch = self.branch = defaultBranch
  62. self.branchType = branchType
  63. self.method = method
  64. self.clobberOnBranchChange = clobberOnBranchChange
  65. self.mode = mode
  66. Source.__init__(self, **kwargs)
  67. errors = []
  68. if not self._hasAttrGroupMember('mode', self.mode):
  69. errors.append("mode %s is not one of %s" %
  70. (self.mode, self._listAttrGroupMembers('mode')))
  71. if self.method not in self.possible_methods:
  72. errors.append("method %s is not one of %s" %
  73. (self.method, self.possible_methods))
  74. if self.branchType not in self.possible_branchTypes:
  75. errors.append("branchType %s is not one of %s" %
  76. (self.branchType, self.possible_branchTypes))
  77. if repourl is None:
  78. errors.append("you must provide a repourl")
  79. if errors:
  80. raise ConfigErrors(errors)
  81. def startVC(self, branch, revision, patch):
  82. self.revision = revision
  83. self.method = self._getMethod()
  84. self.stdio_log = self.addLogForRemoteCommands("stdio")
  85. d = self.checkHg()
  86. @d.addCallback
  87. def checkInstall(hgInstalled):
  88. if not hgInstalled:
  89. raise WorkerTooOldError(
  90. "Mercurial is not installed on worker")
  91. return 0
  92. d.addCallback(lambda _: self.sourcedirIsPatched())
  93. if self.branchType == 'dirname':
  94. self.repourl = self.repourl + (branch or '')
  95. self.branch = self.defaultBranch
  96. self.update_branch = branch
  97. elif self.branchType == 'inrepo':
  98. self.update_branch = (branch or 'default')
  99. d.addCallback(self._getAttrGroupMember('mode', self.mode))
  100. if patch:
  101. d.addCallback(self.patch, patch)
  102. d.addCallback(self.parseGotRevision)
  103. d.addCallback(self.finish)
  104. d.addErrback(self.failed)
  105. @defer.inlineCallbacks
  106. def mode_full(self, _):
  107. if self.method == 'clobber':
  108. yield self.clobber()
  109. return
  110. updatable = yield self._sourcedirIsUpdatable()
  111. if not updatable:
  112. yield self._clone()
  113. yield self._update(None)
  114. elif self.method == 'clean':
  115. yield self.clean(None)
  116. elif self.method == 'fresh':
  117. yield self.fresh(None)
  118. else:
  119. raise ValueError("Unknown method, check your configuration")
  120. def mode_incremental(self, _):
  121. if self.method is not None:
  122. raise ValueError(self.method)
  123. d = self._sourcedirIsUpdatable()
  124. @defer.inlineCallbacks
  125. def _cmd(updatable):
  126. if updatable:
  127. yield self._dovccmd(self.getHgPullCommand())
  128. return
  129. else:
  130. yield self._clone()
  131. return
  132. d.addCallback(_cmd)
  133. d.addCallback(self._checkBranchChange)
  134. return d
  135. def clean(self, _):
  136. command = ['--config', 'extensions.purge=', 'purge']
  137. d = self._dovccmd(command)
  138. d.addCallback(self._pullUpdate)
  139. return d
  140. def _clobber(self):
  141. cmd = remotecommand.RemoteCommand('rmdir', {'dir': self.workdir,
  142. 'logEnviron': self.logEnviron})
  143. cmd.useLog(self.stdio_log, False)
  144. d = self.runCommand(cmd)
  145. return d
  146. def clobber(self):
  147. d = self._clobber()
  148. d.addCallback(lambda _: self._clone())
  149. d.addCallback(self._update)
  150. return d
  151. def fresh(self, _):
  152. command = ['--config', 'extensions.purge=', 'purge', '--all']
  153. d = self._dovccmd(command)
  154. d.addCallback(self._pullUpdate)
  155. return d
  156. def finish(self, res):
  157. d = defer.succeed(res)
  158. @d.addCallback
  159. def _gotResults(results):
  160. self.setStatus(self.cmd, results)
  161. return results
  162. d.addCallback(self.finished)
  163. return d
  164. def parseGotRevision(self, _):
  165. d = self._dovccmd(
  166. ['parents', '--template', '{node}\\n'], collectStdout=True)
  167. @d.addCallback
  168. def _setrev(stdout):
  169. revision = stdout.strip()
  170. if len(revision) != 40:
  171. raise ValueError("Incorrect revision id")
  172. log.msg("Got Mercurial revision %s" % (revision, ))
  173. self.updateSourceProperty('got_revision', revision)
  174. return 0
  175. return d
  176. @defer.inlineCallbacks
  177. def _checkBranchChange(self, _):
  178. current_branch = yield self._getCurrentBranch()
  179. msg = "Working dir is on in-repo branch '%s' and build needs '%s'." % \
  180. (current_branch, self.update_branch)
  181. if current_branch != self.update_branch and self.clobberOnBranchChange:
  182. msg += ' Clobbering.'
  183. log.msg(msg)
  184. yield self.clobber()
  185. return
  186. msg += ' Updating.'
  187. log.msg(msg)
  188. yield self._removeAddedFilesAndUpdate(None)
  189. def getHgPullCommand(self):
  190. command = ['pull', self.repourl]
  191. if self.revision:
  192. command.extend(['--rev', self.revision])
  193. elif self.branchType == 'inrepo':
  194. command.extend(['--rev', self.update_branch])
  195. return command
  196. def _pullUpdate(self, res):
  197. command = self.getHgPullCommand()
  198. d = self._dovccmd(command)
  199. d.addCallback(self._checkBranchChange)
  200. return d
  201. def _dovccmd(self, command, collectStdout=False, initialStdin=None, decodeRC=None,
  202. abandonOnFailure=True):
  203. if not command:
  204. raise ValueError("No command specified")
  205. if decodeRC is None:
  206. decodeRC = {0: SUCCESS}
  207. cmd = remotecommand.RemoteShellCommand(self.workdir, ['hg', '--verbose'] + command,
  208. env=self.env,
  209. logEnviron=self.logEnviron,
  210. timeout=self.timeout,
  211. collectStdout=collectStdout,
  212. initialStdin=initialStdin,
  213. decodeRC=decodeRC)
  214. cmd.useLog(self.stdio_log, False)
  215. d = self.runCommand(cmd)
  216. @d.addCallback
  217. def evaluateCommand(_):
  218. if abandonOnFailure and cmd.didFail():
  219. log.msg("Source step failed while running command %s" % cmd)
  220. raise buildstep.BuildStepFailed()
  221. if collectStdout:
  222. return cmd.stdout
  223. else:
  224. return cmd.rc
  225. return d
  226. def computeSourceRevision(self, changes):
  227. if not changes:
  228. return None
  229. # without knowing the revision ancestry graph, we can't sort the
  230. # changes at all. So for now, assume they were given to us in sorted
  231. # order, and just pay attention to the last one. See ticket #103 for
  232. # more details.
  233. if len(changes) > 1:
  234. log.msg("Mercurial.computeSourceRevision: warning: "
  235. "there are %d changes here, assuming the last one is "
  236. "the most recent" % len(changes))
  237. return changes[-1].revision
  238. def _getCurrentBranch(self):
  239. if self.branchType == 'dirname':
  240. return defer.succeed(self.branch)
  241. else:
  242. d = self._dovccmd(['identify', '--branch'], collectStdout=True)
  243. @d.addCallback
  244. def _getbranch(stdout):
  245. return stdout.strip()
  246. return d
  247. def _getMethod(self):
  248. if self.method is not None and self.mode != 'incremental':
  249. return self.method
  250. elif self.mode == 'incremental':
  251. return None
  252. elif self.method is None and self.mode == 'full':
  253. return 'fresh'
  254. def _sourcedirIsUpdatable(self):
  255. return self.pathExists(self.build.path_module.join(self.workdir, '.hg'))
  256. def _removeAddedFilesAndUpdate(self, _):
  257. command = ['locate', 'set:added()']
  258. d = self._dovccmd(
  259. command, collectStdout=True, decodeRC={0: SUCCESS, 1: SUCCESS})
  260. @d.addCallback
  261. def parseAndRemove(stdout):
  262. files = []
  263. for filename in stdout.splitlines():
  264. filename = self.workdir + '/' + filename
  265. files.append(filename)
  266. if len(files) == 0:
  267. d = defer.succeed(0)
  268. else:
  269. if self.workerVersionIsOlderThan('rmdir', '2.14'):
  270. d = self.removeFiles(files)
  271. else:
  272. cmd = remotecommand.RemoteCommand('rmdir', {'dir': files,
  273. 'logEnviron':
  274. self.logEnviron, })
  275. cmd.useLog(self.stdio_log, False)
  276. d = self.runCommand(cmd)
  277. d.addCallback(lambda _: cmd.rc)
  278. return d
  279. d.addCallback(self._update)
  280. return d
  281. @defer.inlineCallbacks
  282. def removeFiles(self, files):
  283. for filename in files:
  284. cmd = remotecommand.RemoteCommand('rmdir', {'dir': filename,
  285. 'logEnviron': self.logEnviron, })
  286. cmd.useLog(self.stdio_log, False)
  287. yield self.runCommand(cmd)
  288. if cmd.rc != 0:
  289. defer.returnValue(cmd.rc)
  290. return
  291. defer.returnValue(0)
  292. def _update(self, _):
  293. command = ['update', '--clean']
  294. if self.revision:
  295. command += ['--rev', self.revision]
  296. elif self.branchType == 'inrepo':
  297. command += ['--rev', self.update_branch]
  298. d = self._dovccmd(command)
  299. return d
  300. def _clone(self):
  301. if self.retry:
  302. abandonOnFailure = (self.retry[1] <= 0)
  303. else:
  304. abandonOnFailure = True
  305. d = self._dovccmd(['clone', '--noupdate', self.repourl, '.'],
  306. abandonOnFailure=abandonOnFailure)
  307. def _retry(res):
  308. if self.stopped or res == 0:
  309. return res
  310. delay, repeats = self.retry
  311. if repeats > 0:
  312. log.msg("Checkout failed, trying %d more times after %d seconds"
  313. % (repeats, delay))
  314. self.retry = (delay, repeats - 1)
  315. df = defer.Deferred()
  316. df.addCallback(lambda _: self._clobber())
  317. df.addCallback(lambda _: self._clone())
  318. reactor.callLater(delay, df.callback, None)
  319. return df
  320. return res
  321. if self.retry:
  322. d.addCallback(_retry)
  323. return d
  324. def checkHg(self):
  325. d = self._dovccmd(['--version'])
  326. @d.addCallback
  327. def check(res):
  328. return res == 0
  329. return d
  330. def applyPatch(self, patch):
  331. d = self._dovccmd(['import', '--no-commit', '-p', str(patch[0]), '-'],
  332. initialStdin=patch[1])
  333. return d