api.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. # Copyright 2013 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. from recipe_engine import recipe_api
  5. class GerritApi(recipe_api.RecipeApi):
  6. """Module for interact with Gerrit endpoints"""
  7. def __init__(self, *args, **kwargs):
  8. super(GerritApi, self).__init__(*args, **kwargs)
  9. self._changes_target_branch_cache = {}
  10. def __call__(self, name, cmd, infra_step=True, **kwargs):
  11. """Wrapper for easy calling of gerrit_utils steps."""
  12. assert isinstance(cmd, (list, tuple))
  13. prefix = 'gerrit '
  14. env = self.m.context.env
  15. env.setdefault('PATH', '%(PATH)s')
  16. env['PATH'] = self.m.path.pathsep.join([
  17. env['PATH'], str(self.repo_resource())])
  18. with self.m.context(env=env):
  19. return self.m.python(prefix + name,
  20. self.repo_resource('gerrit_client.py'),
  21. cmd,
  22. infra_step=infra_step,
  23. venv=True,
  24. **kwargs)
  25. def create_gerrit_branch(self, host, project, branch, commit, **kwargs):
  26. """Creates a new branch from given project and commit
  27. Returns:
  28. The ref of the branch created
  29. """
  30. args = [
  31. 'branch',
  32. '--host', host,
  33. '--project', project,
  34. '--branch', branch,
  35. '--commit', commit,
  36. '--json_file', self.m.json.output()
  37. ]
  38. step_name = 'create_gerrit_branch (%s %s)' % (project, branch)
  39. step_result = self(step_name, args, **kwargs)
  40. ref = step_result.json.output.get('ref')
  41. return ref
  42. # TODO(machenbach): Rename to get_revision? And maybe above to
  43. # create_ref?
  44. def get_gerrit_branch(self, host, project, branch, **kwargs):
  45. """Gets a branch from given project and commit
  46. Returns:
  47. The revision of the branch
  48. """
  49. args = [
  50. 'branchinfo',
  51. '--host', host,
  52. '--project', project,
  53. '--branch', branch,
  54. '--json_file', self.m.json.output()
  55. ]
  56. step_name = 'get_gerrit_branch (%s %s)' % (project, branch)
  57. step_result = self(step_name, args, **kwargs)
  58. revision = step_result.json.output.get('revision')
  59. return revision
  60. def get_change_description(self,
  61. host,
  62. change,
  63. patchset,
  64. timeout=None,
  65. step_test_data=None):
  66. """Gets the description for a given CL and patchset.
  67. Args:
  68. host: URL of Gerrit host to query.
  69. change: The change number.
  70. patchset: The patchset number.
  71. Returns:
  72. The description corresponding to given CL and patchset.
  73. """
  74. ri = self.get_revision_info(host, change, patchset, timeout, step_test_data)
  75. return ri['commit']['message']
  76. def get_revision_info(self,
  77. host,
  78. change,
  79. patchset,
  80. timeout=None,
  81. step_test_data=None):
  82. """
  83. Returns the info for a given patchset of a given change.
  84. Args:
  85. host: Gerrit host to query.
  86. change: The change number.
  87. patchset: The patchset number.
  88. Returns:
  89. A dict for the target revision as documented here:
  90. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
  91. """
  92. assert int(change), change
  93. assert int(patchset), patchset
  94. step_test_data = step_test_data or (
  95. lambda: self.test_api.get_one_change_response_data(change_number=change,
  96. patchset=patchset))
  97. cls = self.get_changes(host,
  98. query_params=[('change', str(change))],
  99. o_params=['ALL_REVISIONS', 'ALL_COMMITS'],
  100. limit=1,
  101. timeout=timeout,
  102. step_test_data=step_test_data)
  103. cl = cls[0] if len(cls) == 1 else {'revisions': {}}
  104. for ri in cl['revisions'].values():
  105. # TODO(tandrii): add support for patchset=='current'.
  106. if str(ri['_number']) == str(patchset):
  107. return ri
  108. raise self.m.step.InfraFailure(
  109. 'Error querying for CL description: host:%r change:%r; patchset:%r' % (
  110. host, change, patchset))
  111. def get_changes(self, host, query_params, start=None, limit=None,
  112. o_params=None, step_test_data=None, **kwargs):
  113. """Queries changes for the given host.
  114. Args:
  115. * host: URL of Gerrit host to query.
  116. * query_params: Query parameters as list of (key, value) tuples to form a
  117. query as documented here:
  118. https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators
  119. * start: How many changes to skip (starting with the most recent).
  120. * limit: Maximum number of results to return.
  121. * o_params: A list of additional output specifiers, as documented here:
  122. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
  123. * step_test_data: Optional mock test data for the underlying gerrit client.
  124. Returns:
  125. A list of change dicts as documented here:
  126. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
  127. """
  128. args = [
  129. 'changes',
  130. '--host', host,
  131. '--json_file', self.m.json.output()
  132. ]
  133. if start:
  134. args += ['--start', str(start)]
  135. if limit:
  136. args += ['--limit', str(limit)]
  137. for k, v in query_params:
  138. args += ['-p', '%s=%s' % (k, v)]
  139. for v in (o_params or []):
  140. args += ['-o', v]
  141. if not step_test_data:
  142. step_test_data = lambda: self.test_api.get_one_change_response_data()
  143. return self(
  144. kwargs.pop('name', 'changes'),
  145. args,
  146. step_test_data=step_test_data,
  147. **kwargs
  148. ).json.output
  149. def get_related_changes(self, host, change, revision='current', step_test_data=None):
  150. """Queries related changes for a given host, change, and revision.
  151. Args:
  152. * host: URL of Gerrit host to query.
  153. * change: The change-id of the change to get related changes for as
  154. documented here:
  155. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id
  156. * revision: The revision-id of the revision to get related changes for as
  157. documented here:
  158. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-id
  159. This defaults to current, which names the most recent patch set.
  160. * step_test_data: Optional mock test data for the underlying gerrit client.
  161. Returns:
  162. A related changes dictionary as documented here:
  163. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#related-changes-info
  164. """
  165. args = [
  166. 'relatedchanges',
  167. '--host',
  168. host,
  169. '--change',
  170. change,
  171. '--revision',
  172. revision,
  173. '--json_file',
  174. self.m.json.output(),
  175. ]
  176. if not step_test_data:
  177. step_test_data = lambda: self.test_api.get_related_changes_response_data()
  178. return self('relatedchanges', args,
  179. step_test_data=step_test_data).json.output
  180. def abandon_change(self, host, change, message=None, name=None,
  181. step_test_data=None):
  182. args = [
  183. 'abandon',
  184. '--host', host,
  185. '--change', int(change),
  186. '--json_file', self.m.json.output(),
  187. ]
  188. if message:
  189. args.extend(['--message', message])
  190. if not step_test_data:
  191. step_test_data = lambda: self.test_api.get_one_change_response_data(
  192. status='ABANDONED', _number=str(change))
  193. return self(
  194. name or 'abandon',
  195. args,
  196. step_test_data=step_test_data,
  197. ).json.output
  198. def set_change_label(self,
  199. host,
  200. change,
  201. label_name,
  202. label_value,
  203. name=None,
  204. step_test_data=None):
  205. args = [
  206. 'setlabel', '--host', host, '--change',
  207. int(change), '--json_file',
  208. self.m.json.output(), '-l', label_name, label_value
  209. ]
  210. return self(
  211. name or 'setlabel',
  212. args,
  213. step_test_data=step_test_data,
  214. ).json.output
  215. def move_changes(self,
  216. host,
  217. project,
  218. from_branch,
  219. to_branch,
  220. step_test_data=None):
  221. args = [
  222. 'movechanges', '--host', host, '-p',
  223. 'project=%s' % project, '-p',
  224. 'branch=%s' % from_branch, '-p', 'status=open', '--destination_branch',
  225. to_branch, '--json_file',
  226. self.m.json.output()
  227. ]
  228. if not step_test_data:
  229. step_test_data = lambda: self.test_api.get_one_change_response_data(
  230. branch=to_branch)
  231. return self(
  232. 'move changes',
  233. args,
  234. step_test_data=step_test_data,
  235. ).json.output
  236. def update_files(self,
  237. host,
  238. project,
  239. branch,
  240. new_contents_by_file_path,
  241. commit_msg,
  242. params=frozenset(['status=NEW']),
  243. submit=False):
  244. """Update a set of files by creating and submitting a Gerrit CL.
  245. Args:
  246. * host: URL of Gerrit host to name.
  247. * project: Gerrit project name, e.g. chromium/src.
  248. * branch: The branch to land the change, e.g. main
  249. * new_contents_by_file_path: Dict of the new contents with file path as
  250. the key.
  251. * commit_msg: Description to add to the CL.
  252. * params: A list of additional ChangeInput specifiers, with format
  253. 'key=value'.
  254. * submit: Should land this CL instantly.
  255. Returns:
  256. A ChangeInfo dictionary as documented here:
  257. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#create-change
  258. Or if the change is submitted, here:
  259. https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submit-change
  260. """
  261. assert len(new_contents_by_file_path
  262. ) > 0, 'The dict of file paths should not be empty.'
  263. command = [
  264. 'createchange',
  265. '--host',
  266. host,
  267. '--project',
  268. project,
  269. '--branch',
  270. branch,
  271. '--subject',
  272. commit_msg,
  273. '--json_file',
  274. self.m.json.output(),
  275. ]
  276. for p in params:
  277. command.extend(['-p', p])
  278. step_result = self('create change at (%s %s)' % (project, branch), command)
  279. change = int(step_result.json.output.get('_number'))
  280. step_result.presentation.links['change %d' %
  281. change] = '%s/#/q/%d' % (host, change)
  282. with self.m.step.nest('update contents in CL %d' % change):
  283. for path, content in new_contents_by_file_path.items():
  284. _file = self.m.path.mkstemp()
  285. self.m.file.write_raw('store the new content for %s' % path, _file,
  286. content)
  287. self('edit file %s' % path, [
  288. 'changeedit',
  289. '--host',
  290. host,
  291. '--change',
  292. change,
  293. '--path',
  294. path,
  295. '--file',
  296. _file,
  297. ])
  298. self('publish edit', [
  299. 'publishchangeedit',
  300. '--host',
  301. host,
  302. '--change',
  303. change,
  304. ])
  305. if submit:
  306. self('set Bot-Commit+1 for change %d' % change, [
  307. 'setbotcommit',
  308. '--host',
  309. host,
  310. '--change',
  311. change,
  312. ])
  313. submit_cmd = [
  314. 'submitchange',
  315. '--host',
  316. host,
  317. '--change',
  318. change,
  319. '--json_file',
  320. self.m.json.output(),
  321. ]
  322. step_result = self('submit change %d' % change, submit_cmd)
  323. return step_result.json.output