libv8 4.5.95.5 → 5.0.71.48.0beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (332) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +45 -19
  3. data/CHANGELOG.md +14 -0
  4. data/README.md +30 -15
  5. data/Rakefile +7 -6
  6. data/ext/libv8/arch.rb +5 -4
  7. data/ext/libv8/builder.rb +25 -19
  8. data/ext/libv8/compiler.rb +6 -33
  9. data/ext/libv8/location.rb +7 -8
  10. data/lib/libv8/version.rb +1 -1
  11. data/libv8.gemspec +1 -1
  12. data/patches/build-standalone-static-library.patch +14 -0
  13. data/patches/disable-building-tests.patch +48 -10
  14. data/patches/fPIC-for-static.patch +3 -3
  15. data/release/x86-linux/Vagrantfile +8 -4
  16. data/release/x86_64-freebsd10/Vagrantfile +86 -0
  17. data/release/x86_64-linux/Vagrantfile +8 -4
  18. data/spec/compiler_spec.rb +5 -29
  19. data/spec/support/compiler_helpers.rb +2 -4
  20. data/vendor/depot_tools/.gitignore +15 -3
  21. data/vendor/depot_tools/OWNERS +2 -2
  22. data/vendor/depot_tools/PRESUBMIT.py +4 -2
  23. data/vendor/depot_tools/WATCHLISTS +6 -0
  24. data/vendor/depot_tools/apply_issue.py +70 -38
  25. data/vendor/depot_tools/bootstrap/win/README.md +66 -0
  26. data/vendor/depot_tools/bootstrap/win/git-bash.template.sh +12 -0
  27. data/vendor/depot_tools/bootstrap/win/git.template.bat +5 -0
  28. data/vendor/depot_tools/bootstrap/win/profile.d.python.sh +20 -0
  29. data/vendor/depot_tools/bootstrap/win/win_tools.bat +96 -45
  30. data/vendor/depot_tools/breakpad.py +6 -141
  31. data/vendor/depot_tools/buildbucket.py +45 -31
  32. data/vendor/depot_tools/cbuildbot +1 -0
  33. data/vendor/depot_tools/checkout.py +2 -1
  34. data/vendor/depot_tools/chrome_set_ver +1 -0
  35. data/vendor/depot_tools/cit +8 -0
  36. data/vendor/depot_tools/cit.bat +11 -0
  37. data/vendor/depot_tools/cit.py +120 -0
  38. data/vendor/depot_tools/codereview.settings +0 -2
  39. data/vendor/depot_tools/commit_queue +1 -5
  40. data/vendor/depot_tools/commit_queue.bat +1 -4
  41. data/vendor/depot_tools/commit_queue.py +78 -29
  42. data/vendor/depot_tools/cpplint.py +22 -14
  43. data/vendor/depot_tools/cros +1 -0
  44. data/vendor/depot_tools/cros_sdk +1 -0
  45. data/vendor/depot_tools/depot-tools-auth.py +3 -3
  46. data/vendor/depot_tools/download_from_google_storage.py +101 -21
  47. data/vendor/depot_tools/drover.py +2 -3
  48. data/vendor/depot_tools/fetch.py +31 -27
  49. data/vendor/depot_tools/{recipes → fetch_configs}/android.py +4 -4
  50. data/vendor/depot_tools/fetch_configs/breakpad.py +45 -0
  51. data/vendor/depot_tools/{recipes → fetch_configs}/chromium.py +3 -3
  52. data/vendor/depot_tools/{recipes/recipe_util.py → fetch_configs/config_util.py} +3 -3
  53. data/vendor/depot_tools/fetch_configs/crashpad.py +41 -0
  54. data/vendor/depot_tools/{recipes → fetch_configs}/dart.py +3 -3
  55. data/vendor/depot_tools/{recipes/pdfium.py → fetch_configs/dartino.py} +14 -13
  56. data/vendor/depot_tools/{recipes → fetch_configs}/dartium.py +3 -3
  57. data/vendor/depot_tools/{recipes → fetch_configs}/depot_tools.py +3 -3
  58. data/vendor/depot_tools/fetch_configs/gyp.py +42 -0
  59. data/vendor/depot_tools/{recipes → fetch_configs}/infra.py +3 -3
  60. data/vendor/depot_tools/{recipes → fetch_configs}/infra_internal.py +3 -3
  61. data/vendor/depot_tools/{recipes → fetch_configs}/ios.py +4 -4
  62. data/vendor/depot_tools/{recipes → fetch_configs}/mojo.py +3 -3
  63. data/vendor/depot_tools/{recipes → fetch_configs}/nacl.py +3 -3
  64. data/vendor/depot_tools/{recipes → fetch_configs}/naclports.py +3 -3
  65. data/vendor/depot_tools/fetch_configs/pdfium.py +40 -0
  66. data/vendor/depot_tools/{recipes → fetch_configs}/skia.py +3 -3
  67. data/vendor/depot_tools/{recipes → fetch_configs}/skia_buildbot.py +3 -3
  68. data/vendor/depot_tools/fetch_configs/syzygy.py +41 -0
  69. data/vendor/depot_tools/{recipes → fetch_configs}/v8.py +3 -3
  70. data/vendor/depot_tools/{recipes → fetch_configs}/webrtc.py +3 -3
  71. data/vendor/depot_tools/{recipes → fetch_configs}/webrtc_android.py +4 -4
  72. data/vendor/depot_tools/{recipes → fetch_configs}/webrtc_ios.py +4 -4
  73. data/vendor/depot_tools/fix_encoding.py +6 -6
  74. data/vendor/depot_tools/gcl.py +11 -21
  75. data/vendor/depot_tools/gclient +10 -0
  76. data/vendor/depot_tools/gclient-new-workdir.py +7 -38
  77. data/vendor/depot_tools/gclient.bat +2 -2
  78. data/vendor/depot_tools/gclient.py +85 -65
  79. data/vendor/depot_tools/gclient_scm.py +83 -10
  80. data/vendor/depot_tools/gclient_utils.py +5 -1
  81. data/vendor/depot_tools/gerrit_util.py +243 -26
  82. data/vendor/depot_tools/git-auto-svn +1 -1
  83. data/vendor/depot_tools/git-cache +1 -1
  84. data/vendor/depot_tools/git-cherry-pick-upload +1 -1
  85. data/vendor/depot_tools/git-cl +1 -1
  86. data/vendor/depot_tools/git-drover +6 -0
  87. data/vendor/depot_tools/git-find-releases +6 -0
  88. data/vendor/depot_tools/git-footers +1 -1
  89. data/vendor/depot_tools/git-freeze +1 -1
  90. data/vendor/depot_tools/git-gs +1 -1
  91. data/vendor/depot_tools/git-hyper-blame +6 -0
  92. data/vendor/depot_tools/git-map +1 -1
  93. data/vendor/depot_tools/git-map-branches +1 -1
  94. data/vendor/depot_tools/git-mark-merge-base +1 -1
  95. data/vendor/depot_tools/git-nav-downstream +1 -1
  96. data/vendor/depot_tools/git-new-branch +1 -1
  97. data/vendor/depot_tools/git-number +1 -1
  98. data/vendor/depot_tools/git-rebase-update +1 -1
  99. data/vendor/depot_tools/git-rename-branch +1 -1
  100. data/vendor/depot_tools/git-reparent-branch +1 -1
  101. data/vendor/depot_tools/git-retry +1 -1
  102. data/vendor/depot_tools/git-squash-branch +1 -1
  103. data/vendor/depot_tools/git-thaw +1 -1
  104. data/vendor/depot_tools/git-try +1 -1
  105. data/vendor/depot_tools/git-upstream-diff +1 -1
  106. data/vendor/depot_tools/git_auto_svn.py +24 -6
  107. data/vendor/depot_tools/git_cache.py +74 -27
  108. data/vendor/depot_tools/git_cl.py +2118 -747
  109. data/vendor/depot_tools/git_common.py +100 -6
  110. data/vendor/depot_tools/git_dates.py +62 -0
  111. data/vendor/depot_tools/git_drover.py +424 -0
  112. data/vendor/depot_tools/git_find_releases.py +65 -0
  113. data/vendor/depot_tools/git_footers.py +42 -0
  114. data/vendor/depot_tools/git_hyper_blame.py +391 -0
  115. data/vendor/depot_tools/git_map_branches.py +8 -6
  116. data/vendor/depot_tools/git_new_branch.py +6 -1
  117. data/vendor/depot_tools/git_rebase_update.py +56 -16
  118. data/vendor/depot_tools/git_reparent_branch.py +13 -0
  119. data/vendor/depot_tools/git_try.py +0 -2
  120. data/vendor/depot_tools/gsutil.py +51 -20
  121. data/vendor/depot_tools/infra/config/OWNERS +3 -1
  122. data/vendor/depot_tools/infra/config/cq.cfg +7 -3
  123. data/vendor/depot_tools/infra/config/recipes.cfg +9 -0
  124. data/vendor/depot_tools/luci_hacks/README.md +35 -0
  125. data/vendor/depot_tools/{bootstrap/virtualenv/tests → luci_hacks}/__init__.py +0 -0
  126. data/vendor/depot_tools/luci_hacks/luci_recipe_run.isolate +12 -0
  127. data/vendor/depot_tools/luci_hacks/luci_recipe_run.py +81 -0
  128. data/vendor/depot_tools/luci_hacks/trigger_luci_job.py +128 -0
  129. data/vendor/depot_tools/man/html/depot_tools.html +9 -1
  130. data/vendor/depot_tools/man/html/depot_tools_tutorial.html +4 -4
  131. data/vendor/depot_tools/man/html/git-drover.html +191 -35
  132. data/vendor/depot_tools/man/html/git-hyper-blame.html +878 -0
  133. data/vendor/depot_tools/man/html/git-rebase-update.html +9 -4
  134. data/vendor/depot_tools/man/man1/git-drover.1 +189 -36
  135. data/vendor/depot_tools/man/man1/git-hyper-blame.1 +128 -0
  136. data/vendor/depot_tools/man/man1/git-rebase-update.1 +8 -6
  137. data/vendor/depot_tools/man/man7/depot_tools.7 +9 -4
  138. data/vendor/depot_tools/man/src/_git-hyper-blame_desc.helper.txt +1 -0
  139. data/vendor/depot_tools/man/src/common_demo_functions.sh +5 -0
  140. data/vendor/depot_tools/man/src/depot_tools_tutorial.txt +1 -1
  141. data/vendor/depot_tools/man/src/git-drover.demo.1.sh +11 -16
  142. data/vendor/depot_tools/man/src/git-drover.demo.3.sh +27 -0
  143. data/vendor/depot_tools/man/src/git-drover.demo.4.sh +39 -0
  144. data/vendor/depot_tools/man/src/git-drover.txt +49 -3
  145. data/vendor/depot_tools/man/src/git-hyper-blame.demo.1.sh +3 -0
  146. data/vendor/depot_tools/man/src/git-hyper-blame.demo.2.sh +4 -0
  147. data/vendor/depot_tools/man/src/git-hyper-blame.demo.common.sh +57 -0
  148. data/vendor/depot_tools/man/src/git-hyper-blame.txt +85 -0
  149. data/vendor/depot_tools/man/src/git-rebase-update.txt +5 -1
  150. data/vendor/depot_tools/my_activity.py +6 -21
  151. data/vendor/depot_tools/ninja +2 -2
  152. data/vendor/depot_tools/ninja-linux32 +0 -0
  153. data/vendor/depot_tools/ninja-linux64 +0 -0
  154. data/vendor/depot_tools/ninja-mac +0 -0
  155. data/vendor/depot_tools/ninja.exe +0 -0
  156. data/vendor/depot_tools/presubmit_canned_checks.py +83 -69
  157. data/vendor/depot_tools/presubmit_support.py +126 -42
  158. data/vendor/depot_tools/pylint.py +5 -1
  159. data/vendor/depot_tools/python_runner.sh +55 -0
  160. data/vendor/depot_tools/recipe_modules/bot_update/__init__.py +32 -0
  161. data/vendor/depot_tools/recipe_modules/bot_update/api.py +283 -0
  162. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/basic.json +56 -0
  163. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/basic_output_manifest.json +63 -0
  164. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/basic_with_branch_heads.json +57 -0
  165. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/clobber.json +44 -0
  166. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/forced.json +57 -0
  167. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/gerrit_no_reset.json +44 -0
  168. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/no_shallow.json +44 -0
  169. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/off.json +43 -0
  170. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/reset_root_solution_revision.json +43 -0
  171. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/svn_mode.json +59 -0
  172. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/trychange.json +58 -0
  173. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/trychange_oauth2.json +60 -0
  174. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/tryjob.json +58 -0
  175. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/tryjob_fail.json +60 -0
  176. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/tryjob_fail_patch.json +81 -0
  177. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/tryjob_fail_patch_download.json +81 -0
  178. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/tryjob_gerrit_angle.json +49 -0
  179. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/tryjob_v8.json +61 -0
  180. data/vendor/depot_tools/recipe_modules/bot_update/example.expected/tryjob_v8_head_by_default.json +51 -0
  181. data/vendor/depot_tools/recipe_modules/bot_update/example.py +172 -0
  182. data/vendor/depot_tools/{bootstrap/virtualenv/virtualenv_support → recipe_modules/bot_update/resources}/__init__.py +0 -0
  183. data/vendor/depot_tools/recipe_modules/bot_update/resources/bot_update.py +1764 -0
  184. data/vendor/depot_tools/recipe_modules/bot_update/test_api.py +86 -0
  185. data/vendor/depot_tools/recipe_modules/depot_tools/__init__.py +3 -0
  186. data/vendor/depot_tools/recipe_modules/depot_tools/api.py +27 -0
  187. data/vendor/depot_tools/recipe_modules/gclient/__init__.py +10 -0
  188. data/vendor/depot_tools/recipe_modules/gclient/api.py +378 -0
  189. data/vendor/depot_tools/recipe_modules/gclient/config.py +671 -0
  190. data/vendor/depot_tools/recipe_modules/gclient/example.expected/basic.json +172 -0
  191. data/vendor/depot_tools/recipe_modules/gclient/example.expected/revision.json +174 -0
  192. data/vendor/depot_tools/recipe_modules/gclient/example.expected/tryserver.json +185 -0
  193. data/vendor/depot_tools/recipe_modules/gclient/example.py +100 -0
  194. data/vendor/depot_tools/recipe_modules/gclient/test_api.py +37 -0
  195. data/vendor/depot_tools/recipe_modules/git/__init__.py +9 -0
  196. data/vendor/depot_tools/recipe_modules/git/api.py +377 -0
  197. data/vendor/depot_tools/recipe_modules/git/example.expected/basic.json +177 -0
  198. data/vendor/depot_tools/recipe_modules/git/example.expected/basic_branch.json +177 -0
  199. data/vendor/depot_tools/recipe_modules/git/example.expected/basic_file_name.json +179 -0
  200. data/vendor/depot_tools/recipe_modules/git/example.expected/basic_hash.json +176 -0
  201. data/vendor/depot_tools/recipe_modules/git/example.expected/basic_ref.json +177 -0
  202. data/vendor/depot_tools/recipe_modules/git/example.expected/basic_submodule_update_force.json +178 -0
  203. data/vendor/depot_tools/recipe_modules/git/example.expected/can_fail_build.json +153 -0
  204. data/vendor/depot_tools/recipe_modules/git/example.expected/cannot_fail_build.json +181 -0
  205. data/vendor/depot_tools/recipe_modules/git/example.expected/cat-file_test.json +199 -0
  206. data/vendor/depot_tools/recipe_modules/git/example.expected/count-objects_delta.json +250 -0
  207. data/vendor/depot_tools/recipe_modules/git/example.expected/count-objects_failed.json +181 -0
  208. data/vendor/depot_tools/recipe_modules/git/example.expected/count-objects_with_bad_output.json +182 -0
  209. data/vendor/depot_tools/recipe_modules/git/example.expected/count-objects_with_bad_output_fails_build.json +102 -0
  210. data/vendor/depot_tools/recipe_modules/git/example.expected/curl_trace_file.json +181 -0
  211. data/vendor/depot_tools/recipe_modules/git/example.expected/platform_win.json +186 -0
  212. data/vendor/depot_tools/recipe_modules/git/example.expected/rebase_failed.json +179 -0
  213. data/vendor/depot_tools/recipe_modules/git/example.expected/remote_not_origin.json +179 -0
  214. data/vendor/depot_tools/recipe_modules/git/example.expected/set_got_revision.json +178 -0
  215. data/vendor/depot_tools/recipe_modules/git/example.py +147 -0
  216. data/vendor/depot_tools/recipe_modules/git/resources/git_setup.py +61 -0
  217. data/vendor/depot_tools/recipe_modules/git/test_api.py +18 -0
  218. data/vendor/depot_tools/recipe_modules/git_cl/__init__.py +4 -0
  219. data/vendor/depot_tools/recipe_modules/git_cl/api.py +25 -0
  220. data/vendor/depot_tools/recipe_modules/git_cl/config.py +22 -0
  221. data/vendor/depot_tools/recipe_modules/git_cl/example.expected/basic.json +66 -0
  222. data/vendor/depot_tools/recipe_modules/git_cl/example.py +41 -0
  223. data/vendor/depot_tools/recipe_modules/infra_paths/__init__.py +4 -0
  224. data/vendor/depot_tools/recipe_modules/infra_paths/api.py +12 -0
  225. data/vendor/depot_tools/recipe_modules/infra_paths/example.expected/basic.json +14 -0
  226. data/vendor/depot_tools/recipe_modules/infra_paths/example.expected/paths_buildbot_linux.json +14 -0
  227. data/vendor/depot_tools/recipe_modules/infra_paths/example.expected/paths_buildbot_mac.json +14 -0
  228. data/vendor/depot_tools/recipe_modules/infra_paths/example.expected/paths_buildbot_win.json +14 -0
  229. data/vendor/depot_tools/recipe_modules/infra_paths/example.expected/paths_kitchen_linux.json +14 -0
  230. data/vendor/depot_tools/recipe_modules/infra_paths/example.expected/paths_kitchen_mac.json +14 -0
  231. data/vendor/depot_tools/recipe_modules/infra_paths/example.expected/paths_kitchen_win.json +14 -0
  232. data/vendor/depot_tools/recipe_modules/infra_paths/example.py +28 -0
  233. data/vendor/depot_tools/recipe_modules/infra_paths/path_config.py +45 -0
  234. data/vendor/depot_tools/recipe_modules/presubmit/__init__.py +4 -0
  235. data/vendor/depot_tools/recipe_modules/presubmit/api.py +20 -0
  236. data/vendor/depot_tools/recipe_modules/presubmit/example.expected/basic.json +18 -0
  237. data/vendor/depot_tools/recipe_modules/presubmit/example.py +15 -0
  238. data/vendor/depot_tools/recipe_modules/rietveld/__init__.py +5 -0
  239. data/vendor/depot_tools/recipe_modules/rietveld/api.py +94 -0
  240. data/vendor/depot_tools/recipe_modules/rietveld/example.expected/basic.json +30 -0
  241. data/vendor/depot_tools/recipe_modules/rietveld/example.py +24 -0
  242. data/vendor/depot_tools/recipe_modules/tryserver/__init__.py +15 -0
  243. data/vendor/depot_tools/recipe_modules/tryserver/api.py +280 -0
  244. data/vendor/depot_tools/recipe_modules/tryserver/example.expected/with_git_patch.json +104 -0
  245. data/vendor/depot_tools/recipe_modules/tryserver/example.expected/with_rietveld_patch.json +58 -0
  246. data/vendor/depot_tools/recipe_modules/tryserver/example.expected/with_rietveld_patch_new.json +58 -0
  247. data/vendor/depot_tools/recipe_modules/tryserver/example.expected/with_svn_patch.json +68 -0
  248. data/vendor/depot_tools/recipe_modules/tryserver/example.expected/with_wrong_patch.json +43 -0
  249. data/vendor/depot_tools/recipe_modules/tryserver/example.expected/with_wrong_patch_new.json +43 -0
  250. data/vendor/depot_tools/recipe_modules/tryserver/example.py +53 -0
  251. data/vendor/depot_tools/recipe_modules/tryserver/test_api.py +7 -0
  252. data/vendor/depot_tools/recipes.py +136 -0
  253. data/vendor/depot_tools/repo +1 -1
  254. data/vendor/depot_tools/rietveld.py +46 -15
  255. data/vendor/depot_tools/roll_dep.py +97 -36
  256. data/vendor/depot_tools/scm.py +3 -3
  257. data/vendor/depot_tools/setup_color.py +94 -0
  258. data/vendor/depot_tools/subprocess2.py +10 -1
  259. data/vendor/depot_tools/third_party/cq_client/OWNERS +0 -1
  260. data/vendor/depot_tools/third_party/cq_client/README.md +47 -9
  261. data/vendor/depot_tools/third_party/cq_client/cq.pb.go +617 -0
  262. data/vendor/depot_tools/third_party/cq_client/cq.proto +75 -17
  263. data/vendor/depot_tools/third_party/cq_client/cq_pb2.py +168 -41
  264. data/vendor/depot_tools/third_party/cq_client/testdata/cq_gerrit.cfg +55 -0
  265. data/vendor/depot_tools/third_party/cq_client/{test/cq_example.cfg → testdata/cq_rietveld.cfg} +14 -6
  266. data/vendor/depot_tools/third_party/fancy_urllib/README +5 -4
  267. data/vendor/depot_tools/third_party/fancy_urllib/__init__.py +114 -52
  268. data/vendor/depot_tools/third_party/protobuf26/README.chromium +9 -6
  269. data/vendor/depot_tools/third_party/upload.py +17 -31
  270. data/vendor/depot_tools/trychange.py +0 -2
  271. data/vendor/depot_tools/update_depot_tools +29 -11
  272. data/vendor/depot_tools/update_depot_tools.bat +4 -9
  273. data/vendor/depot_tools/upload_to_google_storage.py +42 -5
  274. data/vendor/depot_tools/win_toolchain/OWNERS +1 -0
  275. data/vendor/depot_tools/win_toolchain/get_toolchain_if_necessary.py +227 -52
  276. data/vendor/depot_tools/win_toolchain/package_from_installed.py +203 -88
  277. metadata +161 -81
  278. data/patches/arm/do-not-imply-vfp3-and-armv7.patch +0 -16
  279. data/patches/arm/do-not-use-vfp2.patch +0 -13
  280. data/patches/clang51/no-unused-variable.patch +0 -12
  281. data/vendor/depot_tools/bootstrap/.gitignore +0 -2
  282. data/vendor/depot_tools/bootstrap/bootstrap.py +0 -234
  283. data/vendor/depot_tools/bootstrap/deps.pyl +0 -15
  284. data/vendor/depot_tools/bootstrap/util.py +0 -87
  285. data/vendor/depot_tools/bootstrap/virtualenv/.gitignore +0 -10
  286. data/vendor/depot_tools/bootstrap/virtualenv/.travis.yml +0 -28
  287. data/vendor/depot_tools/bootstrap/virtualenv/AUTHORS.txt +0 -91
  288. data/vendor/depot_tools/bootstrap/virtualenv/CONTRIBUTING.rst +0 -21
  289. data/vendor/depot_tools/bootstrap/virtualenv/LICENSE.txt +0 -22
  290. data/vendor/depot_tools/bootstrap/virtualenv/MANIFEST.in +0 -11
  291. data/vendor/depot_tools/bootstrap/virtualenv/README.rst +0 -10
  292. data/vendor/depot_tools/bootstrap/virtualenv/bin/rebuild-script.py +0 -71
  293. data/vendor/depot_tools/bootstrap/virtualenv/docs/changes.rst +0 -747
  294. data/vendor/depot_tools/bootstrap/virtualenv/docs/conf.py +0 -149
  295. data/vendor/depot_tools/bootstrap/virtualenv/docs/development.rst +0 -61
  296. data/vendor/depot_tools/bootstrap/virtualenv/docs/index.rst +0 -137
  297. data/vendor/depot_tools/bootstrap/virtualenv/docs/installation.rst +0 -58
  298. data/vendor/depot_tools/bootstrap/virtualenv/docs/make.bat +0 -170
  299. data/vendor/depot_tools/bootstrap/virtualenv/docs/reference.rst +0 -256
  300. data/vendor/depot_tools/bootstrap/virtualenv/docs/userguide.rst +0 -249
  301. data/vendor/depot_tools/bootstrap/virtualenv/scripts/virtualenv +0 -3
  302. data/vendor/depot_tools/bootstrap/virtualenv/setup.py +0 -111
  303. data/vendor/depot_tools/bootstrap/virtualenv/tests/test_activate.sh +0 -94
  304. data/vendor/depot_tools/bootstrap/virtualenv/tests/test_activate_expected.output +0 -2
  305. data/vendor/depot_tools/bootstrap/virtualenv/tests/test_virtualenv.py +0 -139
  306. data/vendor/depot_tools/bootstrap/virtualenv/tests/tox.ini +0 -12
  307. data/vendor/depot_tools/bootstrap/virtualenv/tox.ini +0 -17
  308. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv.py +0 -2367
  309. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv_embedded/activate.bat +0 -26
  310. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv_embedded/activate.csh +0 -42
  311. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv_embedded/activate.fish +0 -74
  312. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv_embedded/activate.ps1 +0 -150
  313. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv_embedded/activate.sh +0 -80
  314. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv_embedded/activate_this.py +0 -34
  315. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv_embedded/deactivate.bat +0 -20
  316. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv_embedded/distutils-init.py +0 -101
  317. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv_embedded/distutils.cfg +0 -6
  318. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv_embedded/site.py +0 -758
  319. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv_support/pip-6.0-py2.py3-none-any.whl +0 -0
  320. data/vendor/depot_tools/bootstrap/virtualenv/virtualenv_support/setuptools-8.2.1-py2.py3-none-any.whl +0 -0
  321. data/vendor/depot_tools/bootstrap/win/README.google +0 -16
  322. data/vendor/depot_tools/cbuildbot +0 -96
  323. data/vendor/depot_tools/chrome_set_ver +0 -96
  324. data/vendor/depot_tools/cros +0 -96
  325. data/vendor/depot_tools/cros_sdk +0 -96
  326. data/vendor/depot_tools/git-cl-upload-hook +0 -52
  327. data/vendor/depot_tools/git-crup +0 -45
  328. data/vendor/depot_tools/python_git_runner.sh +0 -36
  329. data/vendor/depot_tools/recipes/blink.py +0 -59
  330. data/vendor/depot_tools/third_party/cq_client/test/validate_config_test.py +0 -52
  331. data/vendor/depot_tools/third_party/cq_client/validate_config.py +0 -108
  332. data/vendor/depot_tools/win_toolchain/toolchain2013.py +0 -494
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ # Copyright 2015 The Chromium Authors. All rights reserved.
3
+ # Use of this source code is governed by a BSD-style license that can be
4
+ # found in the LICENSE file.
5
+
6
+ . $(type -P python_runner.sh)
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ # Copyright 2014 The Chromium Authors. All rights reserved.
3
+ # Use of this source code is governed by a BSD-style license that can be
4
+ # found in the LICENSE file.
5
+
6
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -5,4 +5,4 @@
5
5
 
