diff options
-rw-r--r-- | docs/manifest-format.txt | 7 | ||||
-rw-r--r-- | error.py | 7 | ||||
-rw-r--r-- | manifest_xml.py | 90 | ||||
-rw-r--r-- | project.py | 266 | ||||
-rw-r--r-- | subcmds/upload.py | 46 |
5 files changed, 384 insertions, 32 deletions
diff --git a/docs/manifest-format.txt b/docs/manifest-format.txt index 2e1c8c35..c76df801 100644 --- a/docs/manifest-format.txt +++ b/docs/manifest-format.txt | |||
@@ -25,7 +25,8 @@ following DTD: | |||
25 | default?, | 25 | default?, |
26 | manifest-server?, | 26 | manifest-server?, |
27 | remove-project*, | 27 | remove-project*, |
28 | project*)> | 28 | project*, |
29 | repo-hooks?)> | ||
29 | 30 | ||
30 | <!ELEMENT notice (#PCDATA)> | 31 | <!ELEMENT notice (#PCDATA)> |
31 | 32 | ||
@@ -49,6 +50,10 @@ following DTD: | |||
49 | 50 | ||
50 | <!ELEMENT remove-project (EMPTY)> | 51 | <!ELEMENT remove-project (EMPTY)> |
51 | <!ATTLIST remove-project name CDATA #REQUIRED> | 52 | <!ATTLIST remove-project name CDATA #REQUIRED> |
53 | |||
54 | <!ELEMENT repo-hooks (EMPTY)> | ||
55 | <!ATTLIST repo-hooks in-project CDATA #REQUIRED> | ||
56 | <!ATTLIST repo-hooks enabled-list CDATA #REQUIRED> | ||
52 | ]> | 57 | ]> |
53 | 58 | ||
54 | A description of the elements and their attributes follows. | 59 | A description of the elements and their attributes follows. |
@@ -75,3 +75,10 @@ class RepoChangedException(Exception): | |||
75 | """ | 75 | """ |
76 | def __init__(self, extra_args=[]): | 76 | def __init__(self, extra_args=[]): |
77 | self.extra_args = extra_args | 77 | self.extra_args = extra_args |
78 | |||
79 | class HookError(Exception): | ||
80 | """Thrown if a 'repo-hook' could not be run. | ||
81 | |||
82 | The common case is that the file wasn't present when we tried to run it. | ||
83 | """ | ||
84 | pass | ||
diff --git a/manifest_xml.py b/manifest_xml.py index 0103cf55..0e6421f1 100644 --- a/manifest_xml.py +++ b/manifest_xml.py | |||
@@ -171,6 +171,14 @@ class XmlManifest(object): | |||
171 | ce.setAttribute('dest', c.dest) | 171 | ce.setAttribute('dest', c.dest) |
172 | e.appendChild(ce) | 172 | e.appendChild(ce) |
173 | 173 | ||
174 | if self._repo_hooks_project: | ||
175 | root.appendChild(doc.createTextNode('')) | ||
176 | e = doc.createElement('repo-hooks') | ||
177 | e.setAttribute('in-project', self._repo_hooks_project.name) | ||
178 | e.setAttribute('enabled-list', | ||
179 | ' '.join(self._repo_hooks_project.enabled_repo_hooks)) | ||
180 | root.appendChild(e) | ||
181 | |||
174 | doc.writexml(fd, '', ' ', '\n', 'UTF-8') | 182 | doc.writexml(fd, '', ' ', '\n', 'UTF-8') |
175 | 183 | ||
176 | @property | 184 | @property |
@@ -189,6 +197,11 @@ class XmlManifest(object): | |||
189 | return self._default | 197 | return self._default |
190 | 198 | ||
191 | @property | 199 | @property |
200 | def repo_hooks_project(self): | ||
201 | self._Load() | ||
202 | return self._repo_hooks_project | ||
203 | |||
204 | @property | ||
192 | def notice(self): | 205 | def notice(self): |
193 | self._Load() | 206 | self._Load() |
194 | return self._notice | 207 | return self._notice |
@@ -207,6 +220,7 @@ class XmlManifest(object): | |||
207 | self._projects = {} | 220 | self._projects = {} |
208 | self._remotes = {} | 221 | self._remotes = {} |
209 | self._default = None | 222 | self._default = None |
223 | self._repo_hooks_project = None | ||
210 | self._notice = None | 224 | self._notice = None |
211 | self.branch = None | 225 | self.branch = None |
212 | self._manifest_server = None | 226 | self._manifest_server = None |
@@ -239,15 +253,15 @@ class XmlManifest(object): | |||
239 | def _ParseManifest(self, is_root_file): | 253 | def _ParseManifest(self, is_root_file): |
240 | root = xml.dom.minidom.parse(self.manifestFile) | 254 | root = xml.dom.minidom.parse(self.manifestFile) |
241 | if not root or not root.childNodes: | 255 | if not root or not root.childNodes: |
242 | raise ManifestParseError, \ | 256 | raise ManifestParseError( |
243 | "no root node in %s" % \ | 257 | "no root node in %s" % |
244 | self.manifestFile | 258 | self.manifestFile) |
245 | 259 | ||
246 | config = root.childNodes[0] | 260 | config = root.childNodes[0] |
247 | if config.nodeName != 'manifest': | 261 | if config.nodeName != 'manifest': |
248 | raise ManifestParseError, \ | 262 | raise ManifestParseError( |
249 | "no <manifest> in %s" % \ | 263 | "no <manifest> in %s" % |
250 | self.manifestFile | 264 | self.manifestFile) |
251 | 265 | ||
252 | for node in config.childNodes: | 266 | for node in config.childNodes: |
253 | if node.nodeName == 'remove-project': | 267 | if node.nodeName == 'remove-project': |
@@ -255,25 +269,30 @@ class XmlManifest(object): | |||
255 | try: | 269 | try: |
256 | del self._projects[name] | 270 | del self._projects[name] |
257 | except KeyError: | 271 | except KeyError: |
258 | raise ManifestParseError, \ | 272 | raise ManifestParseError( |
259 | 'project %s not found' % \ | 273 | 'project %s not found' % |
260 | (name) | 274 | (name)) |
275 | |||
276 | # If the manifest removes the hooks project, treat it as if it deleted | ||
277 | # the repo-hooks element too. | ||
278 | if self._repo_hooks_project and (self._repo_hooks_project.name == name): | ||
279 | self._repo_hooks_project = None | ||
261 | 280 | ||
262 | for node in config.childNodes: | 281 | for node in config.childNodes: |
263 | if node.nodeName == 'remote': | 282 | if node.nodeName == 'remote': |
264 | remote = self._ParseRemote(node) | 283 | remote = self._ParseRemote(node) |
265 | if self._remotes.get(remote.name): | 284 | if self._remotes.get(remote.name): |
266 | raise ManifestParseError, \ | 285 | raise ManifestParseError( |
267 | 'duplicate remote %s in %s' % \ | 286 | 'duplicate remote %s in %s' % |
268 | (remote.name, self.manifestFile) | 287 | (remote.name, self.manifestFile)) |
269 | self._remotes[remote.name] = remote | 288 | self._remotes[remote.name] = remote |
270 | 289 | ||
271 | for node in config.childNodes: | 290 | for node in config.childNodes: |
272 | if node.nodeName == 'default': | 291 | if node.nodeName == 'default': |
273 | if self._default is not None: | 292 | if self._default is not None: |
274 | raise ManifestParseError, \ | 293 | raise ManifestParseError( |
275 | 'duplicate default in %s' % \ | 294 | 'duplicate default in %s' % |
276 | (self.manifestFile) | 295 | (self.manifestFile)) |
277 | self._default = self._ParseDefault(node) | 296 | self._default = self._ParseDefault(node) |
278 | if self._default is None: | 297 | if self._default is None: |
279 | self._default = _Default() | 298 | self._default = _Default() |
@@ -281,29 +300,52 @@ class XmlManifest(object): | |||
281 | for node in config.childNodes: | 300 | for node in config.childNodes: |
282 | if node.nodeName == 'notice': | 301 | if node.nodeName == 'notice': |
283 | if self._notice is not None: | 302 | if self._notice is not None: |
284 | raise ManifestParseError, \ | 303 | raise ManifestParseError( |
285 | 'duplicate notice in %s' % \ | 304 | 'duplicate notice in %s' % |
286 | (self.manifestFile) | 305 | (self.manifestFile)) |
287 | self._notice = self._ParseNotice(node) | 306 | self._notice = self._ParseNotice(node) |
288 | 307 | ||
289 | for node in config.childNodes: | 308 | for node in config.childNodes: |
290 | if node.nodeName == 'manifest-server': | 309 | if node.nodeName == 'manifest-server': |
291 | url = self._reqatt(node, 'url') | 310 | url = self._reqatt(node, 'url') |
292 | if self._manifest_server is not None: | 311 | if self._manifest_server is not None: |
293 | raise ManifestParseError, \ | 312 | raise ManifestParseError( |
294 | 'duplicate manifest-server in %s' % \ | 313 | 'duplicate manifest-server in %s' % |
295 | (self.manifestFile) | 314 | (self.manifestFile)) |
296 | self._manifest_server = url | 315 | self._manifest_server = url |
297 | 316 | ||
298 | for node in config.childNodes: | 317 | for node in config.childNodes: |
299 | if node.nodeName == 'project': | 318 | if node.nodeName == 'project': |
300 | project = self._ParseProject(node) | 319 | project = self._ParseProject(node) |
301 | if self._projects.get(project.name): | 320 | if self._projects.get(project.name): |
302 | raise ManifestParseError, \ | 321 | raise ManifestParseError( |
303 | 'duplicate project %s in %s' % \ | 322 | 'duplicate project %s in %s' % |
304 | (project.name, self.manifestFile) | 323 | (project.name, self.manifestFile)) |
305 | self._projects[project.name] = project | 324 | self._projects[project.name] = project |
306 | 325 | ||
326 | for node in config.childNodes: | ||
327 | if node.nodeName == 'repo-hooks': | ||
328 | # Get the name of the project and the (space-separated) list of enabled. | ||
329 | repo_hooks_project = self._reqatt(node, 'in-project') | ||
330 | enabled_repo_hooks = self._reqatt(node, 'enabled-list').split() | ||
331 | |||
332 | # Only one project can be the hooks project | ||
333 | if self._repo_hooks_project is not None: | ||
334 | raise ManifestParseError( | ||
335 | 'duplicate repo-hooks in %s' % | ||
336 | (self.manifestFile)) | ||
337 | |||
338 | # Store a reference to the Project. | ||
339 | try: | ||
340 | self._repo_hooks_project = self._projects[repo_hooks_project] | ||
341 | except KeyError: | ||
342 | raise ManifestParseError( | ||
343 | 'project %s not found for repo-hooks' % | ||
344 | (repo_hooks_project)) | ||
345 | |||
346 | # Store the enabled hooks in the Project object. | ||
347 | self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks | ||
348 | |||
307 | def _AddMetaProjectMirror(self, m): | 349 | def _AddMetaProjectMirror(self, m): |
308 | name = None | 350 | name = None |
309 | m_url = m.GetRemote(m.remote.name).url | 351 | m_url = m.GetRemote(m.remote.name).url |
@@ -12,6 +12,7 @@ | |||
12 | # See the License for the specific language governing permissions and | 12 | # See the License for the specific language governing permissions and |
13 | # limitations under the License. | 13 | # limitations under the License. |
14 | 14 | ||
15 | import traceback | ||
15 | import errno | 16 | import errno |
16 | import filecmp | 17 | import filecmp |
17 | import os | 18 | import os |
@@ -24,7 +25,7 @@ import urllib2 | |||
24 | from color import Coloring | 25 | from color import Coloring |
25 | from git_command import GitCommand | 26 | from git_command import GitCommand |
26 | from git_config import GitConfig, IsId | 27 | from git_config import GitConfig, IsId |
27 | from error import GitError, ImportError, UploadError | 28 | from error import GitError, HookError, ImportError, UploadError |
28 | from error import ManifestInvalidRevisionError | 29 | from error import ManifestInvalidRevisionError |
29 | 30 | ||
30 | from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M | 31 | from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M |
@@ -234,6 +235,249 @@ class RemoteSpec(object): | |||
234 | self.url = url | 235 | self.url = url |
235 | self.review = review | 236 | self.review = review |
236 | 237 | ||
238 | class RepoHook(object): | ||
239 | """A RepoHook contains information about a script to run as a hook. | ||
240 | |||
241 | Hooks are used to run a python script before running an upload (for instance, | ||
242 | to run presubmit checks). Eventually, we may have hooks for other actions. | ||
243 | |||
244 | This shouldn't be confused with files in the 'repo/hooks' directory. Those | ||
245 | files are copied into each '.git/hooks' folder for each project. Repo-level | ||
246 | hooks are associated instead with repo actions. | ||
247 | |||
248 | Hooks are always python. When a hook is run, we will load the hook into the | ||
249 | interpreter and execute its main() function. | ||
250 | """ | ||
251 | def __init__(self, | ||
252 | hook_type, | ||
253 | hooks_project, | ||
254 | topdir, | ||
255 | abort_if_user_denies=False): | ||
256 | """RepoHook constructor. | ||
257 | |||
258 | Params: | ||
259 | hook_type: A string representing the type of hook. This is also used | ||
260 | to figure out the name of the file containing the hook. For | ||
261 | example: 'pre-upload'. | ||
262 | hooks_project: The project containing the repo hooks. If you have a | ||
263 | manifest, this is manifest.repo_hooks_project. OK if this is None, | ||
264 | which will make the hook a no-op. | ||
265 | topdir: Repo's top directory (the one containing the .repo directory). | ||
266 | Scripts will run with CWD as this directory. If you have a manifest, | ||
267 | this is manifest.topdir | ||
268 | abort_if_user_denies: If True, we'll throw a HookError() if the user | ||
269 | doesn't allow us to run the hook. | ||
270 | """ | ||
271 | self._hook_type = hook_type | ||
272 | self._hooks_project = hooks_project | ||
273 | self._topdir = topdir | ||
274 | self._abort_if_user_denies = abort_if_user_denies | ||
275 | |||
276 | # Store the full path to the script for convenience. | ||
277 | if self._hooks_project: | ||
278 | self._script_fullpath = os.path.join(self._hooks_project.worktree, | ||
279 | self._hook_type + '.py') | ||
280 | else: | ||
281 | self._script_fullpath = None | ||
282 | |||
283 | def _GetHash(self): | ||
284 | """Return a hash of the contents of the hooks directory. | ||
285 | |||
286 | We'll just use git to do this. This hash has the property that if anything | ||
287 | changes in the directory we will return a different has. | ||
288 | |||
289 | SECURITY CONSIDERATION: | ||
290 | This hash only represents the contents of files in the hook directory, not | ||
291 | any other files imported or called by hooks. Changes to imported files | ||
292 | can change the script behavior without affecting the hash. | ||
293 | |||
294 | Returns: | ||
295 | A string representing the hash. This will always be ASCII so that it can | ||
296 | be printed to the user easily. | ||
297 | """ | ||
298 | assert self._hooks_project, "Must have hooks to calculate their hash." | ||
299 | |||
300 | # We will use the work_git object rather than just calling GetRevisionId(). | ||
301 | # That gives us a hash of the latest checked in version of the files that | ||
302 | # the user will actually be executing. Specifically, GetRevisionId() | ||
303 | # doesn't appear to change even if a user checks out a different version | ||
304 | # of the hooks repo (via git checkout) nor if a user commits their own revs. | ||
305 | # | ||
306 | # NOTE: Local (non-committed) changes will not be factored into this hash. | ||
307 | # I think this is OK, since we're really only worried about warning the user | ||
308 | # about upstream changes. | ||
309 | return self._hooks_project.work_git.rev_parse('HEAD') | ||
310 | |||
311 | def _GetMustVerb(self): | ||
312 | """Return 'must' if the hook is required; 'should' if not.""" | ||
313 | if self._abort_if_user_denies: | ||
314 | return 'must' | ||
315 | else: | ||
316 | return 'should' | ||
317 | |||
318 | def _CheckForHookApproval(self): | ||
319 | """Check to see whether this hook has been approved. | ||
320 | |||
321 | We'll look at the hash of all of the hooks. If this matches the hash that | ||
322 | the user last approved, we're done. If it doesn't, we'll ask the user | ||
323 | about approval. | ||
324 | |||
325 | Note that we ask permission for each individual hook even though we use | ||
326 | the hash of all hooks when detecting changes. We'd like the user to be | ||
327 | able to approve / deny each hook individually. We only use the hash of all | ||
328 | hooks because there is no other easy way to detect changes to local imports. | ||
329 | |||
330 | Returns: | ||
331 | True if this hook is approved to run; False otherwise. | ||
332 | |||
333 | Raises: | ||
334 | HookError: Raised if the user doesn't approve and abort_if_user_denies | ||
335 | was passed to the consturctor. | ||
336 | """ | ||
337 | hooks_dir = self._hooks_project.worktree | ||
338 | hooks_config = self._hooks_project.config | ||
339 | git_approval_key = 'repo.hooks.%s.approvedhash' % self._hook_type | ||
340 | |||
341 | # Get the last hash that the user approved for this hook; may be None. | ||
342 | old_hash = hooks_config.GetString(git_approval_key) | ||
343 | |||
344 | # Get the current hash so we can tell if scripts changed since approval. | ||
345 | new_hash = self._GetHash() | ||
346 | |||
347 | if old_hash is not None: | ||
348 | # User previously approved hook and asked not to be prompted again. | ||
349 | if new_hash == old_hash: | ||
350 | # Approval matched. We're done. | ||
351 | return True | ||
352 | else: | ||
353 | # Give the user a reason why we're prompting, since they last told | ||
354 | # us to "never ask again". | ||
355 | prompt = 'WARNING: Scripts have changed since %s was allowed.\n\n' % ( | ||
356 | self._hook_type) | ||
357 | else: | ||
358 | prompt = '' | ||
359 | |||
360 | # Prompt the user if we're not on a tty; on a tty we'll assume "no". | ||
361 | if sys.stdout.isatty(): | ||
362 | prompt += ('Repo %s run the script:\n' | ||
363 | ' %s\n' | ||
364 | '\n' | ||
365 | 'Do you want to allow this script to run ' | ||
366 | '(yes/yes-never-ask-again/NO)? ') % ( | ||
367 | self._GetMustVerb(), self._script_fullpath) | ||
368 | response = raw_input(prompt).lower() | ||
369 | |||
370 | |||
371 | # User is doing a one-time approval. | ||
372 | if response in ('y', 'yes'): | ||
373 | return True | ||
374 | elif response == 'yes-never-ask-again': | ||
375 | hooks_config.SetString(git_approval_key, new_hash) | ||
376 | return True | ||
377 | |||
378 | # For anything else, we'll assume no approval. | ||
379 | if self._abort_if_user_denies: | ||
380 | raise HookError('You must allow the %s hook or use --no-verify.' % | ||
381 | self._hook_type) | ||
382 | |||
383 | return False | ||
384 | |||
385 | def _ExecuteHook(self, **kwargs): | ||
386 | """Actually execute the given hook. | ||
387 | |||
388 | This will run the hook's 'main' function in our python interpreter. | ||
389 | |||
390 | Args: | ||
391 | kwargs: Keyword arguments to pass to the hook. These are often specific | ||
392 | to the hook type. For instance, pre-upload hooks will contain | ||
393 | a project_list. | ||
394 | """ | ||
395 | # Keep sys.path and CWD stashed away so that we can always restore them | ||
396 | # upon function exit. | ||
397 | orig_path = os.getcwd() | ||
398 | orig_syspath = sys.path | ||
399 | |||
400 | try: | ||
401 | # Always run hooks with CWD as topdir. | ||
402 | os.chdir(self._topdir) | ||
403 | |||
404 | # Put the hook dir as the first item of sys.path so hooks can do | ||
405 | # relative imports. We want to replace the repo dir as [0] so | ||
406 | # hooks can't import repo files. | ||
407 | sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:] | ||
408 | |||
409 | # Exec, storing global context in the context dict. We catch exceptions | ||
410 | # and convert to a HookError w/ just the failing traceback. | ||
411 | context = {} | ||
412 | try: | ||
413 | execfile(self._script_fullpath, context) | ||
414 | except Exception: | ||
415 | raise HookError('%s\nFailed to import %s hook; see traceback above.' % ( | ||
416 | traceback.format_exc(), self._hook_type)) | ||
417 | |||
418 | # Running the script should have defined a main() function. | ||
419 | if 'main' not in context: | ||
420 | raise HookError('Missing main() in: "%s"' % self._script_fullpath) | ||
421 | |||
422 | |||
423 | # Add 'hook_should_take_kwargs' to the arguments to be passed to main. | ||
424 | # We don't actually want hooks to define their main with this argument-- | ||
425 | # it's there to remind them that their hook should always take **kwargs. | ||
426 | # For instance, a pre-upload hook should be defined like: | ||
427 | # def main(project_list, **kwargs): | ||
428 | # | ||
429 | # This allows us to later expand the API without breaking old hooks. | ||
430 | kwargs = kwargs.copy() | ||
431 | kwargs['hook_should_take_kwargs'] = True | ||
432 | |||
433 | # Call the main function in the hook. If the hook should cause the | ||
434 | # build to fail, it will raise an Exception. We'll catch that convert | ||
435 | # to a HookError w/ just the failing traceback. | ||
436 | try: | ||
437 | context['main'](**kwargs) | ||
438 | except Exception: | ||
439 | raise HookError('%s\nFailed to run main() for %s hook; see traceback ' | ||
440 | 'above.' % ( | ||
441 | traceback.format_exc(), self._hook_type)) | ||
442 | finally: | ||
443 | # Restore sys.path and CWD. | ||
444 | sys.path = orig_syspath | ||
445 | os.chdir(orig_path) | ||
446 | |||
447 | def Run(self, user_allows_all_hooks, **kwargs): | ||
448 | """Run the hook. | ||
449 | |||
450 | If the hook doesn't exist (because there is no hooks project or because | ||
451 | this particular hook is not enabled), this is a no-op. | ||
452 | |||
453 | Args: | ||
454 | user_allows_all_hooks: If True, we will never prompt about running the | ||
455 | hook--we'll just assume it's OK to run it. | ||
456 | kwargs: Keyword arguments to pass to the hook. These are often specific | ||
457 | to the hook type. For instance, pre-upload hooks will contain | ||
458 | a project_list. | ||
459 | |||
460 | Raises: | ||
461 | HookError: If there was a problem finding the hook or the user declined | ||
462 | to run a required hook (from _CheckForHookApproval). | ||
463 | """ | ||
464 | # No-op if there is no hooks project or if hook is disabled. | ||
465 | if ((not self._hooks_project) or | ||
466 | (self._hook_type not in self._hooks_project.enabled_repo_hooks)): | ||
467 | return | ||
468 | |||
469 | # Bail with a nice error if we can't find the hook. | ||
470 | if not os.path.isfile(self._script_fullpath): | ||
471 | raise HookError('Couldn\'t find repo hook: "%s"' % self._script_fullpath) | ||
472 | |||
473 | # Make sure the user is OK with running the hook. | ||
474 | if (not user_allows_all_hooks) and (not self._CheckForHookApproval()): | ||
475 | return | ||
476 | |||
477 | # Run the hook with the same version of python we're using. | ||
478 | self._ExecuteHook(**kwargs) | ||
479 | |||
480 | |||
237 | class Project(object): | 481 | class Project(object): |
238 | def __init__(self, | 482 | def __init__(self, |
239 | manifest, | 483 | manifest, |
@@ -275,6 +519,10 @@ class Project(object): | |||
275 | self.bare_git = self._GitGetByExec(self, bare=True) | 519 | self.bare_git = self._GitGetByExec(self, bare=True) |
276 | self.bare_ref = GitRefs(gitdir) | 520 | self.bare_ref = GitRefs(gitdir) |
277 | 521 | ||
522 | # This will be filled in if a project is later identified to be the | ||
523 | # project containing repo hooks. | ||
524 | self.enabled_repo_hooks = [] | ||
525 | |||
278 | @property | 526 | @property |
279 | def Exists(self): | 527 | def Exists(self): |
280 | return os.path.isdir(self.gitdir) | 528 | return os.path.isdir(self.gitdir) |
@@ -1457,6 +1705,22 @@ class Project(object): | |||
1457 | return r | 1705 | return r |
1458 | 1706 | ||
1459 | def __getattr__(self, name): | 1707 | def __getattr__(self, name): |
1708 | """Allow arbitrary git commands using pythonic syntax. | ||
1709 | |||
1710 | This allows you to do things like: | ||
1711 | git_obj.rev_parse('HEAD') | ||
1712 | |||
1713 | Since we don't have a 'rev_parse' method defined, the __getattr__ will | ||
1714 | run. We'll replace the '_' with a '-' and try to run a git command. | ||
1715 | Any other arguments will be passed to the git command. | ||
1716 | |||
1717 | Args: | ||
1718 | name: The name of the git command to call. Any '_' characters will | ||
1719 | be replaced with '-'. | ||
1720 | |||
1721 | Returns: | ||
1722 | A callable object that will try to call git with the named command. | ||
1723 | """ | ||
1460 | name = name.replace('_', '-') | 1724 | name = name.replace('_', '-') |
1461 | def runner(*args): | 1725 | def runner(*args): |
1462 | cmdv = [name] | 1726 | cmdv = [name] |
diff --git a/subcmds/upload.py b/subcmds/upload.py index 20822096..c561b8aa 100644 --- a/subcmds/upload.py +++ b/subcmds/upload.py | |||
@@ -19,7 +19,8 @@ import sys | |||
19 | 19 | ||
20 | from command import InteractiveCommand | 20 | from command import InteractiveCommand |
21 | from editor import Editor | 21 | from editor import Editor |
22 | from error import UploadError | 22 | from error import HookError, UploadError |
23 | from project import RepoHook | ||
23 | 24 | ||
24 | UNUSUAL_COMMIT_THRESHOLD = 5 | 25 | UNUSUAL_COMMIT_THRESHOLD = 5 |
25 | 26 | ||
@@ -120,6 +121,29 @@ Gerrit Code Review: http://code.google.com/p/gerrit/ | |||
120 | type='string', action='append', dest='cc', | 121 | type='string', action='append', dest='cc', |
121 | help='Also send email to these email addresses.') | 122 | help='Also send email to these email addresses.') |
122 | 123 | ||
124 | # Options relating to upload hook. Note that verify and no-verify are NOT | ||
125 | # opposites of each other, which is why they store to different locations. | ||
126 | # We are using them to match 'git commit' syntax. | ||
127 | # | ||
128 | # Combinations: | ||
129 | # - no-verify=False, verify=False (DEFAULT): | ||
130 | # If stdout is a tty, can prompt about running upload hooks if needed. | ||
131 | # If user denies running hooks, the upload is cancelled. If stdout is | ||
132 | # not a tty and we would need to prompt about upload hooks, upload is | ||
133 | # cancelled. | ||
134 | # - no-verify=False, verify=True: | ||
135 | # Always run upload hooks with no prompt. | ||
136 | # - no-verify=True, verify=False: | ||
137 | # Never run upload hooks, but upload anyway (AKA bypass hooks). | ||
138 | # - no-verify=True, verify=True: | ||
139 | # Invalid | ||
140 | p.add_option('--no-verify', | ||
141 | dest='bypass_hooks', action='store_true', | ||
142 | help='Do not run the upload hook.') | ||
143 | p.add_option('--verify', | ||
144 | dest='allow_all_hooks', action='store_true', | ||
145 | help='Run the upload hook without prompting.') | ||
146 | |||
123 | def _SingleBranch(self, opt, branch, people): | 147 | def _SingleBranch(self, opt, branch, people): |
124 | project = branch.project | 148 | project = branch.project |
125 | name = branch.name | 149 | name = branch.name |
@@ -313,17 +337,27 @@ Gerrit Code Review: http://code.google.com/p/gerrit/ | |||
313 | reviewers = [] | 337 | reviewers = [] |
314 | cc = [] | 338 | cc = [] |
315 | 339 | ||
340 | for project in project_list: | ||
341 | avail = project.GetUploadableBranches() | ||
342 | if avail: | ||
343 | pending.append((project, avail)) | ||
344 | |||
345 | if pending and (not opt.bypass_hooks): | ||
346 | hook = RepoHook('pre-upload', self.manifest.repo_hooks_project, | ||
347 | self.manifest.topdir, abort_if_user_denies=True) | ||
348 | pending_proj_names = [project.name for (project, avail) in pending] | ||
349 | try: | ||
350 | hook.Run(opt.allow_all_hooks, project_list=pending_proj_names) | ||
351 | except HookError, e: | ||
352 | print >>sys.stderr, "ERROR: %s" % str(e) | ||
353 | return | ||
354 | |||
316 | if opt.reviewers: | 355 | if opt.reviewers: |
317 | reviewers = _SplitEmails(opt.reviewers) | 356 | reviewers = _SplitEmails(opt.reviewers) |
318 | if opt.cc: | 357 | if opt.cc: |
319 | cc = _SplitEmails(opt.cc) | 358 | cc = _SplitEmails(opt.cc) |
320 | people = (reviewers,cc) | 359 | people = (reviewers,cc) |
321 | 360 | ||
322 | for project in project_list: | ||
323 | avail = project.GetUploadableBranches() | ||
324 | if avail: | ||
325 | pending.append((project, avail)) | ||
326 | |||
327 | if not pending: | 361 | if not pending: |
328 | print >>sys.stdout, "no branches ready for upload" | 362 | print >>sys.stdout, "no branches ready for upload" |
329 | elif len(pending) == 1 and len(pending[0][1]) == 1: | 363 | elif len(pending) == 1 and len(pending[0][1]) == 1: |