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