6
6
  SCRIPT=git_freezer.py
7
7
  set -- freeze "$@"
8
- . $(type -P python_git_runner.sh)
8
+ . $(type -P python_runner.sh)
@@ -6,4 +6,4 @@ git grep -n -e "$@" -- "*.h" "*.hpp" "*.cpp" "*.c" "*.cc" "*.cpp" "*.inl"\
6
6
  "*.grd" "*.grdp" "*.idl" "*.m" "*.mm" "*.py" "*.sh" "*.cfg" "*.tac" "*.go"\
7
7
  "*SConscript" "SConscript*" "*.scons" "*.vcproj" "*.vsprops" "*.make"\
8
8
  "*.gyp" "*.gypi" "*.isolate" "*.java" "*.js" "*.html" "*.css" "*.ebuild" \
9
- "*.pl" "*.pm" "*.yaml" "*.gn" "*.gni" "*.json"
9
+ "*.pl" "*.pm" "*.yaml" "*.gn" "*.gni" "*.json" "DEPS" "*/DEPS"
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ # Copyright 2016 The Chromium Authors. All rights reserved.
3
+ # Use of this source code is governed by a BSD-style license that can be
4
+ # found in the LICENSE file.
5
+
6
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh) | less -R
6
+ . $(type -P python_runner.sh) | less -R
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -5,4 +5,4 @@
5
5
 
6
6
  SCRIPT=git_retry.py
7
7
  set -- retry "$@"
8
- . $(type -P python_git_runner.sh)
8
+ . $(type -P python_runner.sh)
@@ -3,4 +3,4 @@
3
3
  # Use of this source code is governed by a BSD-style license that can be
4
4
  # found in the LICENSE file.
5
5
 
6
- . $(type -P python_git_runner.sh)
6
+ . $(type -P python_runner.sh)
@@ -10,4 +10,4 @@
10
10
 
11
11
  SCRIPT=git_freezer.py
12
12
  set -- thaw "$@"
13
- . $(type -P python_git_runner.sh)
13
+ . $(type -P python_runner.sh)
@@ -6,4 +6,4 @@
6
6
  # git_try.py - Commits a patch to the SVN try patch repo.
7
7
  # It is highly recommended to use `git cl try` instead.
8
8
 
9
- . $(type -P python_git_runner.sh)
9
+ . $(type -P python_runner.sh)
@@ -6,4 +6,4 @@
6
6
  # git_upstream_diff.py -- Provide the diff between the current branch and its
7
7
  # upstream.
8
8
 
9
- . $(type -P python_git_runner.sh)
9
+ . $(type -P python_runner.sh)
@@ -22,7 +22,8 @@ import subprocess2
22
22
 
23
23
  from git_common import run as run_git
24
24
  from git_common import run_stream_with_retcode as run_git_stream_with_retcode
25
- from git_common import set_config, root, ROOT
25
+ from git_common import set_config, root, ROOT, current_branch
26
+ from git_common import upstream as get_upstream
26
27
  from git_footers import get_footer_svn_id
27
28
 
28
29
 
@@ -57,7 +58,24 @@ def main(argv):
57
58
  description='Automatically set up git-svn for a repo mirrored from svn.')
58
59
  parser.parse_args(argv)
59
60
 
60
- upstream = root()
61
+ upstreams = []
62
+ # Always configure the upstream trunk.
63
+ upstreams.append(root())
64
+ # Optionally configure whatever upstream branch might be currently checked
65
+ # out. This is needed for work on svn-based branches, otherwise git-svn gets
66
+ # very confused and tries to relate branch commits back to trunk, making a big
67
+ # mess of the codereview patches, and generating all kinds of spurious errors
68
+ # about the repo being in some sort of bad state.
69
+ curr_upstream = get_upstream(current_branch())
70
+ # There will be no upstream if the checkout is in detached HEAD.
71
+ if curr_upstream:
72
+ upstreams.append(curr_upstream)
73
+ for upstream in upstreams:
74
+ config_svn(upstream)
75
+ return 0
76
+
77
+
78
+ def config_svn(upstream):
61
79
  svn_id = get_footer_svn_id(upstream)
62
80
  assert svn_id, 'No valid git-svn-id footer found on %s.' % upstream
63
81
  print 'Found git-svn-id footer %s on %s' % (svn_id, upstream)
@@ -86,14 +104,14 @@ def main(argv):
86
104
  assert svn_repo is not None, 'Unable to find svn repo for %s' % svn_id
87
105
  print 'Found upstream svn repo %s and path %s' % (svn_repo, svn_path)
88
106
 
89
- set_config('svn-remote.svn.url', svn_repo)
90
- set_config('svn-remote.svn.fetch',
91
- '%s:refs/remotes/%s' % (svn_path, upstream))
107
+ run_git('config', '--local', '--replace-all', 'svn-remote.svn.url', svn_repo)
108
+ run_git('config', '--local', '--replace-all', 'svn-remote.svn.fetch',
109
+ '%s:refs/remotes/%s' % (svn_path, upstream),
110
+ 'refs/remotes/%s$' % upstream)
92
111
  print 'Configured metadata, running "git svn fetch". This may take some time.'
93
112
  with run_git_stream_with_retcode('svn', 'fetch') as stdout:
94
113
  for line in stdout.xreadlines():
95
114
  print line.strip()
96
- return 0
97
115
 
98
116
 
99
117
  if __name__ == '__main__':
@@ -44,8 +44,9 @@ class RefsHeadsFailedToFetch(Exception):
44
44
  class Lockfile(object):
45
45
  """Class to represent a cross-platform process-specific lockfile."""
46
46
 
47
- def __init__(self, path):
47
+ def __init__(self, path, timeout=0):
48
48
  self.path = os.path.abspath(path)
49
+ self.timeout = timeout
49
50
  self.lockfile = self.path + ".lock"
50
51
  self.pid = os.getpid()
51
52
 
@@ -91,16 +92,25 @@ class Lockfile(object):
91
92
  def lock(self):
92
93
  """Acquire the lock.
93
94
 
94
- Note: This is a NON-BLOCKING FAIL-FAST operation.
95
- Do. Or do not. There is no try.
95
+ This will block with a deadline of self.timeout seconds.
96
96
  """
97
- try:
98
- self._make_lockfile()
99
- except OSError as e:
100
- if e.errno == errno.EEXIST:
101
- raise LockError("%s is already locked" % self.path)
102
- else:
103
- raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
97
+ elapsed = 0
98
+ while True:
99
+ try:
100
+ self._make_lockfile()
101
+ return
102
+ except OSError as e:
103
+ if elapsed < self.timeout:
104
+ sleep_time = max(10, min(3, self.timeout - elapsed))
105
+ logging.info('Could not create git cache lockfile; '
106
+ 'will retry after sleep(%d).', sleep_time);
107
+ elapsed += sleep_time
108
+ time.sleep(sleep_time)
109
+ continue
110
+ if e.errno == errno.EEXIST:
111
+ raise LockError("%s is already locked" % self.path)
112
+ else:
113
+ raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
104
114
 
105
115
  def unlock(self):
106
116
  """Release the lock."""
@@ -145,9 +155,24 @@ class Mirror(object):
145
155
  os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
146
156
  cachepath_lock = threading.Lock()
147
157
 
158
+ @staticmethod
159
+ def parse_fetch_spec(spec):
160
+ """Parses and canonicalizes a fetch spec.
161
+
162
+ Returns (fetchspec, value_regex), where value_regex can be used
163
+ with 'git config --replace-all'.
164
+ """
165
+ parts = spec.split(':', 1)
166
+ src = parts[0].lstrip('+').rstrip('/')
167
+ if not src.startswith('refs/'):
168
+ src = 'refs/heads/%s' % src
169
+ dest = parts[1].rstrip('/') if len(parts) > 1 else src
170
+ regex = r'\+%s:.*' % src.replace('*', r'\*')
171
+ return ('+%s:%s' % (src, dest), regex)
172
+
148
173
  def __init__(self, url, refs=None, print_func=None):
149
174
  self.url = url
150
- self.refs = refs or []
175
+ self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])])
151
176
  self.basedir = self.UrlToCacheDir(url)
152
177
  self.mirror_path = os.path.join(self.GetCachePath(), self.basedir)
153
178
  if print_func:
@@ -236,16 +261,9 @@ class Mirror(object):
236
261
  self.RunGit(['config', 'remote.origin.url', self.url], cwd=cwd)
237
262
  self.RunGit(['config', '--replace-all', 'remote.origin.fetch',
238
263
  '+refs/heads/*:refs/heads/*', r'\+refs/heads/\*:.*'], cwd=cwd)
239
- for ref in self.refs:
240
- ref = ref.lstrip('+').rstrip('/')
241
- if ref.startswith('refs/'):
242
- refspec = '+%s:%s' % (ref, ref)
243
- regex = r'\+%s:.*' % ref.replace('*', r'\*')
244
- else:
245
- refspec = '+refs/%s/*:refs/%s/*' % (ref, ref)
246
- regex = r'\+refs/heads/%s:.*' % ref.replace('*', r'\*')
264
+ for spec, value_regex in self.fetch_specs:
247
265
  self.RunGit(
248
- ['config', '--replace-all', 'remote.origin.fetch', refspec, regex],
266
+ ['config', '--replace-all', 'remote.origin.fetch', spec, value_regex],
249
267
  cwd=cwd)
250
268
 
251
269
  def bootstrap_repo(self, directory):
@@ -314,9 +332,27 @@ class Mirror(object):
314
332
  def exists(self):
315
333
  return os.path.isfile(os.path.join(self.mirror_path, 'config'))
316
334
 
335
+ def _preserve_fetchspec(self):
336
+ """Read and preserve remote.origin.fetch from an existing mirror.
337
+
338
+ This modifies self.fetch_specs.
339
+ """
340
+ if not self.exists():
341
+ return
342
+ try:
343
+ config_fetchspecs = subprocess.check_output(
344
+ [self.git_exe, 'config', '--get-all', 'remote.origin.fetch'],
345
+ cwd=self.mirror_path)
346
+ for fetchspec in config_fetchspecs.splitlines():
347
+ self.fetch_specs.add(self.parse_fetch_spec(fetchspec))
348
+ except subprocess.CalledProcessError:
349
+ logging.warn('Tried and failed to preserve remote.origin.fetch from the '
350
+ 'existing cache directory. You may need to manually edit '
351
+ '%s and "git cache fetch" again.'
352
+ % os.path.join(self.mirror_path, 'config'))
353
+
317
354
  def _ensure_bootstrapped(self, depth, bootstrap, force=False):
318
355
  tempdir = None
319
- config_file = os.path.join(self.mirror_path, 'config')
320
356
  pack_dir = os.path.join(self.mirror_path, 'objects', 'pack')
321
357
  pack_files = []
322
358
 
@@ -324,16 +360,19 @@ class Mirror(object):
324
360
  pack_files = [f for f in os.listdir(pack_dir) if f.endswith('.pack')]
325
361
 
326
362
  should_bootstrap = (force or
327
- not os.path.exists(config_file) or
363
+ not self.exists() or
328
364
  len(pack_files) > GC_AUTOPACKLIMIT)
329
365
  if should_bootstrap:
366
+ if self.exists():
367
+ # Re-bootstrapping an existing mirror; preserve existing fetch spec.
368
+ self._preserve_fetchspec()
330
369
  tempdir = tempfile.mkdtemp(
331
370
  prefix='_cache_tmp', suffix=self.basedir, dir=self.GetCachePath())
332
371
  bootstrapped = not depth and bootstrap and self.bootstrap_repo(tempdir)
333
372
  if bootstrapped:
334
373
  # Bootstrap succeeded; delete previous cache, if any.
335
374
  gclient_utils.rmtree(self.mirror_path)
336
- elif not os.path.exists(config_file):
375
+ elif not self.exists():
337
376
  # Bootstrap failed, no previous cache; start with a bare git dir.
338
377
  self.RunGit(['init', '--bare'], cwd=tempdir)
339
378
  else:
@@ -372,13 +411,13 @@ class Mirror(object):
372
411
  logging.warn('Fetch of %s failed' % spec)
373
412
 
374
413
  def populate(self, depth=None, shallow=False, bootstrap=False,
375
- verbose=False, ignore_lock=False):
414
+ verbose=False, ignore_lock=False, lock_timeout=0):
376
415
  assert self.GetCachePath()
377
416
  if shallow and not depth:
378
417
  depth = 10000
379
418
  gclient_utils.safe_makedirs(self.GetCachePath())
380
419
 
381
- lockfile = Lockfile(self.mirror_path)
420
+ lockfile = Lockfile(self.mirror_path, lock_timeout)
382
421
  if not ignore_lock:
383
422
  lockfile.lock()
384
423
 
@@ -553,6 +592,7 @@ def CMDpopulate(parser, args):
553
592
  'shallow': options.shallow,
554
593
  'bootstrap': not options.no_bootstrap,
555
594
  'ignore_lock': options.ignore_locks,
595
+ 'lock_timeout': options.timeout,
556
596
  }
557
597
  if options.depth:
558
598
  kwargs['depth'] = options.depth
@@ -563,6 +603,9 @@ def CMDpopulate(parser, args):
563
603
  def CMDfetch(parser, args):
564
604
  """Update mirror, and fetch in cwd."""
565
605
  parser.add_option('--all', action='store_true', help='Fetch all remotes')
606
+ parser.add_option('--no_bootstrap', '--no-bootstrap',
607
+ action='store_true',
608
+ help='Don\'t (re)bootstrap from Google Storage')
566
609
  options, args = parser.parse_args(args)
567
610
 
568
611
  # Figure out which remotes to fetch. This mimics the behavior of regular
@@ -593,7 +636,8 @@ def CMDfetch(parser, args):
593
636
  git_dir = os.path.abspath(git_dir)
594
637
  if git_dir.startswith(cachepath):
595
638
  mirror = Mirror.FromPath(git_dir)
