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: |
