diff options
Diffstat (limited to 'project.py')
| -rw-r--r-- | project.py | 814 |
1 files changed, 658 insertions, 156 deletions
| @@ -12,22 +12,28 @@ | |||
| 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 |
| 19 | import random | ||
| 18 | import re | 20 | import re |
| 19 | import shutil | 21 | import shutil |
| 20 | import stat | 22 | import stat |
| 23 | import subprocess | ||
| 21 | import sys | 24 | import sys |
| 22 | import urllib2 | 25 | import time |
| 23 | 26 | ||
| 24 | from color import Coloring | 27 | from color import Coloring |
| 25 | from git_command import GitCommand | 28 | from git_command import GitCommand |
| 26 | from git_config import GitConfig, IsId | 29 | from git_config import GitConfig, IsId, GetSchemeFromUrl, ID_RE |
| 27 | from error import GitError, ImportError, UploadError | 30 | from error import DownloadError |
| 31 | from error import GitError, HookError, ImportError, UploadError | ||
| 28 | from error import ManifestInvalidRevisionError | 32 | from error import ManifestInvalidRevisionError |
| 33 | from progress import Progress | ||
| 34 | from trace import IsTrace, Trace | ||
| 29 | 35 | ||
| 30 | from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB | 36 | from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M |
| 31 | 37 | ||
| 32 | def _lwrite(path, content): | 38 | def _lwrite(path, content): |
| 33 | lock = '%s.lock' % path | 39 | lock = '%s.lock' % path |
| @@ -54,29 +60,25 @@ def not_rev(r): | |||
| 54 | def sq(r): | 60 | def sq(r): |
| 55 | return "'" + r.replace("'", "'\''") + "'" | 61 | return "'" + r.replace("'", "'\''") + "'" |
| 56 | 62 | ||
| 57 | hook_list = None | 63 | _project_hook_list = None |
| 58 | def repo_hooks(): | 64 | def _ProjectHooks(): |
| 59 | global hook_list | 65 | """List the hooks present in the 'hooks' directory. |
| 60 | if hook_list is None: | 66 | |
| 61 | d = os.path.abspath(os.path.dirname(__file__)) | 67 | These hooks are project hooks and are copied to the '.git/hooks' directory |
| 62 | d = os.path.join(d , 'hooks') | 68 | of all subprojects. |
| 63 | hook_list = map(lambda x: os.path.join(d, x), os.listdir(d)) | ||
| 64 | return hook_list | ||
| 65 | 69 | ||
| 66 | def relpath(dst, src): | 70 | This function caches the list of hooks (based on the contents of the |
| 67 | src = os.path.dirname(src) | 71 | 'repo/hooks' directory) on the first call. |
| 68 | top = os.path.commonprefix([dst, src]) | ||
| 69 | if top.endswith('/'): | ||
| 70 | top = top[:-1] | ||
| 71 | else: | ||
| 72 | top = os.path.dirname(top) | ||
| 73 | 72 | ||
| 74 | tmp = src | 73 | Returns: |
| 75 | rel = '' | 74 | A list of absolute paths to all of the files in the hooks directory. |
| 76 | while top != tmp: | 75 | """ |
| 77 | rel += '../' | 76 | global _project_hook_list |
| 78 | tmp = os.path.dirname(tmp) | 77 | if _project_hook_list is None: |
| 79 | return rel + dst[len(top) + 1:] | 78 | d = os.path.abspath(os.path.dirname(__file__)) |
| 79 | d = os.path.join(d , 'hooks') | ||
| 80 | _project_hook_list = map(lambda x: os.path.join(d, x), os.listdir(d)) | ||
| 81 | return _project_hook_list | ||
| 80 | 82 | ||
| 81 | 83 | ||
| 82 | class DownloadedChange(object): | 84 | class DownloadedChange(object): |
| @@ -148,10 +150,11 @@ class ReviewableBranch(object): | |||
| 148 | R_HEADS + self.name, | 150 | R_HEADS + self.name, |
| 149 | '--') | 151 | '--') |
| 150 | 152 | ||
| 151 | def UploadForReview(self, people, auto_topic=False): | 153 | def UploadForReview(self, people, auto_topic=False, draft=False): |
| 152 | self.project.UploadForReview(self.name, | 154 | self.project.UploadForReview(self.name, |
| 153 | people, | 155 | people, |
| 154 | auto_topic=auto_topic) | 156 | auto_topic=auto_topic, |
| 157 | draft=draft) | ||
| 155 | 158 | ||
| 156 | def GetPublishedRefs(self): | 159 | def GetPublishedRefs(self): |
| 157 | refs = {} | 160 | refs = {} |
| @@ -185,6 +188,11 @@ class DiffColoring(Coloring): | |||
| 185 | Coloring.__init__(self, config, 'diff') | 188 | Coloring.__init__(self, config, 'diff') |
| 186 | self.project = self.printer('header', attr = 'bold') | 189 | self.project = self.printer('header', attr = 'bold') |
| 187 | 190 | ||
| 191 | class _Annotation: | ||
| 192 | def __init__(self, name, value, keep): | ||
| 193 | self.name = name | ||
| 194 | self.value = value | ||
| 195 | self.keep = keep | ||
| 188 | 196 | ||
| 189 | class _CopyFile: | 197 | class _CopyFile: |
| 190 | def __init__(self, src, dest, abssrc, absdest): | 198 | def __init__(self, src, dest, abssrc, absdest): |
| @@ -223,6 +231,249 @@ class RemoteSpec(object): | |||
| 223 | self.url = url | 231 | self.url = url |
| 224 | self.review = review | 232 | self.review = review |
| 225 | 233 | ||
| 234 | class RepoHook(object): | ||
| 235 | """A RepoHook contains information about a script to run as a hook. | ||
| 236 | |||
| 237 | Hooks are used to run a python script before running an upload (for instance, | ||
| 238 | to run presubmit checks). Eventually, we may have hooks for other actions. | ||
| 239 | |||
| 240 | This shouldn't be confused with files in the 'repo/hooks' directory. Those | ||
| 241 | files are copied into each '.git/hooks' folder for each project. Repo-level | ||
| 242 | hooks are associated instead with repo actions. | ||
| 243 | |||
| 244 | Hooks are always python. When a hook is run, we will load the hook into the | ||
| 245 | interpreter and execute its main() function. | ||
| 246 | """ | ||
| 247 | def __init__(self, | ||
| 248 | hook_type, | ||
| 249 | hooks_project, | ||
| 250 | topdir, | ||
| 251 | abort_if_user_denies=False): | ||
| 252 | """RepoHook constructor. | ||
| 253 | |||
| 254 | Params: | ||
| 255 | hook_type: A string representing the type of hook. This is also used | ||
| 256 | to figure out the name of the file containing the hook. For | ||
| 257 | example: 'pre-upload'. | ||
| 258 | hooks_project: The project containing the repo hooks. If you have a | ||
| 259 | manifest, this is manifest.repo_hooks_project. OK if this is None, | ||
| 260 | which will make the hook a no-op. | ||
| 261 | topdir: Repo's top directory (the one containing the .repo directory). | ||
| 262 | Scripts will run with CWD as this directory. If you have a manifest, | ||
| 263 | this is manifest.topdir | ||
| 264 | abort_if_user_denies: If True, we'll throw a HookError() if the user | ||
| 265 | doesn't allow us to run the hook. | ||
| 266 | """ | ||
| 267 | self._hook_type = hook_type | ||
| 268 | self._hooks_project = hooks_project | ||
| 269 | self._topdir = topdir | ||
| 270 | self._abort_if_user_denies = abort_if_user_denies | ||
| 271 | |||
| 272 | # Store the full path to the script for convenience. | ||
| 273 | if self._hooks_project: | ||
| 274 | self._script_fullpath = os.path.join(self._hooks_project.worktree, | ||
| 275 | self._hook_type + '.py') | ||
| 276 | else: | ||
| 277 | self._script_fullpath = None | ||
| 278 | |||
| 279 | def _GetHash(self): | ||
| 280 | """Return a hash of the contents of the hooks directory. | ||
| 281 | |||
| 282 | We'll just use git to do this. This hash has the property that if anything | ||
| 283 | changes in the directory we will return a different has. | ||
| 284 | |||
| 285 | SECURITY CONSIDERATION: | ||
| 286 | This hash only represents the contents of files in the hook directory, not | ||
| 287 | any other files imported or called by hooks. Changes to imported files | ||
| 288 | can change the script behavior without affecting the hash. | ||
| 289 | |||
| 290 | Returns: | ||
| 291 | A string representing the hash. This will always be ASCII so that it can | ||
| 292 | be printed to the user easily. | ||
| 293 | """ | ||
| 294 | assert self._hooks_project, "Must have hooks to calculate their hash." | ||
| 295 | |||
| 296 | # We will use the work_git object rather than just calling GetRevisionId(). | ||
| 297 | # That gives us a hash of the latest checked in version of the files that | ||
| 298 | # the user will actually be executing. Specifically, GetRevisionId() | ||
| 299 | # doesn't appear to change even if a user checks out a different version | ||
| 300 | # of the hooks repo (via git checkout) nor if a user commits their own revs. | ||
| 301 | # | ||
| 302 | # NOTE: Local (non-committed) changes will not be factored into this hash. | ||
| 303 | # I think this is OK, since we're really only worried about warning the user | ||
| 304 | # about upstream changes. | ||
| 305 | return self._hooks_project.work_git.rev_parse('HEAD') | ||
| 306 | |||
| 307 | def _GetMustVerb(self): | ||
| 308 | """Return 'must' if the hook is required; 'should' if not.""" | ||
| 309 | if self._abort_if_user_denies: | ||
| 310 | return 'must' | ||
| 311 | else: | ||
| 312 | return 'should' | ||
| 313 | |||
| 314 | def _CheckForHookApproval(self): | ||
| 315 | """Check to see whether this hook has been approved. | ||
| 316 | |||
| 317 | We'll look at the hash of all of the hooks. If this matches the hash that | ||
| 318 | the user last approved, we're done. If it doesn't, we'll ask the user | ||
| 319 | about approval. | ||
| 320 | |||
| 321 | Note that we ask permission for each individual hook even though we use | ||
| 322 | the hash of all hooks when detecting changes. We'd like the user to be | ||
| 323 | able to approve / deny each hook individually. We only use the hash of all | ||
| 324 | hooks because there is no other easy way to detect changes to local imports. | ||
| 325 | |||
| 326 | Returns: | ||
| 327 | True if this hook is approved to run; False otherwise. | ||
| 328 | |||
| 329 | Raises: | ||
| 330 | HookError: Raised if the user doesn't approve and abort_if_user_denies | ||
| 331 | was passed to the consturctor. | ||
| 332 | """ | ||
| 333 | hooks_dir = self._hooks_project.worktree | ||
| 334 | hooks_config = self._hooks_project.config | ||
| 335 | git_approval_key = 'repo.hooks.%s.approvedhash' % self._hook_type | ||
| 336 | |||
| 337 | # Get the last hash that the user approved for this hook; may be None. | ||
| 338 | old_hash = hooks_config.GetString(git_approval_key) | ||
| 339 | |||
| 340 | # Get the current hash so we can tell if scripts changed since approval. | ||
| 341 | new_hash = self._GetHash() | ||
| 342 | |||
| 343 | if old_hash is not None: | ||
| 344 | # User previously approved hook and asked not to be prompted again. | ||
| 345 | if new_hash == old_hash: | ||
| 346 | # Approval matched. We're done. | ||
| 347 | return True | ||
| 348 | else: | ||
| 349 | # Give the user a reason why we're prompting, since they last told | ||
| 350 | # us to "never ask again". | ||
| 351 | prompt = 'WARNING: Scripts have changed since %s was allowed.\n\n' % ( | ||
| 352 | self._hook_type) | ||
| 353 | else: | ||
| 354 | prompt = '' | ||
| 355 | |||
| 356 | # Prompt the user if we're not on a tty; on a tty we'll assume "no". | ||
| 357 | if sys.stdout.isatty(): | ||
| 358 | prompt += ('Repo %s run the script:\n' | ||
| 359 | ' %s\n' | ||
| 360 | '\n' | ||
| 361 | 'Do you want to allow this script to run ' | ||
| 362 | '(yes/yes-never-ask-again/NO)? ') % ( | ||
| 363 | self._GetMustVerb(), self._script_fullpath) | ||
| 364 | response = raw_input(prompt).lower() | ||
| 365 | |||
| 366 | |||
| 367 | # User is doing a one-time approval. | ||
| 368 | if response in ('y', 'yes'): | ||
| 369 | return True | ||
| 370 | elif response == 'yes-never-ask-again': | ||
| 371 | hooks_config.SetString(git_approval_key, new_hash) | ||
| 372 | return True | ||
| 373 | |||
| 374 | # For anything else, we'll assume no approval. | ||
| 375 | if self._abort_if_user_denies: | ||
| 376 | raise HookError('You must allow the %s hook or use --no-verify.' % | ||
| 377 | self._hook_type) | ||
| 378 | |||
| 379 | return False | ||
| 380 | |||
| 381 | def _ExecuteHook(self, **kwargs): | ||
| 382 | """Actually execute the given hook. | ||
| 383 | |||
| 384 | This will run the hook's 'main' function in our python interpreter. | ||
| 385 | |||
| 386 | Args: | ||
| 387 | kwargs: Keyword arguments to pass to the hook. These are often specific | ||
| 388 | to the hook type. For instance, pre-upload hooks will contain | ||
| 389 | a project_list. | ||
| 390 | """ | ||
| 391 | # Keep sys.path and CWD stashed away so that we can always restore them | ||
| 392 | # upon function exit. | ||
| 393 | orig_path = os.getcwd() | ||
| 394 | orig_syspath = sys.path | ||
| 395 | |||
| 396 | try: | ||
| 397 | # Always run hooks with CWD as topdir. | ||
| 398 | os.chdir(self._topdir) | ||
| 399 | |||
| 400 | # Put the hook dir as the first item of sys.path so hooks can do | ||
| 401 | # relative imports. We want to replace the repo dir as [0] so | ||
| 402 | # hooks can't import repo files. | ||
| 403 | sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:] | ||
| 404 | |||
| 405 | # Exec, storing global context in the context dict. We catch exceptions | ||
| 406 | # and convert to a HookError w/ just the failing traceback. | ||
| 407 | context = {} | ||
| 408 | try: | ||
| 409 | execfile(self._script_fullpath, context) | ||
| 410 | except Exception: | ||
| 411 | raise HookError('%s\nFailed to import %s hook; see traceback above.' % ( | ||
| 412 | traceback.format_exc(), self._hook_type)) | ||
| 413 | |||
| 414 | # Running the script should have defined a main() function. | ||
| 415 | if 'main' not in context: | ||
| 416 | raise HookError('Missing main() in: "%s"' % self._script_fullpath) | ||
| 417 | |||
| 418 | |||
| 419 | # Add 'hook_should_take_kwargs' to the arguments to be passed to main. | ||
| 420 | # We don't actually want hooks to define their main with this argument-- | ||
| 421 | # it's there to remind them that their hook should always take **kwargs. | ||
| 422 | # For instance, a pre-upload hook should be defined like: | ||
| 423 | # def main(project_list, **kwargs): | ||
| 424 | # | ||
| 425 | # This allows us to later expand the API without breaking old hooks. | ||
| 426 | kwargs = kwargs.copy() | ||
| 427 | kwargs['hook_should_take_kwargs'] = True | ||
| 428 | |||
| 429 | # Call the main function in the hook. If the hook should cause the | ||
| 430 | # build to fail, it will raise an Exception. We'll catch that convert | ||
| 431 | # to a HookError w/ just the failing traceback. | ||
| 432 | try: | ||
| 433 | context['main'](**kwargs) | ||
| 434 | except Exception: | ||
| 435 | raise HookError('%s\nFailed to run main() for %s hook; see traceback ' | ||
| 436 | 'above.' % ( | ||
| 437 | traceback.format_exc(), self._hook_type)) | ||
| 438 | finally: | ||
| 439 | # Restore sys.path and CWD. | ||
| 440 | sys.path = orig_syspath | ||
| 441 | os.chdir(orig_path) | ||
| 442 | |||
| 443 | def Run(self, user_allows_all_hooks, **kwargs): | ||
| 444 | """Run the hook. | ||
| 445 | |||
| 446 | If the hook doesn't exist (because there is no hooks project or because | ||
| 447 | this particular hook is not enabled), this is a no-op. | ||
| 448 | |||
| 449 | Args: | ||
| 450 | user_allows_all_hooks: If True, we will never prompt about running the | ||
| 451 | hook--we'll just assume it's OK to run it. | ||
| 452 | kwargs: Keyword arguments to pass to the hook. These are often specific | ||
| 453 | to the hook type. For instance, pre-upload hooks will contain | ||
| 454 | a project_list. | ||
| 455 | |||
| 456 | Raises: | ||
| 457 | HookError: If there was a problem finding the hook or the user declined | ||
| 458 | to run a required hook (from _CheckForHookApproval). | ||
| 459 | """ | ||
| 460 | # No-op if there is no hooks project or if hook is disabled. | ||
| 461 | if ((not self._hooks_project) or | ||
| 462 | (self._hook_type not in self._hooks_project.enabled_repo_hooks)): | ||
| 463 | return | ||
| 464 | |||
| 465 | # Bail with a nice error if we can't find the hook. | ||
| 466 | if not os.path.isfile(self._script_fullpath): | ||
| 467 | raise HookError('Couldn\'t find repo hook: "%s"' % self._script_fullpath) | ||
| 468 | |||
| 469 | # Make sure the user is OK with running the hook. | ||
| 470 | if (not user_allows_all_hooks) and (not self._CheckForHookApproval()): | ||
| 471 | return | ||
| 472 | |||
| 473 | # Run the hook with the same version of python we're using. | ||
| 474 | self._ExecuteHook(**kwargs) | ||
| 475 | |||
| 476 | |||
| 226 | class Project(object): | 477 | class Project(object): |
| 227 | def __init__(self, | 478 | def __init__(self, |
| 228 | manifest, | 479 | manifest, |
| @@ -232,7 +483,10 @@ class Project(object): | |||
| 232 | worktree, | 483 | worktree, |
| 233 | relpath, | 484 | relpath, |
| 234 | revisionExpr, | 485 | revisionExpr, |
| 235 | revisionId): | 486 | revisionId, |
| 487 | rebase = True, | ||
| 488 | groups = None, | ||
| 489 | sync_c = False): | ||
| 236 | self.manifest = manifest | 490 | self.manifest = manifest |
| 237 | self.name = name | 491 | self.name = name |
| 238 | self.remote = remote | 492 | self.remote = remote |
| @@ -251,8 +505,13 @@ class Project(object): | |||
| 251 | else: | 505 | else: |
| 252 | self.revisionId = revisionId | 506 | self.revisionId = revisionId |
| 253 | 507 | ||
| 508 | self.rebase = rebase | ||
| 509 | self.groups = groups | ||
| 510 | self.sync_c = sync_c | ||
| 511 | |||
| 254 | self.snapshots = {} | 512 | self.snapshots = {} |
| 255 | self.copyfiles = [] | 513 | self.copyfiles = [] |
| 514 | self.annotations = [] | ||
| 256 | self.config = GitConfig.ForRepository( | 515 | self.config = GitConfig.ForRepository( |
| 257 | gitdir = self.gitdir, | 516 | gitdir = self.gitdir, |
| 258 | defaults = self.manifest.globalConfig) | 517 | defaults = self.manifest.globalConfig) |
| @@ -264,6 +523,10 @@ class Project(object): | |||
| 264 | self.bare_git = self._GitGetByExec(self, bare=True) | 523 | self.bare_git = self._GitGetByExec(self, bare=True) |
| 265 | self.bare_ref = GitRefs(gitdir) | 524 | self.bare_ref = GitRefs(gitdir) |
| 266 | 525 | ||
| 526 | # This will be filled in if a project is later identified to be the | ||
| 527 | # project containing repo hooks. | ||
| 528 | self.enabled_repo_hooks = [] | ||
| 529 | |||
| 267 | @property | 530 | @property |
| 268 | def Exists(self): | 531 | def Exists(self): |
| 269 | return os.path.isdir(self.gitdir) | 532 | return os.path.isdir(self.gitdir) |
| @@ -367,6 +630,27 @@ class Project(object): | |||
| 367 | 630 | ||
| 368 | return heads | 631 | return heads |
| 369 | 632 | ||
| 633 | def MatchesGroups(self, manifest_groups): | ||
| 634 | """Returns true if the manifest groups specified at init should cause | ||
| 635 | this project to be synced. | ||
| 636 | Prefixing a manifest group with "-" inverts the meaning of a group. | ||
| 637 | All projects are implicitly labelled with "default". | ||
| 638 | |||
| 639 | labels are resolved in order. In the example case of | ||
| 640 | project_groups: "default,group1,group2" | ||
| 641 | manifest_groups: "-group1,group2" | ||
| 642 | the project will be matched. | ||
| 643 | """ | ||
| 644 | if self.groups is None: | ||
| 645 | return True | ||
| 646 | matched = False | ||
| 647 | for group in manifest_groups: | ||
| 648 | if group.startswith('-') and group[1:] in self.groups: | ||
| 649 | matched = False | ||
| 650 | elif group in self.groups: | ||
| 651 | matched = True | ||
| 652 | |||
| 653 | return matched | ||
| 370 | 654 | ||
| 371 | ## Status Display ## | 655 | ## Status Display ## |
| 372 | 656 | ||
| @@ -391,13 +675,18 @@ class Project(object): | |||
| 391 | 675 | ||
| 392 | return False | 676 | return False |
| 393 | 677 | ||
| 394 | def PrintWorkTreeStatus(self): | 678 | def PrintWorkTreeStatus(self, output_redir=None): |
| 395 | """Prints the status of the repository to stdout. | 679 | """Prints the status of the repository to stdout. |
| 680 | |||
| 681 | Args: | ||
| 682 | output: If specified, redirect the output to this object. | ||
| 396 | """ | 683 | """ |
| 397 | if not os.path.isdir(self.worktree): | 684 | if not os.path.isdir(self.worktree): |
| 398 | print '' | 685 | if output_redir == None: |
| 399 | print 'project %s/' % self.relpath | 686 | output_redir = sys.stdout |
| 400 | print ' missing (run "repo sync")' | 687 | print >>output_redir, '' |
| 688 | print >>output_redir, 'project %s/' % self.relpath | ||
| 689 | print >>output_redir, ' missing (run "repo sync")' | ||
| 401 | return | 690 | return |
| 402 | 691 | ||
| 403 | self.work_git.update_index('-q', | 692 | self.work_git.update_index('-q', |
| @@ -408,10 +697,12 @@ class Project(object): | |||
| 408 | di = self.work_git.DiffZ('diff-index', '-M', '--cached', HEAD) | 697 | di = self.work_git.DiffZ('diff-index', '-M', '--cached', HEAD) |
| 409 | df = self.work_git.DiffZ('diff-files') | 698 | df = self.work_git.DiffZ('diff-files') |
| 410 | do = self.work_git.LsOthers() | 699 | do = self.work_git.LsOthers() |
| 411 | if not rb and not di and not df and not do: | 700 | if not rb and not di and not df and not do and not self.CurrentBranch: |
| 412 | return 'CLEAN' | 701 | return 'CLEAN' |
| 413 | 702 | ||
| 414 | out = StatusColoring(self.config) | 703 | out = StatusColoring(self.config) |
| 704 | if not output_redir == None: | ||
| 705 | out.redirect(output_redir) | ||
| 415 | out.project('project %-40s', self.relpath + '/') | 706 | out.project('project %-40s', self.relpath + '/') |
| 416 | 707 | ||
| 417 | branch = self.CurrentBranch | 708 | branch = self.CurrentBranch |
| @@ -461,9 +752,10 @@ class Project(object): | |||
| 461 | else: | 752 | else: |
| 462 | out.write('%s', line) | 753 | out.write('%s', line) |
| 463 | out.nl() | 754 | out.nl() |
| 755 | |||
| 464 | return 'DIRTY' | 756 | return 'DIRTY' |
| 465 | 757 | ||
| 466 | def PrintWorkTreeDiff(self): | 758 | def PrintWorkTreeDiff(self, absolute_paths=False): |
| 467 | """Prints the status of the repository to stdout. | 759 | """Prints the status of the repository to stdout. |
| 468 | """ | 760 | """ |
| 469 | out = DiffColoring(self.config) | 761 | out = DiffColoring(self.config) |
| @@ -471,6 +763,9 @@ class Project(object): | |||
| 471 | if out.is_on: | 763 | if out.is_on: |
| 472 | cmd.append('--color') | 764 | cmd.append('--color') |
| 473 | cmd.append(HEAD) | 765 | cmd.append(HEAD) |
| 766 | if absolute_paths: | ||
| 767 | cmd.append('--src-prefix=a/%s/' % self.relpath) | ||
| 768 | cmd.append('--dst-prefix=b/%s/' % self.relpath) | ||
| 474 | cmd.append('--') | 769 | cmd.append('--') |
| 475 | p = GitCommand(self, | 770 | p = GitCommand(self, |
| 476 | cmd, | 771 | cmd, |
| @@ -524,7 +819,7 @@ class Project(object): | |||
| 524 | if R_HEADS + n not in heads: | 819 | if R_HEADS + n not in heads: |
| 525 | self.bare_git.DeleteRef(name, id) | 820 | self.bare_git.DeleteRef(name, id) |
| 526 | 821 | ||
| 527 | def GetUploadableBranches(self): | 822 | def GetUploadableBranches(self, selected_branch=None): |
| 528 | """List any branches which can be uploaded for review. | 823 | """List any branches which can be uploaded for review. |
| 529 | """ | 824 | """ |
| 530 | heads = {} | 825 | heads = {} |
| @@ -540,6 +835,8 @@ class Project(object): | |||
| 540 | for branch, id in heads.iteritems(): | 835 | for branch, id in heads.iteritems(): |
| 541 | if branch in pubed and pubed[branch] == id: | 836 | if branch in pubed and pubed[branch] == id: |
| 542 | continue | 837 | continue |
| 838 | if selected_branch and branch != selected_branch: | ||
| 839 | continue | ||
| 543 | 840 | ||
| 544 | rb = self.GetUploadableBranch(branch) | 841 | rb = self.GetUploadableBranch(branch) |
| 545 | if rb: | 842 | if rb: |
| @@ -559,7 +856,8 @@ class Project(object): | |||
| 559 | 856 | ||
| 560 | def UploadForReview(self, branch=None, | 857 | def UploadForReview(self, branch=None, |
| 561 | people=([],[]), | 858 | people=([],[]), |
| 562 | auto_topic=False): | 859 | auto_topic=False, |
| 860 | draft=False): | ||
| 563 | """Uploads the named branch for code review. | 861 | """Uploads the named branch for code review. |
| 564 | """ | 862 | """ |
| 565 | if branch is None: | 863 | if branch is None: |
| @@ -581,31 +879,36 @@ class Project(object): | |||
| 581 | branch.remote.projectname = self.name | 879 | branch.remote.projectname = self.name |
| 582 | branch.remote.Save() | 880 | branch.remote.Save() |
| 583 | 881 | ||
| 584 | if branch.remote.ReviewProtocol == 'ssh': | 882 | url = branch.remote.ReviewUrl(self.UserEmail) |
| 585 | if dest_branch.startswith(R_HEADS): | 883 | if url is None: |
| 586 | dest_branch = dest_branch[len(R_HEADS):] | 884 | raise UploadError('review not configured') |
| 885 | cmd = ['push'] | ||
| 587 | 886 | ||
| 887 | if url.startswith('ssh://'): | ||
| 588 | rp = ['gerrit receive-pack'] | 888 | rp = ['gerrit receive-pack'] |
| 589 | for e in people[0]: | 889 | for e in people[0]: |
| 590 | rp.append('--reviewer=%s' % sq(e)) | 890 | rp.append('--reviewer=%s' % sq(e)) |
| 591 | for e in people[1]: | 891 | for e in people[1]: |
| 592 | rp.append('--cc=%s' % sq(e)) | 892 | rp.append('--cc=%s' % sq(e)) |
| 893 | cmd.append('--receive-pack=%s' % " ".join(rp)) | ||
| 593 | 894 | ||
| 594 | ref_spec = '%s:refs/for/%s' % (R_HEADS + branch.name, dest_branch) | 895 | cmd.append(url) |
| 595 | if auto_topic: | ||
| 596 | ref_spec = ref_spec + '/' + branch.name | ||
| 597 | 896 | ||
| 598 | cmd = ['push'] | 897 | if dest_branch.startswith(R_HEADS): |
| 599 | cmd.append('--receive-pack=%s' % " ".join(rp)) | 898 | dest_branch = dest_branch[len(R_HEADS):] |
| 600 | cmd.append(branch.remote.SshReviewUrl(self.UserEmail)) | ||
| 601 | cmd.append(ref_spec) | ||
| 602 | 899 | ||
| 603 | if GitCommand(self, cmd, bare = True).Wait() != 0: | 900 | upload_type = 'for' |
| 604 | raise UploadError('Upload failed') | 901 | if draft: |
| 902 | upload_type = 'drafts' | ||
| 605 | 903 | ||
| 606 | else: | 904 | ref_spec = '%s:refs/%s/%s' % (R_HEADS + branch.name, upload_type, |
| 607 | raise UploadError('Unsupported protocol %s' \ | 905 | dest_branch) |
| 608 | % branch.remote.review) | 906 | if auto_topic: |
| 907 | ref_spec = ref_spec + '/' + branch.name | ||
| 908 | cmd.append(ref_spec) | ||
| 909 | |||
| 910 | if GitCommand(self, cmd, bare = True).Wait() != 0: | ||
| 911 | raise UploadError('Upload failed') | ||
| 609 | 912 | ||
| 610 | msg = "posted to %s for %s" % (branch.remote.review, dest_branch) | 913 | msg = "posted to %s for %s" % (branch.remote.review, dest_branch) |
| 611 | self.bare_git.UpdateRef(R_PUB + branch.name, | 914 | self.bare_git.UpdateRef(R_PUB + branch.name, |
| @@ -615,35 +918,53 @@ class Project(object): | |||
| 615 | 918 | ||
| 616 | ## Sync ## | 919 | ## Sync ## |
| 617 | 920 | ||
| 618 | def Sync_NetworkHalf(self, quiet=False): | 921 | def Sync_NetworkHalf(self, |
| 922 | quiet=False, | ||
| 923 | is_new=None, | ||
| 924 | current_branch_only=False, | ||
| 925 | clone_bundle=True): | ||
| 619 | """Perform only the network IO portion of the sync process. | 926 | """Perform only the network IO portion of the sync process. |
| 620 | Local working directory/branch state is not affected. | 927 | Local working directory/branch state is not affected. |
| 621 | """ | 928 | """ |
| 622 | is_new = not self.Exists | 929 | if is_new is None: |
| 930 | is_new = not self.Exists | ||
| 623 | if is_new: | 931 | if is_new: |
| 624 | if not quiet: | ||
| 625 | print >>sys.stderr | ||
| 626 | print >>sys.stderr, 'Initializing project %s ...' % self.name | ||
| 627 | self._InitGitDir() | 932 | self._InitGitDir() |
| 628 | |||
| 629 | self._InitRemote() | 933 | self._InitRemote() |
| 630 | if not self._RemoteFetch(initial=is_new, quiet=quiet): | ||
| 631 | return False | ||
| 632 | 934 | ||
| 633 | #Check that the requested ref was found after fetch | 935 | if is_new: |
| 634 | # | 936 | alt = os.path.join(self.gitdir, 'objects/info/alternates') |
| 635 | try: | 937 | try: |
| 636 | self.GetRevisionId() | 938 | fd = open(alt, 'rb') |
| 637 | except ManifestInvalidRevisionError: | 939 | try: |
| 638 | # if the ref is a tag. We can try fetching | 940 | alt_dir = fd.readline().rstrip() |
| 639 | # the tag manually as a last resort | 941 | finally: |
| 640 | # | 942 | fd.close() |
| 641 | rev = self.revisionExpr | 943 | except IOError: |
| 642 | if rev.startswith(R_TAGS): | 944 | alt_dir = None |
| 643 | self._RemoteFetch(None, rev[len(R_TAGS):], quiet=quiet) | 945 | else: |
| 946 | alt_dir = None | ||
| 947 | |||
| 948 | if clone_bundle \ | ||
| 949 | and alt_dir is None \ | ||
| 950 | and self._ApplyCloneBundle(initial=is_new, quiet=quiet): | ||
| 951 | is_new = False | ||
| 952 | |||
| 953 | if not current_branch_only: | ||
| 954 | if self.sync_c: | ||
| 955 | current_branch_only = True | ||
| 956 | elif not self.manifest._loaded: | ||
| 957 | # Manifest cannot check defaults until it syncs. | ||
| 958 | current_branch_only = False | ||
| 959 | elif self.manifest.default.sync_c: | ||
| 960 | current_branch_only = True | ||
| 961 | |||
| 962 | if not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir, | ||
| 963 | current_branch_only=current_branch_only): | ||
| 964 | return False | ||
| 644 | 965 | ||
| 645 | if self.worktree: | 966 | if self.worktree: |
| 646 | self.manifest.SetMRefs(self) | 967 | self._InitMRef() |
| 647 | else: | 968 | else: |
| 648 | self._InitMirrorHead() | 969 | self._InitMirrorHead() |
| 649 | try: | 970 | try: |
| @@ -680,11 +1001,11 @@ class Project(object): | |||
| 680 | """Perform only the local IO portion of the sync process. | 1001 | """Perform only the local IO portion of the sync process. |
| 681 | Network access is not required. | 1002 | Network access is not required. |
| 682 | """ | 1003 | """ |
| 683 | self._InitWorkTree() | ||
| 684 | all = self.bare_ref.all | 1004 | all = self.bare_ref.all |
| 685 | self.CleanPublishedCache(all) | 1005 | self.CleanPublishedCache(all) |
| 686 | |||
| 687 | revid = self.GetRevisionId(all) | 1006 | revid = self.GetRevisionId(all) |
| 1007 | |||
| 1008 | self._InitWorkTree() | ||
| 688 | head = self.work_git.GetHead() | 1009 | head = self.work_git.GetHead() |
| 689 | if head.startswith(R_HEADS): | 1010 | if head.startswith(R_HEADS): |
| 690 | branch = head[len(R_HEADS):] | 1011 | branch = head[len(R_HEADS):] |
| @@ -705,12 +1026,15 @@ class Project(object): | |||
| 705 | 1026 | ||
| 706 | if head == revid: | 1027 | if head == revid: |
| 707 | # No changes; don't do anything further. | 1028 | # No changes; don't do anything further. |
| 1029 | # Except if the head needs to be detached | ||
| 708 | # | 1030 | # |
| 709 | return | 1031 | if not syncbuf.detach_head: |
| 1032 | return | ||
| 1033 | else: | ||
| 1034 | lost = self._revlist(not_rev(revid), HEAD) | ||
| 1035 | if lost: | ||
| 1036 | syncbuf.info(self, "discarding %d commits", len(lost)) | ||
| 710 | 1037 | ||
| 711 | lost = self._revlist(not_rev(revid), HEAD) | ||
| 712 | if lost: | ||
| 713 | syncbuf.info(self, "discarding %d commits", len(lost)) | ||
| 714 | try: | 1038 | try: |
| 715 | self._Checkout(revid, quiet=True) | 1039 | self._Checkout(revid, quiet=True) |
| 716 | except GitError, e: | 1040 | except GitError, e: |
| @@ -728,7 +1052,7 @@ class Project(object): | |||
| 728 | 1052 | ||
| 729 | if not branch.LocalMerge: | 1053 | if not branch.LocalMerge: |
| 730 | # The current branch has no tracking configuration. | 1054 | # The current branch has no tracking configuration. |
| 731 | # Jump off it to a deatched HEAD. | 1055 | # Jump off it to a detached HEAD. |
| 732 | # | 1056 | # |
| 733 | syncbuf.info(self, | 1057 | syncbuf.info(self, |
| 734 | "leaving %s; does not track upstream", | 1058 | "leaving %s; does not track upstream", |
| @@ -806,10 +1130,12 @@ class Project(object): | |||
| 806 | len(local_changes) - cnt_mine) | 1130 | len(local_changes) - cnt_mine) |
| 807 | 1131 | ||
| 808 | branch.remote = self.GetRemote(self.remote.name) | 1132 | branch.remote = self.GetRemote(self.remote.name) |
| 809 | branch.merge = self.revisionExpr | 1133 | if not ID_RE.match(self.revisionExpr): |
| 1134 | # in case of manifest sync the revisionExpr might be a SHA1 | ||
| 1135 | branch.merge = self.revisionExpr | ||
| 810 | branch.Save() | 1136 | branch.Save() |
| 811 | 1137 | ||
| 812 | if cnt_mine > 0: | 1138 | if cnt_mine > 0 and self.rebase: |
| 813 | def _dorebase(): | 1139 | def _dorebase(): |
| 814 | self._Rebase(upstream = '%s^1' % last_mine, onto = revid) | 1140 | self._Rebase(upstream = '%s^1' % last_mine, onto = revid) |
| 815 | self._CopyFiles() | 1141 | self._CopyFiles() |
| @@ -833,6 +1159,9 @@ class Project(object): | |||
| 833 | abssrc = os.path.join(self.worktree, src) | 1159 | abssrc = os.path.join(self.worktree, src) |
| 834 | self.copyfiles.append(_CopyFile(src, dest, abssrc, absdest)) | 1160 | self.copyfiles.append(_CopyFile(src, dest, abssrc, absdest)) |
| 835 | 1161 | ||
| 1162 | def AddAnnotation(self, name, value, keep): | ||
| 1163 | self.annotations.append(_Annotation(name, value, keep)) | ||
| 1164 | |||
| 836 | def DownloadPatchSet(self, change_id, patch_id): | 1165 | def DownloadPatchSet(self, change_id, patch_id): |
| 837 | """Download a single patch set of a single change to FETCH_HEAD. | 1166 | """Download a single patch set of a single change to FETCH_HEAD. |
| 838 | """ | 1167 | """ |
| @@ -900,6 +1229,13 @@ class Project(object): | |||
| 900 | 1229 | ||
| 901 | def CheckoutBranch(self, name): | 1230 | def CheckoutBranch(self, name): |
| 902 | """Checkout a local topic branch. | 1231 | """Checkout a local topic branch. |
| 1232 | |||
| 1233 | Args: | ||
| 1234 | name: The name of the branch to checkout. | ||
| 1235 | |||
| 1236 | Returns: | ||
| 1237 | True if the checkout succeeded; False if it didn't; None if the branch | ||
| 1238 | didn't exist. | ||
| 903 | """ | 1239 | """ |
| 904 | rev = R_HEADS + name | 1240 | rev = R_HEADS + name |
| 905 | head = self.work_git.GetHead() | 1241 | head = self.work_git.GetHead() |
| @@ -914,7 +1250,7 @@ class Project(object): | |||
| 914 | except KeyError: | 1250 | except KeyError: |
| 915 | # Branch does not exist in this project | 1251 | # Branch does not exist in this project |
| 916 | # | 1252 | # |
| 917 | return False | 1253 | return None |
| 918 | 1254 | ||
| 919 | if head.startswith(R_HEADS): | 1255 | if head.startswith(R_HEADS): |
| 920 | try: | 1256 | try: |
| @@ -937,13 +1273,19 @@ class Project(object): | |||
| 937 | 1273 | ||
| 938 | def AbandonBranch(self, name): | 1274 | def AbandonBranch(self, name): |
| 939 | """Destroy a local topic branch. | 1275 | """Destroy a local topic branch. |
| 1276 | |||
| 1277 | Args: | ||
| 1278 | name: The name of the branch to abandon. | ||
| 1279 | |||
| 1280 | Returns: | ||
| 1281 | True if the abandon succeeded; False if it didn't; None if the branch | ||
| 1282 | didn't exist. | ||
| 940 | """ | 1283 | """ |
| 941 | rev = R_HEADS + name | 1284 | rev = R_HEADS + name |
| 942 | all = self.bare_ref.all | 1285 | all = self.bare_ref.all |
| 943 | if rev not in all: | 1286 | if rev not in all: |
| 944 | # Doesn't exist; assume already abandoned. | 1287 | # Doesn't exist |
| 945 | # | 1288 | return None |
| 946 | return True | ||
| 947 | 1289 | ||
| 948 | head = self.work_git.GetHead() | 1290 | head = self.work_git.GetHead() |
| 949 | if head == rev: | 1291 | if head == rev: |
| @@ -1023,31 +1365,43 @@ class Project(object): | |||
| 1023 | 1365 | ||
| 1024 | ## Direct Git Commands ## | 1366 | ## Direct Git Commands ## |
| 1025 | 1367 | ||
| 1026 | def _RemoteFetch(self, name=None, tag=None, | 1368 | def _RemoteFetch(self, name=None, |
| 1369 | current_branch_only=False, | ||
| 1027 | initial=False, | 1370 | initial=False, |
| 1028 | quiet=False): | 1371 | quiet=False, |
| 1372 | alt_dir=None): | ||
| 1373 | |||
| 1374 | is_sha1 = False | ||
| 1375 | tag_name = None | ||
| 1376 | |||
| 1377 | if current_branch_only: | ||
| 1378 | if ID_RE.match(self.revisionExpr) is not None: | ||
| 1379 | is_sha1 = True | ||
| 1380 | elif self.revisionExpr.startswith(R_TAGS): | ||
| 1381 | # this is a tag and its sha1 value should never change | ||
| 1382 | tag_name = self.revisionExpr[len(R_TAGS):] | ||
| 1383 | |||
| 1384 | if is_sha1 or tag_name is not None: | ||
| 1385 | try: | ||
| 1386 | # if revision (sha or tag) is not present then following function | ||
| 1387 | # throws an error. | ||
| 1388 | self.bare_git.rev_parse('--verify', '%s^0' % self.revisionExpr) | ||
| 1389 | return True | ||
| 1390 | except GitError: | ||
| 1391 | # There is no such persistent revision. We have to fetch it. | ||
| 1392 | pass | ||
| 1393 | |||
| 1029 | if not name: | 1394 | if not name: |
| 1030 | name = self.remote.name | 1395 | name = self.remote.name |
| 1031 | 1396 | ||
| 1032 | ssh_proxy = False | 1397 | ssh_proxy = False |
| 1033 | if self.GetRemote(name).PreConnectFetch(): | 1398 | remote = self.GetRemote(name) |
| 1399 | if remote.PreConnectFetch(): | ||
| 1034 | ssh_proxy = True | 1400 | ssh_proxy = True |
| 1035 | 1401 | ||
| 1036 | if initial: | 1402 | if initial: |
| 1037 | alt = os.path.join(self.gitdir, 'objects/info/alternates') | 1403 | if alt_dir and 'objects' == os.path.basename(alt_dir): |
| 1038 | try: | 1404 | ref_dir = os.path.dirname(alt_dir) |
| 1039 | fd = open(alt, 'rb') | ||
| 1040 | try: | ||
| 1041 | ref_dir = fd.readline() | ||
| 1042 | if ref_dir and ref_dir.endswith('\n'): | ||
| 1043 | ref_dir = ref_dir[:-1] | ||
| 1044 | finally: | ||
| 1045 | fd.close() | ||
| 1046 | except IOError, e: | ||
| 1047 | ref_dir = None | ||
| 1048 | |||
| 1049 | if ref_dir and 'objects' == os.path.basename(ref_dir): | ||
| 1050 | ref_dir = os.path.dirname(ref_dir) | ||
| 1051 | packed_refs = os.path.join(self.gitdir, 'packed-refs') | 1405 | packed_refs = os.path.join(self.gitdir, 'packed-refs') |
| 1052 | remote = self.GetRemote(name) | 1406 | remote = self.GetRemote(name) |
| 1053 | 1407 | ||
| @@ -1083,35 +1437,130 @@ class Project(object): | |||
| 1083 | old_packed += line | 1437 | old_packed += line |
| 1084 | 1438 | ||
| 1085 | _lwrite(packed_refs, tmp_packed) | 1439 | _lwrite(packed_refs, tmp_packed) |
| 1086 | |||
| 1087 | else: | 1440 | else: |
| 1088 | ref_dir = None | 1441 | alt_dir = None |
| 1089 | 1442 | ||
| 1090 | cmd = ['fetch'] | 1443 | cmd = ['fetch'] |
| 1444 | |||
| 1445 | # The --depth option only affects the initial fetch; after that we'll do | ||
| 1446 | # full fetches of changes. | ||
| 1447 | depth = self.manifest.manifestProject.config.GetString('repo.depth') | ||
| 1448 | if depth and initial: | ||
| 1449 | cmd.append('--depth=%s' % depth) | ||
| 1450 | |||
| 1091 | if quiet: | 1451 | if quiet: |
| 1092 | cmd.append('--quiet') | 1452 | cmd.append('--quiet') |
| 1093 | if not self.worktree: | 1453 | if not self.worktree: |
| 1094 | cmd.append('--update-head-ok') | 1454 | cmd.append('--update-head-ok') |
| 1095 | cmd.append(name) | 1455 | cmd.append(name) |
| 1096 | if tag is not None: | ||
| 1097 | cmd.append('tag') | ||
| 1098 | cmd.append(tag) | ||
| 1099 | 1456 | ||
| 1100 | ok = GitCommand(self, | 1457 | if not current_branch_only or is_sha1: |
| 1101 | cmd, | 1458 | # Fetch whole repo |
| 1102 | bare = True, | 1459 | cmd.append('--tags') |
| 1103 | ssh_proxy = ssh_proxy).Wait() == 0 | 1460 | cmd.append((u'+refs/heads/*:') + remote.ToLocal('refs/heads/*')) |
| 1461 | elif tag_name is not None: | ||
| 1462 | cmd.append('tag') | ||
| 1463 | cmd.append(tag_name) | ||
| 1464 | else: | ||
| 1465 | branch = self.revisionExpr | ||
| 1466 | if branch.startswith(R_HEADS): | ||
| 1467 | branch = branch[len(R_HEADS):] | ||
| 1468 | cmd.append((u'+refs/heads/%s:' % branch) + remote.ToLocal('refs/heads/%s' % branch)) | ||
| 1469 | |||
| 1470 | ok = False | ||
| 1471 | for i in range(2): | ||
| 1472 | if GitCommand(self, cmd, bare=True, ssh_proxy=ssh_proxy).Wait() == 0: | ||
| 1473 | ok = True | ||
| 1474 | break | ||
| 1475 | time.sleep(random.randint(30, 45)) | ||
| 1104 | 1476 | ||
| 1105 | if initial: | 1477 | if initial: |
| 1106 | if ref_dir: | 1478 | if alt_dir: |
| 1107 | if old_packed != '': | 1479 | if old_packed != '': |
| 1108 | _lwrite(packed_refs, old_packed) | 1480 | _lwrite(packed_refs, old_packed) |
| 1109 | else: | 1481 | else: |
| 1110 | os.remove(packed_refs) | 1482 | os.remove(packed_refs) |
| 1111 | self.bare_git.pack_refs('--all', '--prune') | 1483 | self.bare_git.pack_refs('--all', '--prune') |
| 1484 | return ok | ||
| 1485 | |||
| 1486 | def _ApplyCloneBundle(self, initial=False, quiet=False): | ||
| 1487 | if initial and self.manifest.manifestProject.config.GetString('repo.depth'): | ||
| 1488 | return False | ||
| 1489 | |||
| 1490 | remote = self.GetRemote(self.remote.name) | ||
| 1491 | bundle_url = remote.url + '/clone.bundle' | ||
| 1492 | bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url) | ||
| 1493 | if GetSchemeFromUrl(bundle_url) in ('persistent-http', 'persistent-https'): | ||
| 1494 | bundle_url = bundle_url[len('persistent-'):] | ||
| 1495 | if GetSchemeFromUrl(bundle_url) not in ('http', 'https'): | ||
| 1496 | return False | ||
| 1497 | |||
| 1498 | bundle_dst = os.path.join(self.gitdir, 'clone.bundle') | ||
| 1499 | bundle_tmp = os.path.join(self.gitdir, 'clone.bundle.tmp') | ||
| 1500 | |||
| 1501 | exist_dst = os.path.exists(bundle_dst) | ||
| 1502 | exist_tmp = os.path.exists(bundle_tmp) | ||
| 1503 | |||
| 1504 | if not initial and not exist_dst and not exist_tmp: | ||
| 1505 | return False | ||
| 1112 | 1506 | ||
| 1507 | if not exist_dst: | ||
| 1508 | exist_dst = self._FetchBundle(bundle_url, bundle_tmp, bundle_dst, quiet) | ||
| 1509 | if not exist_dst: | ||
| 1510 | return False | ||
| 1511 | |||
| 1512 | cmd = ['fetch'] | ||
| 1513 | if quiet: | ||
| 1514 | cmd.append('--quiet') | ||
| 1515 | if not self.worktree: | ||
| 1516 | cmd.append('--update-head-ok') | ||
| 1517 | cmd.append(bundle_dst) | ||
| 1518 | for f in remote.fetch: | ||
| 1519 | cmd.append(str(f)) | ||
| 1520 | cmd.append('refs/tags/*:refs/tags/*') | ||
| 1521 | |||
| 1522 | ok = GitCommand(self, cmd, bare=True).Wait() == 0 | ||
| 1523 | if os.path.exists(bundle_dst): | ||
| 1524 | os.remove(bundle_dst) | ||
| 1525 | if os.path.exists(bundle_tmp): | ||
| 1526 | os.remove(bundle_tmp) | ||
| 1113 | return ok | 1527 | return ok |
| 1114 | 1528 | ||
| 1529 | def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet): | ||
| 1530 | if os.path.exists(dstPath): | ||
| 1531 | os.remove(dstPath) | ||
| 1532 | |||
| 1533 | cmd = ['curl', '--output', tmpPath, '--netrc', '--location'] | ||
| 1534 | if quiet: | ||
| 1535 | cmd += ['--silent'] | ||
| 1536 | if os.path.exists(tmpPath): | ||
| 1537 | size = os.stat(tmpPath).st_size | ||
| 1538 | if size >= 1024: | ||
| 1539 | cmd += ['--continue-at', '%d' % (size,)] | ||
| 1540 | else: | ||
| 1541 | os.remove(tmpPath) | ||
| 1542 | if 'http_proxy' in os.environ and 'darwin' == sys.platform: | ||
| 1543 | cmd += ['--proxy', os.environ['http_proxy']] | ||
| 1544 | cmd += [srcUrl] | ||
| 1545 | |||
| 1546 | if IsTrace(): | ||
| 1547 | Trace('%s', ' '.join(cmd)) | ||
| 1548 | try: | ||
| 1549 | proc = subprocess.Popen(cmd) | ||
| 1550 | except OSError: | ||
| 1551 | return False | ||
| 1552 | |||
| 1553 | ok = proc.wait() == 0 | ||
| 1554 | if os.path.exists(tmpPath): | ||
| 1555 | if ok and os.stat(tmpPath).st_size > 16: | ||
| 1556 | os.rename(tmpPath, dstPath) | ||
| 1557 | return True | ||
| 1558 | else: | ||
| 1559 | os.remove(tmpPath) | ||
| 1560 | return False | ||
| 1561 | else: | ||
| 1562 | return False | ||
| 1563 | |||
| 1115 | def _Checkout(self, rev, quiet=False): | 1564 | def _Checkout(self, rev, quiet=False): |
| 1116 | cmd = ['checkout'] | 1565 | cmd = ['checkout'] |
| 1117 | if quiet: | 1566 | if quiet: |
| @@ -1122,6 +1571,23 @@ class Project(object): | |||
| 1122 | if self._allrefs: | 1571 | if self._allrefs: |
| 1123 | raise GitError('%s checkout %s ' % (self.name, rev)) | 1572 | raise GitError('%s checkout %s ' % (self.name, rev)) |
| 1124 | 1573 | ||
| 1574 | def _CherryPick(self, rev, quiet=False): | ||
| 1575 | cmd = ['cherry-pick'] | ||
| 1576 | cmd.append(rev) | ||
| 1577 | cmd.append('--') | ||
| 1578 | if GitCommand(self, cmd).Wait() != 0: | ||
| 1579 | if self._allrefs: | ||
| 1580 | raise GitError('%s cherry-pick %s ' % (self.name, rev)) | ||
| 1581 | |||
| 1582 | def _Revert(self, rev, quiet=False): | ||
| 1583 | cmd = ['revert'] | ||
| 1584 | cmd.append('--no-edit') | ||
| 1585 | cmd.append(rev) | ||
| 1586 | cmd.append('--') | ||
| 1587 | if GitCommand(self, cmd).Wait() != 0: | ||
| 1588 | if self._allrefs: | ||
| 1589 | raise GitError('%s revert %s ' % (self.name, rev)) | ||
| 1590 | |||
| 1125 | def _ResetHard(self, rev, quiet=True): | 1591 | def _ResetHard(self, rev, quiet=True): |
| 1126 | cmd = ['reset', '--hard'] | 1592 | cmd = ['reset', '--hard'] |
| 1127 | if quiet: | 1593 | if quiet: |
| @@ -1138,8 +1604,10 @@ class Project(object): | |||
| 1138 | if GitCommand(self, cmd).Wait() != 0: | 1604 | if GitCommand(self, cmd).Wait() != 0: |
| 1139 | raise GitError('%s rebase %s ' % (self.name, upstream)) | 1605 | raise GitError('%s rebase %s ' % (self.name, upstream)) |
| 1140 | 1606 | ||
| 1141 | def _FastForward(self, head): | 1607 | def _FastForward(self, head, ffonly=False): |
| 1142 | cmd = ['merge', head] | 1608 | cmd = ['merge', head] |
| 1609 | if ffonly: | ||
| 1610 | cmd.append("--ff-only") | ||
| 1143 | if GitCommand(self, cmd).Wait() != 0: | 1611 | if GitCommand(self, cmd).Wait() != 0: |
| 1144 | raise GitError('%s merge %s ' % (self.name, head)) | 1612 | raise GitError('%s merge %s ' % (self.name, head)) |
| 1145 | 1613 | ||
| @@ -1192,13 +1660,16 @@ class Project(object): | |||
| 1192 | hooks = self._gitdir_path('hooks') | 1660 | hooks = self._gitdir_path('hooks') |
| 1193 | if not os.path.exists(hooks): | 1661 | if not os.path.exists(hooks): |
| 1194 | os.makedirs(hooks) | 1662 | os.makedirs(hooks) |
| 1195 | for stock_hook in repo_hooks(): | 1663 | for stock_hook in _ProjectHooks(): |
| 1196 | name = os.path.basename(stock_hook) | 1664 | name = os.path.basename(stock_hook) |
| 1197 | 1665 | ||
| 1198 | if name in ('commit-msg') and not self.remote.review: | 1666 | if name in ('commit-msg',) and not self.remote.review \ |
| 1667 | and not self is self.manifest.manifestProject: | ||
| 1199 | # Don't install a Gerrit Code Review hook if this | 1668 | # Don't install a Gerrit Code Review hook if this |
| 1200 | # project does not appear to use it for reviews. | 1669 | # project does not appear to use it for reviews. |
| 1201 | # | 1670 | # |
| 1671 | # Since the manifest project is one of those, but also | ||
| 1672 | # managed through gerrit, it's excluded | ||
| 1202 | continue | 1673 | continue |
| 1203 | 1674 | ||
| 1204 | dst = os.path.join(hooks, name) | 1675 | dst = os.path.join(hooks, name) |
| @@ -1211,7 +1682,7 @@ class Project(object): | |||
| 1211 | _error("%s: Not replacing %s hook", self.relpath, name) | 1682 | _error("%s: Not replacing %s hook", self.relpath, name) |
| 1212 | continue | 1683 | continue |
| 1213 | try: | 1684 | try: |
| 1214 | os.symlink(relpath(stock_hook, dst), dst) | 1685 | os.symlink(os.path.relpath(stock_hook, os.path.dirname(dst)), dst) |
| 1215 | except OSError, e: | 1686 | except OSError, e: |
| 1216 | if e.errno == errno.EPERM: | 1687 | if e.errno == errno.EPERM: |
| 1217 | raise GitError('filesystem must support symlinks') | 1688 | raise GitError('filesystem must support symlinks') |
| @@ -1231,6 +1702,10 @@ class Project(object): | |||
| 1231 | remote.ResetFetch(mirror=True) | 1702 | remote.ResetFetch(mirror=True) |
| 1232 | remote.Save() | 1703 | remote.Save() |
| 1233 | 1704 | ||
| 1705 | def _InitMRef(self): | ||
| 1706 | if self.manifest.branch: | ||
| 1707 | self._InitAnyMRef(R_M + self.manifest.branch) | ||
| 1708 | |||
| 1234 | def _InitMirrorHead(self): | 1709 | def _InitMirrorHead(self): |
| 1235 | self._InitAnyMRef(HEAD) | 1710 | self._InitAnyMRef(HEAD) |
| 1236 | 1711 | ||
| @@ -1249,40 +1724,33 @@ class Project(object): | |||
| 1249 | msg = 'manifest set to %s' % self.revisionExpr | 1724 | msg = 'manifest set to %s' % self.revisionExpr |
| 1250 | self.bare_git.symbolic_ref('-m', msg, ref, dst) | 1725 | self.bare_git.symbolic_ref('-m', msg, ref, dst) |
| 1251 | 1726 | ||
| 1252 | def _LinkWorkTree(self, relink=False): | ||
| 1253 | dotgit = os.path.join(self.worktree, '.git') | ||
| 1254 | if not relink: | ||
| 1255 | os.makedirs(dotgit) | ||
| 1256 | |||
| 1257 | for name in ['config', | ||
| 1258 | 'description', | ||
| 1259 | 'hooks', | ||
| 1260 | 'info', | ||
| 1261 | 'logs', | ||
| 1262 | 'objects', | ||
| 1263 | 'packed-refs', | ||
| 1264 | 'refs', | ||
| 1265 | 'rr-cache', | ||
| 1266 | 'svn']: | ||
| 1267 | try: | ||
| 1268 | src = os.path.join(self.gitdir, name) | ||
| 1269 | dst = os.path.join(dotgit, name) | ||
| 1270 | if relink: | ||
| 1271 | os.remove(dst) | ||
| 1272 | if os.path.islink(dst) or not os.path.exists(dst): | ||
| 1273 | os.symlink(relpath(src, dst), dst) | ||
| 1274 | else: | ||
| 1275 | raise GitError('cannot overwrite a local work tree') | ||
| 1276 | except OSError, e: | ||
| 1277 | if e.errno == errno.EPERM: | ||
| 1278 | raise GitError('filesystem must support symlinks') | ||
| 1279 | else: | ||
| 1280 | raise | ||
| 1281 | |||
| 1282 | def _InitWorkTree(self): | 1727 | def _InitWorkTree(self): |
| 1283 | dotgit = os.path.join(self.worktree, '.git') | 1728 | dotgit = os.path.join(self.worktree, '.git') |
| 1284 | if not os.path.exists(dotgit): | 1729 | if not os.path.exists(dotgit): |
| 1285 | self._LinkWorkTree() | 1730 | os.makedirs(dotgit) |
| 1731 | |||
| 1732 | for name in ['config', | ||
| 1733 | 'description', | ||
| 1734 | 'hooks', | ||
| 1735 | 'info', | ||
| 1736 | 'logs', | ||
| 1737 | 'objects', | ||
| 1738 | 'packed-refs', | ||
| 1739 | 'refs', | ||
| 1740 | 'rr-cache', | ||
| 1741 | 'svn']: | ||
| 1742 | try: | ||
| 1743 | src = os.path.join(self.gitdir, name) | ||
| 1744 | dst = os.path.join(dotgit, name) | ||
| 1745 | if os.path.islink(dst) or not os.path.exists(dst): | ||
| 1746 | os.symlink(os.path.relpath(src, os.path.dirname(dst)), dst) | ||
| 1747 | else: | ||
| 1748 | raise GitError('cannot overwrite a local work tree') | ||
| 1749 | except OSError, e: | ||
| 1750 | if e.errno == errno.EPERM: | ||
| 1751 | raise GitError('filesystem must support symlinks') | ||
| 1752 | else: | ||
| 1753 | raise | ||
| 1286 | 1754 | ||
| 1287 | _lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId()) | 1755 | _lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId()) |
| 1288 | 1756 | ||
| @@ -1291,6 +1759,11 @@ class Project(object): | |||
| 1291 | cmd.append(HEAD) | 1759 | cmd.append(HEAD) |
| 1292 | if GitCommand(self, cmd).Wait() != 0: | 1760 | if GitCommand(self, cmd).Wait() != 0: |
| 1293 | raise GitError("cannot initialize work tree") | 1761 | raise GitError("cannot initialize work tree") |
| 1762 | |||
| 1763 | rr_cache = os.path.join(self.gitdir, 'rr-cache') | ||
| 1764 | if not os.path.exists(rr_cache): | ||
| 1765 | os.makedirs(rr_cache) | ||
| 1766 | |||
| 1294 | self._CopyFiles() | 1767 | self._CopyFiles() |
| 1295 | 1768 | ||
| 1296 | def _gitdir_path(self, path): | 1769 | def _gitdir_path(self, path): |
| @@ -1449,6 +1922,22 @@ class Project(object): | |||
| 1449 | return r | 1922 | return r |
| 1450 | 1923 | ||
| 1451 | def __getattr__(self, name): | 1924 | def __getattr__(self, name): |
| 1925 | """Allow arbitrary git commands using pythonic syntax. | ||
| 1926 | |||
| 1927 | This allows you to do things like: | ||
| 1928 | git_obj.rev_parse('HEAD') | ||
| 1929 | |||
| 1930 | Since we don't have a 'rev_parse' method defined, the __getattr__ will | ||
| 1931 | run. We'll replace the '_' with a '-' and try to run a git command. | ||
| 1932 | Any other arguments will be passed to the git command. | ||
| 1933 | |||
| 1934 | Args: | ||
| 1935 | name: The name of the git command to call. Any '_' characters will | ||
| 1936 | be replaced with '-'. | ||
| 1937 | |||
| 1938 | Returns: | ||
| 1939 | A callable object that will try to call git with the named command. | ||
| 1940 | """ | ||
| 1452 | name = name.replace('_', '-') | 1941 | name = name.replace('_', '-') |
| 1453 | def runner(*args): | 1942 | def runner(*args): |
| 1454 | cmdv = [name] | 1943 | cmdv = [name] |
| @@ -1580,30 +2069,43 @@ class SyncBuffer(object): | |||
| 1580 | class MetaProject(Project): | 2069 | class MetaProject(Project): |
| 1581 | """A special project housed under .repo. | 2070 | """A special project housed under .repo. |
| 1582 | """ | 2071 | """ |
| 1583 | def __init__(self, manifest, name, gitdir, worktree, relpath=None): | 2072 | def __init__(self, manifest, name, gitdir, worktree): |
| 1584 | repodir = manifest.repodir | 2073 | repodir = manifest.repodir |
| 1585 | if relpath is None: | ||
| 1586 | relpath = '.repo/%s' % name | ||
| 1587 | Project.__init__(self, | 2074 | Project.__init__(self, |
| 1588 | manifest = manifest, | 2075 | manifest = manifest, |
| 1589 | name = name, | 2076 | name = name, |
| 1590 | gitdir = gitdir, | 2077 | gitdir = gitdir, |
| 1591 | worktree = worktree, | 2078 | worktree = worktree, |
| 1592 | remote = RemoteSpec('origin'), | 2079 | remote = RemoteSpec('origin'), |
| 1593 | relpath = relpath, | 2080 | relpath = '.repo/%s' % name, |
| 1594 | revisionExpr = 'refs/heads/master', | 2081 | revisionExpr = 'refs/heads/master', |
| 1595 | revisionId = None) | 2082 | revisionId = None, |
| 2083 | groups = None) | ||
| 1596 | 2084 | ||
| 1597 | def PreSync(self): | 2085 | def PreSync(self): |
| 1598 | if self.Exists: | 2086 | if self.Exists: |
| 1599 | cb = self.CurrentBranch | 2087 | cb = self.CurrentBranch |
| 1600 | if cb: | 2088 | if cb: |
| 1601 | cb = self.GetBranch(cb) | 2089 | base = self.GetBranch(cb).merge |
| 1602 | if cb.merge: | 2090 | if base: |
| 1603 | self.revisionExpr = cb.merge | 2091 | self.revisionExpr = base |
| 1604 | self.revisionId = None | 2092 | self.revisionId = None |
| 1605 | if cb.remote and cb.remote.name: | 2093 | |
| 1606 | self.remote.name = cb.remote.name | 2094 | def MetaBranchSwitch(self, target): |
| 2095 | """ Prepare MetaProject for manifest branch switch | ||
| 2096 | """ | ||
| 2097 | |||
| 2098 | # detach and delete manifest branch, allowing a new | ||
| 2099 | # branch to take over | ||
| 2100 | syncbuf = SyncBuffer(self.config, detach_head = True) | ||
| 2101 | self.Sync_LocalHalf(syncbuf) | ||
| 2102 | syncbuf.Finish() | ||
| 2103 | |||
| 2104 | return GitCommand(self, | ||
| 2105 | ['update-ref', '-d', 'refs/heads/default'], | ||
| 2106 | capture_stdout = True, | ||
| 2107 | capture_stderr = True).Wait() == 0 | ||
| 2108 | |||
| 1607 | 2109 | ||
| 1608 | @property | 2110 | @property |
| 1609 | def LastFetch(self): | 2111 | def LastFetch(self): |