596
- mirror.populate()
639
+ mirror.populate(
640
+ bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
597
641
  return 0
598
642
  for remote in remotes:
599
643
  remote_url = subprocess.check_output(
@@ -602,7 +646,8 @@ def CMDfetch(parser, args):
602
646
  mirror = Mirror.FromPath(remote_url)
603
647
  mirror.print = lambda *args: None
604
648
  print('Updating git cache...')
605
- mirror.populate()
649
+ mirror.populate(
650
+ bootstrap=not options.no_bootstrap, lock_timeout=options.timeout)
606
651
  subprocess.check_call([Mirror.git_exe, 'fetch', remote])
607
652
  return 0
608
653
 
@@ -651,6 +696,8 @@ class OptionParser(optparse.OptionParser):
651
696
  help='Increase verbosity (can be passed multiple times)')
652
697
  self.add_option('-q', '--quiet', action='store_true',
653
698
  help='Suppress all extraneous output')
699
+ self.add_option('--timeout', type='int', default=0,
700
+ help='Timeout for acquiring cache lock, in seconds')
654
701
 
655
702
  def parse_args(self, args=None, values=None):
656
703
  options, args = optparse.OptionParser.parse_args(self, args, values)
@@ -5,7 +5,7 @@
5
5
 
6
6
  # Copyright (C) 2008 Evan Martin <martine@danga.com>
7
7
 
8
- """A git-command for integrating reviews on Rietveld."""
8
+ """A git-command for integrating reviews on Rietveld and Gerrit."""
9
9
 
10
10
  from distutils.version import LooseVersion
11
11
  from multiprocessing.pool import ThreadPool
@@ -15,6 +15,7 @@ import glob
15
15
  import httplib
16
16
  import json
17
17
  import logging
18
+ import multiprocessing
18
19
  import optparse
19
20
  import os
20
21
  import Queue
@@ -25,8 +26,10 @@ import tempfile
25
26
  import textwrap
26
27
  import time
27
28
  import traceback
29
+ import urllib
28
30
  import urllib2
29
31
  import urlparse
32
+ import uuid
30
33
  import webbrowser
31
34
  import zlib
32
35
 
@@ -39,13 +42,17 @@ from third_party import colorama
39
42
  from third_party import httplib2
40
43
  from third_party import upload
41
44
  import auth
42
- import breakpad # pylint: disable=W0611
45
+ from luci_hacks import trigger_luci_job as luci_trigger
43
46
  import clang_format
47
+ import commit_queue
44
48
  import dart_format
49
+ import setup_color
45
50
  import fix_encoding
46
51
  import gclient_utils
52
+ import gerrit_util
53
+ import git_cache
47
54
  import git_common
48
- from git_footers import get_footer_svn_id
55
+ import git_footers
49
56
  import owners
50
57
  import owners_finder
51
58
  import presubmit_support
@@ -61,15 +68,11 @@ DEFAULT_SERVER = 'https://codereview.appspot.com'
61
68
  POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
62
69
  DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
63
70
  GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
64
- CHANGE_ID = 'Change-Id:'
65
71
  REFS_THAT_ALIAS_TO_OTHER_REFS = {
66
72
  'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
67
73
  'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
68
74
  }
69
75
 
70
- # Buildbucket-related constants
71
- BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
72
-
73
76
  # Valid extensions for files we want to lint.
74
77
  DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
75
78
  DEFAULT_LINT_IGNORE_REGEX = r"$^"
@@ -93,9 +96,9 @@ def GetNoGitPagerEnv():
93
96
  return env
94
97
 
95
98
 
96
- def RunCommand(args, error_ok=False, error_message=None, **kwargs):
99
+ def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
97
100
  try:
98
- return subprocess2.check_output(args, shell=False, **kwargs)
101
+ return subprocess2.check_output(args, shell=shell, **kwargs)
99
102
  except subprocess2.CalledProcessError as e:
100
103
  logging.debug('Failed running %s', args)
101
104
  if not error_ok:
@@ -156,7 +159,7 @@ def ask_for_data(prompt):
156
159
 
157
160
 
158
161
  def git_set_branch_value(key, value):
159
- branch = Changelist().GetBranch()
162
+ branch = GetCurrentBranch()
160
163
  if not branch:
161
164
  return
162
165
 
@@ -168,7 +171,7 @@ def git_set_branch_value(key, value):
168
171
 
169
172
 
170
173
  def git_get_branch_default(key, default):
171
- branch = Changelist().GetBranch()
174
+ branch = GetCurrentBranch()
172
175
  if branch:
173
176
  git_key = 'branch.%s.%s' % (branch, key)
174
177
  (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
@@ -217,6 +220,16 @@ def add_git_similarity(parser):
217
220
  parser.parse_args = Parse
218
221
 
219
222
 
223
+ def _get_properties_from_options(options):
224
+ properties = dict(x.split('=', 1) for x in options.properties)
225
+ for key, val in properties.iteritems():
226
+ try:
227
+ properties[key] = json.loads(val)
228
+ except ValueError:
229
+ pass # If a value couldn't be evaluated, treat it as a string.
230
+ return properties
231
+
232
+
220
233
  def _prefix_master(master):
221
234
  """Convert user-specified master name to full master name.
222
235
 
@@ -231,8 +244,57 @@ def _prefix_master(master):
231
244
  return '%s%s' % (prefix, master)
232
245
 
233
246
 
234
- def trigger_try_jobs(auth_config, changelist, options, masters, category,
235
- override_properties=None):
247
+ def _buildbucket_retry(operation_name, http, *args, **kwargs):
248
+ """Retries requests to buildbucket service and returns parsed json content."""
249
+ try_count = 0
250
+ while True:
251
+ response, content = http.request(*args, **kwargs)
252
+ try:
253
+ content_json = json.loads(content)
254
+ except ValueError:
255
+ content_json = None
256
+
257
+ # Buildbucket could return an error even if status==200.
258
+ if content_json and content_json.get('error'):
259
+ error = content_json.get('error')
260
+ if error.get('code') == 403:
261
+ raise BuildbucketResponseException(
262
+ 'Access denied: %s' % error.get('message', ''))
263
+ msg = 'Error in response. Reason: %s. Message: %s.' % (
264
+ error.get('reason', ''), error.get('message', ''))
265
+ raise BuildbucketResponseException(msg)
266
+
267
+ if response.status == 200:
268
+ if not content_json:
269
+ raise BuildbucketResponseException(
270
+ 'Buildbucket returns invalid json content: %s.\n'
271
+ 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
272
+ content)
273
+ return content_json
274
+ if response.status < 500 or try_count >= 2:
275
+ raise httplib2.HttpLib2Error(content)
276
+
277
+ # status >= 500 means transient failures.
278
+ logging.debug('Transient errors when %s. Will retry.', operation_name)
279
+ time.sleep(0.5 + 1.5*try_count)
280
+ try_count += 1
281
+ assert False, 'unreachable'
282
+
283
+
284
+ def trigger_luci_job(changelist, masters, options):
285
+ """Send a job to run on LUCI."""
286
+ issue_props = changelist.GetIssueProperties()
287
+ issue = changelist.GetIssue()
288
+ patchset = changelist.GetMostRecentPatchset()
289
+ for builders_and_tests in sorted(masters.itervalues()):
290
+ # TODO(hinoka et al): add support for other properties.
291
+ # Currently, this completely ignores testfilter and other properties.
292
+ for builder in sorted(builders_and_tests):
293
+ luci_trigger.trigger(
294
+ builder, 'HEAD', issue, patchset, issue_props['project'])
295
+
296
+
297
+ def trigger_try_jobs(auth_config, changelist, options, masters, category):
236
298
  rietveld_url = settings.GetDefaultServerUrl()
237
299
  rietveld_host = urlparse.urlparse(rietveld_url).hostname
238
300
  authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
@@ -241,10 +303,11 @@ def trigger_try_jobs(auth_config, changelist, options, masters, category,
241
303
  issue_props = changelist.GetIssueProperties()
242
304
  issue = changelist.GetIssue()
243
305
  patchset = changelist.GetMostRecentPatchset()
306
+ properties = _get_properties_from_options(options)
244
307
 
245
308
  buildbucket_put_url = (
246
309
  'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
247
- hostname=BUILDBUCKET_HOST))
310
+ hostname=options.buildbucket_host))
248
311
  buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
249
312
  hostname=rietveld_host,
250
313
  issue=issue,
@@ -260,9 +323,10 @@ def trigger_try_jobs(auth_config, changelist, options, masters, category,
260
323
  print_text.append(' %s: %s' % (builder, tests))
261
324
  parameters = {
262
325
  'builder_name': builder,
263
- 'changes': [
264
- {'author': {'email': issue_props['owner_email']}},
265
- ],
326
+ 'changes': [{
327
+ 'author': {'email': issue_props['owner_email']},
328
+ 'revision': options.revision,
329
+ }],
266
330
  'properties': {
267
331
  'category': category,
268
332
  'issue': issue,
@@ -271,19 +335,22 @@ def trigger_try_jobs(auth_config, changelist, options, masters, category,
271
335
  'patch_storage': 'rietveld',
272
336
  'patchset': patchset,
273
337
  'reason': options.name,
274
- 'revision': options.revision,
275
338
  'rietveld': rietveld_url,
276
- 'testfilter': tests,
277
339
  },
278
340
  }
279
- if override_properties:
280
- parameters['properties'].update(override_properties)
341
+ if 'presubmit' in builder.lower():
342
+ parameters['properties']['dry_run'] = 'true'
343
+ if tests:
344
+ parameters['properties']['testfilter'] = tests
345
+ if properties:
346
+ parameters['properties'].update(properties)
281
347
  if options.clobber:
282
348
  parameters['properties']['clobber'] = True
283
349
  batch_req_body['builds'].append(
284
350
  {
285
351
  'bucket': bucket,
286
352
  'parameters_json': json.dumps(parameters),
353
+ 'client_operation_id': str(uuid.uuid4()),
287
354
  'tags': ['builder:%s' % builder,
288
355
  'buildset:%s' % buildset,
289
356
  'master:%s' % master,
@@ -291,42 +358,155 @@ def trigger_try_jobs(auth_config, changelist, options, masters, category,
291
358
  }
292
359
  )
293
360
 
294
- for try_count in xrange(3):
295
- response, content = http.request(
296
- buildbucket_put_url,
297
- 'PUT',
298
- body=json.dumps(batch_req_body),
299
- headers={'Content-Type': 'application/json'},
300
- )
301
- content_json = None
302
- try:
303
- content_json = json.loads(content)
304
- except ValueError:
305
- pass
361
+ _buildbucket_retry(
362
+ 'triggering tryjobs',
363
+ http,
364
+ buildbucket_put_url,
365
+ 'PUT',
366
+ body=json.dumps(batch_req_body),
367
+ headers={'Content-Type': 'application/json'}
368
+ )
369
+ print_text.append('To see results here, run: git cl try-results')
370
+ print_text.append('To see results in browser, run: git cl web')
371
+ print '\n'.join(print_text)
306
372
 
307
- # Buildbucket could return an error even if status==200.
308
- if content_json and content_json.get('error'):
309
- msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
310
- content_json['error'].get('code', ''),
311
- content_json['error'].get('reason', ''),
312
- content_json['error'].get('message', ''))
313
- raise BuildbucketResponseException(msg)
314
373
 
315
- if response.status == 200:
316
- if not content_json:
317
- raise BuildbucketResponseException(
318
- 'Buildbucket returns invalid json content: %s.\n'
319
- 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
320
- content)
374
+ def fetch_try_jobs(auth_config, changelist, options):
375
+ """Fetches tryjobs from buildbucket.
376
+
377
+ Returns a map from build id to build info as json dictionary.
378
+ """
379
+ rietveld_url = settings.GetDefaultServerUrl()
380
+ rietveld_host = urlparse.urlparse(rietveld_url).hostname
381
+ authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
382
+ if authenticator.has_cached_credentials():
383
+ http = authenticator.authorize(httplib2.Http())
384
+ else:
385
+ print ('Warning: Some results might be missing because %s' %
386
+ # Get the message on how to login.
387
+ auth.LoginRequiredError(rietveld_host).message)
388
+ http = httplib2.Http()
389
+
390
+ http.force_exception_to_status_code = True
391
+
392
+ buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
393
+ hostname=rietveld_host,
394
+ issue=changelist.GetIssue(),
395
+ patch=options.patchset)
396
+ params = {'tag': 'buildset:%s' % buildset}
397
+
398
+ builds = {}
399
+ while True:
400
+ url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
401
+ hostname=options.buildbucket_host,
402
+ params=urllib.urlencode(params))
403
+ content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
404
+ for build in content.get('builds', []):
405
+ builds[build['id']] = build
406
+ if 'next_cursor' in content:
407
+ params['start_cursor'] = content['next_cursor']
408
+ else:
321
409
  break
322
- if response.status < 500 or try_count >= 2:
323
- raise httplib2.HttpLib2Error(content)
410
+ return builds
324
411
 
325
- # status >= 500 means transient failures.
326
- logging.debug('Transient errors when triggering tryjobs. Will retry.')
327
- time.sleep(0.5 + 1.5*try_count)
328
412
 
329
- print '\n'.join(print_text)
413
+ def print_tryjobs(options, builds):
414
+ """Prints nicely result of fetch_try_jobs."""
415
+ if not builds:
416
+ print 'No tryjobs scheduled'
417
+ return
418
+
419
+ # Make a copy, because we'll be modifying builds dictionary.
420
+ builds = builds.copy()
421
+ builder_names_cache = {}
422
+
423
+ def get_builder(b):
424
+ try:
425
+ return builder_names_cache[b['id']]
426
+ except KeyError:
427
+ try:
428
+ parameters = json.loads(b['parameters_json'])
429
+ name = parameters['builder_name']
430
+ except (ValueError, KeyError) as error:
431
+ print 'WARNING: failed to get builder name for build %s: %s' % (
432
+ b['id'], error)
433
+ name = None
434
+ builder_names_cache[b['id']] = name
435
+ return name
436
+
437
+ def get_bucket(b):
438
+ bucket = b['bucket']
439
+ if bucket.startswith('master.'):
440
+ return bucket[len('master.'):]
441
+ return bucket
442
+
443
+ if options.print_master:
444
+ name_fmt = '%%-%ds %%-%ds' % (
445
+ max(len(str(get_bucket(b))) for b in builds.itervalues()),
446
+ max(len(str(get_builder(b))) for b in builds.itervalues()))
447
+ def get_name(b):
448
+ return name_fmt % (get_bucket(b), get_builder(b))
449
+ else:
450
+ name_fmt = '%%-%ds' % (
451
+ max(len(str(get_builder(b))) for b in builds.itervalues()))
452
+ def get_name(b):
453
+ return name_fmt % get_builder(b)
454
+
455
+ def sort_key(b):
456
+ return b['status'], b.get('result'), get_name(b), b.get('url')
457
+
458
+ def pop(title, f, color=None, **kwargs):
459
+ """Pop matching builds from `builds` dict and print them."""
460
+
461
+ if not options.color or color is None:
462
+ colorize = str
463
+ else:
464
+ colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
465
+
466
+ result = []
467
+ for b in builds.values():
468
+ if all(b.get(k) == v for k, v in kwargs.iteritems()):
469
+ builds.pop(b['id'])
470
+ result.append(b)
471
+ if result:
472
+ print colorize(title)
473
+ for b in sorted(result, key=sort_key):
474
+ print ' ', colorize('\t'.join(map(str, f(b))))
475
+
476
+ total = len(builds)
477
+ pop(status='COMPLETED', result='SUCCESS',
478
+ title='Successes:', color=Fore.GREEN,
479
+ f=lambda b: (get_name(b), b.get('url')))
480
+ pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
481
+ title='Infra Failures:', color=Fore.MAGENTA,
482
+ f=lambda b: (get_name(b), b.get('url')))
483
+ pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
484
+ title='Failures:', color=Fore.RED,
485
+ f=lambda b: (get_name(b), b.get('url')))
486
+ pop(status='COMPLETED', result='CANCELED',
487
+ title='Canceled:', color=Fore.MAGENTA,
488
+ f=lambda b: (get_name(b),))
489
+ pop(status='COMPLETED', result='FAILURE',
490
+ failure_reason='INVALID_BUILD_DEFINITION',
491
+ title='Wrong master/builder name:', color=Fore.MAGENTA,
492
+ f=lambda b: (get_name(b),))
493
+ pop(status='COMPLETED', result='FAILURE',
494
+ title='Other failures:',
495
+ f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
496
+ pop(status='COMPLETED',
497
+ title='Other finished:',
498
+ f=lambda b: (get_name(b), b.get('result'), b.get('url')))
499
+ pop(status='STARTED',
500
+ title='Started:', color=Fore.YELLOW,
501
+ f=lambda b: (get_name(b), b.get('url')))
502
+ pop(status='SCHEDULED',
503
+ title='Scheduled:',
504
+ f=lambda b: (get_name(b), 'id=%s' % b['id']))
505
+ # The last section is just in case buildbucket API changes OR there is a bug.
506
+ pop(title='Other:',
507
+ f=lambda b: (get_name(b), 'id=%s' % b['id']))
508
+ assert len(builds) == 0
509
+ print 'Total: %d tryjobs' % total
330
510
 
331
511
 
332
512
  def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
@@ -411,6 +591,8 @@ class Settings(object):
411
591
  self.viewvc_url = None
412
592
  self.updated = False
413
593
  self.is_gerrit = None
594
+ self.squash_gerrit_uploads = None
595
+ self.gerrit_skip_ensure_authenticated = None
414
596
  self.git_editor = None
415
597
  self.project = None
416
598
  self.force_https_commit_url = None
@@ -428,10 +610,6 @@ class Settings(object):
428
610
  cr_settings_file = FindCodereviewSettingsFile()
429
611
  if autoupdate != 'false' and cr_settings_file:
430
612
  LoadCodereviewSettingsFromFile(cr_settings_file)
431
- # set updated to True to avoid infinite calling loop
432
- # through DownloadHooks
433
- self.updated = True
434
- DownloadHooks(False)
435
613
  self.updated = True
436
614
 
437
615
  def GetDefaultServerUrl(self, error_ok=False):
@@ -457,6 +635,19 @@ class Settings(object):
457
635
  self.root = os.path.abspath(self.GetRelativeRoot())
458
636
  return self.root
459
637
 
638
+ def GetGitMirror(self, remote='origin'):
639
+ """If this checkout is from a local git mirror, return a Mirror object."""
640
+ local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
641
+ if not os.path.isdir(local_url):
642
+ return None
643
+ git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
644
+ remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
645
+ # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
646
+ mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
647
+ if mirror.exists():
648
+ return mirror
649
+ return None
650
+
460
651
  def GetIsGitSvn(self):
461
652
  """Return true if this repo looks like it's using git-svn."""
462
653
  if self.is_git_svn is None:
@@ -554,6 +745,11 @@ class Settings(object):
554
745
  def GetBugPrefix(self):
555
746
  return self._GetRietveldConfig('bug-prefix', error_ok=True)
556
747
 
748
+ def GetIsSkipDependencyUpload(self, branch_name):
749
+ """Returns true if specified branch should skip dep uploads."""
750
+ return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
751
+ error_ok=True)
752
+
557
753
  def GetRunPostUploadHook(self):
558
754
  run_post_upload_hook = self._GetRietveldConfig(
559
755
  'run-post-upload-hook', error_ok=True)
@@ -571,6 +767,23 @@ class Settings(object):
571
767
  self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
572
768
  return self.is_gerrit
573
769
 
770
+ def GetSquashGerritUploads(self):
771
+ """Return true if uploads to Gerrit should be squashed by default."""
772
+ if self.squash_gerrit_uploads is None:
773
+ self.squash_gerrit_uploads = (
774
+ RunGit(['config', '--bool', 'gerrit.squash-uploads'],
775
+ error_ok=True).strip() == 'true')
776
+ return self.squash_gerrit_uploads
777
+
778
+ def GetGerritSkipEnsureAuthenticated(self):
779
+ """Return True if EnsureAuthenticated should not be done for Gerrit
780
+ uploads."""
781
+ if self.gerrit_skip_ensure_authenticated is None:
782
+ self.gerrit_skip_ensure_authenticated = (
783
+ RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
784
+ error_ok=True).strip() == 'true')
785
+ return self.gerrit_skip_ensure_authenticated
786
+
574
787
  def GetGitEditor(self):
575
788
  """Return the editor specified in the git config, or None if none is."""
576
789
  if self.git_editor is None:
@@ -605,6 +818,9 @@ class Settings(object):
605
818
  def _GetRietveldConfig(self, param, **kwargs):
606
819
  return self._GetConfig('rietveld.' + param, **kwargs)
607
820
 
821
+ def _GetBranchConfig(self, branch_name, param, **kwargs):
822
+ return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
823
+
608
824
  def _GetConfig(self, param, **kwargs):
609
825
  self.LazyUpdateIfNeeded()
610
826
  return RunGit(['config', param], **kwargs).strip()
@@ -612,23 +828,110 @@ class Settings(object):
612
828
 
613
829
  def ShortBranchName(branch):
614
830
  """Convert a name like 'refs/heads/foo' to just 'foo'."""
615
- return branch.replace('refs/heads/', '')
831
+ return branch.replace('refs/heads/', '', 1)
832
+
833
+
834
+ def GetCurrentBranchRef():
835
+ """Returns branch ref (e.g., refs/heads/master) or None."""
836
+ return RunGit(['symbolic-ref', 'HEAD'],
837
+ stderr=subprocess2.VOID, error_ok=True).strip() or None
838
+
839
+
840
+ def GetCurrentBranch():
841
+ """Returns current branch or None.
842
+
843
+ For refs/heads/* branches, returns just last part. For others, full ref.
844
+ """
845
+ branchref = GetCurrentBranchRef()
846
+ if branchref:
847
+ return ShortBranchName(branchref)
848
+ return None
849
+
850
+
851
+ class _CQState(object):
852
+ """Enum for states of CL with respect to Commit Queue."""
853
+ NONE = 'none'
854
+ DRY_RUN = 'dry_run'
855
+ COMMIT = 'commit'
856
+
857
+ ALL_STATES = [NONE, DRY_RUN, COMMIT]
858
+
859
+
860
+ class _ParsedIssueNumberArgument(object):
861
+ def __init__(self, issue=None, patchset=None, hostname=None):
862
+ self.issue = issue
863
+ self.patchset = patchset
864
+ self.hostname = hostname
865
+
866
+ @property
867
+ def valid(self):
868
+ return self.issue is not None
869
+
870
+
871
+ class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
872
+ def __init__(self, *args, **kwargs):
873
+ self.patch_url = kwargs.pop('patch_url', None)
874
+ super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
875
+
876
+
877
+ def ParseIssueNumberArgument(arg):
878
+ """Parses the issue argument and returns _ParsedIssueNumberArgument."""
879
+ fail_result = _ParsedIssueNumberArgument()
880
+
881
+ if arg.isdigit():
882
+ return _ParsedIssueNumberArgument(issue=int(arg))
883
+ if not arg.startswith('http'):
884
+ return fail_result
885
+ url = gclient_utils.UpgradeToHttps(arg)
886
+ try:
887
+ parsed_url = urlparse.urlparse(url)
888
+ except ValueError:
889
+ return fail_result
890
+ for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
891
+ tmp = cls.ParseIssueURL(parsed_url)
892
+ if tmp is not None:
893
+ return tmp
894
+ return fail_result
616
895
 
617
896
 
618
897
  class Changelist(object):
619
- def __init__(self, branchref=None, issue=None, auth_config=None):
898
+ """Changelist works with one changelist in local branch.
899
+
900
+ Supports two codereview backends: Rietveld or Gerrit, selected at object
901
+ creation.
902
+
903
+ Notes:
904
+ * Not safe for concurrent multi-{thread,process} use.
905
+ * Caches values from current branch. Therefore, re-use after branch change
906
+ with care.
907
+ """
908
+
909
+ def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
910
+ """Create a new ChangeList instance.
911
+
912
+ If issue is given, the codereview must be given too.
913
+
914
+ If `codereview` is given, it must be 'rietveld' or 'gerrit'.
915
+ Otherwise, it's decided based on current configuration of the local branch,
916
+ with default being 'rietveld' for backwards compatibility.
917
+ See _load_codereview_impl for more details.
918
+
919
+ **kwargs will be passed directly to codereview implementation.
920
+ """
620
921
  # Poke settings so we get the "configure your server" message if necessary.
621
922
  global settings
622
923
  if not settings:
623
924
  # Happens when git_cl.py is used as a utility library.
624
925
  settings = Settings()
625
- settings.GetDefaultServerUrl()
926
+
927
+ if issue:
928
+ assert codereview, 'codereview must be known, if issue is known'
929
+
626
930
  self.branchref = branchref
627
931
  if self.branchref:
628
932
  self.branch = ShortBranchName(self.branchref)
629
933
  else:
630
934
  self.branch = None
631
- self.rietveld_server = None
632
935
  self.upstream_branch = None
633
936
  self.lookedup_issue = False
634
937
  self.issue = issue or None
@@ -638,14 +941,43 @@ class Changelist(object):
638
941
  self.patchset = None
639
942
  self.cc = None
640
943
  self.watchers = ()
641
- self._auth_config = auth_config
642
- self._props = None
643
944
  self._remote = None
644
- self._rpc_server = None
645
945
 
646
- @property
647
- def auth_config(self):
648
- return self._auth_config
946
+ self._codereview_impl = None
947
+ self._codereview = None
948
+ self._load_codereview_impl(codereview, **kwargs)
949
+ assert self._codereview_impl
950
+ assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
951
+
952
+ def _load_codereview_impl(self, codereview=None, **kwargs):
953
+ if codereview:
954
+ assert codereview in _CODEREVIEW_IMPLEMENTATIONS
955
+ cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
956
+ self._codereview = codereview
957
+ self._codereview_impl = cls(self, **kwargs)
958
+ return
959
+
960
+ # Automatic selection based on issue number set for a current branch.
961
+ # Rietveld takes precedence over Gerrit.
962
+ assert not self.issue
963
+ # Whether we find issue or not, we are doing the lookup.
964
+ self.lookedup_issue = True
965
+ for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
966
+ setting = cls.IssueSetting(self.GetBranch())
967
+ issue = RunGit(['config', setting], error_ok=True).strip()
968
+ if issue:
969
+ self._codereview = codereview
970
+ self._codereview_impl = cls(self, **kwargs)
971
+ self.issue = int(issue)
972
+ return
973
+
974
+ # No issue is set for this branch, so decide based on repo-wide settings.
975
+ return self._load_codereview_impl(
976
+ codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
977
+ **kwargs)
978
+
979
+ def IsGerrit(self):
980
+ return self._codereview == 'gerrit'
649
981
 
650
982
  def GetCCList(self):
651
983
  """Return the users cc'd on this CL.
@@ -673,8 +1005,7 @@ class Changelist(object):
673
1005
  def GetBranch(self):
674
1006
  """Returns the short branch name, e.g. 'master'."""
675
1007
  if not self.branch:
676
- branchref = RunGit(['symbolic-ref', 'HEAD'],
677
- stderr=subprocess2.VOID, error_ok=True).strip()
1008
+ branchref = GetCurrentBranchRef()
678
1009
  if not branchref:
679
1010
  return None
680
1011
  self.branchref = branchref
@@ -686,6 +1017,10 @@ class Changelist(object):
686
1017
  self.GetBranch() # Poke the lazy loader.
687
1018
  return self.branchref
688
1019
 
1020
+ def ClearBranch(self):
1021
+ """Clears cached branch data of this object."""
1022
+ self.branch = self.branchref = None
1023
+
689
1024
  @staticmethod
690
1025
  def FetchUpstreamTuple(branch):
691
1026
  """Returns a tuple containing remote and remote ref,
@@ -718,11 +1053,12 @@ class Changelist(object):
718
1053
  remote = 'origin'
719
1054
  upstream_branch = 'refs/heads/trunk'
720
1055
  else:
721
- DieWithError("""Unable to determine default branch to diff against.
722
- Either pass complete "git diff"-style arguments, like
723
- git cl upload origin/master
724
- or verify this branch is set up to track another (via the --track argument to
725
- "git checkout -b ...").""")
1056
+ DieWithError(
1057
+ 'Unable to determine default branch to diff against.\n'
1058
+ 'Either pass complete "git diff"-style arguments, like\n'
1059
+ ' git cl upload origin/master\n'
1060
+ 'or verify this branch is set up to track another \n'
1061
+ '(via the --track argument to "git checkout -b ...").')
726
1062
 
727
1063
  return remote, upstream_branch
728
1064
 
@@ -787,7 +1123,7 @@ or verify this branch is set up to track another (via the --track argument to
787
1123
  if upstream_git_obj is None:
788
1124
  if self.GetBranch() is None:
789
1125
  print >> sys.stderr, (
790
- 'ERROR: unable to dertermine current branch (detached HEAD?)')
1126
+ 'ERROR: unable to determine current branch (detached HEAD?)')
791
1127
  else:
792
1128
  print >> sys.stderr, (
793
1129
  'ERROR: no upstream branch')
@@ -864,56 +1200,24 @@ or verify this branch is set up to track another (via the --track argument to
864
1200
  def GetIssue(self):
865
1201
  """Returns the issue number as a int or None if not set."""
866
1202
  if self.issue is None and not self.lookedup_issue:
867
- issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
1203
+ issue = RunGit(['config',
1204
+ self._codereview_impl.IssueSetting(self.GetBranch())],
1205
+ error_ok=True).strip()
868
1206
  self.issue = int(issue) or None if issue else None
869
1207
  self.lookedup_issue = True
870
1208
  return self.issue
871
1209
 
872
- def GetRietveldServer(self):
873
- if not self.rietveld_server:
874
- # If we're on a branch then get the server potentially associated
875
- # with that branch.
876
- if self.GetIssue():
877
- rietveld_server_config = self._RietveldServer()
878
- if rietveld_server_config:
879
- self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
880
- ['config', rietveld_server_config], error_ok=True).strip())
881
- if not self.rietveld_server:
882
- self.rietveld_server = settings.GetDefaultServerUrl()
883
- return self.rietveld_server
884
-
885
1210
  def GetIssueURL(self):
886
1211
  """Get the URL for a particular issue."""
887
- if not self.GetIssue():
1212
+ issue = self.GetIssue()
1213
+ if not issue:
888
1214
  return None
889
- return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1215
+ return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
890
1216
 
891
1217
  def GetDescription(self, pretty=False):
892
1218
  if not self.has_description:
893
1219
  if self.GetIssue():
894
- issue = self.GetIssue()
895
- try:
896
- self.description = self.RpcServer().get_description(issue).strip()
897
- except urllib2.HTTPError as e:
898
- if e.code == 404:
899
- DieWithError(
900
- ('\nWhile fetching the description for issue %d, received a '
901
- '404 (not found)\n'
902
- 'error. It is likely that you deleted this '
903
- 'issue on the server. If this is the\n'
904
- 'case, please run\n\n'
905
- ' git cl issue 0\n\n'
906
- 'to clear the association with the deleted issue. Then run '
907
- 'this command again.') % issue)
908
- else:
909
- DieWithError(
910
- '\nFailed to fetch issue description. HTTP error %d' % e.code)
911
- except urllib2.URLError as e:
912
- print >> sys.stderr, (
913
- 'Warning: Failed to retrieve CL description due to network '
914
- 'failure.')
915
- self.description = ''
916
-
1220
+ self.description = self._codereview_impl.FetchDescription()
917
1221
  self.has_description = True
918
1222
  if pretty:
919
1223
  wrapper = textwrap.TextWrapper()
