upload.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. #
  2. # Copyright (C) 2008 The Android Open Source Project
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import re
  16. import sys
  17. from command import InteractiveCommand
  18. from editor import Editor
  19. from error import UploadError
  20. def _die(fmt, *args):
  21. msg = fmt % args
  22. print >>sys.stderr, 'error: %s' % msg
  23. sys.exit(1)
  24. def _SplitEmails(values):
  25. result = []
  26. for str in values:
  27. result.extend([s.strip() for s in str.split(',')])
  28. return result
  29. class Upload(InteractiveCommand):
  30. common = True
  31. helpSummary = "Upload changes for code review"
  32. helpUsage="""
  33. %prog [--re --cc] {[<project>]... | --replace <project>}
  34. """
  35. helpDescription = """
  36. The '%prog' command is used to send changes to the Gerrit Code
  37. Review system. It searches for topic branches in local projects
  38. that have not yet been published for review. If multiple topic
  39. branches are found, '%prog' opens an editor to allow the user to
  40. select which branches to upload.
  41. '%prog' searches for uploadable changes in all projects listed at
  42. the command line. Projects can be specified either by name, or by
  43. a relative or absolute path to the project's local directory. If no
  44. projects are specified, '%prog' will search for uploadable changes
  45. in all projects listed in the manifest.
  46. If the --reviewers or --cc options are passed, those emails are
  47. added to the respective list of users, and emails are sent to any
  48. new users. Users passed as --reviewers must already be registered
  49. with the code review system, or the upload will fail.
  50. If the --replace option is passed the user can designate which
  51. existing change(s) in Gerrit match up to the commits in the branch
  52. being uploaded. For each matched pair of change,commit the commit
  53. will be added as a new patch set, completely replacing the set of
  54. files and description associated with the change in Gerrit.
  55. Configuration
  56. -------------
  57. review.URL.autoupload:
  58. To disable the "Upload ... (y/n)?" prompt, you can set a per-project
  59. or global Git configuration option. If review.URL.autoupload is set
  60. to "true" then repo will assume you always answer "y" at the prompt,
  61. and will not prompt you further. If it is set to "false" then repo
  62. will assume you always answer "n", and will abort.
  63. The URL must match the review URL listed in the manifest XML file,
  64. or in the .git/config within the project. For example:
  65. [remote "origin"]
  66. url = git://git.example.com/project.git
  67. review = http://review.example.com/
  68. [review "http://review.example.com/"]
  69. autoupload = true
  70. References
  71. ----------
  72. Gerrit Code Review: http://code.google.com/p/gerrit/
  73. """
  74. def _Options(self, p):
  75. p.add_option('--replace',
  76. dest='replace', action='store_true',
  77. help='Upload replacement patchesets from this branch')
  78. p.add_option('--re', '--reviewers',
  79. type='string', action='append', dest='reviewers',
  80. help='Request reviews from these people.')
  81. p.add_option('--cc',
  82. type='string', action='append', dest='cc',
  83. help='Also send email to these email addresses.')
  84. def _SingleBranch(self, branch, people):
  85. project = branch.project
  86. name = branch.name
  87. remote = project.GetBranch(name).remote
  88. key = 'review.%s.autoupload' % remote.review
  89. answer = project.config.GetBoolean(key)
  90. if answer is False:
  91. _die("upload blocked by %s = false" % key)
  92. if answer is None:
  93. date = branch.date
  94. list = branch.commits
  95. print 'Upload project %s/:' % project.relpath
  96. print ' branch %s (%2d commit%s, %s):' % (
  97. name,
  98. len(list),
  99. len(list) != 1 and 's' or '',
  100. date)
  101. for commit in list:
  102. print ' %s' % commit
  103. sys.stdout.write('to %s (y/n)? ' % remote.review)
  104. answer = sys.stdin.readline().strip()
  105. answer = answer in ('y', 'Y', 'yes', '1', 'true', 't')
  106. if answer:
  107. self._UploadAndReport([branch], people)
  108. else:
  109. _die("upload aborted by user")
  110. def _MultipleBranches(self, pending, people):
  111. projects = {}
  112. branches = {}
  113. script = []
  114. script.append('# Uncomment the branches to upload:')
  115. for project, avail in pending:
  116. script.append('#')
  117. script.append('# project %s/:' % project.relpath)
  118. b = {}
  119. for branch in avail:
  120. name = branch.name
  121. date = branch.date
  122. list = branch.commits
  123. if b:
  124. script.append('#')
  125. script.append('# branch %s (%2d commit%s, %s):' % (
  126. name,
  127. len(list),
  128. len(list) != 1 and 's' or '',
  129. date))
  130. for commit in list:
  131. script.append('# %s' % commit)
  132. b[name] = branch
  133. projects[project.relpath] = project
  134. branches[project.name] = b
  135. script.append('')
  136. script = Editor.EditString("\n".join(script)).split("\n")
  137. project_re = re.compile(r'^#?\s*project\s*([^\s]+)/:$')
  138. branch_re = re.compile(r'^\s*branch\s*([^\s(]+)\s*\(.*')
  139. project = None
  140. todo = []
  141. for line in script:
  142. m = project_re.match(line)
  143. if m:
  144. name = m.group(1)
  145. project = projects.get(name)
  146. if not project:
  147. _die('project %s not available for upload', name)
  148. continue
  149. m = branch_re.match(line)
  150. if m:
  151. name = m.group(1)
  152. if not project:
  153. _die('project for branch %s not in script', name)
  154. branch = branches[project.name].get(name)
  155. if not branch:
  156. _die('branch %s not in %s', name, project.relpath)
  157. todo.append(branch)
  158. if not todo:
  159. _die("nothing uncommented for upload")
  160. self._UploadAndReport(todo, people)
  161. def _FindGerritChange(self, branch):
  162. last_pub = branch.project.WasPublished(branch.name)
  163. if last_pub is None:
  164. return ""
  165. refs = branch.GetPublishedRefs()
  166. try:
  167. # refs/changes/XYZ/N --> XYZ
  168. return refs.get(last_pub).split('/')[-2]
  169. except:
  170. return ""
  171. def _ReplaceBranch(self, project, people):
  172. branch = project.CurrentBranch
  173. if not branch:
  174. print >>sys.stdout, "no branches ready for upload"
  175. return
  176. branch = project.GetUploadableBranch(branch)
  177. if not branch:
  178. print >>sys.stdout, "no branches ready for upload"
  179. return
  180. script = []
  181. script.append('# Replacing from branch %s' % branch.name)
  182. if len(branch.commits) == 1:
  183. change = self._FindGerritChange(branch)
  184. script.append('[%-6s] %s' % (change, branch.commits[0]))
  185. else:
  186. for commit in branch.commits:
  187. script.append('[ ] %s' % commit)
  188. script.append('')
  189. script.append('# Insert change numbers in the brackets to add a new patch set.')
  190. script.append('# To create a new change record, leave the brackets empty.')
  191. script = Editor.EditString("\n".join(script)).split("\n")
  192. change_re = re.compile(r'^\[\s*(\d{1,})\s*\]\s*([0-9a-f]{1,}) .*$')
  193. to_replace = dict()
  194. full_hashes = branch.unabbrev_commits
  195. for line in script:
  196. m = change_re.match(line)
  197. if m:
  198. c = m.group(1)
  199. f = m.group(2)
  200. try:
  201. f = full_hashes[f]
  202. except KeyError:
  203. print 'fh = %s' % full_hashes
  204. print >>sys.stderr, "error: commit %s not found" % f
  205. sys.exit(1)
  206. if c in to_replace:
  207. print >>sys.stderr,\
  208. "error: change %s cannot accept multiple commits" % c
  209. sys.exit(1)
  210. to_replace[c] = f
  211. if not to_replace:
  212. print >>sys.stderr, "error: no replacements specified"
  213. print >>sys.stderr, " use 'repo upload' without --replace"
  214. sys.exit(1)
  215. branch.replace_changes = to_replace
  216. self._UploadAndReport([branch], people)
  217. def _UploadAndReport(self, todo, people):
  218. have_errors = False
  219. for branch in todo:
  220. try:
  221. branch.UploadForReview(people)
  222. branch.uploaded = True
  223. except UploadError, e:
  224. branch.error = e
  225. branch.uploaded = False
  226. have_errors = True
  227. print >>sys.stderr, ''
  228. print >>sys.stderr, '--------------------------------------------'
  229. if have_errors:
  230. for branch in todo:
  231. if not branch.uploaded:
  232. print >>sys.stderr, '[FAILED] %-15s %-15s (%s)' % (
  233. branch.project.relpath + '/', \
  234. branch.name, \
  235. branch.error)
  236. print >>sys.stderr, ''
  237. for branch in todo:
  238. if branch.uploaded:
  239. print >>sys.stderr, '[OK ] %-15s %s' % (
  240. branch.project.relpath + '/',
  241. branch.name)
  242. if have_errors:
  243. sys.exit(1)
  244. def Execute(self, opt, args):
  245. project_list = self.GetProjects(args)
  246. pending = []
  247. reviewers = []
  248. cc = []
  249. if opt.reviewers:
  250. reviewers = _SplitEmails(opt.reviewers)
  251. if opt.cc:
  252. cc = _SplitEmails(opt.cc)
  253. people = (reviewers,cc)
  254. if opt.replace:
  255. if len(project_list) != 1:
  256. print >>sys.stderr, \
  257. 'error: --replace requires exactly one project'
  258. sys.exit(1)
  259. self._ReplaceBranch(project_list[0], people)
  260. return
  261. for project in project_list:
  262. avail = project.GetUploadableBranches()
  263. if avail:
  264. pending.append((project, avail))
  265. if not pending:
  266. print >>sys.stdout, "no branches ready for upload"
  267. elif len(pending) == 1 and len(pending[0][1]) == 1:
  268. self._SingleBranch(pending[0][1][0], people)
  269. else:
  270. self._MultipleBranches(pending, people)