@@ -924,7 +1228,7 @@ or verify this branch is set up to track another (via the --track argument to
924
1228
  def GetPatchset(self):
925
1229
  """Returns the patchset number as a int or None if not set."""
926
1230
  if self.patchset is None and not self.lookedup_patchset:
927
- patchset = RunGit(['config', self._PatchsetSetting()],
1231
+ patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
928
1232
  error_ok=True).strip()
929
1233
  self.patchset = int(patchset) or None if patchset else None
930
1234
  self.lookedup_patchset = True
@@ -932,47 +1236,29 @@ or verify this branch is set up to track another (via the --track argument to
932
1236
 
933
1237
  def SetPatchset(self, patchset):
934
1238
  """Set this branch's patchset. If patchset=0, clears the patchset."""
1239
+ patchset_setting = self._codereview_impl.PatchsetSetting()
935
1240
  if patchset:
936
- RunGit(['config', self._PatchsetSetting(), str(patchset)])
1241
+ RunGit(['config', patchset_setting, str(patchset)])
937
1242
  self.patchset = patchset
938
1243
  else:
939
- RunGit(['config', '--unset', self._PatchsetSetting()],
1244
+ RunGit(['config', '--unset', patchset_setting],
940
1245
  stderr=subprocess2.PIPE, error_ok=True)
941
1246
  self.patchset = None
942
1247
 
943
- def GetMostRecentPatchset(self):
944
- return self.GetIssueProperties()['patchsets'][-1]
945
-
946
- def GetPatchSetDiff(self, issue, patchset):
947
- return self.RpcServer().get(
948
- '/download/issue%s_%s.diff' % (issue, patchset))
949
-
950
- def GetIssueProperties(self):
951
- if self._props is None:
952
- issue = self.GetIssue()
953
- if not issue:
954
- self._props = {}
955
- else:
956
- self._props = self.RpcServer().get_issue_properties(issue, True)
957
- return self._props
958
-
959
- def GetApprovingReviewers(self):
960
- return get_approving_reviewers(self.GetIssueProperties())
961
-
962
- def AddComment(self, message):
963
- return self.RpcServer().add_comment(self.GetIssue(), message)
964
-
965
- def SetIssue(self, issue):
966
- """Set this branch's issue. If issue=0, clears the issue."""
1248
+ def SetIssue(self, issue=None):
1249
+ """Set this branch's issue. If issue isn't given, clears the issue."""
1250
+ issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1251
+ codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
967
1252
  if issue:
968
1253
  self.issue = issue
969
- RunGit(['config', self._IssueSetting(), str(issue)])
970
- if self.rietveld_server:
971
- RunGit(['config', self._RietveldServer(), self.rietveld_server])
1254
+ RunGit(['config', issue_setting, str(issue)])
1255
+ codereview_server = self._codereview_impl.GetCodereviewServer()
1256
+ if codereview_server:
1257
+ RunGit(['config', codereview_setting, codereview_server])
972
1258
  else:
973
1259
  current_issue = self.GetIssue()
974
1260
  if current_issue:
975
- RunGit(['config', '--unset', self._IssueSetting()])
1261
+ RunGit(['config', '--unset', issue_setting])
976
1262
  self.issue = None
977
1263
  self.SetPatchset(None)
978
1264
 
@@ -1022,6 +1308,343 @@ or verify this branch is set up to track another (via the --track argument to
1022
1308
  author,
1023
1309
  upstream=upstream_branch)
1024
1310
 
1311
+ def UpdateDescription(self, description):
1312
+ self.description = description
1313
+ return self._codereview_impl.UpdateDescriptionRemote(description)
1314
+
1315
+ def RunHook(self, committing, may_prompt, verbose, change):
1316
+ """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1317
+ try:
1318
+ return presubmit_support.DoPresubmitChecks(change, committing,
1319
+ verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1320
+ default_presubmit=None, may_prompt=may_prompt,
1321
+ rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1322
+ gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
1323
+ except presubmit_support.PresubmitFailure, e:
1324
+ DieWithError(
1325
+ ('%s\nMaybe your depot_tools is out of date?\n'
1326
+ 'If all fails, contact maruel@') % e)
1327
+
1328
+ def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1329
+ """Fetches and applies the issue patch from codereview to local branch."""
1330
+ if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1331
+ parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
1332
+ else:
1333
+ # Assume url.
1334
+ parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1335
+ urlparse.urlparse(issue_arg))
1336
+ if not parsed_issue_arg or not parsed_issue_arg.valid:
1337
+ DieWithError('Failed to parse issue argument "%s". '
1338
+ 'Must be an issue number or a valid URL.' % issue_arg)
1339
+ return self._codereview_impl.CMDPatchWithParsedIssue(
1340
+ parsed_issue_arg, reject, nocommit, directory)
1341
+
1342
+ def CMDUpload(self, options, git_diff_args, orig_args):
1343
+ """Uploads a change to codereview."""
1344
+ if git_diff_args:
1345
+ # TODO(ukai): is it ok for gerrit case?
1346
+ base_branch = git_diff_args[0]
1347
+ else:
1348
+ if self.GetBranch() is None:
1349
+ DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1350
+
1351
+ # Default to diffing against common ancestor of upstream branch
1352
+ base_branch = self.GetCommonAncestorWithUpstream()
1353
+ git_diff_args = [base_branch, 'HEAD']
1354
+
1355
+ # Make sure authenticated to codereview before running potentially expensive
1356
+ # hooks. It is a fast, best efforts check. Codereview still can reject the
1357
+ # authentication during the actual upload.
1358
+ self._codereview_impl.EnsureAuthenticated(force=options.force)
1359
+
1360
+ # Apply watchlists on upload.
1361
+ change = self.GetChange(base_branch, None)
1362
+ watchlist = watchlists.Watchlists(change.RepositoryRoot())
1363
+ files = [f.LocalPath() for f in change.AffectedFiles()]
1364
+ if not options.bypass_watchlists:
1365
+ self.SetWatchers(watchlist.GetWatchersForPaths(files))
1366
+
1367
+ if not options.bypass_hooks:
1368
+ if options.reviewers or options.tbr_owners:
1369
+ # Set the reviewer list now so that presubmit checks can access it.
1370
+ change_description = ChangeDescription(change.FullDescriptionText())
1371
+ change_description.update_reviewers(options.reviewers,
1372
+ options.tbr_owners,
1373
+ change)
1374
+ change.SetDescriptionText(change_description.description)
1375
+ hook_results = self.RunHook(committing=False,
1376
+ may_prompt=not options.force,
1377
+ verbose=options.verbose,
1378
+ change=change)
1379
+ if not hook_results.should_continue():
1380
+ return 1
1381
+ if not options.reviewers and hook_results.reviewers:
1382
+ options.reviewers = hook_results.reviewers.split(',')
1383
+
1384
+ if self.GetIssue():
1385
+ latest_patchset = self.GetMostRecentPatchset()
1386
+ local_patchset = self.GetPatchset()
1387
+ if (latest_patchset and local_patchset and
1388
+ local_patchset != latest_patchset):
1389
+ print ('The last upload made from this repository was patchset #%d but '
1390
+ 'the most recent patchset on the server is #%d.'
1391
+ % (local_patchset, latest_patchset))
1392
+ print ('Uploading will still work, but if you\'ve uploaded to this '
1393
+ 'issue from another machine or branch the patch you\'re '
1394
+ 'uploading now might not include those changes.')
1395
+ ask_for_data('About to upload; enter to confirm.')
1396
+
1397
+ print_stats(options.similarity, options.find_copies, git_diff_args)
1398
+ ret = self.CMDUploadChange(options, git_diff_args, change)
1399
+ if not ret:
1400
+ git_set_branch_value('last-upload-hash',
1401
+ RunGit(['rev-parse', 'HEAD']).strip())
1402
+ # Run post upload hooks, if specified.
1403
+ if settings.GetRunPostUploadHook():
1404
+ presubmit_support.DoPostUploadExecuter(
1405
+ change,
1406
+ self,
1407
+ settings.GetRoot(),
1408
+ options.verbose,
1409
+ sys.stdout)
1410
+
1411
+ # Upload all dependencies if specified.
1412
+ if options.dependencies:
1413
+ print
1414
+ print '--dependencies has been specified.'
1415
+ print 'All dependent local branches will be re-uploaded.'
1416
+ print
1417
+ # Remove the dependencies flag from args so that we do not end up in a
1418
+ # loop.
1419
+ orig_args.remove('--dependencies')
1420
+ ret = upload_branch_deps(self, orig_args)
1421
+ return ret
1422
+
1423
+ def SetCQState(self, new_state):
1424
+ """Update the CQ state for latest patchset.
1425
+
1426
+ Issue must have been already uploaded and known.
1427
+ """
1428
+ assert new_state in _CQState.ALL_STATES
1429
+ assert self.GetIssue()
1430
+ return self._codereview_impl.SetCQState(new_state)
1431
+
1432
+ # Forward methods to codereview specific implementation.
1433
+
1434
+ def CloseIssue(self):
1435
+ return self._codereview_impl.CloseIssue()
1436
+
1437
+ def GetStatus(self):
1438
+ return self._codereview_impl.GetStatus()
1439
+
1440
+ def GetCodereviewServer(self):
1441
+ return self._codereview_impl.GetCodereviewServer()
1442
+
1443
+ def GetApprovingReviewers(self):
1444
+ return self._codereview_impl.GetApprovingReviewers()
1445
+
1446
+ def GetMostRecentPatchset(self):
1447
+ return self._codereview_impl.GetMostRecentPatchset()
1448
+
1449
+ def __getattr__(self, attr):
1450
+ # This is because lots of untested code accesses Rietveld-specific stuff
1451
+ # directly, and it's hard to fix for sure. So, just let it work, and fix
1452
+ # on a cases by case basis.
1453
+ return getattr(self._codereview_impl, attr)
1454
+
1455
+
1456
+ class _ChangelistCodereviewBase(object):
1457
+ """Abstract base class encapsulating codereview specifics of a changelist."""
1458
+ def __init__(self, changelist):
1459
+ self._changelist = changelist # instance of Changelist
1460
+
1461
+ def __getattr__(self, attr):
1462
+ # Forward methods to changelist.
1463
+ # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1464
+ # _RietveldChangelistImpl to avoid this hack?
1465
+ return getattr(self._changelist, attr)
1466
+
1467
+ def GetStatus(self):
1468
+ """Apply a rough heuristic to give a simple summary of an issue's review
1469
+ or CQ status, assuming adherence to a common workflow.
1470
+
1471
+ Returns None if no issue for this branch, or specific string keywords.
1472
+ """
1473
+ raise NotImplementedError()
1474
+
1475
+ def GetCodereviewServer(self):
1476
+ """Returns server URL without end slash, like "https://codereview.com"."""
1477
+ raise NotImplementedError()
1478
+
1479
+ def FetchDescription(self):
1480
+ """Fetches and returns description from the codereview server."""
1481
+ raise NotImplementedError()
1482
+
1483
+ def GetCodereviewServerSetting(self):
1484
+ """Returns git config setting for the codereview server."""
1485
+ raise NotImplementedError()
1486
+
1487
+ @classmethod
1488
+ def IssueSetting(cls, branch):
1489
+ return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
1490
+
1491
+ @classmethod
1492
+ def IssueSettingSuffix(cls):
1493
+ """Returns name of git config setting which stores issue number for a given
1494
+ branch."""
1495
+ raise NotImplementedError()
1496
+
1497
+ def PatchsetSetting(self):
1498
+ """Returns name of git config setting which stores issue number."""
1499
+ raise NotImplementedError()
1500
+
1501
+ def GetRieveldObjForPresubmit(self):
1502
+ # This is an unfortunate Rietveld-embeddedness in presubmit.
1503
+ # For non-Rietveld codereviews, this probably should return a dummy object.
1504
+ raise NotImplementedError()
1505
+
1506
+ def GetGerritObjForPresubmit(self):
1507
+ # None is valid return value, otherwise presubmit_support.GerritAccessor.
1508
+ return None
1509
+
1510
+ def UpdateDescriptionRemote(self, description):
1511
+ """Update the description on codereview site."""
1512
+ raise NotImplementedError()
1513
+
1514
+ def CloseIssue(self):
1515
+ """Closes the issue."""
1516
+ raise NotImplementedError()
1517
+
1518
+ def GetApprovingReviewers(self):
1519
+ """Returns a list of reviewers approving the change.
1520
+
1521
+ Note: not necessarily committers.
1522
+ """
1523
+ raise NotImplementedError()
1524
+
1525
+ def GetMostRecentPatchset(self):
1526
+ """Returns the most recent patchset number from the codereview site."""
1527
+ raise NotImplementedError()
1528
+
1529
+ def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1530
+ directory):
1531
+ """Fetches and applies the issue.
1532
+
1533
+ Arguments:
1534
+ parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1535
+ reject: if True, reject the failed patch instead of switching to 3-way
1536
+ merge. Rietveld only.
1537
+ nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1538
+ only.
1539
+ directory: switch to directory before applying the patch. Rietveld only.
1540
+ """
1541
+ raise NotImplementedError()
1542
+
1543
+ @staticmethod
1544
+ def ParseIssueURL(parsed_url):
1545
+ """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1546
+ failed."""
1547
+ raise NotImplementedError()
1548
+
1549
+ def EnsureAuthenticated(self, force):
1550
+ """Best effort check that user is authenticated with codereview server.
1551
+
1552
+ Arguments:
1553
+ force: whether to skip confirmation questions.
1554
+ """
1555
+ raise NotImplementedError()
1556
+
1557
+ def CMDUploadChange(self, options, args, change):
1558
+ """Uploads a change to codereview."""
1559
+ raise NotImplementedError()
1560
+
1561
+ def SetCQState(self, new_state):
1562
+ """Update the CQ state for latest patchset.
1563
+
1564
+ Issue must have been already uploaded and known.
1565
+ """
1566
+ raise NotImplementedError()
1567
+
1568
+
1569
+ class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1570
+ def __init__(self, changelist, auth_config=None, rietveld_server=None):
1571
+ super(_RietveldChangelistImpl, self).__init__(changelist)
1572
+ assert settings, 'must be initialized in _ChangelistCodereviewBase'
1573
+ settings.GetDefaultServerUrl()
1574
+
1575
+ self._rietveld_server = rietveld_server
1576
+ self._auth_config = auth_config
1577
+ self._props = None
1578
+ self._rpc_server = None
1579
+
1580
+ def GetCodereviewServer(self):
1581
+ if not self._rietveld_server:
1582
+ # If we're on a branch then get the server potentially associated
1583
+ # with that branch.
1584
+ if self.GetIssue():
1585
+ rietveld_server_setting = self.GetCodereviewServerSetting()
1586
+ if rietveld_server_setting:
1587
+ self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1588
+ ['config', rietveld_server_setting], error_ok=True).strip())
1589
+ if not self._rietveld_server:
1590
+ self._rietveld_server = settings.GetDefaultServerUrl()
1591
+ return self._rietveld_server
1592
+
1593
+ def EnsureAuthenticated(self, force):
1594
+ """Best effort check that user is authenticated with Rietveld server."""
1595
+ if self._auth_config.use_oauth2:
1596
+ authenticator = auth.get_authenticator_for_host(
1597
+ self.GetCodereviewServer(), self._auth_config)
1598
+ if not authenticator.has_cached_credentials():
1599
+ raise auth.LoginRequiredError(self.GetCodereviewServer())
1600
+
1601
+ def FetchDescription(self):
1602
+ issue = self.GetIssue()
1603
+ assert issue
1604
+ try:
1605
+ return self.RpcServer().get_description(issue).strip()
1606
+ except urllib2.HTTPError as e:
1607
+ if e.code == 404:
1608
+ DieWithError(
1609
+ ('\nWhile fetching the description for issue %d, received a '
1610
+ '404 (not found)\n'
1611
+ 'error. It is likely that you deleted this '
1612
+ 'issue on the server. If this is the\n'
1613
+ 'case, please run\n\n'
1614
+ ' git cl issue 0\n\n'
1615
+ 'to clear the association with the deleted issue. Then run '
1616
+ 'this command again.') % issue)
1617
+ else:
1618
+ DieWithError(
1619
+ '\nFailed to fetch issue description. HTTP error %d' % e.code)
1620
+ except urllib2.URLError as e:
1621
+ print >> sys.stderr, (
1622
+ 'Warning: Failed to retrieve CL description due to network '
1623
+ 'failure.')
1624
+ return ''
1625
+
1626
+ def GetMostRecentPatchset(self):
1627
+ return self.GetIssueProperties()['patchsets'][-1]
1628
+
1629
+ def GetPatchSetDiff(self, issue, patchset):
1630
+ return self.RpcServer().get(
1631
+ '/download/issue%s_%s.diff' % (issue, patchset))
1632
+
1633
+ def GetIssueProperties(self):
1634
+ if self._props is None:
1635
+ issue = self.GetIssue()
1636
+ if not issue:
1637
+ self._props = {}
1638
+ else:
1639
+ self._props = self.RpcServer().get_issue_properties(issue, True)
1640
+ return self._props
1641
+
1642
+ def GetApprovingReviewers(self):
1643
+ return get_approving_reviewers(self.GetIssueProperties())
1644
+
1645
+ def AddComment(self, message):
1646
+ return self.RpcServer().add_comment(self.GetIssue(), message)
1647
+
1025
1648
  def GetStatus(self):
1026
1649
  """Apply a rough heuristic to give a simple summary of an issue's review
1027
1650
  or CQ status, assuming adherence to a common workflow.
@@ -1046,7 +1669,7 @@ or verify this branch is set up to track another (via the --track argument to
1046
1669
  if props.get('closed'):
1047
1670
  # Issue is closed.
1048
1671
  return 'closed'
1049
- if props.get('commit'):
1672
+ if props.get('commit') and not props.get('cq_dry_run', False):
1050
1673
  # Issue is in the commit queue.
1051
1674
  return 'commit'
1052
1675
 
@@ -1069,26 +1692,11 @@ or verify this branch is set up to track another (via the --track argument to
1069
1692
  return 'reply'
1070
1693
  return 'waiting'
1071
1694
 
1072
- def RunHook(self, committing, may_prompt, verbose, change):
1073
- """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1074
-
1075
- try:
1076
- return presubmit_support.DoPresubmitChecks(change, committing,
1077
- verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1078
- default_presubmit=None, may_prompt=may_prompt,
1079
- rietveld_obj=self.RpcServer())
1080
- except presubmit_support.PresubmitFailure, e:
1081
- DieWithError(
1082
- ('%s\nMaybe your depot_tools is out of date?\n'
1083
- 'If all fails, contact maruel@') % e)
1084
-
1085
- def UpdateDescription(self, description):
1086
- self.description = description
1695
+ def UpdateDescriptionRemote(self, description):
1087
1696
  return self.RpcServer().update_description(
1088
1697
  self.GetIssue(), self.description)
1089
1698
 
1090
1699
  def CloseIssue(self):
1091
- """Updates the description and closes the issue."""
1092
1700
  return self.RpcServer().close_issue(self.GetIssue())
1093
1701
 
1094
1702
  def SetFlag(self, flag, value):
@@ -1112,65 +1720,851 @@ or verify this branch is set up to track another (via the --track argument to
1112
1720
  """
1113
1721
  if not self._rpc_server:
1114
1722
  self._rpc_server = rietveld.CachingRietveld(
1115
- self.GetRietveldServer(),
1723
+ self.GetCodereviewServer(),
1116
1724
  self._auth_config or auth.make_auth_config())
1117
1725
  return self._rpc_server
1118
1726
 
1119
- def _IssueSetting(self):
1120
- """Return the git setting that stores this change's issue."""
1121
- return 'branch.%s.rietveldissue' % self.GetBranch()
1727
+ @classmethod
1728
+ def IssueSettingSuffix(cls):
1729
+ return 'rietveldissue'
1122
1730
 
1123
- def _PatchsetSetting(self):
1731
+ def PatchsetSetting(self):
1124
1732
  """Return the git setting that stores this change's most recent patchset."""
1125
1733
  return 'branch.%s.rietveldpatchset' % self.GetBranch()
1126
1734
 
1127
- def _RietveldServer(self):
1735
+ def GetCodereviewServerSetting(self):
1128
1736
  """Returns the git setting that stores this change's rietveld server."""
1129
1737
  branch = self.GetBranch()
1130
1738
  if branch:
1131
1739
  return 'branch.%s.rietveldserver' % branch
1132
1740
  return None
1133
1741
 
1742
+ def GetRieveldObjForPresubmit(self):
1743
+ return self.RpcServer()
1134
1744
 
1135
- def GetCodereviewSettingsInteractively():
1136
- """Prompt the user for settings."""
1137
- # TODO(ukai): ask code review system is rietveld or gerrit?
1138
- server = settings.GetDefaultServerUrl(error_ok=True)
1139
- prompt = 'Rietveld server (host[:port])'
1140
- prompt += ' [%s]' % (server or DEFAULT_SERVER)
1141
- newserver = ask_for_data(prompt + ':')
1142
- if not server and not newserver:
1143
- newserver = DEFAULT_SERVER
1144
- if newserver:
1145
- newserver = gclient_utils.UpgradeToHttps(newserver)
1146
- if newserver != server:
1147
- RunGit(['config', 'rietveld.server', newserver])
1745
+ def SetCQState(self, new_state):
1746
+ props = self.GetIssueProperties()
1747
+ if props.get('private'):
1748
+ DieWithError('Cannot set-commit on private issue')
1148
1749
 
1149
- def SetProperty(initial, caption, name, is_url):
1150
- prompt = caption
1151
- if initial:
1152
- prompt += ' ("x" to clear) [%s]' % initial
1153
- new_val = ask_for_data(prompt + ':')
1154
- if new_val == 'x':
1155
- RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
1156
- elif new_val:
1157
- if is_url:
1158
- new_val = gclient_utils.UpgradeToHttps(new_val)
1159
- if new_val != initial:
1160
- RunGit(['config', 'rietveld.' + name, new_val])
1750
+ if new_state == _CQState.COMMIT:
1751
+ self.SetFlag('commit', '1')
1752
+ elif new_state == _CQState.NONE:
1753
+ self.SetFlag('commit', '0')
1754
+ else:
1755
+ raise NotImplementedError()
1756
+
1757
+
1758
+ def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1759
+ directory):
1760
+ # TODO(maruel): Use apply_issue.py
1761
+
1762
+ # PatchIssue should never be called with a dirty tree. It is up to the
1763
+ # caller to check this, but just in case we assert here since the
1764
+ # consequences of the caller not checking this could be dire.
1765
+ assert(not git_common.is_dirty_git_tree('apply'))
1766
+ assert(parsed_issue_arg.valid)
1767
+ self._changelist.issue = parsed_issue_arg.issue
1768
+ if parsed_issue_arg.hostname:
1769
+ self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1770
+
1771
+ if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1772
+ parsed_issue_arg.patch_url):
1773
+ assert parsed_issue_arg.patchset
1774
+ patchset = parsed_issue_arg.patchset
1775
+ patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1776
+ else:
1777
+ patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1778
+ patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1779
+
1780
+ # Switch up to the top-level directory, if necessary, in preparation for
1781
+ # applying the patch.
1782
+ top = settings.GetRelativeRoot()
1783
+ if top:
1784
+ os.chdir(top)
1785
+
1786
+ # Git patches have a/ at the beginning of source paths. We strip that out
1787
+ # with a sed script rather than the -p flag to patch so we can feed either
1788
+ # Git or svn-style patches into the same apply command.
1789
+ # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1790
+ try:
1791
+ patch_data = subprocess2.check_output(
1792
+ ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1793
+ except subprocess2.CalledProcessError:
1794
+ DieWithError('Git patch mungling failed.')
1795
+ logging.info(patch_data)
1796
+
1797
+ # We use "git apply" to apply the patch instead of "patch" so that we can
1798
+ # pick up file adds.
1799
+ # The --index flag means: also insert into the index (so we catch adds).
1800
+ cmd = ['git', 'apply', '--index', '-p0']
1801
+ if directory:
1802
+ cmd.extend(('--directory', directory))
1803
+ if reject:
1804
+ cmd.append('--reject')
1805
+ elif IsGitVersionAtLeast('1.7.12'):
1806
+ cmd.append('--3way')
1807
+ try:
1808
+ subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1809
+ stdin=patch_data, stdout=subprocess2.VOID)
1810
+ except subprocess2.CalledProcessError:
1811
+ print 'Failed to apply the patch'
1812
+ return 1
1161
1813
 
1162
- SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
1163
- SetProperty(settings.GetDefaultPrivateFlag(),
1164
- 'Private flag (rietveld only)', 'private', False)
1165
- SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
1166
- 'tree-status-url', False)
1167
- SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
1168
- SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
1169
- SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1170
- 'run-post-upload-hook', False)
1814
+ # If we had an issue, commit the current state and register the issue.
1815
+ if not nocommit:
1816
+ RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1817
+ 'patch from issue %(i)s at patchset '
1818
+ '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1819
+ % {'i': self.GetIssue(), 'p': patchset})])
1820
+ self.SetIssue(self.GetIssue())
1821
+ self.SetPatchset(patchset)
1822
+ print "Committed patch locally."
1823
+ else:
1824
+ print "Patch applied to index."
1825
+ return 0
1826
+
1827
+ @staticmethod
1828
+ def ParseIssueURL(parsed_url):
1829
+ if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1830
+ return None
1831
+ # Typical url: https://domain/<issue_number>[/[other]]
1832
+ match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1833
+ if match:
1834
+ return _RietveldParsedIssueNumberArgument(
1835
+ issue=int(match.group(1)),
1836
+ hostname=parsed_url.netloc)
1837
+ # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1838
+ match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1839
+ if match:
1840
+ return _RietveldParsedIssueNumberArgument(
1841
+ issue=int(match.group(1)),
1842
+ patchset=int(match.group(2)),
1843
+ hostname=parsed_url.netloc,
1844
+ patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1845
+ return None
1846
+
1847
+ def CMDUploadChange(self, options, args, change):
1848
+ """Upload the patch to Rietveld."""
1849
+ upload_args = ['--assume_yes'] # Don't ask about untracked files.
1850
+ upload_args.extend(['--server', self.GetCodereviewServer()])
1851
+ upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1852
+ if options.emulate_svn_auto_props:
1853
+ upload_args.append('--emulate_svn_auto_props')
1854
+
1855
+ change_desc = None
1856
+
1857
+ if options.email is not None:
1858
+ upload_args.extend(['--email', options.email])
1859
+
1860
+ if self.GetIssue():
1861
+ if options.title:
1862
+ upload_args.extend(['--title', options.title])
1863
+ if options.message:
1864
+ upload_args.extend(['--message', options.message])
1865
+ upload_args.extend(['--issue', str(self.GetIssue())])
1866
+ print ('This branch is associated with issue %s. '
1867
+ 'Adding patch to that issue.' % self.GetIssue())
1868
+ else:
1869
+ if options.title:
1870
+ upload_args.extend(['--title', options.title])
1871
+ message = (options.title or options.message or
1872
+ CreateDescriptionFromLog(args))
1873
+ change_desc = ChangeDescription(message)
1874
+ if options.reviewers or options.tbr_owners:
1875
+ change_desc.update_reviewers(options.reviewers,
1876
+ options.tbr_owners,
1877
+ change)
1878
+ if not options.force:
1879
+ change_desc.prompt()
1880
+
1881
+ if not change_desc.description:
1882
+ print "Description is empty; aborting."
1883
+ return 1
1884
+
1885
+ upload_args.extend(['--message', change_desc.description])
1886
+ if change_desc.get_reviewers():
1887
+ upload_args.append('--reviewers=%s' % ','.join(
1888
+ change_desc.get_reviewers()))
1889
+ if options.send_mail:
1890
+ if not change_desc.get_reviewers():
1891
+ DieWithError("Must specify reviewers to send email.")
1892
+ upload_args.append('--send_mail')
1893
+
1894
+ # We check this before applying rietveld.private assuming that in
1895
+ # rietveld.cc only addresses which we can send private CLs to are listed
1896
+ # if rietveld.private is set, and so we should ignore rietveld.cc only
1897
+ # when --private is specified explicitly on the command line.
1898
+ if options.private:
1899
+ logging.warn('rietveld.cc is ignored since private flag is specified. '
1900
+ 'You need to review and add them manually if necessary.')
1901
+ cc = self.GetCCListWithoutDefault()
1902
+ else:
1903
+ cc = self.GetCCList()
1904
+ cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1905
+ if cc:
1906
+ upload_args.extend(['--cc', cc])
1907
+
1908
+ if options.private or settings.GetDefaultPrivateFlag() == "True":
1909
+ upload_args.append('--private')
1910
+
1911
+ upload_args.extend(['--git_similarity', str(options.similarity)])
1912
+ if not options.find_copies:
1913
+ upload_args.extend(['--git_no_find_copies'])
1914
+
1915
+ # Include the upstream repo's URL in the change -- this is useful for
1916
+ # projects that have their source spread across multiple repos.
1917
+ remote_url = self.GetGitBaseUrlFromConfig()
1918
+ if not remote_url:
1919
+ if settings.GetIsGitSvn():
1920
+ remote_url = self.GetGitSvnRemoteUrl()
1921
+ else:
1922
+ if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1923
+ remote_url = '%s@%s' % (self.GetRemoteUrl(),
1924
+ self.GetUpstreamBranch().split('/')[-1])
1925
+ if remote_url:
1926
+ upload_args.extend(['--base_url', remote_url])
1927
+ remote, remote_branch = self.GetRemoteBranch()
1928
+ target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1929
+ settings.GetPendingRefPrefix())
1930
+ if target_ref:
1931
+ upload_args.extend(['--target_ref', target_ref])
1932
+
1933
+ # Look for dependent patchsets. See crbug.com/480453 for more details.
1934
+ remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1935
+ upstream_branch = ShortBranchName(upstream_branch)
1936
+ if remote is '.':
1937
+ # A local branch is being tracked.
1938
+ local_branch = ShortBranchName(upstream_branch)
1939
+ if settings.GetIsSkipDependencyUpload(local_branch):
1940
+ print
1941
+ print ('Skipping dependency patchset upload because git config '
1942
+ 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1943
+ print
1944
+ else:
1945
+ auth_config = auth.extract_auth_config_from_options(options)
1946
+ branch_cl = Changelist(branchref=local_branch,
1947
+ auth_config=auth_config)
1948
+ branch_cl_issue_url = branch_cl.GetIssueURL()
1949
+ branch_cl_issue = branch_cl.GetIssue()
1950
+ branch_cl_patchset = branch_cl.GetPatchset()
1951
+ if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1952
+ upload_args.extend(
1953
+ ['--depends_on_patchset', '%s:%s' % (
1954
+ branch_cl_issue, branch_cl_patchset)])
1955
+ print(
1956
+ '\n'
1957
+ 'The current branch (%s) is tracking a local branch (%s) with '
1958
+ 'an associated CL.\n'
1959
+ 'Adding %s/#ps%s as a dependency patchset.\n'
1960
+ '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1961
+ branch_cl_patchset))
1962
+
1963
+ project = settings.GetProject()
1964
+ if project:
1965
+ upload_args.extend(['--project', project])
1966
+
1967
+ if options.cq_dry_run:
1968
+ upload_args.extend(['--cq_dry_run'])
1969
+
1970
+ try:
1971
+ upload_args = ['upload'] + upload_args + args
1972
+ logging.info('upload.RealMain(%s)', upload_args)
1973
+ issue, patchset = upload.RealMain(upload_args)
1974
+ issue = int(issue)
1975
+ patchset = int(patchset)
1976
+ except KeyboardInterrupt:
1977
+ sys.exit(1)
1978
+ except:
1979
+ # If we got an exception after the user typed a description for their
1980
+ # change, back up the description before re-raising.
1981
+ if change_desc:
1982
+ backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1983
+ print('\nGot exception while uploading -- saving description to %s\n' %
1984
+ backup_path)
1985
+ backup_file = open(backup_path, 'w')
1986
+ backup_file.write(change_desc.description)
1987
+ backup_file.close()
1988
+ raise
1989
+
1990
+ if not self.GetIssue():
1991
+ self.SetIssue(issue)
1992
+ self.SetPatchset(patchset)
1993
+
1994
+ if options.use_commit_queue:
1995
+ self.SetCQState(_CQState.COMMIT)
1996
+ return 0
1997
+
1998
+
1999
+ class _GerritChangelistImpl(_ChangelistCodereviewBase):
2000
+ def __init__(self, changelist, auth_config=None):
2001
+ # auth_config is Rietveld thing, kept here to preserve interface only.
2002
+ super(_GerritChangelistImpl, self).__init__(changelist)
2003
+ self._change_id = None
2004
+ # Lazily cached values.
2005
+ self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
2006
+ self._gerrit_host = None # e.g. chromium-review.googlesource.com
2007
+
2008
+ def _GetGerritHost(self):
2009
+ # Lazy load of configs.
2010
+ self.GetCodereviewServer()
2011
+ return self._gerrit_host
2012
+
2013
+ def _GetGitHost(self):
2014
+ """Returns git host to be used when uploading change to Gerrit."""
2015
+ return urlparse.urlparse(self.GetRemoteUrl()).netloc
2016
+
2017
+ def GetCodereviewServer(self):
2018
+ if not self._gerrit_server:
2019
+ # If we're on a branch then get the server potentially associated
2020
+ # with that branch.
2021
+ if self.GetIssue():
2022
+ gerrit_server_setting = self.GetCodereviewServerSetting()
2023
+ if gerrit_server_setting:
2024
+ self._gerrit_server = RunGit(['config', gerrit_server_setting],
2025
+ error_ok=True).strip()
2026
+ if self._gerrit_server:
2027
+ self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2028
+ if not self._gerrit_server:
2029
+ # We assume repo to be hosted on Gerrit, and hence Gerrit server
2030
+ # has "-review" suffix for lowest level subdomain.
2031
+ parts = self._GetGitHost().split('.')
2032
+ parts[0] = parts[0] + '-review'
2033
+ self._gerrit_host = '.'.join(parts)
2034
+ self._gerrit_server = 'https://%s' % self._gerrit_host
2035
+ return self._gerrit_server
2036
+
2037
+ @classmethod
2038
+ def IssueSettingSuffix(cls):
2039
+ return 'gerritissue'
2040
+
2041
+ def EnsureAuthenticated(self, force):
2042
+ """Best effort check that user is authenticated with Gerrit server."""
2043
+ if settings.GetGerritSkipEnsureAuthenticated():
2044
+ # For projects with unusual authentication schemes.
2045
+ # See http://crbug.com/603378.
2046
+ return
2047
+ # Lazy-loader to identify Gerrit and Git hosts.
2048
+ if gerrit_util.GceAuthenticator.is_gce():
2049
+ return
2050
+ self.GetCodereviewServer()
2051
+ git_host = self._GetGitHost()
2052
+ assert self._gerrit_server and self._gerrit_host
2053
+ cookie_auth = gerrit_util.CookiesAuthenticator()
2054
+
2055
+ gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2056
+ git_auth = cookie_auth.get_auth_header(git_host)
2057
+ if gerrit_auth and git_auth:
2058
+ if gerrit_auth == git_auth:
2059
+ return
2060
+ print((
2061
+ 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2062
+ ' Check your %s or %s file for credentials of hosts:\n'
2063
+ ' %s\n'
2064
+ ' %s\n'
2065
+ ' %s') %
2066
+ (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2067
+ git_host, self._gerrit_host,
2068
+ cookie_auth.get_new_password_message(git_host)))
2069
+ if not force:
2070
+ ask_for_data('If you know what you are doing, press Enter to continue, '
2071
+ 'Ctrl+C to abort.')
2072
+ return
2073
+ else:
2074
+ missing = (
2075
+ [] if gerrit_auth else [self._gerrit_host] +
2076
+ [] if git_auth else [git_host])
2077
+ DieWithError('Credentials for the following hosts are required:\n'
2078
+ ' %s\n'
2079
+ 'These are read from %s (or legacy %s)\n'
2080
+ '%s' % (
2081
+ '\n '.join(missing),
2082
+ cookie_auth.get_gitcookies_path(),
2083
+ cookie_auth.get_netrc_path(),
2084
+ cookie_auth.get_new_password_message(git_host)))
2085
+
2086
+
2087
+ def PatchsetSetting(self):
2088
+ """Return the git setting that stores this change's most recent patchset."""
2089
+ return 'branch.%s.gerritpatchset' % self.GetBranch()
2090
+
2091
+ def GetCodereviewServerSetting(self):
2092
+ """Returns the git setting that stores this change's Gerrit server."""
2093
+ branch = self.GetBranch()
2094
+ if branch:
2095
+ return 'branch.%s.gerritserver' % branch
2096
+ return None
2097
+
2098
+ def GetRieveldObjForPresubmit(self):
2099
+ class ThisIsNotRietveldIssue(object):
2100
+ def __nonzero__(self):
2101
+ # This is a hack to make presubmit_support think that rietveld is not
2102
+ # defined, yet still ensure that calls directly result in a decent
2103
+ # exception message below.
2104
+ return False
2105
+
2106
+ def __getattr__(self, attr):
2107
+ print(
2108
+ 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2109
+ 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2110
+ 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2111
+ 'or use Rietveld for codereview.\n'
2112
+ 'See also http://crbug.com/579160.' % attr)
2113
+ raise NotImplementedError()
2114
+ return ThisIsNotRietveldIssue()
2115
+
2116
+ def GetGerritObjForPresubmit(self):
2117
+ return presubmit_support.GerritAccessor(self._GetGerritHost())
2118
+
2119
+ def GetStatus(self):
2120
+ """Apply a rough heuristic to give a simple summary of an issue's review
2121
+ or CQ status, assuming adherence to a common workflow.
2122
+
2123
+ Returns None if no issue for this branch, or one of the following keywords:
2124
+ * 'error' - error from review tool (including deleted issues)
2125
+ * 'unsent' - no reviewers added
2126
+ * 'waiting' - waiting for review
2127
+ * 'reply' - waiting for owner to reply to review
2128
+ * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2129
+ * 'lgtm' - Code-Review +2 from at least one approved reviewer
2130
+ * 'commit' - in the commit queue
2131
+ * 'closed' - abandoned
2132
+ """
2133
+ if not self.GetIssue():
2134
+ return None
2135
+
2136
+ try:
2137
+ data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2138
+ except httplib.HTTPException:
2139
+ return 'error'
2140
+
2141
+ if data['status'] == 'ABANDONED':
2142
+ return 'closed'
2143
+
2144
+ cq_label = data['labels'].get('Commit-Queue', {})
2145
+ if cq_label:
2146
+ # Vote value is a stringified integer, which we expect from 0 to 2.
2147
+ vote_value = cq_label.get('value', '0')
2148
+ vote_text = cq_label.get('values', {}).get(vote_value, '')
2149
+ if vote_text.lower() == 'commit':
2150
+ return 'commit'
2151
+
2152
+ lgtm_label = data['labels'].get('Code-Review', {})
2153
+ if lgtm_label:
2154
+ if 'rejected' in lgtm_label:
2155
+ return 'not lgtm'
2156
+ if 'approved' in lgtm_label:
2157
+ return 'lgtm'
2158
+
2159
+ if not data.get('reviewers', {}).get('REVIEWER', []):
2160
+ return 'unsent'
2161
+
2162
+ messages = data.get('messages', [])
2163
+ if messages:
2164
+ owner = data['owner'].get('_account_id')
2165
+ last_message_author = messages[-1].get('author', {}).get('_account_id')
2166
+ if owner != last_message_author:
2167
+ # Some reply from non-owner.
2168
+ return 'reply'
2169
+
2170
+ return 'waiting'
2171
+
2172
+ def GetMostRecentPatchset(self):
2173
+ data = self._GetChangeDetail(['CURRENT_REVISION'])
2174
+ return data['revisions'][data['current_revision']]['_number']
2175
+
2176
+ def FetchDescription(self):
2177
+ data = self._GetChangeDetail(['CURRENT_REVISION'])
2178
+ current_rev = data['current_revision']
2179
+ url = data['revisions'][current_rev]['fetch']['http']['url']
2180
+ return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
2181
+
2182
+ def UpdateDescriptionRemote(self, description):
2183
+ gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2184
+ description)
2185
+
2186
+ def CloseIssue(self):
2187
+ gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2188
+
2189
+ def GetApprovingReviewers(self):
2190
+ """Returns a list of reviewers approving the change.
2191
+
2192
+ Note: not necessarily committers.
2193
+ """
2194
+ raise NotImplementedError()
2195
+
2196
+ def SubmitIssue(self, wait_for_merge=True):
2197
+ gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2198
+ wait_for_merge=wait_for_merge)
2199
+
2200
+ def _GetChangeDetail(self, options=None, issue=None):
2201
+ options = options or []
2202
+ issue = issue or self.GetIssue()
2203
+ assert issue, 'issue required to query Gerrit'
2204
+ return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2205
+ options)
2206
+
2207
+ def CMDLand(self, force, bypass_hooks, verbose):
2208
+ if git_common.is_dirty_git_tree('land'):
2209
+ return 1
2210
+ differs = True
2211
+ last_upload = RunGit(['config',
2212
+ 'branch.%s.gerritsquashhash' % self.GetBranch()],
2213
+ error_ok=True).strip()
2214
+ # Note: git diff outputs nothing if there is no diff.
2215
+ if not last_upload or RunGit(['diff', last_upload]).strip():
2216
+ print('WARNING: some changes from local branch haven\'t been uploaded')
2217
+ else:
2218
+ detail = self._GetChangeDetail(['CURRENT_REVISION'])
2219
+ if detail['current_revision'] == last_upload:
2220
+ differs = False
2221
+ else:
2222
+ print('WARNING: local branch contents differ from latest uploaded '
2223
+ 'patchset')
2224
+ if differs:
2225
+ if not force:
2226
+ ask_for_data(
2227
+ 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2228
+ print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2229
+ elif not bypass_hooks:
2230
+ hook_results = self.RunHook(
2231
+ committing=True,
2232
+ may_prompt=not force,
2233
+ verbose=verbose,
2234
+ change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2235
+ if not hook_results.should_continue():
2236
+ return 1
2237
+
2238
+ self.SubmitIssue(wait_for_merge=True)
2239
+ print('Issue %s has been submitted.' % self.GetIssueURL())
2240
+ return 0
2241
+
2242
+ def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2243
+ directory):
2244
+ assert not reject
2245
+ assert not nocommit
2246
+ assert not directory
2247
+ assert parsed_issue_arg.valid
2248
+
2249
+ self._changelist.issue = parsed_issue_arg.issue
2250
+
2251
+ if parsed_issue_arg.hostname:
2252
+ self._gerrit_host = parsed_issue_arg.hostname
2253
+ self._gerrit_server = 'https://%s' % self._gerrit_host
2254
+
2255
+ detail = self._GetChangeDetail(['ALL_REVISIONS'])
2256
+
2257
+ if not parsed_issue_arg.patchset:
2258
+ # Use current revision by default.
2259
+ revision_info = detail['revisions'][detail['current_revision']]
2260
+ patchset = int(revision_info['_number'])
2261
+ else:
2262
+ patchset = parsed_issue_arg.patchset
2263
+ for revision_info in detail['revisions'].itervalues():
2264
+ if int(revision_info['_number']) == parsed_issue_arg.patchset:
2265
+ break
2266
+ else:
2267
+ DieWithError('Couldn\'t find patchset %i in issue %i' %
2268
+ (parsed_issue_arg.patchset, self.GetIssue()))
2269
+
2270
+ fetch_info = revision_info['fetch']['http']
2271
+ RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2272
+ RunGit(['cherry-pick', 'FETCH_HEAD'])
2273
+ self.SetIssue(self.GetIssue())
2274
+ self.SetPatchset(patchset)
2275
+ print('Committed patch for issue %i pathset %i locally' %
2276
+ (self.GetIssue(), self.GetPatchset()))
2277
+ return 0
2278
+
2279
+ @staticmethod
2280
+ def ParseIssueURL(parsed_url):
2281
+ if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2282
+ return None
2283
+ # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2284
+ # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2285
+ # Short urls like https://domain/<issue_number> can be used, but don't allow
2286
+ # specifying the patchset (you'd 404), but we allow that here.
2287
+ if parsed_url.path == '/':
2288
+ part = parsed_url.fragment
2289
+ else:
2290
+ part = parsed_url.path
2291
+ match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2292
+ if match:
2293
+ return _ParsedIssueNumberArgument(
2294
+ issue=int(match.group(2)),
2295
+ patchset=int(match.group(4)) if match.group(4) else None,
2296
+ hostname=parsed_url.netloc)
2297
+ return None
2298
+
2299
+ def CMDUploadChange(self, options, args, change):
2300
+ """Upload the current branch to Gerrit."""
2301
+ if options.squash and options.no_squash:
2302
+ DieWithError('Can only use one of --squash or --no-squash')
2303
+ options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2304
+ not options.no_squash)
2305
+ # We assume the remote called "origin" is the one we want.
2306
+ # It is probably not worthwhile to support different workflows.
2307
+ gerrit_remote = 'origin'
2308
+
2309
+ remote, remote_branch = self.GetRemoteBranch()
2310
+ branch = GetTargetRef(remote, remote_branch, options.target_branch,
2311
+ pending_prefix='')
2312
+
2313
+ if options.squash:
2314
+ if not self.GetIssue():
2315
+ # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2316
+ # with shadow branch, which used to contain change-id for a given
2317
+ # branch, using which we can fetch actual issue number and set it as the
2318
+ # property of the branch, which is the new way.
2319
+ message = RunGitSilent([
2320
+ 'show', '--format=%B', '-s',
2321
+ 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2322
+ if message:
2323
+ change_ids = git_footers.get_footer_change_id(message.strip())
2324
+ if change_ids and len(change_ids) == 1:
2325
+ details = self._GetChangeDetail(issue=change_ids[0])
2326
+ if details:
2327
+ print('WARNING: found old upload in branch git_cl_uploads/%s '
2328
+ 'corresponding to issue %s' %
2329
+ (self.GetBranch(), details['_number']))
2330
+ self.SetIssue(details['_number'])
2331
+ if not self.GetIssue():
2332
+ DieWithError(
2333
+ '\n' # For readability of the blob below.
2334
+ 'Found old upload in branch git_cl_uploads/%s, '
2335
+ 'but failed to find corresponding Gerrit issue.\n'
2336
+ 'If you know the issue number, set it manually first:\n'
2337
+ ' git cl issue 123456\n'
2338
+ 'If you intended to upload this CL as new issue, '
2339
+ 'just delete or rename the old upload branch:\n'
2340
+ ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2341
+ 'After that, please run git cl upload again.' %
2342
+ tuple([self.GetBranch()] * 3))
2343
+ # End of backwards compatability.
2344
+
2345
+ if self.GetIssue():
2346
+ # Try to get the message from a previous upload.
2347
+ message = self.GetDescription()
2348
+ if not message:
2349
+ DieWithError(
2350
+ 'failed to fetch description from current Gerrit issue %d\n'
2351
+ '%s' % (self.GetIssue(), self.GetIssueURL()))
2352
+ change_id = self._GetChangeDetail()['change_id']
2353
+ while True:
2354
+ footer_change_ids = git_footers.get_footer_change_id(message)
2355
+ if footer_change_ids == [change_id]:
2356
+ break
2357
+ if not footer_change_ids:
2358
+ message = git_footers.add_footer_change_id(message, change_id)
2359
+ print('WARNING: appended missing Change-Id to issue description')
2360
+ continue
2361
+ # There is already a valid footer but with different or several ids.
2362
+ # Doing this automatically is non-trivial as we don't want to lose
2363
+ # existing other footers, yet we want to append just 1 desired
2364
+ # Change-Id. Thus, just create a new footer, but let user verify the
2365
+ # new description.
2366
+ message = '%s\n\nChange-Id: %s' % (message, change_id)
2367
+ print(
2368
+ 'WARNING: issue %s has Change-Id footer(s):\n'
2369
+ ' %s\n'
2370
+ 'but issue has Change-Id %s, according to Gerrit.\n'
2371
+ 'Please, check the proposed correction to the description, '
2372
+ 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2373
+ % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2374
+ change_id))
2375
+ ask_for_data('Press enter to edit now, Ctrl+C to abort')
2376
+ if not options.force:
2377
+ change_desc = ChangeDescription(message)
2378
+ change_desc.prompt()
2379
+ message = change_desc.description
2380
+ if not message:
2381
+ DieWithError("Description is empty. Aborting...")
2382
+ # Continue the while loop.
2383
+ # Sanity check of this code - we should end up with proper message
2384
+ # footer.
2385
+ assert [change_id] == git_footers.get_footer_change_id(message)
2386
+ change_desc = ChangeDescription(message)
2387
+ else:
2388
+ change_desc = ChangeDescription(
2389
+ options.message or CreateDescriptionFromLog(args))
2390
+ if not options.force:
2391
+ change_desc.prompt()
2392
+ if not change_desc.description:
2393
+ DieWithError("Description is empty. Aborting...")
2394
+ message = change_desc.description
2395
+ change_ids = git_footers.get_footer_change_id(message)
2396
+ if len(change_ids) > 1:
2397
+ DieWithError('too many Change-Id footers, at most 1 allowed.')
2398
+ if not change_ids:
2399
+ # Generate the Change-Id automatically.
2400
+ message = git_footers.add_footer_change_id(
2401
+ message, GenerateGerritChangeId(message))
2402
+ change_desc.set_description(message)
2403
+ change_ids = git_footers.get_footer_change_id(message)
2404
+ assert len(change_ids) == 1
2405
+ change_id = change_ids[0]
2406
+
2407
+ remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2408
+ if remote is '.':
2409
+ # If our upstream branch is local, we base our squashed commit on its
2410
+ # squashed version.
2411
+ upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2412
+ # Check the squashed hash of the parent.
2413
+ parent = RunGit(['config',
2414
+ 'branch.%s.gerritsquashhash' % upstream_branch_name],
2415
+ error_ok=True).strip()
2416
+ # Verify that the upstream branch has been uploaded too, otherwise
2417
+ # Gerrit will create additional CLs when uploading.
2418
+ if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2419
+ RunGitSilent(['rev-parse', parent + ':'])):
2420
+ # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2421
+ DieWithError(
2422
+ 'Upload upstream branch %s first.\n'
2423
+ 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2424
+ 'version of depot_tools. If so, then re-upload it with:\n'
2425
+ ' git cl upload --squash\n' % upstream_branch_name)
2426
+ else:
2427
+ parent = self.GetCommonAncestorWithUpstream()
2428
+
2429
+ tree = RunGit(['rev-parse', 'HEAD:']).strip()
2430
+ ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2431
+ '-m', message]).strip()
2432
+ else:
2433
+ change_desc = ChangeDescription(
2434
+ options.message or CreateDescriptionFromLog(args))
2435
+ if not change_desc.description:
2436
+ DieWithError("Description is empty. Aborting...")
2437
+
2438
+ if not git_footers.get_footer_change_id(change_desc.description):
2439
+ DownloadGerritHook(False)
2440
+ change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2441
+ args))
2442
+ ref_to_push = 'HEAD'
2443
+ parent = '%s/%s' % (gerrit_remote, branch)
2444
+ change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2445
+
2446
+ assert change_desc
2447
+ commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2448
+ ref_to_push)]).splitlines()
2449
+ if len(commits) > 1:
2450
+ print('WARNING: This will upload %d commits. Run the following command '
2451
+ 'to see which commits will be uploaded: ' % len(commits))
2452
+ print('git log %s..%s' % (parent, ref_to_push))
2453
+ print('You can also use `git squash-branch` to squash these into a '
2454
+ 'single commit.')
2455
+ ask_for_data('About to upload; enter to confirm.')
2456
+
2457
+ if options.reviewers or options.tbr_owners:
2458
+ change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2459
+ change)
2460
+
2461
+ # Extra options that can be specified at push time. Doc:
2462
+ # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2463
+ refspec_opts = []
2464
+ if options.title:
2465
+ # Per doc, spaces must be converted to underscores, and Gerrit will do the
2466
+ # reverse on its side.
2467
+ if '_' in options.title:
2468
+ print('WARNING: underscores in title will be converted to spaces.')
2469
+ refspec_opts.append('m=' + options.title.replace(' ', '_'))
2470
+
2471
+ cc = self.GetCCList().split(',')
2472
+ if options.cc:
2473
+ cc.extend(options.cc)
2474
+ cc = filter(None, cc)
2475
+ if cc:
2476
+ # refspec_opts.extend('cc=' + email.strip() for email in cc)
2477
+ # TODO(tandrii): enable this back. http://crbug.com/604377
2478
+ print('WARNING: Gerrit doesn\'t yet support cc-ing arbitrary emails.\n'
2479
+ ' Ignoring cc-ed emails. See http://crbug.com/604377.')
2480
+
2481
+ if change_desc.get_reviewers():
2482
+ refspec_opts.extend('r=' + email.strip()
2483
+ for email in change_desc.get_reviewers())
2484
+
2485
+
2486
+ refspec_suffix = ''
2487
+ if refspec_opts:
2488
+ refspec_suffix = '%' + ','.join(refspec_opts)
2489
+ assert ' ' not in refspec_suffix, (
2490
+ 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2491
+ refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2492
+
2493
+ push_stdout = gclient_utils.CheckCallAndFilter(
2494
+ ['git', 'push', gerrit_remote, refspec],
2495
+ print_stdout=True,
2496
+ # Flush after every line: useful for seeing progress when running as
2497
+ # recipe.
2498
+ filter_fn=lambda _: sys.stdout.flush())
2499
+
2500
+ if options.squash:
2501
+ regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2502
+ change_numbers = [m.group(1)
2503
+ for m in map(regex.match, push_stdout.splitlines())
2504
+ if m]
2505
+ if len(change_numbers) != 1:
2506
+ DieWithError(
2507
+ ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2508
+ 'Change-Id: %s') % (len(change_numbers), change_id))
2509
+ self.SetIssue(change_numbers[0])
2510
+ RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2511
+ ref_to_push])
2512
+ return 0
2513
+
2514
+ def _AddChangeIdToCommitMessage(self, options, args):
2515
+ """Re-commits using the current message, assumes the commit hook is in
2516
+ place.
2517
+ """
2518
+ log_desc = options.message or CreateDescriptionFromLog(args)
2519
+ git_command = ['commit', '--amend', '-m', log_desc]
2520
+ RunGit(git_command)
2521
+ new_log_desc = CreateDescriptionFromLog(args)
2522
+ if git_footers.get_footer_change_id(new_log_desc):
2523
+ print 'git-cl: Added Change-Id to commit message.'
2524
+ return new_log_desc
2525
+ else:
2526
+ print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2527
+
2528
+ def SetCQState(self, new_state):
2529
+ """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2530
+ # TODO(tandrii): maybe allow configurability in codereview.settings or by
2531
+ # self-discovery of label config for this CL using REST API.
2532
+ vote_map = {
2533
+ _CQState.NONE: 0,
2534
+ _CQState.DRY_RUN: 1,
2535
+ _CQState.COMMIT : 2,
2536
+ }
2537
+ gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2538
+ labels={'Commit-Queue': vote_map[new_state]})
2539
+
2540
+
2541
+ _CODEREVIEW_IMPLEMENTATIONS = {
2542
+ 'rietveld': _RietveldChangelistImpl,
2543
+ 'gerrit': _GerritChangelistImpl,
2544
+ }
2545
+
2546
+
2547
+ def _add_codereview_select_options(parser):
2548
+ """Appends --gerrit and --rietveld options to force specific codereview."""
2549
+ parser.codereview_group = optparse.OptionGroup(
2550
+ parser, 'EXPERIMENTAL! Codereview override options')
2551
+ parser.add_option_group(parser.codereview_group)
2552
+ parser.codereview_group.add_option(
2553
+ '--gerrit', action='store_true',
2554
+ help='Force the use of Gerrit for codereview')
2555
+ parser.codereview_group.add_option(
2556
+ '--rietveld', action='store_true',
2557
+ help='Force the use of Rietveld for codereview')
1171
2558
 
1172
- # TODO: configure a default branch to diff against, rather than this
1173
- # svn-based hackery.
2559
+
2560
+ def _process_codereview_select_options(parser, options):
2561
+ if options.gerrit and options.rietveld:
2562
+ parser.error('Options --gerrit and --rietveld are mutually exclusive')
2563
+ options.forced_codereview = None
2564
+ if options.gerrit:
2565
+ options.forced_codereview = 'gerrit'
2566
+ elif options.rietveld:
2567
+ options.forced_codereview = 'rietveld'
1174
2568
 
1175
2569
 
1176
2570
  class ChangeDescription(object):
@@ -1356,6 +2750,14 @@ def LoadCodereviewSettingsFromFile(fileobj):
1356
2750
  if 'GERRIT_HOST' in keyvals:
1357
2751
  RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
1358
2752
 
2753
+ if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2754
+ RunGit(['config', 'gerrit.squash-uploads',
2755
+ keyvals['GERRIT_SQUASH_UPLOADS']])
2756
+
2757
+ if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
2758
+ RunGit(['config', 'gerrit.skip-ensure-authenticated',
2759
+ keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2760
+
1359
2761
  if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1360
2762
  #should be of the form
1361
2763
  #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
@@ -1377,8 +2779,13 @@ def hasSheBang(fname):
1377
2779
  return f.read(2).startswith('#!')
1378
2780
 
1379
2781
 
1380
- def DownloadHooks(force):
1381
- """downloads hooks
2782
+ # TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2783
+ def DownloadHooks(*args, **kwargs):
2784
+ pass
2785
+
2786
+
2787
+ def DownloadGerritHook(force):
2788
+ """Download and install Gerrit commit-msg hook.
1382
2789
 
1383
2790
  Args:
1384
2791
  force: True to update hooks. False to install hooks if not present.
@@ -1392,6 +2799,10 @@ def DownloadHooks(force):
1392
2799
  if not force:
1393
2800
  return
1394
2801
  try:
2802
+ print(
2803
+ 'WARNING: installing Gerrit commit-msg hook.\n'
2804
+ ' This behavior of git cl will soon be disabled.\n'
2805
+ ' See bug http://crbug.com/579176.')
1395
2806
  urlretrieve(src, dst)
1396
2807
  if not hasSheBang(dst):
1397
2808
  DieWithError('Not a script: %s\n'
@@ -1408,10 +2819,50 @@ def DownloadHooks(force):
1408
2819
  'chmod +x .git/hooks/commit-msg' % src)
1409
2820
 
1410
2821
 
2822
+
2823
+ def GetRietveldCodereviewSettingsInteractively():
2824
+ """Prompt the user for settings."""
2825
+ server = settings.GetDefaultServerUrl(error_ok=True)
2826
+ prompt = 'Rietveld server (host[:port])'
2827
+ prompt += ' [%s]' % (server or DEFAULT_SERVER)
2828
+ newserver = ask_for_data(prompt + ':')
2829
+ if not server and not newserver:
2830
+ newserver = DEFAULT_SERVER
2831
+ if newserver:
2832
+ newserver = gclient_utils.UpgradeToHttps(newserver)
2833
+ if newserver != server:
2834
+ RunGit(['config', 'rietveld.server', newserver])
2835
+
2836
+ def SetProperty(initial, caption, name, is_url):
2837
+ prompt = caption
2838
+ if initial:
2839
+ prompt += ' ("x" to clear) [%s]' % initial
2840
+ new_val = ask_for_data(prompt + ':')
2841
+ if new_val == 'x':
2842
+ RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2843
+ elif new_val:
2844
+ if is_url:
2845
+ new_val = gclient_utils.UpgradeToHttps(new_val)
2846
+ if new_val != initial:
2847
+ RunGit(['config', 'rietveld.' + name, new_val])
2848
+
2849
+ SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2850
+ SetProperty(settings.GetDefaultPrivateFlag(),
2851
+ 'Private flag (rietveld only)', 'private', False)
2852
+ SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2853
+ 'tree-status-url', False)
2854
+ SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2855
+ SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2856
+ SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2857
+ 'run-post-upload-hook', False)
2858
+
1411
2859
  @subcommand.usage('[repo root containing codereview.settings]')
1412
2860
  def CMDconfig(parser, args):
1413
2861
  """Edits configuration for this tree."""
1414
2862
 
2863
+ print('WARNING: git cl config works for Rietveld only.\n'
2864
+ 'For Gerrit, see http://crbug.com/603116.')
2865
+ # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
1415
2866
  parser.add_option('--activate-update', action='store_true',
1416
2867
  help='activate auto-updating [rietveld] section in '
1417
2868
  '.git/config')
@@ -1429,8 +2880,7 @@ def CMDconfig(parser, args):
1429
2880
  return
1430
2881
 
1431
2882
  if len(args) == 0:
1432
- GetCodereviewSettingsInteractively()
1433
- DownloadHooks(True)
2883
+ GetRietveldCodereviewSettingsInteractively()
1434
2884
  return 0
1435
2885
 
1436
2886
  url = args[0]
@@ -1439,7 +2889,6 @@ def CMDconfig(parser, args):
1439
2889
 
1440
2890
  # Load code review settings and download hooks (if available).
1441
2891
  LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
1442
- DownloadHooks(True)
1443
2892
  return 0
1444
2893
 
1445
2894
 
@@ -1484,7 +2933,7 @@ def fetch_cl_status(branch, auth_config=None):
1484
2933
 
1485
2934
  def get_cl_statuses(
1486
2935
  branches, fine_grained, max_processes=None, auth_config=None):
1487
- """Returns a blocking iterable of (branch, issue, color) for given branches.
2936
+ """Returns a blocking iterable of (branch, issue, status) for given branches.
1488
2937
 
1489
2938
  If fine_grained is true, this will fetch CL statuses from the server.
1490
2939
  Otherwise, simply indicate if there's a matching url for the given branches.
@@ -1492,7 +2941,15 @@ def get_cl_statuses(
1492
2941
  If max_processes is specified, it is used as the maximum number of processes
1493
2942
  to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1494
2943
  spawned.
2944
+
2945
+ See GetStatus() for a list of possible statuses.
1495
2946
  """
2947
+ def fetch(branch):
2948
+ if not branch:
2949
+ return None
2950
+
2951
+ return fetch_cl_status(branch, auth_config=auth_config)
2952
+
1496
2953
  # Silence upload.py otherwise it becomes unwieldly.
1497
2954
  upload.verbosity = 0
1498
2955
 
@@ -1500,7 +2957,7 @@ def get_cl_statuses(
1500
2957
  # Process one branch synchronously to work through authentication, then
1501
2958
  # spawn processes to process all the other branches in parallel.
1502
2959
  if branches:
1503
- fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2960
+
1504
2961
  yield fetch(branches[0])
1505
2962
 
1506
2963
  branches_to_fetch = branches[1:]
@@ -1508,13 +2965,28 @@ def get_cl_statuses(
1508
2965
  min(max_processes, len(branches_to_fetch))
1509
2966
  if max_processes is not None
1510
2967
  else len(branches_to_fetch))
1511
- for x in pool.imap_unordered(fetch, branches_to_fetch):
1512
- yield x
2968
+
2969
+ fetched_branches = set()
2970
+ it = pool.imap_unordered(fetch, branches_to_fetch).__iter__()
2971
+ while True:
2972
+ try:
2973
+ row = it.next(timeout=5)
2974
+ except multiprocessing.TimeoutError:
2975
+ break
2976
+
2977
+ fetched_branches.add(row[0])
2978
+ yield row
2979
+
2980
+ # Add any branches that failed to fetch.
2981
+ for b in set(branches_to_fetch) - fetched_branches:
2982
+ cl = Changelist(branchref=b, auth_config=auth_config)
2983
+ yield (b, cl.GetIssueURL() if b else None, 'error')
2984
+
1513
2985
  else:
1514
2986
  # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1515
2987
  for b in branches:
1516
2988
  cl = Changelist(branchref=b, auth_config=auth_config)
1517
- url = cl.GetIssueURL()
2989
+ url = cl.GetIssueURL() if b else None
1518
2990
  yield (b, url, 'waiting' if url else 'error')
1519
2991
 
1520
2992
 
@@ -1583,8 +3055,10 @@ def upload_branch_deps(cl, args):
1583
3055
  '"git cl upload".')
1584
3056
  ask_for_data('[Press enter to continue or ctrl-C to quit]')
1585
3057
 
1586
- # Add a default patchset title to all upload calls.
1587
- args.extend(['-t', 'Updated patchset dependency'])
3058
+ # Add a default patchset title to all upload calls in Rietveld.
3059
+ if not cl.IsGerrit():
3060
+ args.extend(['-t', 'Updated patchset dependency'])
3061
+
1588
3062
  # Record all dependents that failed to upload.
1589
3063
  failures = {}
1590
3064
  # Go through all dependents, checkout the branch and upload.
@@ -1669,6 +3143,7 @@ def CMDstatus(parser, args):
1669
3143
  changes = (
1670
3144
  Changelist(branchref=b, auth_config=auth_config)
1671
3145
  for b in branches.splitlines())
3146
+ # TODO(tandrii): refactor to use CLs list instead of branches list.
1672
3147
  branches = [c.GetBranch() for c in changes]
1673
3148
  alignment = max(5, max(len(b) for b in branches))
1674
3149
  print 'Branches associated with reviews:'
@@ -1686,7 +3161,7 @@ def CMDstatus(parser, args):
1686
3161
  issue_url, status = branch_statuses.pop(branch)
1687
3162
  color = color_for_status(status)
1688
3163
  reset = Fore.RESET
1689
- if not sys.stdout.isatty():
3164
+ if not setup_color.IS_TTY:
1690
3165
  color = ''
1691
3166
  reset = ''
1692
3167
  status_str = '(%s)' % status if status else ''
@@ -1697,10 +3172,10 @@ def CMDstatus(parser, args):
1697
3172
  cl = Changelist(auth_config=auth_config)
1698
3173
  print
1699
3174
  print 'Current branch:',
3175
+ print cl.GetBranch()
1700
3176
  if not cl.GetIssue():
1701
- print 'no issue assigned.'
3177
+ print 'No issue assigned.'
1702
3178
  return 0
1703
- print cl.GetBranch()
1704
3179
  print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1705
3180
  if not options.fast:
1706
3181
  print 'Issue description:'
@@ -1734,7 +3209,9 @@ def CMDissue(parser, args):
1734
3209
  help='Lookup the branch(es) for the specified issues. If '
1735
3210
  'no issues are specified, all branches with mapped '
1736
3211
  'issues will be listed.')
3212
+ _add_codereview_select_options(parser)
1737
3213
  options, args = parser.parse_args(args)
3214
+ _process_codereview_select_options(parser, options)
1738
3215
 
1739
3216
  if options.reverse:
1740
3217
  branches = RunGit(['for-each-ref', 'refs/heads',
@@ -1753,13 +3230,13 @@ def CMDissue(parser, args):
1753
3230
  print 'Branch for issue number %s: %s' % (
1754
3231
  issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1755
3232
  else:
1756
- cl = Changelist()
3233
+ cl = Changelist(codereview=options.forced_codereview)
1757
3234
  if len(args) > 0:
1758
3235
  try:
1759
3236
  issue = int(args[0])
1760
3237
  except ValueError:
1761
3238
  DieWithError('Pass a number to set the issue or none to list it.\n'
1762
- 'Maybe you want to run git cl status?')
3239
+ 'Maybe you want to run git cl status?')
1763
3240
  cl.SetIssue(issue)
1764
3241
  print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1765
3242
  return 0
@@ -1771,6 +3248,8 @@ def CMDcomments(parser, args):
1771
3248
  help='comment to add to an issue')
1772
3249
  parser.add_option('-i', dest='issue',
1773
3250
  help="review issue id (defaults to current issue)")
3251
+ parser.add_option('-j', '--json-file',
3252
+ help='File to write JSON summary to')
1774
3253
  auth.add_auth_options(parser)
1775
3254
  options, args = parser.parse_args(args)
1776
3255
  auth_config = auth.extract_auth_config_from_options(options)
@@ -1782,18 +3261,28 @@ def CMDcomments(parser, args):
1782
3261
  except ValueError:
1783
3262
  DieWithError('A review issue id is expected to be a number')
1784
3263
 
1785
- cl = Changelist(issue=issue, auth_config=auth_config)
3264
+ cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
1786
3265
 
1787
3266
  if options.comment:
1788
3267
  cl.AddComment(options.comment)
1789
3268
  return 0
1790
3269
 
1791
3270
  data = cl.GetIssueProperties()
3271
+ summary = []
1792
3272
  for message in sorted(data.get('messages', []), key=lambda x: x['date']):
3273
+ summary.append({
3274
+ 'date': message['date'],
3275
+ 'lgtm': False,
3276
+ 'message': message['text'],
3277
+ 'not_lgtm': False,
3278
+ 'sender': message['sender'],
3279
+ })
1793
3280
  if message['disapproval']:
1794
3281
  color = Fore.RED
3282
+ summary[-1]['not lgtm'] = True
1795
3283
  elif message['approval']:
1796
3284
  color = Fore.GREEN
3285
+ summary[-1]['lgtm'] = True
1797
3286
  elif message['sender'] == data['owner_email']:
1798
3287
  color = Fore.MAGENTA
1799
3288
  else:
@@ -1803,19 +3292,56 @@ def CMDcomments(parser, args):
1803
3292
  Fore.RESET)
1804
3293
  if message['text'].strip():
1805
3294
  print '\n'.join(' ' + l for l in message['text'].splitlines())
3295
+ if options.json_file:
3296
+ with open(options.json_file, 'wb') as f:
3297
+ json.dump(summary, f)
1806
3298
  return 0
1807
3299
 
1808
3300
 
3301
+ @subcommand.usage('[codereview url or issue id]')
1809
3302
  def CMDdescription(parser, args):
1810
3303
  """Brings up the editor for the current CL's description."""
3304
+ parser.add_option('-d', '--display', action='store_true',
3305
+ help='Display the description instead of opening an editor')
3306
+ parser.add_option('-n', '--new-description',
3307
+ help='New description to set for this issue (- for stdin)')
3308
+
3309
+ _add_codereview_select_options(parser)
1811
3310
  auth.add_auth_options(parser)
1812
- options, _ = parser.parse_args(args)
3311
+ options, args = parser.parse_args(args)
3312
+ _process_codereview_select_options(parser, options)
3313
+
3314
+ target_issue = None
3315
+ if len(args) > 0:
3316
+ issue_arg = ParseIssueNumberArgument(args[0])
3317
+ if not issue_arg.valid:
3318
+ parser.print_help()
3319
+ return 1
3320
+ target_issue = issue_arg.issue
3321
+
1813
3322
  auth_config = auth.extract_auth_config_from_options(options)
1814
- cl = Changelist(auth_config=auth_config)
3323
+
3324
+ cl = Changelist(
3325
+ auth_config=auth_config, issue=target_issue,
3326
+ codereview=options.forced_codereview)
3327
+
1815
3328
  if not cl.GetIssue():
1816
3329
  DieWithError('This branch has no associated changelist.')
1817
3330
  description = ChangeDescription(cl.GetDescription())
1818
- description.prompt()
3331
+
3332
+ if options.display:
3333
+ print description.description
3334
+ return 0
3335
+
3336
+ if options.new_description:
3337
+ text = options.new_description
3338
+ if text == '-':
3339
+ text = '\n'.join(l.rstrip() for l in sys.stdin)
3340
+
3341
+ description.set_description(text)
3342
+ else:
3343
+ description.prompt()
3344
+
1819
3345
  if cl.GetDescription() != description.description:
1820
3346
  cl.UpdateDescription(description.description)
1821
3347
  return 0
@@ -1919,131 +3445,34 @@ def CMDpresubmit(parser, args):
1919
3445
  return 0
1920
3446
 
1921
3447
 
1922
- def AddChangeIdToCommitMessage(options, args):
1923
- """Re-commits using the current message, assumes the commit hook is in
1924
- place.
1925
- """
1926
- log_desc = options.message or CreateDescriptionFromLog(args)
1927
- git_command = ['commit', '--amend', '-m', log_desc]
1928
- RunGit(git_command)
1929
- new_log_desc = CreateDescriptionFromLog(args)
1930
- if CHANGE_ID in new_log_desc:
1931
- print 'git-cl: Added Change-Id to commit message.'
1932
- else:
1933
- print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1934
-
1935
-
1936
- def GerritUpload(options, args, cl, change):
1937
- """upload the current branch to gerrit."""
1938
- # We assume the remote called "origin" is the one we want.
1939
- # It is probably not worthwhile to support different workflows.
1940
- gerrit_remote = 'origin'
1941
-
1942
- remote, remote_branch = cl.GetRemoteBranch()
1943
- branch = GetTargetRef(remote, remote_branch, options.target_branch,
1944
- pending_prefix='')
1945
-
1946
- change_desc = ChangeDescription(
1947
- options.message or CreateDescriptionFromLog(args))
1948
- if not change_desc.description:
1949
- print "Description is empty; aborting."
1950
- return 1
1951
-
1952
- if options.squash:
1953
- # Try to get the message from a previous upload.
1954
- shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1955
- message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1956
- if not message:
1957
- if not options.force:
1958
- change_desc.prompt()
1959
-
1960
- if CHANGE_ID not in change_desc.description:
1961
- # Run the commit-msg hook without modifying the head commit by writing
1962
- # the commit message to a temporary file and running the hook over it,
1963
- # then reading the file back in.
1964
- commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1965
- 'commit-msg')
1966
- file_handle, msg_file = tempfile.mkstemp(text=True,
1967
- prefix='commit_msg')
1968
- try:
1969
- try:
1970
- with os.fdopen(file_handle, 'w') as fileobj:
1971
- fileobj.write(change_desc.description)
1972
- finally:
1973
- os.close(file_handle)
1974
- RunCommand([commit_msg_hook, msg_file])
1975
- change_desc.set_description(gclient_utils.FileRead(msg_file))
1976
- finally:
1977
- os.remove(msg_file)
1978
-
1979
- if not change_desc.description:
1980
- print "Description is empty; aborting."
1981
- return 1
1982
-
1983
- message = change_desc.description
1984
-
1985
- remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1986
- if remote is '.':
1987
- # If our upstream branch is local, we base our squashed commit on its
1988
- # squashed version.
1989
- parent = ('refs/heads/git_cl_uploads/' +
1990
- scm.GIT.ShortBranchName(upstream_branch))
3448
+ def GenerateGerritChangeId(message):
3449
+ """Returns Ixxxxxx...xxx change id.
1991
3450
 
1992
- # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1993
- # will create additional CLs when uploading.
1994
- if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1995
- RunGitSilent(['rev-parse', parent + ':'])):
1996
- print 'Upload upstream branch ' + upstream_branch + ' first.'
1997
- return 1
1998
- else:
1999
- parent = cl.GetCommonAncestorWithUpstream()
3451
+ Works the same way as
3452
+ https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3453
+ but can be called on demand on all platforms.
2000
3454
 
2001
- tree = RunGit(['rev-parse', 'HEAD:']).strip()
2002
- ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2003
- '-m', message]).strip()
2004
- else:
2005
- if CHANGE_ID not in change_desc.description:
2006
- AddChangeIdToCommitMessage(options, args)
2007
- ref_to_push = 'HEAD'
2008
- parent = '%s/%s' % (gerrit_remote, branch)
2009
-
2010
- commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2011
- ref_to_push)]).splitlines()
2012
- if len(commits) > 1:
2013
- print('WARNING: This will upload %d commits. Run the following command '
2014
- 'to see which commits will be uploaded: ' % len(commits))
2015
- print('git log %s..%s' % (parent, ref_to_push))
2016
- print('You can also use `git squash-branch` to squash these into a single '
2017
- 'commit.')
2018
- ask_for_data('About to upload; enter to confirm.')
2019
-
2020
- if options.reviewers or options.tbr_owners:
2021
- change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
2022
-
2023
- receive_options = []
2024
- cc = cl.GetCCList().split(',')
2025
- if options.cc:
2026
- cc.extend(options.cc)
2027
- cc = filter(None, cc)
2028
- if cc:
2029
- receive_options += ['--cc=' + email for email in cc]
2030
- if change_desc.get_reviewers():
2031
- receive_options.extend(
2032
- '--reviewer=' + email for email in change_desc.get_reviewers())
2033
-
2034
- git_command = ['push']
2035
- if receive_options:
2036
- git_command.append('--receive-pack=git receive-pack %s' %
2037
- ' '.join(receive_options))
2038
- git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
2039
- RunGit(git_command)
2040
-
2041
- if options.squash:
2042
- head = RunGit(['rev-parse', 'HEAD']).strip()
2043
- RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2044
-
2045
- # TODO(ukai): parse Change-Id: and set issue number?
2046
- return 0
3455
+ The basic idea is to generate git hash of a state of the tree, original commit
3456
+ message, author/committer info and timestamps.
3457
+ """
3458
+ lines = []
3459
+ tree_hash = RunGitSilent(['write-tree'])
3460
+ lines.append('tree %s' % tree_hash.strip())
3461
+ code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3462
+ if code == 0:
3463
+ lines.append('parent %s' % parent.strip())
3464
+ author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3465
+ lines.append('author %s' % author.strip())
3466
+ committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3467
+ lines.append('committer %s' % committer.strip())
3468
+ lines.append('')
3469
+ # Note: Gerrit's commit-hook actually cleans message of some lines and
3470
+ # whitespace. This code is not doing this, but it clearly won't decrease
3471
+ # entropy.
3472
+ lines.append(message)
3473
+ change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3474
+ stdin='\n'.join(lines))
3475
+ return 'I%s' % change_hash.strip()
2047
3476
 
2048
3477
 
2049
3478
  def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
@@ -2101,148 +3530,6 @@ def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2101
3530
  return remote_branch
2102
3531
 
2103
3532
 
2104
- def RietveldUpload(options, args, cl, change):
2105
- """upload the patch to rietveld."""
2106
- upload_args = ['--assume_yes'] # Don't ask about untracked files.
2107
- upload_args.extend(['--server', cl.GetRietveldServer()])
2108
- upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
2109
- if options.emulate_svn_auto_props:
2110
- upload_args.append('--emulate_svn_auto_props')
2111
-
2112
- change_desc = None
2113
-
2114
- if options.email is not None:
2115
- upload_args.extend(['--email', options.email])
2116
-
2117
- if cl.GetIssue():
2118
- if options.title:
2119
- upload_args.extend(['--title', options.title])
2120
- if options.message:
2121
- upload_args.extend(['--message', options.message])
2122
- upload_args.extend(['--issue', str(cl.GetIssue())])
2123
- print ("This branch is associated with issue %s. "
2124
- "Adding patch to that issue." % cl.GetIssue())
2125
- else:
2126
- if options.title:
2127
- upload_args.extend(['--title', options.title])
2128
- message = options.title or options.message or CreateDescriptionFromLog(args)
2129
- change_desc = ChangeDescription(message)
2130
- if options.reviewers or options.tbr_owners:
2131
- change_desc.update_reviewers(options.reviewers,
2132
- options.tbr_owners,
2133
- change)
2134
- if not options.force:
2135
- change_desc.prompt()
2136
-
2137
- if not change_desc.description:
2138
- print "Description is empty; aborting."
2139
- return 1
2140
-
2141
- upload_args.extend(['--message', change_desc.description])
2142
- if change_desc.get_reviewers():
2143
- upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
2144
- if options.send_mail:
2145
- if not change_desc.get_reviewers():
2146
- DieWithError("Must specify reviewers to send email.")
2147
- upload_args.append('--send_mail')
2148
-
2149
- # We check this before applying rietveld.private assuming that in
2150
- # rietveld.cc only addresses which we can send private CLs to are listed
2151
- # if rietveld.private is set, and so we should ignore rietveld.cc only when
2152
- # --private is specified explicitly on the command line.
2153
- if options.private:
2154
- logging.warn('rietveld.cc is ignored since private flag is specified. '
2155
- 'You need to review and add them manually if necessary.')
2156
- cc = cl.GetCCListWithoutDefault()
2157
- else:
2158
- cc = cl.GetCCList()
2159
- cc = ','.join(filter(None, (cc, ','.join(options.cc))))
2160
- if cc:
2161
- upload_args.extend(['--cc', cc])
2162
-
2163
- if options.private or settings.GetDefaultPrivateFlag() == "True":
2164
- upload_args.append('--private')
2165
-
2166
- upload_args.extend(['--git_similarity', str(options.similarity)])
2167
- if not options.find_copies:
2168
- upload_args.extend(['--git_no_find_copies'])
2169
-
2170
- # Include the upstream repo's URL in the change -- this is useful for
2171
- # projects that have their source spread across multiple repos.
2172
- remote_url = cl.GetGitBaseUrlFromConfig()
2173
- if not remote_url:
2174
- if settings.GetIsGitSvn():
2175
- remote_url = cl.GetGitSvnRemoteUrl()
2176
- else:
2177
- if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2178
- remote_url = (cl.GetRemoteUrl() + '@'
2179
- + cl.GetUpstreamBranch().split('/')[-1])
2180
- if remote_url:
2181
- upload_args.extend(['--base_url', remote_url])
2182
- remote, remote_branch = cl.GetRemoteBranch()
2183
- target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2184
- settings.GetPendingRefPrefix())
2185
- if target_ref:
2186
- upload_args.extend(['--target_ref', target_ref])
2187
-
2188
- # Look for dependent patchsets. See crbug.com/480453 for more details.
2189
- remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2190
- upstream_branch = ShortBranchName(upstream_branch)
2191
- if remote is '.':
2192
- # A local branch is being tracked.
2193
- local_branch = ShortBranchName(upstream_branch)
2194
- auth_config = auth.extract_auth_config_from_options(options)
2195
- branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2196
- branch_cl_issue_url = branch_cl.GetIssueURL()
2197
- branch_cl_issue = branch_cl.GetIssue()
2198
- branch_cl_patchset = branch_cl.GetPatchset()
2199
- if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2200
- upload_args.extend(
2201
- ['--depends_on_patchset', '%s:%s' % (
2202
- branch_cl_issue, branch_cl_patchset)])
2203
- print
2204
- print ('The current branch (%s) is tracking a local branch (%s) with '
2205
- 'an associated CL.') % (cl.GetBranch(), local_branch)
2206
- print 'Adding %s/#ps%s as a dependency patchset.' % (
2207
- branch_cl_issue_url, branch_cl_patchset)
2208
- print
2209
-
2210
- project = settings.GetProject()
2211
- if project:
2212
- upload_args.extend(['--project', project])
2213
-
2214
- if options.cq_dry_run:
2215
- upload_args.extend(['--cq_dry_run'])
2216
-
2217
- try:
2218
- upload_args = ['upload'] + upload_args + args
2219
- logging.info('upload.RealMain(%s)', upload_args)
2220
- issue, patchset = upload.RealMain(upload_args)
2221
- issue = int(issue)
2222
- patchset = int(patchset)
2223
- except KeyboardInterrupt:
2224
- sys.exit(1)
2225
- except:
2226
- # If we got an exception after the user typed a description for their
2227
- # change, back up the description before re-raising.
2228
- if change_desc:
2229
- backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2230
- print '\nGot exception while uploading -- saving description to %s\n' \
2231
- % backup_path
2232
- backup_file = open(backup_path, 'w')
2233
- backup_file.write(change_desc.description)
2234
- backup_file.close()
2235
- raise
2236
-
2237
- if not cl.GetIssue():
2238
- cl.SetIssue(issue)
2239
- cl.SetPatchset(patchset)
2240
-
2241
- if options.use_commit_queue:
2242
- cl.SetFlag('commit', '1')
2243
- return 0
2244
-
2245
-
2246
3533
  def cleanup_list(l):
2247
3534
  """Fixes a list so that comma separated items are put as individual items.
2248
3535
 
@@ -2256,7 +3543,14 @@ def cleanup_list(l):
2256
3543
 
2257
3544
  @subcommand.usage('[args to "git diff"]')
2258
3545
  def CMDupload(parser, args):
2259
- """Uploads the current changelist to codereview."""
3546
+ """Uploads the current changelist to codereview.
3547
+
3548
+ Can skip dependency patchset uploads for a branch by running:
3549
+ git config branch.branch_name.skip-deps-uploads True
3550
+ To unset run:
3551
+ git config --unset branch.branch_name.skip-deps-uploads
3552
+ Can also set the above globally by using the --global flag.
3553
+ """
2260
3554
  parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2261
3555
  help='bypass upload presubmit hook')
2262
3556
  parser.add_option('--bypass-watchlists', action='store_true',
@@ -2265,7 +3559,8 @@ def CMDupload(parser, args):
2265
3559
  parser.add_option('-f', action='store_true', dest='force',
2266
3560
  help="force yes to questions (don't prompt)")
2267
3561
  parser.add_option('-m', dest='message', help='message for patchset')
2268
- parser.add_option('-t', dest='title', help='title for patchset')
3562
+ parser.add_option('-t', dest='title',
3563
+ help='title for patchset (Rietveld only)')
2269
3564
  parser.add_option('-r', '--reviewers',
2270
3565
  action='append', default=[],
2271
3566
  help='reviewer email addresses')
@@ -2290,11 +3585,15 @@ def CMDupload(parser, args):
2290
3585
  'Default: remote branch head, or master')
2291
3586
  parser.add_option('--squash', action='store_true',
2292
3587
  help='Squash multiple commits into one (Gerrit only)')
3588
+ parser.add_option('--no-squash', action='store_true',
3589
+ help='Don\'t squash multiple commits into one ' +
3590
+ '(Gerrit only)')
2293
3591
  parser.add_option('--email', default=None,
2294
3592
  help='email address to use to connect to Rietveld')
2295
3593
  parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2296
3594
  help='add a set of OWNERS to TBR')
2297
- parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
3595
+ parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3596
+ action='store_true',
2298
3597
  help='Send the patchset to do a CQ dry run right after '
2299
3598
  'upload.')
2300
3599
  parser.add_option('--dependencies', action='store_true',
@@ -2304,7 +3603,9 @@ def CMDupload(parser, args):
2304
3603
  orig_args = args
2305
3604
  add_git_similarity(parser)
2306
3605
  auth.add_auth_options(parser)
3606
+ _add_codereview_select_options(parser)
2307
3607
  (options, args) = parser.parse_args(args)
3608
+ _process_codereview_select_options(parser, options)
2308
3609
  auth_config = auth.extract_auth_config_from_options(options)
2309
3610
 
2310
3611
  if git_common.is_dirty_git_tree('upload'):
@@ -2313,90 +3614,11 @@ def CMDupload(parser, args):
2313
3614
  options.reviewers = cleanup_list(options.reviewers)
2314
3615
  options.cc = cleanup_list(options.cc)
2315
3616
 
2316
- cl = Changelist(auth_config=auth_config)
2317
- if args:
2318
- # TODO(ukai): is it ok for gerrit case?
2319
- base_branch = args[0]
2320
- else:
2321
- if cl.GetBranch() is None:
2322
- DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2323
-
2324
- # Default to diffing against common ancestor of upstream branch
2325
- base_branch = cl.GetCommonAncestorWithUpstream()
2326
- args = [base_branch, 'HEAD']
2327
-
2328
- # Make sure authenticated to Rietveld before running expensive hooks. It is
2329
- # a fast, best efforts check. Rietveld still can reject the authentication
2330
- # during the actual upload.
2331
- if not settings.GetIsGerrit() and auth_config.use_oauth2:
2332
- authenticator = auth.get_authenticator_for_host(
2333
- cl.GetRietveldServer(), auth_config)
2334
- if not authenticator.has_cached_credentials():
2335
- raise auth.LoginRequiredError(cl.GetRietveldServer())
2336
-
2337
- # Apply watchlists on upload.
2338
- change = cl.GetChange(base_branch, None)
2339
- watchlist = watchlists.Watchlists(change.RepositoryRoot())
2340
- files = [f.LocalPath() for f in change.AffectedFiles()]
2341
- if not options.bypass_watchlists:
2342
- cl.SetWatchers(watchlist.GetWatchersForPaths(files))
2343
-
2344
- if not options.bypass_hooks:
2345
- if options.reviewers or options.tbr_owners:
2346
- # Set the reviewer list now so that presubmit checks can access it.
2347
- change_description = ChangeDescription(change.FullDescriptionText())
2348
- change_description.update_reviewers(options.reviewers,
2349
- options.tbr_owners,
2350
- change)
2351
- change.SetDescriptionText(change_description.description)
2352
- hook_results = cl.RunHook(committing=False,
2353
- may_prompt=not options.force,
2354
- verbose=options.verbose,
2355
- change=change)
2356
- if not hook_results.should_continue():
2357
- return 1
2358
- if not options.reviewers and hook_results.reviewers:
2359
- options.reviewers = hook_results.reviewers.split(',')
2360
-
2361
- if cl.GetIssue():
2362
- latest_patchset = cl.GetMostRecentPatchset()
2363
- local_patchset = cl.GetPatchset()
2364
- if latest_patchset and local_patchset and local_patchset != latest_patchset:
2365
- print ('The last upload made from this repository was patchset #%d but '
2366
- 'the most recent patchset on the server is #%d.'
2367
- % (local_patchset, latest_patchset))
2368
- print ('Uploading will still work, but if you\'ve uploaded to this issue '
2369
- 'from another machine or branch the patch you\'re uploading now '
2370
- 'might not include those changes.')
2371
- ask_for_data('About to upload; enter to confirm.')
2372
-
2373
- print_stats(options.similarity, options.find_copies, args)
2374
- if settings.GetIsGerrit():
2375
- return GerritUpload(options, args, cl, change)
2376
- ret = RietveldUpload(options, args, cl, change)
2377
- if not ret:
2378
- git_set_branch_value('last-upload-hash',
2379
- RunGit(['rev-parse', 'HEAD']).strip())
2380
- # Run post upload hooks, if specified.
2381
- if settings.GetRunPostUploadHook():
2382
- presubmit_support.DoPostUploadExecuter(
2383
- change,
2384
- cl,
2385
- settings.GetRoot(),
2386
- options.verbose,
2387
- sys.stdout)
3617
+ # For sanity of test expectations, do this otherwise lazy-loading *now*.
3618
+ settings.GetIsGerrit()
2388
3619
 
2389
- # Upload all dependencies if specified.
2390
- if options.dependencies:
2391
- print
2392
- print '--dependencies has been specified.'
2393
- print 'All dependent local branches will be re-uploaded.'
2394
- print
2395
- # Remove the dependencies flag from args so that we do not end up in a
2396
- # loop.
2397
- orig_args.remove('--dependencies')
2398
- upload_branch_deps(cl, orig_args)
2399
- return ret
3620
+ cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
3621
+ return cl.CMDUpload(options, args, orig_args)
2400
3622
 
2401
3623
 
2402
3624
  def IsSubmoduleMergeCommit(ref):
@@ -2410,10 +3632,14 @@ def IsSubmoduleMergeCommit(ref):
2410
3632
  def SendUpstream(parser, args, cmd):
2411
3633
  """Common code for CMDland and CmdDCommit
2412
3634
 
2413
- Squashes branch into a single commit.
2414
- Updates changelog with metadata (e.g. pointer to review).
2415
- Pushes/dcommits the code upstream.
2416
- Updates review and closes.
3635
+ In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3636
+ upstream and closes the issue automatically and atomically.
3637
+
3638
+ Otherwise (in case of Rietveld):
3639
+ Squashes branch into a single commit.
3640
+ Updates changelog with metadata (e.g. pointer to review).
3641
+ Pushes/dcommits the code upstream.
3642
+ Updates review and closes.
2417
3643
  """
2418
3644
  parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2419
3645
  help='bypass upload presubmit hook')
@@ -2432,6 +3658,24 @@ def SendUpstream(parser, args, cmd):
2432
3658
 
2433
3659
  cl = Changelist(auth_config=auth_config)
2434
3660
 
3661
+ # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3662
+ if cl.IsGerrit():
3663
+ if options.message:
3664
+ # This could be implemented, but it requires sending a new patch to
3665
+ # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3666
+ # Besides, Gerrit has the ability to change the commit message on submit
3667
+ # automatically, thus there is no need to support this option (so far?).
3668
+ parser.error('-m MESSAGE option is not supported for Gerrit.')
3669
+ if options.contributor:
3670
+ parser.error(
3671
+ '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3672
+ 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3673
+ 'the contributor\'s "name <email>". If you can\'t upload such a '
3674
+ 'commit for review, contact your repository admin and request'
3675
+ '"Forge-Author" permission.')
3676
+ return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3677
+ options.verbose)
3678
+
2435
3679
  current = cl.GetBranch()
2436
3680
  remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2437
3681
  if not settings.GetIsGitSvn() and remote == '.':
@@ -2516,12 +3760,6 @@ def SendUpstream(parser, args, cmd):
2516
3760
  print('Unable to determine tree status. Please verify manually and '
2517
3761
  'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2518
3762
  return 1
2519
- else:
2520
- breakpad.SendStack(
2521
- 'GitClHooksBypassedCommit',
2522
- 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2523
- (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
2524
- verbose=False)
2525
3763
 
2526
3764
  change_desc = ChangeDescription(options.message)
2527
3765
  if not change_desc.description and cl.GetIssue():
@@ -2545,8 +3783,10 @@ def SendUpstream(parser, args, cmd):
2545
3783
  commit_desc = ChangeDescription(change_desc.description)
2546
3784
  if cl.GetIssue():
2547
3785
  # Xcode won't linkify this URL unless there is a non-whitespace character
2548
- # after it. Add a period on a new line to circumvent this.
2549
- commit_desc.append_footer('Review URL: %s.' % cl.GetIssueURL())
3786
+ # after it. Add a period on a new line to circumvent this. Also add a space
3787
+ # before the period to make sure that Gitiles continues to correctly resolve
3788
+ # the URL.
3789
+ commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
2550
3790
  if options.contributor:
2551
3791
  commit_desc.append_footer('Patch from %s.' % options.contributor)
2552
3792
 
@@ -2603,19 +3843,21 @@ def SendUpstream(parser, args, cmd):
2603
3843
  RunGit(['cherry-pick', cherry_pick_commit])
2604
3844
  if cmd == 'land':
2605
3845
  remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3846
+ mirror = settings.GetGitMirror(remote)
3847
+ pushurl = mirror.url if mirror else remote
2606
3848
  pending_prefix = settings.GetPendingRefPrefix()
2607
3849
  if not pending_prefix or branch.startswith(pending_prefix):
2608
3850
  # If not using refs/pending/heads/* at all, or target ref is already set
2609
3851
  # to pending, then push to the target ref directly.
2610
3852
  retcode, output = RunGitWithCode(
2611
- ['push', '--porcelain', remote, 'HEAD:%s' % branch])
3853
+ ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
2612
3854
  pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
2613
3855
  else:
2614
3856
  # Cherry-pick the change on top of pending ref and then push it.
2615
3857
  assert branch.startswith('refs/'), branch
2616
3858
  assert pending_prefix[-1] == '/', pending_prefix
2617
3859
  pending_ref = pending_prefix + branch[len('refs/'):]
2618
- retcode, output = PushToGitPending(remote, pending_ref, branch)
3860
+ retcode, output = PushToGitPending(pushurl, pending_ref, branch)
2619
3861
  pushed_to_pending = (retcode == 0)
2620
3862
  if retcode == 0:
2621
3863
  revision = RunGit(['rev-parse', 'HEAD']).strip()
@@ -2703,6 +3945,7 @@ def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2703
3945
  print '(If you are impatient, you may Ctrl-C once without harm)'
2704
3946
  target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2705
3947
  current_rev = RunGit(['rev-parse', local_base_ref]).strip()
3948
+ mirror = settings.GetGitMirror(remote)
2706
3949
 
2707
3950
  loop = 0
2708
3951
  while True:
@@ -2710,6 +3953,8 @@ def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2710
3953
  sys.stdout.flush()
2711
3954
  loop += 1
2712
3955
 
3956
+ if mirror:
3957
+ mirror.populate()
2713
3958
  RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2714
3959
  to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2715
3960
  commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
@@ -2793,7 +4038,7 @@ def IsFatalPushFailure(push_stdout):
2793
4038
  def CMDdcommit(parser, args):
2794
4039
  """Commits the current changelist via git-svn."""
2795
4040
  if not settings.GetIsGitSvn():
2796
- if get_footer_svn_id():
4041
+ if git_footers.get_footer_svn_id():
2797
4042
  # If it looks like previous commits were mirrored with git-svn.
2798
4043
  message = """This repository appears to be a git-svn mirror, but no
2799
4044
  upstream SVN master is set. You probably need to run 'git auto-svn' once."""
@@ -2815,7 +4060,7 @@ proceed, please verify that the commit lands upstream as expected."""
2815
4060
  @subcommand.usage('[upstream branch to apply against]')
2816
4061
  def CMDland(parser, args):
2817
4062
  """Commits the current changelist via git."""
2818
- if settings.GetIsGitSvn() or get_footer_svn_id():
4063
+ if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
2819
4064
  print('This appears to be an SVN repository.')
2820
4065
  print('Are you sure you didn\'t mean \'git cl dcommit\'?')
2821
4066
  print('(Ignore if this is the first commit after migrating from svn->git)')
@@ -2823,7 +4068,7 @@ def CMDland(parser, args):
2823
4068
  return SendUpstream(parser, args, 'land')
2824
4069
 
2825
4070
 
2826
- @subcommand.usage('<patch url or issue id>')
4071
+ @subcommand.usage('<patch url or issue id or issue url>')
2827
4072
  def CMDpatch(parser, args):
2828
4073
  """Patches in a code review."""
2829
4074
  parser.add_option('-b', dest='newbranch',
@@ -2832,111 +4077,82 @@ def CMDpatch(parser, args):
2832
4077
  help='with -b, clobber any existing branch')
2833
4078
  parser.add_option('-d', '--directory', action='store', metavar='DIR',
2834
4079
  help='Change to the directory DIR immediately, '
2835
- 'before doing anything else.')
4080
+ 'before doing anything else. Rietveld only.')
2836
4081
  parser.add_option('--reject', action='store_true',
2837
4082
  help='failed patches spew .rej files rather than '
2838
- 'attempting a 3-way merge')
4083
+ 'attempting a 3-way merge. Rietveld only.')
2839
4084
  parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2840
- help="don't commit after patch applies")
4085
+ help='don\'t commit after patch applies. Rietveld only.')
4086
+
4087
+
4088
+ group = optparse.OptionGroup(
4089
+ parser,
4090
+ 'Options for continuing work on the current issue uploaded from a '
4091
+ 'different clone (e.g. different machine). Must be used independently '
4092
+ 'from the other options. No issue number should be specified, and the '
4093
+ 'branch must have an issue number associated with it')
4094
+ group.add_option('--reapply', action='store_true', dest='reapply',
4095
+ help='Reset the branch and reapply the issue.\n'
4096
+ 'CAUTION: This will undo any local changes in this '
4097
+ 'branch')
4098
+
4099
+ group.add_option('--pull', action='store_true', dest='pull',
4100
+ help='Performs a pull before reapplying.')
4101
+ parser.add_option_group(group)
4102
+
2841
4103
  auth.add_auth_options(parser)
4104
+ _add_codereview_select_options(parser)
2842
4105
  (options, args) = parser.parse_args(args)
4106
+ _process_codereview_select_options(parser, options)
2843
4107
  auth_config = auth.extract_auth_config_from_options(options)
2844
4108
 
2845
- if len(args) != 1:
4109
+ cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4110
+
4111
+ issue_arg = None
4112
+ if options.reapply :
4113
+ if len(args) > 0:
4114
+ parser.error('--reapply implies no additional arguments.')
4115
+
4116
+ issue_arg = cl.GetIssue()
4117
+ upstream = cl.GetUpstreamBranch()
4118
+ if upstream == None:
4119
+ parser.error('No upstream branch specified. Cannot reset branch')
4120
+
4121
+ RunGit(['reset', '--hard', upstream])
4122
+ if options.pull:
4123
+ RunGit(['pull'])
4124
+ else:
4125
+ if len(args) != 1:
4126
+ parser.error('Must specify issue number or url')
4127
+ issue_arg = args[0]
4128
+
4129
+ if not issue_arg:
2846
4130
  parser.print_help()
2847
4131
  return 1
2848
- issue_arg = args[0]
4132
+
4133
+ if cl.IsGerrit():
4134
+ if options.reject:
4135
+ parser.error('--reject is not supported with Gerrit codereview.')
4136
+ if options.nocommit:
4137
+ parser.error('--nocommit is not supported with Gerrit codereview.')
4138
+ if options.directory:
4139
+ parser.error('--directory is not supported with Gerrit codereview.')
2849
4140
 
2850
4141
  # We don't want uncommitted changes mixed up with the patch.
2851
4142
  if git_common.is_dirty_git_tree('patch'):
2852
4143
  return 1
2853
4144
 
2854
- # TODO(maruel): Use apply_issue.py
2855
- # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
2856
-
2857
4145
  if options.newbranch:
4146
+ if options.reapply:
4147
+ parser.error("--reapply excludes any option other than --pull")
2858
4148
  if options.force:
2859
4149
  RunGit(['branch', '-D', options.newbranch],
2860
4150
  stderr=subprocess2.PIPE, error_ok=True)
2861
4151
  RunGit(['checkout', '-b', options.newbranch,
2862
4152
  Changelist().GetUpstreamBranch()])
2863
4153
 
2864
- return PatchIssue(issue_arg, options.reject, options.nocommit,
2865
- options.directory, auth_config)
2866
-
2867
-
2868
- def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
2869
- # PatchIssue should never be called with a dirty tree. It is up to the
2870
- # caller to check this, but just in case we assert here since the
2871
- # consequences of the caller not checking this could be dire.
2872
- assert(not git_common.is_dirty_git_tree('apply'))
2873
-
2874
- if type(issue_arg) is int or issue_arg.isdigit():
2875
- # Input is an issue id. Figure out the URL.
2876
- issue = int(issue_arg)
2877
- cl = Changelist(issue=issue, auth_config=auth_config)
2878
- patchset = cl.GetMostRecentPatchset()
2879
- patch_data = cl.GetPatchSetDiff(issue, patchset)
2880
- else:
2881
- # Assume it's a URL to the patch. Default to https.
2882
- issue_url = gclient_utils.UpgradeToHttps(issue_arg)
2883
- match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
2884
- if not match:
2885
- DieWithError('Must pass an issue ID or full URL for '
2886
- '\'Download raw patch set\'')
2887
- issue = int(match.group(2))
2888
- cl = Changelist(issue=issue, auth_config=auth_config)
2889
- cl.rietveld_server = match.group(1)
2890
- patchset = int(match.group(3))
2891
- patch_data = urllib2.urlopen(issue_arg).read()
2892
-
2893
- # Switch up to the top-level directory, if necessary, in preparation for
2894
- # applying the patch.
2895
- top = settings.GetRelativeRoot()
2896
- if top:
2897
- os.chdir(top)
2898
-
2899
- # Git patches have a/ at the beginning of source paths. We strip that out
2900
- # with a sed script rather than the -p flag to patch so we can feed either
2901
- # Git or svn-style patches into the same apply command.
2902
- # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
2903
- try:
2904
- patch_data = subprocess2.check_output(
2905
- ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2906
- except subprocess2.CalledProcessError:
2907
- DieWithError('Git patch mungling failed.')
2908
- logging.info(patch_data)
2909
-
2910
- # We use "git apply" to apply the patch instead of "patch" so that we can
2911
- # pick up file adds.
2912
- # The --index flag means: also insert into the index (so we catch adds).
2913
- cmd = ['git', 'apply', '--index', '-p0']
2914
- if directory:
2915
- cmd.extend(('--directory', directory))
2916
- if reject:
2917
- cmd.append('--reject')
2918
- elif IsGitVersionAtLeast('1.7.12'):
2919
- cmd.append('--3way')
2920
- try:
2921
- subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
2922
- stdin=patch_data, stdout=subprocess2.VOID)
2923
- except subprocess2.CalledProcessError:
2924
- print 'Failed to apply the patch'
2925
- return 1
2926
-
2927
- # If we had an issue, commit the current state and register the issue.
2928
- if not nocommit:
2929
- RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2930
- 'patch from issue %(i)s at patchset '
2931
- '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2932
- % {'i': issue, 'p': patchset})])
2933
- cl = Changelist(auth_config=auth_config)
2934
- cl.SetIssue(issue)
2935
- cl.SetPatchset(patchset)
2936
- print "Committed patch locally."
2937
- else:
2938
- print "Patch applied to index."
2939
- return 0
4154
+ return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
4155
+ options.directory)
2940
4156
 
2941
4157
 
2942
4158
  def CMDrebase(parser, args):
@@ -3022,7 +4238,7 @@ def CMDtree(parser, args):
3022
4238
 
3023
4239
 
3024
4240
  def CMDtry(parser, args):
3025
- """Triggers a try job through Rietveld."""
4241
+ """Triggers try jobs through BuildBucket."""
3026
4242
  group = optparse.OptionGroup(parser, "Try job options")
3027
4243
  group.add_option(
3028
4244
  "-b", "--bot", action="append",
@@ -3034,6 +4250,7 @@ def CMDtry(parser, args):
3034
4250
  group.add_option(
3035
4251
  "-m", "--master", default='',
3036
4252
  help=("Specify a try master where to run the tries."))
4253
+ group.add_option( "--luci", action='store_true')
3037
4254
  group.add_option(
3038
4255
  "-r", "--revision",
3039
4256
  help="Revision to use for the try job; default: the "
@@ -3047,16 +4264,32 @@ def CMDtry(parser, args):
3047
4264
  "--project",
3048
4265
  help="Override which project to use. Projects are defined "
3049
4266
  "server-side to define what default bot set to use")
4267
+ group.add_option(
4268
+ "-p", "--property", dest="properties", action="append", default=[],
4269
+ help="Specify generic properties in the form -p key1=value1 -p "
4270
+ "key2=value2 etc (buildbucket only). The value will be treated as "
4271
+ "json if decodable, or as string otherwise.")
3050
4272
  group.add_option(
3051
4273
  "-n", "--name", help="Try job name; default to current branch name")
3052
4274
  group.add_option(
3053
- "--use-buildbucket", action="store_true", default=False,
3054
- help="Use buildbucket to trigger try jobs.")
4275
+ "--use-rietveld", action="store_true", default=False,
4276
+ help="Use Rietveld to trigger try jobs.")
4277
+ group.add_option(
4278
+ "--buildbucket-host", default='cr-buildbucket.appspot.com',
4279
+ help="Host of buildbucket. The default host is %default.")
3055
4280
  parser.add_option_group(group)
3056
4281
  auth.add_auth_options(parser)
3057
4282
  options, args = parser.parse_args(args)
3058
4283
  auth_config = auth.extract_auth_config_from_options(options)
3059
4284
 
4285
+ if options.use_rietveld and options.properties:
4286
+ parser.error('Properties can only be specified with buildbucket')
4287
+
4288
+ # Make sure that all properties are prop=value pairs.
4289
+ bad_params = [x for x in options.properties if '=' not in x]
4290
+ if bad_params:
4291
+ parser.error('Got properties with missing "=": %s' % bad_params)
4292
+
3060
4293
  if args:
3061
4294
  parser.error('Unknown arguments: %s' % args)
3062
4295
 
@@ -3064,6 +4297,14 @@ def CMDtry(parser, args):
3064
4297
  if not cl.GetIssue():
3065
4298
  parser.error('Need to upload first')
3066
4299
 
4300
+ if cl.IsGerrit():
4301
+ parser.error(
4302
+ 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4303
+ 'If your project has Commit Queue, dry run is a workaround:\n'
4304
+ ' git cl set-commit --dry-run')
4305
+ # Code below assumes Rietveld issue.
4306
+ # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4307
+
3067
4308
  props = cl.GetIssueProperties()
3068
4309
  if props.get('closed'):
3069
4310
  parser.error('Cannot send tryjobs for a closed CL')
@@ -3107,6 +4348,24 @@ def CMDtry(parser, args):
3107
4348
  None,
3108
4349
  options.verbose,
3109
4350
  sys.stdout)
4351
+
4352
+ if not options.bot:
4353
+ # Get try masters from cq.cfg if any.
4354
+ # TODO(tandrii): some (but very few) projects store cq.cfg in different
4355
+ # location.
4356
+ cq_cfg = os.path.join(change.RepositoryRoot(),
4357
+ 'infra', 'config', 'cq.cfg')
4358
+ if os.path.exists(cq_cfg):
4359
+ masters = {}
4360
+ cq_masters = commit_queue.get_master_builder_map(
4361
+ cq_cfg, include_experimental=False, include_triggered=False)
4362
+ for master, builders in cq_masters.iteritems():
4363
+ for builder in builders:
4364
+ # Skip presubmit builders, because these will fail without LGTM.
4365
+ masters.setdefault(master, {})[builder] = ['defaulttests']
4366
+ if masters:
4367
+ return masters
4368
+
3110
4369
  if not options.bot:
3111
4370
  parser.error('No default try builder to try, use --bot')
3112
4371
 
@@ -3122,7 +4381,7 @@ def CMDtry(parser, args):
3122
4381
  elif ',' in bot:
3123
4382
  parser.error('Specify one bot per --bot flag')
3124
4383
  else:
3125
- builders_and_tests.setdefault(bot, []).append('defaulttests')
4384
+ builders_and_tests.setdefault(bot, [])
3126
4385
 
3127
4386
  for bot, tests in new_style:
3128
4387
  builders_and_tests.setdefault(bot, []).extend(tests)
@@ -3149,7 +4408,9 @@ def CMDtry(parser, args):
3149
4408
  '\nWARNING Mismatch between local config and server. Did a previous '
3150
4409
  'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3151
4410
  'Continuing using\npatchset %s.\n' % patchset)
3152
- if options.use_buildbucket:
4411
+ if options.luci:
4412
+ trigger_luci_job(cl, masters, options)
4413
+ elif not options.use_rietveld:
3153
4414
  try:
3154
4415
  trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3155
4416
  except BuildbucketResponseException as ex:
@@ -3181,6 +4442,50 @@ def CMDtry(parser, args):
3181
4442
  return 0
3182
4443
 
3183
4444
 
4445
+ def CMDtry_results(parser, args):
4446
+ group = optparse.OptionGroup(parser, "Try job results options")
4447
+ group.add_option(
4448
+ "-p", "--patchset", type=int, help="patchset number if not current.")
4449
+ group.add_option(
4450
+ "--print-master", action='store_true', help="print master name as well.")
4451
+ group.add_option(
4452
+ "--color", action='store_true', default=setup_color.IS_TTY,
4453
+ help="force color output, useful when piping output.")
4454
+ group.add_option(
4455
+ "--buildbucket-host", default='cr-buildbucket.appspot.com',
4456
+ help="Host of buildbucket. The default host is %default.")
4457
+ parser.add_option_group(group)
4458
+ auth.add_auth_options(parser)
4459
+ options, args = parser.parse_args(args)
4460
+ if args:
4461
+ parser.error('Unrecognized args: %s' % ' '.join(args))
4462
+
4463
+ auth_config = auth.extract_auth_config_from_options(options)
4464
+ cl = Changelist(auth_config=auth_config)
4465
+ if not cl.GetIssue():
4466
+ parser.error('Need to upload first')
4467
+
4468
+ if not options.patchset:
4469
+ options.patchset = cl.GetMostRecentPatchset()
4470
+ if options.patchset and options.patchset != cl.GetPatchset():
4471
+ print(
4472
+ '\nWARNING Mismatch between local config and server. Did a previous '
4473
+ 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4474
+ 'Continuing using\npatchset %s.\n' % options.patchset)
4475
+ try:
4476
+ jobs = fetch_try_jobs(auth_config, cl, options)
4477
+ except BuildbucketResponseException as ex:
4478
+ print 'Buildbucket error: %s' % ex
4479
+ return 1
4480
+ except Exception as e:
4481
+ stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4482
+ print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4483
+ e, stacktrace)
4484
+ return 1
4485
+ print_tryjobs(options, jobs)
4486
+ return 0
4487
+
4488
+
3184
4489
  @subcommand.usage('[new upstream branch]')
3185
4490
  def CMDupstream(parser, args):
3186
4491
  """Prints or sets the name of the upstream branch, if any."""
@@ -3220,16 +4525,28 @@ def CMDweb(parser, args):
3220
4525
 
3221
4526
  def CMDset_commit(parser, args):
3222
4527
  """Sets the commit bit to trigger the Commit Queue."""
4528
+ parser.add_option('-d', '--dry-run', action='store_true',
4529
+ help='trigger in dry run mode')
4530
+ parser.add_option('-c', '--clear', action='store_true',
4531
+ help='stop CQ run, if any')
3223
4532
  auth.add_auth_options(parser)
3224
4533
  options, args = parser.parse_args(args)
3225
4534
  auth_config = auth.extract_auth_config_from_options(options)
3226
4535
  if args:
3227
4536
  parser.error('Unrecognized args: %s' % ' '.join(args))
4537
+ if options.dry_run and options.clear:
4538
+ parser.error('Make up your mind: both --dry-run and --clear not allowed')
4539
+
3228
4540
  cl = Changelist(auth_config=auth_config)
3229
- props = cl.GetIssueProperties()
3230
- if props.get('private'):
3231
- parser.error('Cannot set commit on private issue')
3232
- cl.SetFlag('commit', '1')
4541
+ if options.clear:
4542
+ state = _CQState.CLEAR
4543
+ elif options.dry_run:
4544
+ state = _CQState.DRY_RUN
4545
+ else:
4546
+ state = _CQState.COMMIT
4547
+ if not cl.GetIssue():
4548
+ parser.error('Must upload the issue first')
4549
+ cl.SetCQState(state)
3233
4550
  return 0
3234
4551
 
3235
4552
 
@@ -3256,7 +4573,7 @@ def CMDdiff(parser, args):
3256
4573
  parser.error('Unrecognized args: %s' % ' '.join(args))
3257
4574
 
3258
4575
  # Uncommitted (staged and unstaged) changes will be destroyed by
3259
- # "git reset --hard" if there are merging conflicts in PatchIssue().
4576
+ # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
3260
4577
  # Staged changes would be committed along with the patch from last
3261
4578
  # upload, hence counted toward the "last upload" side in the final
3262
4579
  # diff output, and this is not what we want.
@@ -3273,9 +4590,11 @@ def CMDdiff(parser, args):
3273
4590
 
3274
4591
  # Create a new branch based on the merge-base
3275
4592
  RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
4593
+ # Clear cached branch in cl object, to avoid overwriting original CL branch
4594
+ # properties.
4595
+ cl.ClearBranch()
3276
4596
  try:
3277
- # Patch in the latest changes from rietveld.
3278
- rtn = PatchIssue(issue, False, False, None, auth_config)
4597
+ rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
3279
4598
  if rtn != 0:
3280
4599
  RunGit(['reset', '--hard'])
3281
4600
  return rtn
@@ -3321,7 +4640,7 @@ def CMDowners(parser, args):
3321
4640
  disable_color=options.no_color).run()
3322
4641
 
3323
4642
 
3324
- def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
4643
+ def BuildGitDiffCmd(diff_type, upstream_commit, args):
3325
4644
  """Generates a diff command."""
3326
4645
  # Generate diff for the current branch's changes.
3327
4646
  diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
@@ -3329,22 +4648,22 @@ def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3329
4648
 
3330
4649
  if args:
3331
4650
  for arg in args:
3332
- if os.path.isdir(arg):
3333
- diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3334
- elif os.path.isfile(arg):
4651
+ if os.path.isdir(arg) or os.path.isfile(arg):
3335
4652
  diff_cmd.append(arg)
3336
4653
  else:
3337
4654
  DieWithError('Argument "%s" is not a file or a directory' % arg)
3338
- else:
3339
- diff_cmd.extend('*' + ext for ext in extensions)
3340
4655
 
3341
4656
  return diff_cmd
3342
4657
 
4658
+ def MatchingFileType(file_name, extensions):
4659
+ """Returns true if the file name ends with one of the given extensions."""
4660
+ return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
3343
4661
 
3344
4662
  @subcommand.usage('[files or directories to diff]')
3345
4663
  def CMDformat(parser, args):
3346
4664
  """Runs auto-formatting tools (clang-format etc.) on the diff."""
3347
4665
  CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
4666
+ GN_EXTS = ['.gn', '.gni']
3348
4667
  parser.add_option('--full', action='store_true',
3349
4668
  help='Reformat the full content of all touched files')
3350
4669
  parser.add_option('--dry-run', action='store_true',
@@ -3376,75 +4695,73 @@ def CMDformat(parser, args):
3376
4695
  DieWithError('Could not find base commit for this branch. '
3377
4696
  'Are you in detached state?')
3378
4697
 
3379
- if opts.full:
3380
- # Only list the names of modified files.
3381
- diff_type = '--name-only'
3382
- else:
3383
- # Only generate context-less patches.
3384
- diff_type = '-U0'
4698
+ changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4699
+ diff_output = RunGit(changed_files_cmd)
4700
+ diff_files = diff_output.splitlines()
4701
+ # Filter out files deleted by this CL
4702
+ diff_files = [x for x in diff_files if os.path.isfile(x)]
3385
4703
 
3386
- diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
3387
- diff_output = RunGit(diff_cmd)
4704
+ clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4705
+ python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4706
+ dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
4707
+ gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
3388
4708
 
3389
4709
  top_dir = os.path.normpath(
3390
4710
  RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3391
4711
 
3392
- # Locate the clang-format binary in the checkout
3393
- try:
3394
- clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3395
- except clang_format.NotFoundError, e:
3396
- DieWithError(e)
3397
-
3398
4712
  # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3399
4713
  # formatted. This is used to block during the presubmit.
3400
4714
  return_value = 0
3401
4715
 
3402
- if opts.full:
3403
- # diff_output is a list of files to send to clang-format.
3404
- files = diff_output.splitlines()
3405
- if files:
4716
+ if clang_diff_files:
4717
+ # Locate the clang-format binary in the checkout
4718
+ try:
4719
+ clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4720
+ except clang_format.NotFoundError, e:
4721
+ DieWithError(e)
4722
+
4723
+ if opts.full:
3406
4724
  cmd = [clang_format_tool]
3407
4725
  if not opts.dry_run and not opts.diff:
3408
4726
  cmd.append('-i')
3409
- stdout = RunCommand(cmd + files, cwd=top_dir)
4727
+ stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
3410
4728
  if opts.diff:
3411
4729
  sys.stdout.write(stdout)
3412
- else:
3413
- env = os.environ.copy()
3414
- env['PATH'] = str(os.path.dirname(clang_format_tool))
3415
- # diff_output is a patch to send to clang-format-diff.py
3416
- try:
3417
- script = clang_format.FindClangFormatScriptInChromiumTree(
3418
- 'clang-format-diff.py')
3419
- except clang_format.NotFoundError, e:
3420
- DieWithError(e)
4730
+ else:
4731
+ env = os.environ.copy()
4732
+ env['PATH'] = str(os.path.dirname(clang_format_tool))
4733
+ try:
4734
+ script = clang_format.FindClangFormatScriptInChromiumTree(
4735
+ 'clang-format-diff.py')
4736
+ except clang_format.NotFoundError, e:
4737
+ DieWithError(e)
3421
4738
 
3422
- cmd = [sys.executable, script, '-p0']
3423
- if not opts.dry_run and not opts.diff:
3424
- cmd.append('-i')
4739
+ cmd = [sys.executable, script, '-p0']
4740
+ if not opts.dry_run and not opts.diff:
4741
+ cmd.append('-i')
3425
4742
 
3426
- stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3427
- if opts.diff:
3428
- sys.stdout.write(stdout)
3429
- if opts.dry_run and len(stdout) > 0:
3430
- return_value = 2
4743
+ diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4744
+ diff_output = RunGit(diff_cmd)
4745
+
4746
+ stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4747
+ if opts.diff:
4748
+ sys.stdout.write(stdout)
4749
+ if opts.dry_run and len(stdout) > 0:
4750
+ return_value = 2
3431
4751
 
3432
4752
  # Similar code to above, but using yapf on .py files rather than clang-format
3433
4753
  # on C/C++ files
3434
4754
  if opts.python:
3435
- diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3436
- diff_output = RunGit(diff_cmd)
3437
4755
  yapf_tool = gclient_utils.FindExecutable('yapf')
3438
4756
  if yapf_tool is None:
3439
4757
  DieWithError('yapf not found in PATH')
3440
4758
 
3441
4759
  if opts.full:
3442
- files = diff_output.splitlines()
3443
- if files:
4760
+ if python_diff_files:
3444
4761
  cmd = [yapf_tool]
3445
4762
  if not opts.dry_run and not opts.diff:
3446
4763
  cmd.append('-i')
3447
- stdout = RunCommand(cmd + files, cwd=top_dir)
4764
+ stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
3448
4765
  if opts.diff:
3449
4766
  sys.stdout.write(stdout)
3450
4767
  else:
@@ -3452,29 +4769,83 @@ def CMDformat(parser, args):
3452
4769
  # https://github.com/google/yapf/issues/154
3453
4770
  DieWithError('--python currently only works with --full')
3454
4771
 
3455
- # Build a diff command that only operates on dart files. dart's formatter
3456
- # does not have the nice property of only operating on modified chunks, so
3457
- # hard code full.
3458
- dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3459
- args, ['.dart'])
3460
- dart_diff_output = RunGit(dart_diff_cmd)
3461
- if dart_diff_output:
4772
+ # Dart's formatter does not have the nice property of only operating on
4773
+ # modified chunks, so hard code full.
4774
+ if dart_diff_files:
3462
4775
  try:
3463
4776
  command = [dart_format.FindDartFmtToolInChromiumTree()]
3464
4777
  if not opts.dry_run and not opts.diff:
3465
4778
  command.append('-w')
3466
- command.extend(dart_diff_output.splitlines())
4779
+ command.extend(dart_diff_files)
3467
4780
 
3468
- stdout = RunCommand(command, cwd=top_dir, env=env)
4781
+ stdout = RunCommand(command, cwd=top_dir)
3469
4782
  if opts.dry_run and stdout:
3470
4783
  return_value = 2
3471
4784
  except dart_format.NotFoundError as e:
3472
- print ('Unable to check dart code formatting. Dart SDK is not in ' +
3473
- 'this checkout.')
4785
+ print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4786
+ 'found in this checkout. Files in other languages are still ' +
4787
+ 'formatted.')
4788
+
4789
+ # Format GN build files. Always run on full build files for canonical form.
4790
+ if gn_diff_files:
4791
+ cmd = ['gn', 'format']
4792
+ if not opts.dry_run and not opts.diff:
4793
+ cmd.append('--in-place')
4794
+ for gn_diff_file in gn_diff_files:
4795
+ stdout = RunCommand(cmd + [gn_diff_file],
4796
+ shell=sys.platform == 'win32',
4797
+ cwd=top_dir)
4798
+ if opts.diff:
4799
+ sys.stdout.write(stdout)
3474
4800
 
3475
4801
  return return_value
3476
4802
 
3477
4803
 
4804
+ @subcommand.usage('<codereview url or issue id>')
4805
+ def CMDcheckout(parser, args):
4806
+ """Checks out a branch associated with a given Rietveld or Gerrit issue."""
4807
+ _, args = parser.parse_args(args)
4808
+
4809
+ if len(args) != 1:
4810
+ parser.print_help()
4811
+ return 1
4812
+
4813
+ issue_arg = ParseIssueNumberArgument(args[0])
4814
+ if not issue_arg.valid:
4815
+ parser.print_help()
4816
+ return 1
4817
+ target_issue = str(issue_arg.issue)
4818
+
4819
+ def find_issues(issueprefix):
4820
+ output = RunGit(['config', '--local', '--get-regexp',
4821
+ r'branch\..*\.%s' % issueprefix],
4822
+ error_ok=True)
4823
+ for key, issue in [x.split() for x in output.splitlines()]:
4824
+ if issue == target_issue:
4825
+ yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
4826
+
4827
+ branches = []
4828
+ for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4829
+ branches.extend(find_issues(cls.IssueSettingSuffix()))
4830
+ if len(branches) == 0:
4831
+ print 'No branch found for issue %s.' % target_issue
4832
+ return 1
4833
+ if len(branches) == 1:
4834
+ RunGit(['checkout', branches[0]])
4835
+ else:
4836
+ print 'Multiple branches match issue %s:' % target_issue
4837
+ for i in range(len(branches)):
4838
+ print '%d: %s' % (i, branches[i])
4839
+ which = raw_input('Choose by index: ')
4840
+ try:
4841
+ RunGit(['checkout', branches[int(which)]])
4842
+ except (IndexError, ValueError):
4843
+ print 'Invalid selection, not checking out any branch.'
4844
+ return 1
4845
+
4846
+ return 0
4847
+
4848
+
3478
4849
  def CMDlol(parser, args):
3479
4850
  # This command is intentionally undocumented.
3480
4851
  print zlib.decompress(base64.b64decode(
@@ -3531,7 +4902,7 @@ if __name__ == '__main__':
3531
4902
  # These affect sys.stdout so do it outside of main() to simplify mocks in
3532
4903
  # unit testing.
3533
4904
  fix_encoding.fix_encoding()
3534
- colorama.init()
4905
+ setup_color.init()
3535
4906
  try:
3536
4907
  sys.exit(main(sys.argv[1:]))
3537
4908
  except KeyboardInterrupt: