diff options
| -rw-r--r-- | docs/manifest_submodule.txt | 6 | ||||
| -rw-r--r-- | docs/manifest_xml.txt | 30 | ||||
| -rw-r--r-- | editor.py | 2 | ||||
| -rw-r--r-- | git_command.py | 33 | ||||
| -rw-r--r-- | git_config.py | 67 | ||||
| -rwxr-xr-x | git_ssh | 2 | ||||
| -rw-r--r-- | manifest.py | 8 | ||||
| -rw-r--r-- | manifest_submodule.py | 7 | ||||
| -rw-r--r-- | manifest_xml.py | 94 | ||||
| -rw-r--r-- | progress.py | 7 | ||||
| -rw-r--r-- | project.py | 177 | ||||
| -rwxr-xr-x | repo | 3 | ||||
| -rw-r--r-- | subcmds/branches.py | 12 | ||||
| -rw-r--r-- | subcmds/grep.py | 8 | ||||
| -rw-r--r-- | subcmds/init.py | 29 | ||||
| -rw-r--r-- | subcmds/rebase.py | 107 | ||||
| -rw-r--r-- | subcmds/smartsync.py | 33 | ||||
| -rw-r--r-- | subcmds/sync.py | 196 | ||||
| -rw-r--r-- | subcmds/upload.py | 164 |
19 files changed, 806 insertions, 179 deletions
diff --git a/docs/manifest_submodule.txt b/docs/manifest_submodule.txt index e7d1f643..1718284b 100644 --- a/docs/manifest_submodule.txt +++ b/docs/manifest_submodule.txt | |||
| @@ -76,6 +76,12 @@ submodule.<name>.update | |||
| 76 | 76 | ||
| 77 | This key is not supported by repo. If set, it will be ignored. | 77 | This key is not supported by repo. If set, it will be ignored. |
| 78 | 78 | ||
| 79 | repo.notice | ||
| 80 | ----------- | ||
| 81 | |||
| 82 | A message displayed when repo sync uses this manifest. | ||
| 83 | |||
| 84 | |||
| 79 | .review | 85 | .review |
| 80 | ======= | 86 | ======= |
| 81 | 87 | ||
diff --git a/docs/manifest_xml.txt b/docs/manifest_xml.txt index da0e69ff..37fbd5cd 100644 --- a/docs/manifest_xml.txt +++ b/docs/manifest_xml.txt | |||
| @@ -20,11 +20,15 @@ A manifest XML file (e.g. 'default.xml') roughly conforms to the | |||
| 20 | following DTD: | 20 | following DTD: |
| 21 | 21 | ||
| 22 | <!DOCTYPE manifest [ | 22 | <!DOCTYPE manifest [ |
| 23 | <!ELEMENT manifest (remote*, | 23 | <!ELEMENT manifest (notice?, |
| 24 | remote*, | ||
| 24 | default?, | 25 | default?, |
| 26 | manifest-server?, | ||
| 25 | remove-project*, | 27 | remove-project*, |
| 26 | project*)> | 28 | project*)> |
| 27 | 29 | ||
| 30 | <!ELEMENT notice (#PCDATA)> | ||
| 31 | |||
| 28 | <!ELEMENT remote (EMPTY)> | 32 | <!ELEMENT remote (EMPTY)> |
| 29 | <!ATTLIST remote name ID #REQUIRED> | 33 | <!ATTLIST remote name ID #REQUIRED> |
| 30 | <!ATTLIST remote fetch CDATA #REQUIRED> | 34 | <!ATTLIST remote fetch CDATA #REQUIRED> |
| @@ -34,6 +38,9 @@ following DTD: | |||
| 34 | <!ATTLIST default remote IDREF #IMPLIED> | 38 | <!ATTLIST default remote IDREF #IMPLIED> |
| 35 | <!ATTLIST default revision CDATA #IMPLIED> | 39 | <!ATTLIST default revision CDATA #IMPLIED> |
| 36 | 40 | ||
| 41 | <!ELEMENT manifest-server (EMPTY)> | ||
| 42 | <!ATTLIST url CDATA #REQUIRED> | ||
| 43 | |||
| 37 | <!ELEMENT project (EMPTY)> | 44 | <!ELEMENT project (EMPTY)> |
| 38 | <!ATTLIST project name CDATA #REQUIRED> | 45 | <!ATTLIST project name CDATA #REQUIRED> |
| 39 | <!ATTLIST project path CDATA #IMPLIED> | 46 | <!ATTLIST project path CDATA #IMPLIED> |
| @@ -89,6 +96,27 @@ Attribute `revision`: Name of a Git branch (e.g. `master` or | |||
| 89 | revision attribute will use this revision. | 96 | revision attribute will use this revision. |
| 90 | 97 | ||
| 91 | 98 | ||
| 99 | Element manifest-server | ||
| 100 | ----------------------- | ||
| 101 | |||
| 102 | At most one manifest-server may be specified. The url attribute | ||
| 103 | is used to specify the URL of a manifest server, which is an | ||
| 104 | XML RPC service that will return a manifest in which each project | ||
| 105 | is pegged to a known good revision for the current branch and | ||
| 106 | target. | ||
| 107 | |||
| 108 | The manifest server should implement: | ||
| 109 | |||
| 110 | GetApprovedManifest(branch, target) | ||
| 111 | |||
| 112 | The target to use is defined by environment variables TARGET_PRODUCT | ||
| 113 | and TARGET_BUILD_VARIANT. These variables are used to create a string | ||
| 114 | of the form $TARGET_PRODUCT-$TARGET_BUILD_VARIANT, e.g. passion-userdebug. | ||
| 115 | If one of those variables or both are not present, the program will call | ||
| 116 | GetApprovedManifest without the target paramater and the manifest server | ||
| 117 | should choose a reasonable default target. | ||
| 118 | |||
| 119 | |||
| 92 | Element project | 120 | Element project |
| 93 | --------------- | 121 | --------------- |
| 94 | 122 | ||
| @@ -82,7 +82,7 @@ least one of these before using this command.""" | |||
| 82 | fd = None | 82 | fd = None |
| 83 | 83 | ||
| 84 | if re.compile("^.*[$ \t'].*$").match(editor): | 84 | if re.compile("^.*[$ \t'].*$").match(editor): |
| 85 | args = [editor + ' "$@"'] | 85 | args = [editor + ' "$@"', 'sh'] |
| 86 | shell = True | 86 | shell = True |
| 87 | else: | 87 | else: |
| 88 | args = [editor] | 88 | args = [editor] |
diff --git a/git_command.py b/git_command.py index 414c84a2..181e3724 100644 --- a/git_command.py +++ b/git_command.py | |||
| @@ -17,6 +17,7 @@ import os | |||
| 17 | import sys | 17 | import sys |
| 18 | import subprocess | 18 | import subprocess |
| 19 | import tempfile | 19 | import tempfile |
| 20 | from signal import SIGTERM | ||
| 20 | from error import GitError | 21 | from error import GitError |
| 21 | from trace import REPO_TRACE, IsTrace, Trace | 22 | from trace import REPO_TRACE, IsTrace, Trace |
| 22 | 23 | ||
| @@ -29,8 +30,9 @@ LAST_CWD = None | |||
| 29 | 30 | ||
| 30 | _ssh_proxy_path = None | 31 | _ssh_proxy_path = None |
| 31 | _ssh_sock_path = None | 32 | _ssh_sock_path = None |
| 33 | _ssh_clients = [] | ||
| 32 | 34 | ||
| 33 | def _ssh_sock(create=True): | 35 | def ssh_sock(create=True): |
| 34 | global _ssh_sock_path | 36 | global _ssh_sock_path |
| 35 | if _ssh_sock_path is None: | 37 | if _ssh_sock_path is None: |
| 36 | if not create: | 38 | if not create: |
| @@ -51,6 +53,24 @@ def _ssh_proxy(): | |||
| 51 | 'git_ssh') | 53 | 'git_ssh') |
| 52 | return _ssh_proxy_path | 54 | return _ssh_proxy_path |
| 53 | 55 | ||
| 56 | def _add_ssh_client(p): | ||
| 57 | _ssh_clients.append(p) | ||
| 58 | |||
| 59 | def _remove_ssh_client(p): | ||
| 60 | try: | ||
| 61 | _ssh_clients.remove(p) | ||
| 62 | except ValueError: | ||
| 63 | pass | ||
| 64 | |||
| 65 | def terminate_ssh_clients(): | ||
| 66 | global _ssh_clients | ||
| 67 | for p in _ssh_clients: | ||
| 68 | try: | ||
| 69 | os.kill(p.pid, SIGTERM) | ||
| 70 | p.wait() | ||
| 71 | except OSError: | ||
| 72 | pass | ||
| 73 | _ssh_clients = [] | ||
| 54 | 74 | ||
| 55 | class _GitCall(object): | 75 | class _GitCall(object): |
| 56 | def version(self): | 76 | def version(self): |
| @@ -119,7 +139,7 @@ class GitCommand(object): | |||
| 119 | if disable_editor: | 139 | if disable_editor: |
| 120 | env['GIT_EDITOR'] = ':' | 140 | env['GIT_EDITOR'] = ':' |
| 121 | if ssh_proxy: | 141 | if ssh_proxy: |
| 122 | env['REPO_SSH_SOCK'] = _ssh_sock() | 142 | env['REPO_SSH_SOCK'] = ssh_sock() |
| 123 | env['GIT_SSH'] = _ssh_proxy() | 143 | env['GIT_SSH'] = _ssh_proxy() |
| 124 | 144 | ||
| 125 | if project: | 145 | if project: |
| @@ -188,6 +208,9 @@ class GitCommand(object): | |||
| 188 | except Exception, e: | 208 | except Exception, e: |
| 189 | raise GitError('%s: %s' % (command[1], e)) | 209 | raise GitError('%s: %s' % (command[1], e)) |
| 190 | 210 | ||
| 211 | if ssh_proxy: | ||
| 212 | _add_ssh_client(p) | ||
| 213 | |||
| 191 | self.process = p | 214 | self.process = p |
| 192 | self.stdin = p.stdin | 215 | self.stdin = p.stdin |
| 193 | 216 | ||
| @@ -210,4 +233,8 @@ class GitCommand(object): | |||
| 210 | else: | 233 | else: |
| 211 | p.stderr = None | 234 | p.stderr = None |
| 212 | 235 | ||
| 213 | return self.process.wait() | 236 | try: |
| 237 | rc = p.wait() | ||
| 238 | finally: | ||
| 239 | _remove_ssh_client(p) | ||
| 240 | return rc | ||
diff --git a/git_config.py b/git_config.py index 4a42c047..286e89ca 100644 --- a/git_config.py +++ b/git_config.py | |||
| @@ -25,7 +25,10 @@ from signal import SIGTERM | |||
| 25 | from urllib2 import urlopen, HTTPError | 25 | from urllib2 import urlopen, HTTPError |
| 26 | from error import GitError, UploadError | 26 | from error import GitError, UploadError |
| 27 | from trace import Trace | 27 | from trace import Trace |
| 28 | from git_command import GitCommand, _ssh_sock | 28 | |
| 29 | from git_command import GitCommand | ||
| 30 | from git_command import ssh_sock | ||
| 31 | from git_command import terminate_ssh_clients | ||
| 29 | 32 | ||
| 30 | R_HEADS = 'refs/heads/' | 33 | R_HEADS = 'refs/heads/' |
| 31 | R_TAGS = 'refs/tags/' | 34 | R_TAGS = 'refs/tags/' |
| @@ -365,18 +368,21 @@ class RefSpec(object): | |||
| 365 | return s | 368 | return s |
| 366 | 369 | ||
| 367 | 370 | ||
| 368 | _ssh_cache = {} | 371 | _master_processes = [] |
| 372 | _master_keys = set() | ||
| 369 | _ssh_master = True | 373 | _ssh_master = True |
| 370 | 374 | ||
| 371 | def _open_ssh(host, port=None): | 375 | def _open_ssh(host, port=None): |
| 372 | global _ssh_master | 376 | global _ssh_master |
| 373 | 377 | ||
| 378 | # Check to see whether we already think that the master is running; if we | ||
| 379 | # think it's already running, return right away. | ||
| 374 | if port is not None: | 380 | if port is not None: |
| 375 | key = '%s:%s' % (host, port) | 381 | key = '%s:%s' % (host, port) |
| 376 | else: | 382 | else: |
| 377 | key = host | 383 | key = host |
| 378 | 384 | ||
| 379 | if key in _ssh_cache: | 385 | if key in _master_keys: |
| 380 | return True | 386 | return True |
| 381 | 387 | ||
| 382 | if not _ssh_master \ | 388 | if not _ssh_master \ |
| @@ -386,15 +392,39 @@ def _open_ssh(host, port=None): | |||
| 386 | # | 392 | # |
| 387 | return False | 393 | return False |
| 388 | 394 | ||
| 389 | command = ['ssh', | 395 | # We will make two calls to ssh; this is the common part of both calls. |
| 390 | '-o','ControlPath %s' % _ssh_sock(), | 396 | command_base = ['ssh', |
| 391 | '-M', | 397 | '-o','ControlPath %s' % ssh_sock(), |
| 392 | '-N', | 398 | host] |
| 393 | host] | ||
| 394 | |||
| 395 | if port is not None: | 399 | if port is not None: |
| 396 | command[3:3] = ['-p',str(port)] | 400 | command_base[1:1] = ['-p',str(port)] |
| 397 | 401 | ||
| 402 | # Since the key wasn't in _master_keys, we think that master isn't running. | ||
| 403 | # ...but before actually starting a master, we'll double-check. This can | ||
| 404 | # be important because we can't tell that that 'git@myhost.com' is the same | ||
| 405 | # as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file. | ||
| 406 | check_command = command_base + ['-O','check'] | ||
| 407 | try: | ||
| 408 | Trace(': %s', ' '.join(check_command)) | ||
| 409 | check_process = subprocess.Popen(check_command, | ||
| 410 | stdout=subprocess.PIPE, | ||
| 411 | stderr=subprocess.PIPE) | ||
| 412 | check_process.communicate() # read output, but ignore it... | ||
| 413 | isnt_running = check_process.wait() | ||
| 414 | |||
| 415 | if not isnt_running: | ||
| 416 | # Our double-check found that the master _was_ infact running. Add to | ||
| 417 | # the list of keys. | ||
| 418 | _master_keys.add(key) | ||
| 419 | return True | ||
| 420 | except Exception: | ||
| 421 | # Ignore excpetions. We we will fall back to the normal command and print | ||
| 422 | # to the log there. | ||
| 423 | pass | ||
| 424 | |||
| 425 | command = command_base[:1] + \ | ||
| 426 | ['-M', '-N'] + \ | ||
| 427 | command_base[1:] | ||
| 398 | try: | 428 | try: |
| 399 | Trace(': %s', ' '.join(command)) | 429 | Trace(': %s', ' '.join(command)) |
| 400 | p = subprocess.Popen(command) | 430 | p = subprocess.Popen(command) |
| @@ -405,20 +435,24 @@ def _open_ssh(host, port=None): | |||
| 405 | % (host,port, str(e)) | 435 | % (host,port, str(e)) |
| 406 | return False | 436 | return False |
| 407 | 437 | ||
| 408 | _ssh_cache[key] = p | 438 | _master_processes.append(p) |
| 439 | _master_keys.add(key) | ||
| 409 | time.sleep(1) | 440 | time.sleep(1) |
| 410 | return True | 441 | return True |
| 411 | 442 | ||
| 412 | def close_ssh(): | 443 | def close_ssh(): |
| 413 | for key,p in _ssh_cache.iteritems(): | 444 | terminate_ssh_clients() |
| 445 | |||
| 446 | for p in _master_processes: | ||
| 414 | try: | 447 | try: |
| 415 | os.kill(p.pid, SIGTERM) | 448 | os.kill(p.pid, SIGTERM) |
| 416 | p.wait() | 449 | p.wait() |
| 417 | except OSError: | 450 | except OSError: |
| 418 | pass | 451 | pass |
| 419 | _ssh_cache.clear() | 452 | del _master_processes[:] |
| 453 | _master_keys.clear() | ||
| 420 | 454 | ||
| 421 | d = _ssh_sock(create=False) | 455 | d = ssh_sock(create=False) |
| 422 | if d: | 456 | if d: |
| 423 | try: | 457 | try: |
| 424 | os.rmdir(os.path.dirname(d)) | 458 | os.rmdir(os.path.dirname(d)) |
| @@ -540,8 +574,11 @@ class Remote(object): | |||
| 540 | def SshReviewUrl(self, userEmail): | 574 | def SshReviewUrl(self, userEmail): |
| 541 | if self.ReviewProtocol != 'ssh': | 575 | if self.ReviewProtocol != 'ssh': |
| 542 | return None | 576 | return None |
| 577 | username = self._config.GetString('review.%s.username' % self.review) | ||
| 578 | if username is None: | ||
| 579 | username = userEmail.split("@")[0] | ||
| 543 | return 'ssh://%s@%s:%s/%s' % ( | 580 | return 'ssh://%s@%s:%s/%s' % ( |
| 544 | userEmail.split("@")[0], | 581 | username, |
| 545 | self._review_host, | 582 | self._review_host, |
| 546 | self._review_port, | 583 | self._review_port, |
| 547 | self.projectname) | 584 | self.projectname) |
| @@ -1,2 +1,2 @@ | |||
| 1 | #!/bin/sh | 1 | #!/bin/sh |
| 2 | exec ssh -o "ControlPath $REPO_SSH_SOCK" "$@" | 2 | exec ssh -o "ControlMaster no" -o "ControlPath $REPO_SSH_SOCK" "$@" |
diff --git a/manifest.py b/manifest.py index f737e866..c03cb4a7 100644 --- a/manifest.py +++ b/manifest.py | |||
| @@ -41,6 +41,14 @@ class Manifest(object): | |||
| 41 | def projects(self): | 41 | def projects(self): |
| 42 | return {} | 42 | return {} |
| 43 | 43 | ||
| 44 | @property | ||
| 45 | def notice(self): | ||
| 46 | return None | ||
| 47 | |||
| 48 | @property | ||
| 49 | def manifest_server(self): | ||
| 50 | return None | ||
| 51 | |||
| 44 | def InitBranch(self): | 52 | def InitBranch(self): |
| 45 | pass | 53 | pass |
| 46 | 54 | ||
diff --git a/manifest_submodule.py b/manifest_submodule.py index 92f187a0..cac271cd 100644 --- a/manifest_submodule.py +++ b/manifest_submodule.py | |||
| @@ -102,6 +102,10 @@ class SubmoduleManifest(Manifest): | |||
| 102 | self._Load() | 102 | self._Load() |
| 103 | return self._projects | 103 | return self._projects |
| 104 | 104 | ||
| 105 | @property | ||
| 106 | def notice(self): | ||
| 107 | return self._modules.GetString('repo.notice') | ||
| 108 | |||
| 105 | def InitBranch(self): | 109 | def InitBranch(self): |
| 106 | m = self.manifestProject | 110 | m = self.manifestProject |
| 107 | if m.CurrentBranch is None: | 111 | if m.CurrentBranch is None: |
| @@ -266,6 +270,9 @@ class SubmoduleManifest(Manifest): | |||
| 266 | if b.startswith(R_HEADS): | 270 | if b.startswith(R_HEADS): |
| 267 | b = b[len(R_HEADS):] | 271 | b = b[len(R_HEADS):] |
| 268 | 272 | ||
| 273 | if old.notice: | ||
| 274 | gm.SetString('repo.notice', old.notice) | ||
| 275 | |||
| 269 | info = [] | 276 | info = [] |
| 270 | pm = Progress('Converting manifest', len(sort_projects)) | 277 | pm = Progress('Converting manifest', len(sort_projects)) |
| 271 | for p in sort_projects: | 278 | for p in sort_projects: |
diff --git a/manifest_xml.py b/manifest_xml.py index 35318d0a..1d02f9d4 100644 --- a/manifest_xml.py +++ b/manifest_xml.py | |||
| @@ -66,8 +66,8 @@ class XmlManifest(Manifest): | |||
| 66 | 66 | ||
| 67 | self._Unload() | 67 | self._Unload() |
| 68 | 68 | ||
| 69 | def Link(self, name): | 69 | def Override(self, name): |
| 70 | """Update the repo metadata to use a different manifest. | 70 | """Use a different manifest, just for the current instantiation. |
| 71 | """ | 71 | """ |
| 72 | path = os.path.join(self.manifestProject.worktree, name) | 72 | path = os.path.join(self.manifestProject.worktree, name) |
| 73 | if not os.path.isfile(path): | 73 | if not os.path.isfile(path): |
| @@ -81,6 +81,11 @@ class XmlManifest(Manifest): | |||
| 81 | finally: | 81 | finally: |
| 82 | self._manifestFile = old | 82 | self._manifestFile = old |
| 83 | 83 | ||
| 84 | def Link(self, name): | ||
| 85 | """Update the repo metadata to use a different manifest. | ||
| 86 | """ | ||
| 87 | self.Override(name) | ||
| 88 | |||
| 84 | try: | 89 | try: |
| 85 | if os.path.exists(self._manifestFile): | 90 | if os.path.exists(self._manifestFile): |
| 86 | os.remove(self._manifestFile) | 91 | os.remove(self._manifestFile) |
| @@ -103,6 +108,15 @@ class XmlManifest(Manifest): | |||
| 103 | root = doc.createElement('manifest') | 108 | root = doc.createElement('manifest') |
| 104 | doc.appendChild(root) | 109 | doc.appendChild(root) |
| 105 | 110 | ||
| 111 | # Save out the notice. There's a little bit of work here to give it the | ||
| 112 | # right whitespace, which assumes that the notice is automatically indented | ||
| 113 | # by 4 by minidom. | ||
| 114 | if self.notice: | ||
| 115 | notice_element = root.appendChild(doc.createElement('notice')) | ||
| 116 | notice_lines = self.notice.splitlines() | ||
| 117 | indented_notice = ('\n'.join(" "*4 + line for line in notice_lines))[4:] | ||
| 118 | notice_element.appendChild(doc.createTextNode(indented_notice)) | ||
| 119 | |||
| 106 | d = self.default | 120 | d = self.default |
| 107 | sort_remotes = list(self.remotes.keys()) | 121 | sort_remotes = list(self.remotes.keys()) |
| 108 | sort_remotes.sort() | 122 | sort_remotes.sort() |
| @@ -124,6 +138,12 @@ class XmlManifest(Manifest): | |||
| 124 | root.appendChild(e) | 138 | root.appendChild(e) |
| 125 | root.appendChild(doc.createTextNode('')) | 139 | root.appendChild(doc.createTextNode('')) |
| 126 | 140 | ||
| 141 | if self._manifest_server: | ||
| 142 | e = doc.createElement('manifest-server') | ||
| 143 | e.setAttribute('url', self._manifest_server) | ||
| 144 | root.appendChild(e) | ||
| 145 | root.appendChild(doc.createTextNode('')) | ||
| 146 | |||
| 127 | sort_projects = list(self.projects.keys()) | 147 | sort_projects = list(self.projects.keys()) |
| 128 | sort_projects.sort() | 148 | sort_projects.sort() |
| 129 | 149 | ||
| @@ -169,6 +189,16 @@ class XmlManifest(Manifest): | |||
| 169 | self._Load() | 189 | self._Load() |
| 170 | return self._default | 190 | return self._default |
| 171 | 191 | ||
| 192 | @property | ||
| 193 | def notice(self): | ||
| 194 | self._Load() | ||
| 195 | return self._notice | ||
| 196 | |||
| 197 | @property | ||
| 198 | def manifest_server(self): | ||
| 199 | self._Load() | ||
| 200 | return self._manifest_server | ||
| 201 | |||
| 172 | def InitBranch(self): | 202 | def InitBranch(self): |
| 173 | m = self.manifestProject | 203 | m = self.manifestProject |
| 174 | if m.CurrentBranch is None: | 204 | if m.CurrentBranch is None: |
| @@ -184,7 +214,9 @@ class XmlManifest(Manifest): | |||
| 184 | self._projects = {} | 214 | self._projects = {} |
| 185 | self._remotes = {} | 215 | self._remotes = {} |
| 186 | self._default = None | 216 | self._default = None |
| 217 | self._notice = None | ||
| 187 | self.branch = None | 218 | self.branch = None |
| 219 | self._manifest_server = None | ||
| 188 | 220 | ||
| 189 | def _Load(self): | 221 | def _Load(self): |
| 190 | if not self._loaded: | 222 | if not self._loaded: |
| @@ -257,6 +289,23 @@ class XmlManifest(Manifest): | |||
| 257 | self._default = _Default() | 289 | self._default = _Default() |
| 258 | 290 | ||
| 259 | for node in config.childNodes: | 291 | for node in config.childNodes: |
| 292 | if node.nodeName == 'notice': | ||
| 293 | if self._notice is not None: | ||
| 294 | raise ManifestParseError, \ | ||
| 295 | 'duplicate notice in %s' % \ | ||
| 296 | (self.manifestFile) | ||
| 297 | self._notice = self._ParseNotice(node) | ||
| 298 | |||
| 299 | for node in config.childNodes: | ||
| 300 | if node.nodeName == 'manifest-server': | ||
| 301 | url = self._reqatt(node, 'url') | ||
| 302 | if self._manifest_server is not None: | ||
| 303 | raise ManifestParseError, \ | ||
| 304 | 'duplicate manifest-server in %s' % \ | ||
| 305 | (self.manifestFile) | ||
| 306 | self._manifest_server = url | ||
| 307 | |||
| 308 | for node in config.childNodes: | ||
| 260 | if node.nodeName == 'project': | 309 | if node.nodeName == 'project': |
| 261 | project = self._ParseProject(node) | 310 | project = self._ParseProject(node) |
| 262 | if self._projects.get(project.name): | 311 | if self._projects.get(project.name): |
| @@ -322,10 +371,49 @@ class XmlManifest(Manifest): | |||
| 322 | d.revisionExpr = None | 371 | d.revisionExpr = None |
| 323 | return d | 372 | return d |
| 324 | 373 | ||
| 374 | def _ParseNotice(self, node): | ||
| 375 | """ | ||
| 376 | reads a <notice> element from the manifest file | ||
| 377 | |||
| 378 | The <notice> element is distinct from other tags in the XML in that the | ||
| 379 | data is conveyed between the start and end tag (it's not an empty-element | ||
| 380 | tag). | ||
| 381 | |||
| 382 | The white space (carriage returns, indentation) for the notice element is | ||
| 383 | relevant and is parsed in a way that is based on how python docstrings work. | ||
| 384 | In fact, the code is remarkably similar to here: | ||
| 385 | http://www.python.org/dev/peps/pep-0257/ | ||
| 386 | """ | ||
| 387 | # Get the data out of the node... | ||
| 388 | notice = node.childNodes[0].data | ||
| 389 | |||
| 390 | # Figure out minimum indentation, skipping the first line (the same line | ||
| 391 | # as the <notice> tag)... | ||
| 392 | minIndent = sys.maxint | ||
| 393 | lines = notice.splitlines() | ||
| 394 | for line in lines[1:]: | ||
| 395 | lstrippedLine = line.lstrip() | ||
| 396 | if lstrippedLine: | ||
| 397 | indent = len(line) - len(lstrippedLine) | ||
| 398 | minIndent = min(indent, minIndent) | ||
| 399 | |||
| 400 | # Strip leading / trailing blank lines and also indentation. | ||
| 401 | cleanLines = [lines[0].strip()] | ||
| 402 | for line in lines[1:]: | ||
| 403 | cleanLines.append(line[minIndent:].rstrip()) | ||
| 404 | |||
| 405 | # Clear completely blank lines from front and back... | ||
| 406 | while cleanLines and not cleanLines[0]: | ||
| 407 | del cleanLines[0] | ||
| 408 | while cleanLines and not cleanLines[-1]: | ||
| 409 | del cleanLines[-1] | ||
| 410 | |||
| 411 | return '\n'.join(cleanLines) | ||
| 412 | |||
| 325 | def _ParseProject(self, node): | 413 | def _ParseProject(self, node): |
| 326 | """ | 414 | """ |
| 327 | reads a <project> element from the manifest file | 415 | reads a <project> element from the manifest file |
| 328 | """ | 416 | """ |
| 329 | name = self._reqatt(node, 'name') | 417 | name = self._reqatt(node, 'name') |
| 330 | 418 | ||
| 331 | remote = self._get_remote(node) | 419 | remote = self._get_remote(node) |
diff --git a/progress.py b/progress.py index b119b374..2ace7010 100644 --- a/progress.py +++ b/progress.py | |||
| @@ -13,10 +13,13 @@ | |||
| 13 | # See the License for the specific language governing permissions and | 13 | # See the License for the specific language governing permissions and |
| 14 | # limitations under the License. | 14 | # limitations under the License. |
| 15 | 15 | ||
| 16 | import os | ||
| 16 | import sys | 17 | import sys |
| 17 | from time import time | 18 | from time import time |
| 18 | from trace import IsTrace | 19 | from trace import IsTrace |
| 19 | 20 | ||
| 21 | _NOT_TTY = not os.isatty(2) | ||
| 22 | |||
| 20 | class Progress(object): | 23 | class Progress(object): |
| 21 | def __init__(self, title, total=0): | 24 | def __init__(self, title, total=0): |
| 22 | self._title = title | 25 | self._title = title |
| @@ -29,7 +32,7 @@ class Progress(object): | |||
| 29 | def update(self, inc=1): | 32 | def update(self, inc=1): |
| 30 | self._done += inc | 33 | self._done += inc |
| 31 | 34 | ||
| 32 | if IsTrace(): | 35 | if _NOT_TTY or IsTrace(): |
| 33 | return | 36 | return |
| 34 | 37 | ||
| 35 | if not self._show: | 38 | if not self._show: |
| @@ -56,7 +59,7 @@ class Progress(object): | |||
| 56 | sys.stderr.flush() | 59 | sys.stderr.flush() |
| 57 | 60 | ||
| 58 | def end(self): | 61 | def end(self): |
| 59 | if IsTrace() or not self._show: | 62 | if _NOT_TTY or IsTrace() or not self._show: |
| 60 | return | 63 | return |
| 61 | 64 | ||
| 62 | if self._total <= 0: | 65 | if self._total <= 0: |
| @@ -111,7 +111,6 @@ class ReviewableBranch(object): | |||
| 111 | self.project = project | 111 | self.project = project |
| 112 | self.branch = branch | 112 | self.branch = branch |
| 113 | self.base = base | 113 | self.base = base |
| 114 | self.replace_changes = None | ||
| 115 | 114 | ||
| 116 | @property | 115 | @property |
| 117 | def name(self): | 116 | def name(self): |
| @@ -149,10 +148,10 @@ class ReviewableBranch(object): | |||
| 149 | R_HEADS + self.name, | 148 | R_HEADS + self.name, |
| 150 | '--') | 149 | '--') |
| 151 | 150 | ||
| 152 | def UploadForReview(self, people): | 151 | def UploadForReview(self, people, auto_topic=False): |
| 153 | self.project.UploadForReview(self.name, | 152 | self.project.UploadForReview(self.name, |
| 154 | self.replace_changes, | 153 | people, |
| 155 | people) | 154 | auto_topic=auto_topic) |
| 156 | 155 | ||
| 157 | def GetPublishedRefs(self): | 156 | def GetPublishedRefs(self): |
| 158 | refs = {} | 157 | refs = {} |
| @@ -203,6 +202,10 @@ class _CopyFile: | |||
| 203 | # remove existing file first, since it might be read-only | 202 | # remove existing file first, since it might be read-only |
| 204 | if os.path.exists(dest): | 203 | if os.path.exists(dest): |
| 205 | os.remove(dest) | 204 | os.remove(dest) |
| 205 | else: | ||
| 206 | dir = os.path.dirname(dest) | ||
| 207 | if not os.path.isdir(dir): | ||
| 208 | os.makedirs(dir) | ||
| 206 | shutil.copy(src, dest) | 209 | shutil.copy(src, dest) |
| 207 | # make the file read-only | 210 | # make the file read-only |
| 208 | mode = os.stat(dest)[stat.ST_MODE] | 211 | mode = os.stat(dest)[stat.ST_MODE] |
| @@ -279,7 +282,7 @@ class Project(object): | |||
| 279 | return os.path.exists(os.path.join(g, 'rebase-apply')) \ | 282 | return os.path.exists(os.path.join(g, 'rebase-apply')) \ |
| 280 | or os.path.exists(os.path.join(g, 'rebase-merge')) \ | 283 | or os.path.exists(os.path.join(g, 'rebase-merge')) \ |
| 281 | or os.path.exists(os.path.join(w, '.dotest')) | 284 | or os.path.exists(os.path.join(w, '.dotest')) |
| 282 | 285 | ||
| 283 | def IsDirty(self, consider_untracked=True): | 286 | def IsDirty(self, consider_untracked=True): |
| 284 | """Is the working directory modified in some way? | 287 | """Is the working directory modified in some way? |
| 285 | """ | 288 | """ |
| @@ -364,6 +367,27 @@ class Project(object): | |||
| 364 | 367 | ||
| 365 | ## Status Display ## | 368 | ## Status Display ## |
| 366 | 369 | ||
| 370 | def HasChanges(self): | ||
| 371 | """Returns true if there are uncommitted changes. | ||
| 372 | """ | ||
| 373 | self.work_git.update_index('-q', | ||
| 374 | '--unmerged', | ||
| 375 | '--ignore-missing', | ||
| 376 | '--refresh') | ||
| 377 | if self.IsRebaseInProgress(): | ||
| 378 | return True | ||
| 379 | |||
| 380 | if self.work_git.DiffZ('diff-index', '--cached', HEAD): | ||
| 381 | return True | ||
| 382 | |||
| 383 | if self.work_git.DiffZ('diff-files'): | ||
| 384 | return True | ||
| 385 | |||
| 386 | if self.work_git.LsOthers(): | ||
| 387 | return True | ||
| 388 | |||
| 389 | return False | ||
| 390 | |||
| 367 | def PrintWorkTreeStatus(self): | 391 | def PrintWorkTreeStatus(self): |
| 368 | """Prints the status of the repository to stdout. | 392 | """Prints the status of the repository to stdout. |
| 369 | """ | 393 | """ |
| @@ -412,7 +436,7 @@ class Project(object): | |||
| 412 | 436 | ||
| 413 | try: f = df[p] | 437 | try: f = df[p] |
| 414 | except KeyError: f = None | 438 | except KeyError: f = None |
| 415 | 439 | ||
| 416 | if i: i_status = i.status.upper() | 440 | if i: i_status = i.status.upper() |
| 417 | else: i_status = '-' | 441 | else: i_status = '-' |
| 418 | 442 | ||
| @@ -530,7 +554,9 @@ class Project(object): | |||
| 530 | return rb | 554 | return rb |
| 531 | return None | 555 | return None |
| 532 | 556 | ||
| 533 | def UploadForReview(self, branch=None, replace_changes=None, people=([],[])): | 557 | def UploadForReview(self, branch=None, |
| 558 | people=([],[]), | ||
| 559 | auto_topic=False): | ||
| 534 | """Uploads the named branch for code review. | 560 | """Uploads the named branch for code review. |
| 535 | """ | 561 | """ |
| 536 | if branch is None: | 562 | if branch is None: |
| @@ -562,13 +588,15 @@ class Project(object): | |||
| 562 | for e in people[1]: | 588 | for e in people[1]: |
| 563 | rp.append('--cc=%s' % sq(e)) | 589 | rp.append('--cc=%s' % sq(e)) |
| 564 | 590 | ||
| 591 | ref_spec = '%s:refs/for/%s' % (R_HEADS + branch.name, dest_branch) | ||
| 592 | if auto_topic: | ||
| 593 | ref_spec = ref_spec + '/' + branch.name | ||
| 594 | |||
| 565 | cmd = ['push'] | 595 | cmd = ['push'] |
| 566 | cmd.append('--receive-pack=%s' % " ".join(rp)) | 596 | cmd.append('--receive-pack=%s' % " ".join(rp)) |
| 567 | cmd.append(branch.remote.SshReviewUrl(self.UserEmail)) | 597 | cmd.append(branch.remote.SshReviewUrl(self.UserEmail)) |
| 568 | cmd.append('%s:refs/for/%s' % (R_HEADS + branch.name, dest_branch)) | 598 | cmd.append(ref_spec) |
| 569 | if replace_changes: | 599 | |
| 570 | for change_id,commit_id in replace_changes.iteritems(): | ||
| 571 | cmd.append('%s:refs/changes/%s/new' % (commit_id, change_id)) | ||
| 572 | if GitCommand(self, cmd, bare = True).Wait() != 0: | 600 | if GitCommand(self, cmd, bare = True).Wait() != 0: |
| 573 | raise UploadError('Upload failed') | 601 | raise UploadError('Upload failed') |
| 574 | 602 | ||
| @@ -584,19 +612,33 @@ class Project(object): | |||
| 584 | 612 | ||
| 585 | ## Sync ## | 613 | ## Sync ## |
| 586 | 614 | ||
| 587 | def Sync_NetworkHalf(self): | 615 | def Sync_NetworkHalf(self, quiet=False): |
| 588 | """Perform only the network IO portion of the sync process. | 616 | """Perform only the network IO portion of the sync process. |
| 589 | Local working directory/branch state is not affected. | 617 | Local working directory/branch state is not affected. |
| 590 | """ | 618 | """ |
| 591 | if not self.Exists: | 619 | is_new = not self.Exists |
| 592 | print >>sys.stderr | 620 | if is_new: |
| 593 | print >>sys.stderr, 'Initializing project %s ...' % self.name | 621 | if not quiet: |
| 622 | print >>sys.stderr | ||
| 623 | print >>sys.stderr, 'Initializing project %s ...' % self.name | ||
| 594 | self._InitGitDir() | 624 | self._InitGitDir() |
| 595 | 625 | ||
| 596 | self._InitRemote() | 626 | self._InitRemote() |
| 597 | if not self._RemoteFetch(): | 627 | if not self._RemoteFetch(initial=is_new, quiet=quiet): |
| 598 | return False | 628 | return False |
| 599 | 629 | ||
| 630 | #Check that the requested ref was found after fetch | ||
| 631 | # | ||
| 632 | try: | ||
| 633 | self.GetRevisionId() | ||
| 634 | except ManifestInvalidRevisionError: | ||
| 635 | # if the ref is a tag. We can try fetching | ||
| 636 | # the tag manually as a last resort | ||
| 637 | # | ||
| 638 | rev = self.revisionExpr | ||
| 639 | if rev.startswith(R_TAGS): | ||
| 640 | self._RemoteFetch(None, rev[len(R_TAGS):], quiet=quiet) | ||
| 641 | |||
| 600 | if self.worktree: | 642 | if self.worktree: |
| 601 | self.manifest.SetMRefs(self) | 643 | self.manifest.SetMRefs(self) |
| 602 | else: | 644 | else: |
| @@ -978,7 +1020,9 @@ class Project(object): | |||
| 978 | 1020 | ||
| 979 | ## Direct Git Commands ## | 1021 | ## Direct Git Commands ## |
| 980 | 1022 | ||
| 981 | def _RemoteFetch(self, name=None): | 1023 | def _RemoteFetch(self, name=None, tag=None, |
| 1024 | initial=False, | ||
| 1025 | quiet=False): | ||
| 982 | if not name: | 1026 | if not name: |
| 983 | name = self.remote.name | 1027 | name = self.remote.name |
| 984 | 1028 | ||
| @@ -986,14 +1030,84 @@ class Project(object): | |||
| 986 | if self.GetRemote(name).PreConnectFetch(): | 1030 | if self.GetRemote(name).PreConnectFetch(): |
| 987 | ssh_proxy = True | 1031 | ssh_proxy = True |
| 988 | 1032 | ||
| 1033 | if initial: | ||
| 1034 | alt = os.path.join(self.gitdir, 'objects/info/alternates') | ||
| 1035 | try: | ||
| 1036 | fd = open(alt, 'rb') | ||
| 1037 | try: | ||
| 1038 | ref_dir = fd.readline() | ||
| 1039 | if ref_dir and ref_dir.endswith('\n'): | ||
| 1040 | ref_dir = ref_dir[:-1] | ||
| 1041 | finally: | ||
| 1042 | fd.close() | ||
| 1043 | except IOError, e: | ||
| 1044 | ref_dir = None | ||
| 1045 | |||
| 1046 | if ref_dir and 'objects' == os.path.basename(ref_dir): | ||
| 1047 | ref_dir = os.path.dirname(ref_dir) | ||
| 1048 | packed_refs = os.path.join(self.gitdir, 'packed-refs') | ||
| 1049 | remote = self.GetRemote(name) | ||
| 1050 | |||
| 1051 | all = self.bare_ref.all | ||
| 1052 | ids = set(all.values()) | ||
| 1053 | tmp = set() | ||
| 1054 | |||
| 1055 | for r, id in GitRefs(ref_dir).all.iteritems(): | ||
| 1056 | if r not in all: | ||
| 1057 | if r.startswith(R_TAGS) or remote.WritesTo(r): | ||
| 1058 | all[r] = id | ||
| 1059 | ids.add(id) | ||
| 1060 | continue | ||
| 1061 | |||
| 1062 | if id in ids: | ||
| 1063 | continue | ||
| 1064 | |||
| 1065 | r = 'refs/_alt/%s' % id | ||
| 1066 | all[r] = id | ||
| 1067 | ids.add(id) | ||
| 1068 | tmp.add(r) | ||
| 1069 | |||
| 1070 | ref_names = list(all.keys()) | ||
| 1071 | ref_names.sort() | ||
| 1072 | |||
| 1073 | tmp_packed = '' | ||
| 1074 | old_packed = '' | ||
| 1075 | |||
| 1076 | for r in ref_names: | ||
| 1077 | line = '%s %s\n' % (all[r], r) | ||
| 1078 | tmp_packed += line | ||
| 1079 | if r not in tmp: | ||
| 1080 | old_packed += line | ||
| 1081 | |||
| 1082 | _lwrite(packed_refs, tmp_packed) | ||
| 1083 | |||
| 1084 | else: | ||
| 1085 | ref_dir = None | ||
| 1086 | |||
| 989 | cmd = ['fetch'] | 1087 | cmd = ['fetch'] |
| 1088 | if quiet: | ||
| 1089 | cmd.append('--quiet') | ||
| 990 | if not self.worktree: | 1090 | if not self.worktree: |
| 991 | cmd.append('--update-head-ok') | 1091 | cmd.append('--update-head-ok') |
| 992 | cmd.append(name) | 1092 | cmd.append(name) |
| 993 | return GitCommand(self, | 1093 | if tag is not None: |
| 994 | cmd, | 1094 | cmd.append('tag') |
| 995 | bare = True, | 1095 | cmd.append(tag) |
| 996 | ssh_proxy = ssh_proxy).Wait() == 0 | 1096 | |
| 1097 | ok = GitCommand(self, | ||
| 1098 | cmd, | ||
| 1099 | bare = True, | ||
| 1100 | ssh_proxy = ssh_proxy).Wait() == 0 | ||
| 1101 | |||
| 1102 | if initial: | ||
| 1103 | if ref_dir: | ||
| 1104 | if old_packed != '': | ||
| 1105 | _lwrite(packed_refs, old_packed) | ||
| 1106 | else: | ||
| 1107 | os.remove(packed_refs) | ||
| 1108 | self.bare_git.pack_refs('--all', '--prune') | ||
| 1109 | |||
| 1110 | return ok | ||
| 997 | 1111 | ||
| 998 | def _Checkout(self, rev, quiet=False): | 1112 | def _Checkout(self, rev, quiet=False): |
| 999 | cmd = ['checkout'] | 1113 | cmd = ['checkout'] |
| @@ -1031,6 +1145,27 @@ class Project(object): | |||
| 1031 | os.makedirs(self.gitdir) | 1145 | os.makedirs(self.gitdir) |
| 1032 | self.bare_git.init() | 1146 | self.bare_git.init() |
| 1033 | 1147 | ||
| 1148 | mp = self.manifest.manifestProject | ||
| 1149 | ref_dir = mp.config.GetString('repo.reference') | ||
| 1150 | |||
| 1151 | if ref_dir: | ||
| 1152 | mirror_git = os.path.join(ref_dir, self.name + '.git') | ||
| 1153 | repo_git = os.path.join(ref_dir, '.repo', 'projects', | ||
| 1154 | self.relpath + '.git') | ||
| 1155 | |||
| 1156 | if os.path.exists(mirror_git): | ||
| 1157 | ref_dir = mirror_git | ||
| 1158 | |||
| 1159 | elif os.path.exists(repo_git): | ||
| 1160 | ref_dir = repo_git | ||
| 1161 | |||
| 1162 | else: | ||
| 1163 | ref_dir = None | ||
| 1164 | |||
| 1165 | if ref_dir: | ||
| 1166 | _lwrite(os.path.join(self.gitdir, 'objects/info/alternates'), | ||
| 1167 | os.path.join(ref_dir, 'objects') + '\n') | ||
| 1168 | |||
| 1034 | if self.manifest.IsMirror: | 1169 | if self.manifest.IsMirror: |
| 1035 | self.config.SetString('core.bare', 'true') | 1170 | self.config.SetString('core.bare', 'true') |
| 1036 | else: | 1171 | else: |
| @@ -123,6 +123,9 @@ group.add_option('-m', '--manifest-name', | |||
| 123 | group.add_option('--mirror', | 123 | group.add_option('--mirror', |
| 124 | dest='mirror', action='store_true', | 124 | dest='mirror', action='store_true', |
| 125 | help='mirror the forrest') | 125 | help='mirror the forrest') |
| 126 | group.add_option('--reference', | ||
| 127 | dest='reference', | ||
| 128 | help='location of mirror directory', metavar='DIR') | ||
| 126 | 129 | ||
| 127 | # Tool | 130 | # Tool |
| 128 | group = init_optparse.add_option_group('repo Version options') | 131 | group = init_optparse.add_option_group('repo Version options') |
diff --git a/subcmds/branches.py b/subcmds/branches.py index 0e3ab3c2..a4f8d360 100644 --- a/subcmds/branches.py +++ b/subcmds/branches.py | |||
| @@ -136,7 +136,7 @@ is shown, then the branch appears in all projects. | |||
| 136 | hdr('%c%c %-*s' % (current, published, width, name)) | 136 | hdr('%c%c %-*s' % (current, published, width, name)) |
| 137 | out.write(' |') | 137 | out.write(' |') |
| 138 | 138 | ||
| 139 | if in_cnt < project_cnt and (in_cnt == 1): | 139 | if in_cnt < project_cnt: |
| 140 | fmt = out.write | 140 | fmt = out.write |
| 141 | paths = [] | 141 | paths = [] |
| 142 | if in_cnt < project_cnt - in_cnt: | 142 | if in_cnt < project_cnt - in_cnt: |
| @@ -150,15 +150,17 @@ is shown, then the branch appears in all projects. | |||
| 150 | for b in i.projects: | 150 | for b in i.projects: |
| 151 | have.add(b.project) | 151 | have.add(b.project) |
| 152 | for p in projects: | 152 | for p in projects: |
| 153 | paths.append(p.relpath) | 153 | if not p in have: |
| 154 | paths.append(p.relpath) | ||
| 154 | 155 | ||
| 155 | s = ' %s %s' % (type, ', '.join(paths)) | 156 | s = ' %s %s' % (type, ', '.join(paths)) |
| 156 | if width + 7 + len(s) < 80: | 157 | if width + 7 + len(s) < 80: |
| 157 | fmt(s) | 158 | fmt(s) |
| 158 | else: | 159 | else: |
| 159 | out.nl() | 160 | fmt(' %s:' % type) |
| 160 | fmt(' %s:' % type) | ||
| 161 | for p in paths: | 161 | for p in paths: |
| 162 | out.nl() | 162 | out.nl() |
| 163 | fmt(' %s' % p) | 163 | fmt(width*' ' + ' %s' % p) |
| 164 | else: | ||
| 165 | out.write(' in all projects') | ||
| 164 | out.nl() | 166 | out.nl() |
diff --git a/subcmds/grep.py b/subcmds/grep.py index 4f714271..1cb5650b 100644 --- a/subcmds/grep.py +++ b/subcmds/grep.py | |||
| @@ -204,7 +204,7 @@ contain a line that matches both expressions: | |||
| 204 | else: | 204 | else: |
| 205 | out.project('--- project %s ---' % project.relpath) | 205 | out.project('--- project %s ---' % project.relpath) |
| 206 | out.nl() | 206 | out.nl() |
| 207 | out.write(p.stderr) | 207 | out.write("%s", p.stderr) |
| 208 | out.nl() | 208 | out.nl() |
| 209 | continue | 209 | continue |
| 210 | have_match = True | 210 | have_match = True |
| @@ -217,17 +217,17 @@ contain a line that matches both expressions: | |||
| 217 | if have_rev and full_name: | 217 | if have_rev and full_name: |
| 218 | for line in r: | 218 | for line in r: |
| 219 | rev, line = line.split(':', 1) | 219 | rev, line = line.split(':', 1) |
| 220 | out.write(rev) | 220 | out.write("%s", rev) |
| 221 | out.write(':') | 221 | out.write(':') |
| 222 | out.project(project.relpath) | 222 | out.project(project.relpath) |
| 223 | out.write('/') | 223 | out.write('/') |
| 224 | out.write(line) | 224 | out.write("%s", line) |
| 225 | out.nl() | 225 | out.nl() |
| 226 | elif full_name: | 226 | elif full_name: |
| 227 | for line in r: | 227 | for line in r: |
| 228 | out.project(project.relpath) | 228 | out.project(project.relpath) |
| 229 | out.write('/') | 229 | out.write('/') |
| 230 | out.write(line) | 230 | out.write("%s", line) |
| 231 | out.nl() | 231 | out.nl() |
| 232 | else: | 232 | else: |
| 233 | for line in r: | 233 | for line in r: |
diff --git a/subcmds/init.py b/subcmds/init.py index cdbbfdf7..2ca4e163 100644 --- a/subcmds/init.py +++ b/subcmds/init.py | |||
| @@ -40,6 +40,17 @@ current working directory. | |||
| 40 | The optional -b argument can be used to select the manifest branch | 40 | The optional -b argument can be used to select the manifest branch |
| 41 | to checkout and use. If no branch is specified, master is assumed. | 41 | to checkout and use. If no branch is specified, master is assumed. |
| 42 | 42 | ||
| 43 | The optional -m argument can be used to specify an alternate manifest | ||
| 44 | to be used. If no manifest is specified, the manifest default.xml | ||
| 45 | will be used. | ||
| 46 | |||
| 47 | The --reference option can be used to point to a directory that | ||
| 48 | has the content of a --mirror sync. This will make the working | ||
| 49 | directory use as much data as possible from the local reference | ||
| 50 | directory when fetching from the server. This will make the sync | ||
| 51 | go a lot faster by reducing data traffic on the network. | ||
| 52 | |||
| 53 | |||
| 43 | Switching Manifest Branches | 54 | Switching Manifest Branches |
| 44 | --------------------------- | 55 | --------------------------- |
| 45 | 56 | ||
| @@ -76,7 +87,9 @@ to update the working directory files. | |||
| 76 | g.add_option('--mirror', | 87 | g.add_option('--mirror', |
| 77 | dest='mirror', action='store_true', | 88 | dest='mirror', action='store_true', |
| 78 | help='mirror the forrest') | 89 | help='mirror the forrest') |
| 79 | 90 | g.add_option('--reference', | |
| 91 | dest='reference', | ||
| 92 | help='location of mirror directory', metavar='DIR') | ||
| 80 | 93 | ||
| 81 | # Tool | 94 | # Tool |
| 82 | g = p.add_option_group('repo Version options') | 95 | g = p.add_option_group('repo Version options') |
| @@ -132,6 +145,9 @@ to update the working directory files. | |||
| 132 | r.ResetFetch() | 145 | r.ResetFetch() |
| 133 | r.Save() | 146 | r.Save() |
| 134 | 147 | ||
| 148 | if opt.reference: | ||
| 149 | m.config.SetString('repo.reference', opt.reference) | ||
| 150 | |||
| 135 | if opt.mirror: | 151 | if opt.mirror: |
| 136 | if is_new: | 152 | if is_new: |
| 137 | m.config.SetString('repo.mirror', 'true') | 153 | m.config.SetString('repo.mirror', 'true') |
| @@ -162,7 +178,11 @@ to update the working directory files. | |||
| 162 | syncbuf = SyncBuffer(m.config) | 178 | syncbuf = SyncBuffer(m.config) |
| 163 | m.Sync_LocalHalf(syncbuf) | 179 | m.Sync_LocalHalf(syncbuf) |
| 164 | syncbuf.Finish() | 180 | syncbuf.Finish() |
| 181 | |||
| 182 | if isinstance(self.manifest, XmlManifest): | ||
| 183 | self._LinkManifest(opt.manifest_name) | ||
| 165 | _ReloadManifest(self) | 184 | _ReloadManifest(self) |
| 185 | |||
| 166 | self._ApplyOptions(opt, is_new) | 186 | self._ApplyOptions(opt, is_new) |
| 167 | 187 | ||
| 168 | if not self.manifest.InitBranch(): | 188 | if not self.manifest.InitBranch(): |
| @@ -200,8 +220,9 @@ to update the working directory files. | |||
| 200 | 220 | ||
| 201 | print '' | 221 | print '' |
| 202 | print 'Your identity is: %s <%s>' % (name, email) | 222 | print 'Your identity is: %s <%s>' % (name, email) |
| 203 | sys.stdout.write('is this correct [yes/no]? ') | 223 | sys.stdout.write('is this correct [y/n]? ') |
| 204 | if 'yes' == sys.stdin.readline().strip(): | 224 | a = sys.stdin.readline().strip() |
| 225 | if a in ('yes', 'y', 't', 'true'): | ||
| 205 | break | 226 | break |
| 206 | 227 | ||
| 207 | if name != mp.UserName: | 228 | if name != mp.UserName: |
| @@ -249,8 +270,6 @@ to update the working directory files. | |||
| 249 | def Execute(self, opt, args): | 270 | def Execute(self, opt, args): |
| 250 | git_require(MIN_GIT_VERSION, fail=True) | 271 | git_require(MIN_GIT_VERSION, fail=True) |
| 251 | self._SyncManifest(opt) | 272 | self._SyncManifest(opt) |
| 252 | if isinstance(self.manifest, XmlManifest): | ||
| 253 | self._LinkManifest(opt.manifest_name) | ||
| 254 | 273 | ||
| 255 | if os.isatty(0) and os.isatty(1) and not self.manifest.IsMirror: | 274 | if os.isatty(0) and os.isatty(1) and not self.manifest.IsMirror: |
| 256 | self._ConfigureUser() | 275 | self._ConfigureUser() |
diff --git a/subcmds/rebase.py b/subcmds/rebase.py new file mode 100644 index 00000000..e341296d --- /dev/null +++ b/subcmds/rebase.py | |||
| @@ -0,0 +1,107 @@ | |||
| 1 | # | ||
| 2 | # Copyright (C) 2010 The Android Open Source Project | ||
| 3 | # | ||
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 5 | # you may not use this file except in compliance with the License. | ||
| 6 | # You may obtain a copy of the License at | ||
| 7 | # | ||
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
| 9 | # | ||
| 10 | # Unless required by applicable law or agreed to in writing, software | ||
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
| 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 13 | # See the License for the specific language governing permissions and | ||
| 14 | # limitations under the License. | ||
| 15 | |||
| 16 | import sys | ||
| 17 | |||
| 18 | from command import Command | ||
| 19 | from git_command import GitCommand | ||
| 20 | from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB | ||
| 21 | from error import GitError | ||
| 22 | |||
| 23 | class Rebase(Command): | ||
| 24 | common = True | ||
| 25 | helpSummary = "Rebase local branches on upstream branch" | ||
| 26 | helpUsage = """ | ||
| 27 | %prog {[<project>...] | -i <project>...} | ||
| 28 | """ | ||
| 29 | helpDescription = """ | ||
| 30 | '%prog' uses git rebase to move local changes in the current topic branch to | ||
| 31 | the HEAD of the upstream history, useful when you have made commits in a topic | ||
| 32 | branch but need to incorporate new upstream changes "underneath" them. | ||
| 33 | """ | ||
| 34 | |||
| 35 | def _Options(self, p): | ||
| 36 | p.add_option('-i', '--interactive', | ||
| 37 | dest="interactive", action="store_true", | ||
| 38 | help="interactive rebase (single project only)") | ||
| 39 | |||
| 40 | p.add_option('-f', '--force-rebase', | ||
| 41 | dest='force_rebase', action='store_true', | ||
| 42 | help='Pass --force-rebase to git rebase') | ||
| 43 | p.add_option('--no-ff', | ||
| 44 | dest='no_ff', action='store_true', | ||
| 45 | help='Pass --no-ff to git rebase') | ||
| 46 | p.add_option('-q', '--quiet', | ||
| 47 | dest='quiet', action='store_true', | ||
| 48 | help='Pass --quiet to git rebase') | ||
| 49 | p.add_option('--autosquash', | ||
| 50 | dest='autosquash', action='store_true', | ||
| 51 | help='Pass --autosquash to git rebase') | ||
| 52 | p.add_option('--whitespace', | ||
| 53 | dest='whitespace', action='store', metavar='WS', | ||
| 54 | help='Pass --whitespace to git rebase') | ||
| 55 | |||
| 56 | def Execute(self, opt, args): | ||
| 57 | all = self.GetProjects(args) | ||
| 58 | one_project = len(all) == 1 | ||
| 59 | |||
| 60 | if opt.interactive and not one_project: | ||
| 61 | print >>sys.stderr, 'error: interactive rebase not supported with multiple projects' | ||
| 62 | return -1 | ||
| 63 | |||
| 64 | for project in all: | ||
| 65 | cb = project.CurrentBranch | ||
| 66 | if not cb: | ||
| 67 | if one_project: | ||
| 68 | print >>sys.stderr, "error: project %s has a detatched HEAD" % project.relpath | ||
| 69 | return -1 | ||
| 70 | # ignore branches with detatched HEADs | ||
| 71 | continue | ||
| 72 | |||
| 73 | upbranch = project.GetBranch(cb) | ||
| 74 | if not upbranch.LocalMerge: | ||
| 75 | if one_project: | ||
| 76 | print >>sys.stderr, "error: project %s does not track any remote branches" % project.relpath | ||
| 77 | return -1 | ||
| 78 | # ignore branches without remotes | ||
| 79 | continue | ||
| 80 | |||
| 81 | args = ["rebase"] | ||
| 82 | |||
| 83 | if opt.whitespace: | ||
| 84 | args.append('--whitespace=%s' % opt.whitespace) | ||
| 85 | |||
| 86 | if opt.quiet: | ||
| 87 | args.append('--quiet') | ||
| 88 | |||
| 89 | if opt.force_rebase: | ||
| 90 | args.append('--force-rebase') | ||
| 91 | |||
| 92 | if opt.no_ff: | ||
| 93 | args.append('--no-ff') | ||
| 94 | |||
| 95 | if opt.autosquash: | ||
| 96 | args.append('--autosquash') | ||
| 97 | |||
| 98 | if opt.interactive: | ||
| 99 | args.append("-i") | ||
| 100 | |||
| 101 | args.append(upbranch.LocalMerge) | ||
| 102 | |||
| 103 | print >>sys.stderr, '# %s: rebasing %s -> %s' % \ | ||
| 104 | (project.relpath, cb, upbranch.LocalMerge) | ||
| 105 | |||
| 106 | if GitCommand(project, args).Wait() != 0: | ||
| 107 | return -1 | ||
diff --git a/subcmds/smartsync.py b/subcmds/smartsync.py new file mode 100644 index 00000000..1edbd35b --- /dev/null +++ b/subcmds/smartsync.py | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | # | ||
| 2 | # Copyright (C) 2010 The Android Open Source Project | ||
| 3 | # | ||
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 5 | # you may not use this file except in compliance with the License. | ||
| 6 | # You may obtain a copy of the License at | ||
| 7 | # | ||
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
| 9 | # | ||
| 10 | # Unless required by applicable law or agreed to in writing, software | ||
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
| 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 13 | # See the License for the specific language governing permissions and | ||
| 14 | # limitations under the License. | ||
| 15 | |||
| 16 | from sync import Sync | ||
| 17 | |||
| 18 | class Smartsync(Sync): | ||
| 19 | common = True | ||
| 20 | helpSummary = "Update working tree to the latest known good revision" | ||
| 21 | helpUsage = """ | ||
| 22 | %prog [<project>...] | ||
| 23 | """ | ||
| 24 | helpDescription = """ | ||
| 25 | The '%prog' command is a shortcut for sync -s. | ||
| 26 | """ | ||
| 27 | |||
| 28 | def _Options(self, p): | ||
| 29 | Sync._Options(self, p, show_smart=False) | ||
| 30 | |||
| 31 | def Execute(self, opt, args): | ||
| 32 | opt.smart_sync = True | ||
| 33 | Sync.Execute(self, opt, args) | ||
diff --git a/subcmds/sync.py b/subcmds/sync.py index d89c2b8c..7b77388b 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py | |||
| @@ -17,11 +17,19 @@ from optparse import SUPPRESS_HELP | |||
| 17 | import os | 17 | import os |
| 18 | import re | 18 | import re |
| 19 | import shutil | 19 | import shutil |
| 20 | import socket | ||
| 20 | import subprocess | 21 | import subprocess |
| 21 | import sys | 22 | import sys |
| 22 | import time | 23 | import time |
| 24 | import xmlrpclib | ||
| 25 | |||
| 26 | try: | ||
| 27 | import threading as _threading | ||
| 28 | except ImportError: | ||
| 29 | import dummy_threading as _threading | ||
| 23 | 30 | ||
| 24 | from git_command import GIT | 31 | from git_command import GIT |
| 32 | from git_refs import R_HEADS | ||
| 25 | from project import HEAD | 33 | from project import HEAD |
| 26 | from project import Project | 34 | from project import Project |
| 27 | from project import RemoteSpec | 35 | from project import RemoteSpec |
| @@ -32,6 +40,7 @@ from project import SyncBuffer | |||
| 32 | from progress import Progress | 40 | from progress import Progress |
| 33 | 41 | ||
| 34 | class Sync(Command, MirrorSafeCommand): | 42 | class Sync(Command, MirrorSafeCommand): |
| 43 | jobs = 1 | ||
| 35 | common = True | 44 | common = True |
| 36 | helpSummary = "Update working tree to the latest revision" | 45 | helpSummary = "Update working tree to the latest revision" |
| 37 | helpUsage = """ | 46 | helpUsage = """ |
| @@ -57,6 +66,13 @@ back to the manifest revision. This option is especially helpful | |||
| 57 | if the project is currently on a topic branch, but the manifest | 66 | if the project is currently on a topic branch, but the manifest |
| 58 | revision is temporarily needed. | 67 | revision is temporarily needed. |
| 59 | 68 | ||
| 69 | The -s/--smart-sync option can be used to sync to a known good | ||
| 70 | build as specified by the manifest-server element in the current | ||
| 71 | manifest. | ||
| 72 | |||
| 73 | The -f/--force-broken option can be used to proceed with syncing | ||
| 74 | other projects if a project sync fails. | ||
| 75 | |||
| 60 | SSH Connections | 76 | SSH Connections |
| 61 | --------------- | 77 | --------------- |
| 62 | 78 | ||
| @@ -87,7 +103,10 @@ later is required to fix a server side protocol bug. | |||
| 87 | 103 | ||
| 88 | """ | 104 | """ |
| 89 | 105 | ||
| 90 | def _Options(self, p): | 106 | def _Options(self, p, show_smart=True): |
| 107 | p.add_option('-f', '--force-broken', | ||
| 108 | dest='force_broken', action='store_true', | ||
| 109 | help="continue sync even if a project fails to sync") | ||
| 91 | p.add_option('-l','--local-only', | 110 | p.add_option('-l','--local-only', |
| 92 | dest='local_only', action='store_true', | 111 | dest='local_only', action='store_true', |
| 93 | help="only update working tree, don't fetch") | 112 | help="only update working tree, don't fetch") |
| @@ -97,6 +116,16 @@ later is required to fix a server side protocol bug. | |||
| 97 | p.add_option('-d','--detach', | 116 | p.add_option('-d','--detach', |
| 98 | dest='detach_head', action='store_true', | 117 | dest='detach_head', action='store_true', |
| 99 | help='detach projects back to manifest revision') | 118 | help='detach projects back to manifest revision') |
| 119 | p.add_option('-q','--quiet', | ||
| 120 | dest='quiet', action='store_true', | ||
| 121 | help='be more quiet') | ||
| 122 | p.add_option('-j','--jobs', | ||
| 123 | dest='jobs', action='store', type='int', | ||
| 124 | help="number of projects to fetch simultaneously") | ||
| 125 | if show_smart: | ||
| 126 | p.add_option('-s', '--smart-sync', | ||
| 127 | dest='smart_sync', action='store_true', | ||
| 128 | help='smart sync using manifest from a known good build') | ||
| 100 | 129 | ||
| 101 | g = p.add_option_group('repo Version options') | 130 | g = p.add_option_group('repo Version options') |
| 102 | g.add_option('--no-repo-verify', | 131 | g.add_option('--no-repo-verify', |
| @@ -106,16 +135,55 @@ later is required to fix a server side protocol bug. | |||
| 106 | dest='repo_upgraded', action='store_true', | 135 | dest='repo_upgraded', action='store_true', |
| 107 | help=SUPPRESS_HELP) | 136 | help=SUPPRESS_HELP) |
| 108 | 137 | ||
| 109 | def _Fetch(self, projects): | 138 | def _FetchHelper(self, opt, project, lock, fetched, pm, sem): |
| 139 | if not project.Sync_NetworkHalf(quiet=opt.quiet): | ||
| 140 | print >>sys.stderr, 'error: Cannot fetch %s' % project.name | ||
| 141 | if opt.force_broken: | ||
| 142 | print >>sys.stderr, 'warn: --force-broken, continuing to sync' | ||
| 143 | else: | ||
| 144 | sem.release() | ||
| 145 | sys.exit(1) | ||
| 146 | |||
| 147 | lock.acquire() | ||
| 148 | fetched.add(project.gitdir) | ||
| 149 | pm.update() | ||
| 150 | lock.release() | ||
| 151 | sem.release() | ||
| 152 | |||
| 153 | def _Fetch(self, projects, opt): | ||
| 110 | fetched = set() | 154 | fetched = set() |
| 111 | pm = Progress('Fetching projects', len(projects)) | 155 | pm = Progress('Fetching projects', len(projects)) |
| 112 | for project in projects: | 156 | |
| 113 | pm.update() | 157 | if self.jobs == 1: |
| 114 | if project.Sync_NetworkHalf(): | 158 | for project in projects: |
| 115 | fetched.add(project.gitdir) | 159 | pm.update() |
| 116 | else: | 160 | if project.Sync_NetworkHalf(quiet=opt.quiet): |
| 117 | print >>sys.stderr, 'error: Cannot fetch %s' % project.name | 161 | fetched.add(project.gitdir) |
| 118 | sys.exit(1) | 162 | else: |
| 163 | print >>sys.stderr, 'error: Cannot fetch %s' % project.name | ||
| 164 | if opt.force_broken: | ||
| 165 | print >>sys.stderr, 'warn: --force-broken, continuing to sync' | ||
| 166 | else: | ||
| 167 | sys.exit(1) | ||
| 168 | else: | ||
| 169 | threads = set() | ||
| 170 | lock = _threading.Lock() | ||
| 171 | sem = _threading.Semaphore(self.jobs) | ||
| 172 | for project in projects: | ||
| 173 | sem.acquire() | ||
| 174 | t = _threading.Thread(target = self._FetchHelper, | ||
| 175 | args = (opt, | ||
| 176 | project, | ||
| 177 | lock, | ||
| 178 | fetched, | ||
| 179 | pm, | ||
| 180 | sem)) | ||
| 181 | threads.add(t) | ||
| 182 | t.start() | ||
| 183 | |||
| 184 | for t in threads: | ||
| 185 | t.join() | ||
| 186 | |||
| 119 | pm.end() | 187 | pm.end() |
| 120 | for project in projects: | 188 | for project in projects: |
| 121 | project.bare_git.gc('--auto') | 189 | project.bare_git.gc('--auto') |
| @@ -140,32 +208,36 @@ later is required to fix a server side protocol bug. | |||
| 140 | if not path: | 208 | if not path: |
| 141 | continue | 209 | continue |
| 142 | if path not in new_project_paths: | 210 | if path not in new_project_paths: |
| 143 | project = Project( | 211 | """If the path has already been deleted, we don't need to do it |
| 144 | manifest = self.manifest, | 212 | """ |
| 145 | name = path, | 213 | if os.path.exists(self.manifest.topdir + '/' + path): |
| 146 | remote = RemoteSpec('origin'), | 214 | project = Project( |
| 147 | gitdir = os.path.join(self.manifest.topdir, | 215 | manifest = self.manifest, |
| 148 | path, '.git'), | 216 | name = path, |
| 149 | worktree = os.path.join(self.manifest.topdir, path), | 217 | remote = RemoteSpec('origin'), |
| 150 | relpath = path, | 218 | gitdir = os.path.join(self.manifest.topdir, |
| 151 | revisionExpr = 'HEAD', | 219 | path, '.git'), |
| 152 | revisionId = None) | 220 | worktree = os.path.join(self.manifest.topdir, path), |
| 153 | if project.IsDirty(): | 221 | relpath = path, |
| 154 | print >>sys.stderr, 'error: Cannot remove project "%s": \ | 222 | revisionExpr = 'HEAD', |
| 223 | revisionId = None) | ||
| 224 | |||
| 225 | if project.IsDirty(): | ||
| 226 | print >>sys.stderr, 'error: Cannot remove project "%s": \ | ||
| 155 | uncommitted changes are present' % project.relpath | 227 | uncommitted changes are present' % project.relpath |
| 156 | print >>sys.stderr, ' commit changes, then run sync again' | 228 | print >>sys.stderr, ' commit changes, then run sync again' |
| 157 | return -1 | 229 | return -1 |
| 158 | else: | 230 | else: |
| 159 | print >>sys.stderr, 'Deleting obsolete path %s' % project.worktree | 231 | print >>sys.stderr, 'Deleting obsolete path %s' % project.worktree |
| 160 | shutil.rmtree(project.worktree) | 232 | shutil.rmtree(project.worktree) |
| 161 | # Try deleting parent subdirs if they are empty | 233 | # Try deleting parent subdirs if they are empty |
| 162 | dir = os.path.dirname(project.worktree) | 234 | dir = os.path.dirname(project.worktree) |
| 163 | while dir != self.manifest.topdir: | 235 | while dir != self.manifest.topdir: |
| 164 | try: | 236 | try: |
| 165 | os.rmdir(dir) | 237 | os.rmdir(dir) |
| 166 | except OSError: | 238 | except OSError: |
| 167 | break | 239 | break |
| 168 | dir = os.path.dirname(dir) | 240 | dir = os.path.dirname(dir) |
| 169 | 241 | ||
| 170 | new_project_paths.sort() | 242 | new_project_paths.sort() |
| 171 | fd = open(file_path, 'w') | 243 | fd = open(file_path, 'w') |
| @@ -177,6 +249,8 @@ uncommitted changes are present' % project.relpath | |||
| 177 | return 0 | 249 | return 0 |
| 178 | 250 | ||
| 179 | def Execute(self, opt, args): | 251 | def Execute(self, opt, args): |
| 252 | if opt.jobs: | ||
| 253 | self.jobs = opt.jobs | ||
| 180 | if opt.network_only and opt.detach_head: | 254 | if opt.network_only and opt.detach_head: |
| 181 | print >>sys.stderr, 'error: cannot combine -n and -d' | 255 | print >>sys.stderr, 'error: cannot combine -n and -d' |
| 182 | sys.exit(1) | 256 | sys.exit(1) |
| @@ -184,6 +258,51 @@ uncommitted changes are present' % project.relpath | |||
| 184 | print >>sys.stderr, 'error: cannot combine -n and -l' | 258 | print >>sys.stderr, 'error: cannot combine -n and -l' |
| 185 | sys.exit(1) | 259 | sys.exit(1) |
| 186 | 260 | ||
| 261 | if opt.smart_sync: | ||
| 262 | if not self.manifest.manifest_server: | ||
| 263 | print >>sys.stderr, \ | ||
| 264 | 'error: cannot smart sync: no manifest server defined in manifest' | ||
| 265 | sys.exit(1) | ||
| 266 | try: | ||
| 267 | server = xmlrpclib.Server(self.manifest.manifest_server) | ||
| 268 | p = self.manifest.manifestProject | ||
| 269 | b = p.GetBranch(p.CurrentBranch) | ||
| 270 | branch = b.merge | ||
| 271 | if branch.startswith(R_HEADS): | ||
| 272 | branch = branch[len(R_HEADS):] | ||
| 273 | |||
| 274 | env = dict(os.environ) | ||
| 275 | if (env.has_key('TARGET_PRODUCT') and | ||
| 276 | env.has_key('TARGET_BUILD_VARIANT')): | ||
| 277 | target = '%s-%s' % (env['TARGET_PRODUCT'], | ||
| 278 | env['TARGET_BUILD_VARIANT']) | ||
| 279 | [success, manifest_str] = server.GetApprovedManifest(branch, target) | ||
| 280 | else: | ||
| 281 | [success, manifest_str] = server.GetApprovedManifest(branch) | ||
| 282 | |||
| 283 | if success: | ||
| 284 | manifest_name = "smart_sync_override.xml" | ||
| 285 | manifest_path = os.path.join(self.manifest.manifestProject.worktree, | ||
| 286 | manifest_name) | ||
| 287 | try: | ||
| 288 | f = open(manifest_path, 'w') | ||
| 289 | try: | ||
| 290 | f.write(manifest_str) | ||
| 291 | finally: | ||
| 292 | f.close() | ||
| 293 | except IOError: | ||
| 294 | print >>sys.stderr, 'error: cannot write manifest to %s' % \ | ||
| 295 | manifest_path | ||
| 296 | sys.exit(1) | ||
| 297 | self.manifest.Override(manifest_name) | ||
| 298 | else: | ||
| 299 | print >>sys.stderr, 'error: %s' % manifest_str | ||
| 300 | sys.exit(1) | ||
| 301 | except socket.error: | ||
| 302 | print >>sys.stderr, 'error: cannot connect to manifest server %s' % ( | ||
| 303 | self.manifest.manifest_server) | ||
| 304 | sys.exit(1) | ||
| 305 | |||
| 187 | rp = self.manifest.repoProject | 306 | rp = self.manifest.repoProject |
| 188 | rp.PreSync() | 307 | rp.PreSync() |
| 189 | 308 | ||
| @@ -194,7 +313,7 @@ uncommitted changes are present' % project.relpath | |||
| 194 | _PostRepoUpgrade(self.manifest) | 313 | _PostRepoUpgrade(self.manifest) |
| 195 | 314 | ||
| 196 | if not opt.local_only: | 315 | if not opt.local_only: |
| 197 | mp.Sync_NetworkHalf() | 316 | mp.Sync_NetworkHalf(quiet=opt.quiet) |
| 198 | 317 | ||
| 199 | if mp.HasChanges: | 318 | if mp.HasChanges: |
| 200 | syncbuf = SyncBuffer(mp.config) | 319 | syncbuf = SyncBuffer(mp.config) |
| @@ -211,7 +330,7 @@ uncommitted changes are present' % project.relpath | |||
| 211 | to_fetch.append(rp) | 330 | to_fetch.append(rp) |
| 212 | to_fetch.extend(all) | 331 | to_fetch.extend(all) |
| 213 | 332 | ||
| 214 | fetched = self._Fetch(to_fetch) | 333 | fetched = self._Fetch(to_fetch, opt) |
| 215 | _PostRepoFetch(rp, opt.no_repo_verify) | 334 | _PostRepoFetch(rp, opt.no_repo_verify) |
| 216 | if opt.network_only: | 335 | if opt.network_only: |
| 217 | # bail out now; the rest touches the working tree | 336 | # bail out now; the rest touches the working tree |
| @@ -230,7 +349,7 @@ uncommitted changes are present' % project.relpath | |||
| 230 | for project in all: | 349 | for project in all: |
| 231 | if project.gitdir not in fetched: | 350 | if project.gitdir not in fetched: |
| 232 | missing.append(project) | 351 | missing.append(project) |
| 233 | self._Fetch(missing) | 352 | self._Fetch(missing, opt) |
| 234 | 353 | ||
| 235 | if self.manifest.IsMirror: | 354 | if self.manifest.IsMirror: |
| 236 | # bail out now, we have no working tree | 355 | # bail out now, we have no working tree |
| @@ -258,6 +377,9 @@ def _ReloadManifest(cmd): | |||
| 258 | if old.__class__ != new.__class__: | 377 | if old.__class__ != new.__class__: |
| 259 | print >>sys.stderr, 'NOTICE: manifest format has changed ***' | 378 | print >>sys.stderr, 'NOTICE: manifest format has changed ***' |
| 260 | new.Upgrade_Local(old) | 379 | new.Upgrade_Local(old) |
| 380 | else: | ||
| 381 | if new.notice: | ||
| 382 | print new.notice | ||
| 261 | 383 | ||
| 262 | def _PostRepoUpgrade(manifest): | 384 | def _PostRepoUpgrade(manifest): |
| 263 | for project in manifest.projects.values(): | 385 | for project in manifest.projects.values(): |
diff --git a/subcmds/upload.py b/subcmds/upload.py index 2ab6a484..20822096 100644 --- a/subcmds/upload.py +++ b/subcmds/upload.py | |||
| @@ -13,6 +13,7 @@ | |||
| 13 | # See the License for the specific language governing permissions and | 13 | # See the License for the specific language governing permissions and |
| 14 | # limitations under the License. | 14 | # limitations under the License. |
| 15 | 15 | ||
| 16 | import copy | ||
| 16 | import re | 17 | import re |
| 17 | import sys | 18 | import sys |
| 18 | 19 | ||
| @@ -20,6 +21,17 @@ from command import InteractiveCommand | |||
| 20 | from editor import Editor | 21 | from editor import Editor |
| 21 | from error import UploadError | 22 | from error import UploadError |
| 22 | 23 | ||
| 24 | UNUSUAL_COMMIT_THRESHOLD = 5 | ||
| 25 | |||
| 26 | def _ConfirmManyUploads(multiple_branches=False): | ||
| 27 | if multiple_branches: | ||
| 28 | print "ATTENTION: One or more branches has an unusually high number of commits." | ||
| 29 | else: | ||
| 30 | print "ATTENTION: You are uploading an unusually high number of commits." | ||
| 31 | print "YOU PROBABLY DO NOT MEAN TO DO THIS. (Did you rebase across branches?)" | ||
| 32 | answer = raw_input("If you are sure you intend to do this, type 'yes': ").strip() | ||
| 33 | return answer == "yes" | ||
| 34 | |||
| 23 | def _die(fmt, *args): | 35 | def _die(fmt, *args): |
| 24 | msg = fmt % args | 36 | msg = fmt % args |
| 25 | print >>sys.stderr, 'error: %s' % msg | 37 | print >>sys.stderr, 'error: %s' % msg |
| @@ -35,7 +47,7 @@ class Upload(InteractiveCommand): | |||
| 35 | common = True | 47 | common = True |
| 36 | helpSummary = "Upload changes for code review" | 48 | helpSummary = "Upload changes for code review" |
| 37 | helpUsage=""" | 49 | helpUsage=""" |
| 38 | %prog [--re --cc] {[<project>]... | --replace <project>} | 50 | %prog [--re --cc] [<project>]... |
| 39 | """ | 51 | """ |
| 40 | helpDescription = """ | 52 | helpDescription = """ |
| 41 | The '%prog' command is used to send changes to the Gerrit Code | 53 | The '%prog' command is used to send changes to the Gerrit Code |
| @@ -55,12 +67,6 @@ added to the respective list of users, and emails are sent to any | |||
| 55 | new users. Users passed as --reviewers must already be registered | 67 | new users. Users passed as --reviewers must already be registered |
| 56 | with the code review system, or the upload will fail. | 68 | with the code review system, or the upload will fail. |
| 57 | 69 | ||
| 58 | If the --replace option (deprecated) is passed the user can designate | ||
| 59 | which existing change(s) in Gerrit match up to the commits in the | ||
| 60 | branch being uploaded. For each matched pair of change,commit the | ||
| 61 | commit will be added as a new patch set, completely replacing the | ||
| 62 | set of files and description associated with the change in Gerrit. | ||
| 63 | |||
| 64 | Configuration | 70 | Configuration |
| 65 | ------------- | 71 | ------------- |
| 66 | 72 | ||
| @@ -72,6 +78,19 @@ to "true" then repo will assume you always answer "y" at the prompt, | |||
| 72 | and will not prompt you further. If it is set to "false" then repo | 78 | and will not prompt you further. If it is set to "false" then repo |
| 73 | will assume you always answer "n", and will abort. | 79 | will assume you always answer "n", and will abort. |
| 74 | 80 | ||
| 81 | review.URL.autocopy: | ||
| 82 | |||
| 83 | To automatically copy a user or mailing list to all uploaded reviews, | ||
| 84 | you can set a per-project or global Git option to do so. Specifically, | ||
| 85 | review.URL.autocopy can be set to a comma separated list of reviewers | ||
| 86 | who you always want copied on all uploads with a non-empty --re | ||
| 87 | argument. | ||
| 88 | |||
| 89 | review.URL.username: | ||
| 90 | |||
| 91 | Override the username used to connect to Gerrit Code Review. | ||
| 92 | By default the local part of the email address is used. | ||
| 93 | |||
| 75 | The URL must match the review URL listed in the manifest XML file, | 94 | The URL must match the review URL listed in the manifest XML file, |
| 76 | or in the .git/config within the project. For example: | 95 | or in the .git/config within the project. For example: |
| 77 | 96 | ||
| @@ -81,6 +100,7 @@ or in the .git/config within the project. For example: | |||
| 81 | 100 | ||
| 82 | [review "http://review.example.com/"] | 101 | [review "http://review.example.com/"] |
| 83 | autoupload = true | 102 | autoupload = true |
| 103 | autocopy = johndoe@company.com,my-team-alias@company.com | ||
| 84 | 104 | ||
| 85 | References | 105 | References |
| 86 | ---------- | 106 | ---------- |
| @@ -90,9 +110,9 @@ Gerrit Code Review: http://code.google.com/p/gerrit/ | |||
| 90 | """ | 110 | """ |
| 91 | 111 | ||
| 92 | def _Options(self, p): | 112 | def _Options(self, p): |
| 93 | p.add_option('--replace', | 113 | p.add_option('-t', |
| 94 | dest='replace', action='store_true', | 114 | dest='auto_topic', action='store_true', |
| 95 | help='Upload replacement patchsets from this branch (deprecated)') | 115 | help='Send local branch name to Gerrit Code Review') |
| 96 | p.add_option('--re', '--reviewers', | 116 | p.add_option('--re', '--reviewers', |
| 97 | type='string', action='append', dest='reviewers', | 117 | type='string', action='append', dest='reviewers', |
| 98 | help='Request reviews from these people.') | 118 | help='Request reviews from these people.') |
| @@ -100,7 +120,7 @@ Gerrit Code Review: http://code.google.com/p/gerrit/ | |||
| 100 | type='string', action='append', dest='cc', | 120 | type='string', action='append', dest='cc', |
| 101 | help='Also send email to these email addresses.') | 121 | help='Also send email to these email addresses.') |
| 102 | 122 | ||
| 103 | def _SingleBranch(self, branch, people): | 123 | def _SingleBranch(self, opt, branch, people): |
| 104 | project = branch.project | 124 | project = branch.project |
| 105 | name = branch.name | 125 | name = branch.name |
| 106 | remote = project.GetBranch(name).remote | 126 | remote = project.GetBranch(name).remote |
| @@ -129,11 +149,15 @@ Gerrit Code Review: http://code.google.com/p/gerrit/ | |||
| 129 | answer = answer in ('y', 'Y', 'yes', '1', 'true', 't') | 149 | answer = answer in ('y', 'Y', 'yes', '1', 'true', 't') |
| 130 | 150 | ||
| 131 | if answer: | 151 | if answer: |
| 132 | self._UploadAndReport([branch], people) | 152 | if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD: |
| 153 | answer = _ConfirmManyUploads() | ||
| 154 | |||
| 155 | if answer: | ||
| 156 | self._UploadAndReport(opt, [branch], people) | ||
| 133 | else: | 157 | else: |
| 134 | _die("upload aborted by user") | 158 | _die("upload aborted by user") |
| 135 | 159 | ||
| 136 | def _MultipleBranches(self, pending, people): | 160 | def _MultipleBranches(self, opt, pending, people): |
| 137 | projects = {} | 161 | projects = {} |
| 138 | branches = {} | 162 | branches = {} |
| 139 | 163 | ||
| @@ -192,7 +216,30 @@ Gerrit Code Review: http://code.google.com/p/gerrit/ | |||
| 192 | todo.append(branch) | 216 | todo.append(branch) |
| 193 | if not todo: | 217 | if not todo: |
| 194 | _die("nothing uncommented for upload") | 218 | _die("nothing uncommented for upload") |
| 195 | self._UploadAndReport(todo, people) | 219 | |
| 220 | many_commits = False | ||
| 221 | for branch in todo: | ||
| 222 | if len(branch.commits) > UNUSUAL_COMMIT_THRESHOLD: | ||
| 223 | many_commits = True | ||
| 224 | break | ||
| 225 | if many_commits: | ||
| 226 | if not _ConfirmManyUploads(multiple_branches=True): | ||
| 227 | _die("upload aborted by user") | ||
| 228 | |||
| 229 | self._UploadAndReport(opt, todo, people) | ||
| 230 | |||
| 231 | def _AppendAutoCcList(self, branch, people): | ||
| 232 | """ | ||
| 233 | Appends the list of users in the CC list in the git project's config if a | ||
| 234 | non-empty reviewer list was found. | ||
| 235 | """ | ||
| 236 | |||
| 237 | name = branch.name | ||
| 238 | project = branch.project | ||
| 239 | key = 'review.%s.autocopy' % project.GetBranch(name).remote.review | ||
| 240 | raw_list = project.config.GetString(key) | ||
| 241 | if not raw_list is None and len(people[0]) > 0: | ||
| 242 | people[1].extend([entry.strip() for entry in raw_list.split(',')]) | ||
| 196 | 243 | ||
| 197 | def _FindGerritChange(self, branch): | 244 | def _FindGerritChange(self, branch): |
| 198 | last_pub = branch.project.WasPublished(branch.name) | 245 | last_pub = branch.project.WasPublished(branch.name) |
| @@ -206,66 +253,29 @@ Gerrit Code Review: http://code.google.com/p/gerrit/ | |||
| 206 | except: | 253 | except: |
| 207 | return "" | 254 | return "" |
| 208 | 255 | ||
| 209 | def _ReplaceBranch(self, project, people): | 256 | def _UploadAndReport(self, opt, todo, original_people): |
| 210 | branch = project.CurrentBranch | ||
| 211 | if not branch: | ||
| 212 | print >>sys.stdout, "no branches ready for upload" | ||
| 213 | return | ||
| 214 | branch = project.GetUploadableBranch(branch) | ||
| 215 | if not branch: | ||
| 216 | print >>sys.stdout, "no branches ready for upload" | ||
| 217 | return | ||
| 218 | |||
| 219 | script = [] | ||
| 220 | script.append('# Replacing from branch %s' % branch.name) | ||
| 221 | |||
| 222 | if len(branch.commits) == 1: | ||
| 223 | change = self._FindGerritChange(branch) | ||
| 224 | script.append('[%-6s] %s' % (change, branch.commits[0])) | ||
| 225 | else: | ||
| 226 | for commit in branch.commits: | ||
| 227 | script.append('[ ] %s' % commit) | ||
| 228 | |||
| 229 | script.append('') | ||
| 230 | script.append('# Insert change numbers in the brackets to add a new patch set.') | ||
| 231 | script.append('# To create a new change record, leave the brackets empty.') | ||
| 232 | |||
| 233 | script = Editor.EditString("\n".join(script)).split("\n") | ||
| 234 | |||
| 235 | change_re = re.compile(r'^\[\s*(\d{1,})\s*\]\s*([0-9a-f]{1,}) .*$') | ||
| 236 | to_replace = dict() | ||
| 237 | full_hashes = branch.unabbrev_commits | ||
| 238 | |||
| 239 | for line in script: | ||
| 240 | m = change_re.match(line) | ||
| 241 | if m: | ||
| 242 | c = m.group(1) | ||
| 243 | f = m.group(2) | ||
| 244 | try: | ||
| 245 | f = full_hashes[f] | ||
| 246 | except KeyError: | ||
| 247 | print 'fh = %s' % full_hashes | ||
| 248 | print >>sys.stderr, "error: commit %s not found" % f | ||
| 249 | sys.exit(1) | ||
| 250 | if c in to_replace: | ||
| 251 | print >>sys.stderr,\ | ||
| 252 | "error: change %s cannot accept multiple commits" % c | ||
| 253 | sys.exit(1) | ||
| 254 | to_replace[c] = f | ||
| 255 | |||
| 256 | if not to_replace: | ||
| 257 | print >>sys.stderr, "error: no replacements specified" | ||
| 258 | print >>sys.stderr, " use 'repo upload' without --replace" | ||
| 259 | sys.exit(1) | ||
| 260 | |||
| 261 | branch.replace_changes = to_replace | ||
| 262 | self._UploadAndReport([branch], people) | ||
| 263 | |||
| 264 | def _UploadAndReport(self, todo, people): | ||
| 265 | have_errors = False | 257 | have_errors = False |
| 266 | for branch in todo: | 258 | for branch in todo: |
| 267 | try: | 259 | try: |
| 268 | branch.UploadForReview(people) | 260 | people = copy.deepcopy(original_people) |
| 261 | self._AppendAutoCcList(branch, people) | ||
| 262 | |||
| 263 | # Check if there are local changes that may have been forgotten | ||
| 264 | if branch.project.HasChanges(): | ||
| 265 | key = 'review.%s.autoupload' % branch.project.remote.review | ||
| 266 | answer = branch.project.config.GetBoolean(key) | ||
| 267 | |||
| 268 | # if they want to auto upload, let's not ask because it could be automated | ||
| 269 | if answer is None: | ||
| 270 | sys.stdout.write('Uncommitted changes in ' + branch.project.name + ' (did you forget to amend?). Continue uploading? (y/n) ') | ||
| 271 | a = sys.stdin.readline().strip().lower() | ||
| 272 | if a not in ('y', 'yes', 't', 'true', 'on'): | ||
| 273 | print >>sys.stderr, "skipping upload" | ||
| 274 | branch.uploaded = False | ||
| 275 | branch.error = 'User aborted' | ||
| 276 | continue | ||
| 277 | |||
| 278 | branch.UploadForReview(people, auto_topic=opt.auto_topic) | ||
| 269 | branch.uploaded = True | 279 | branch.uploaded = True |
| 270 | except UploadError, e: | 280 | except UploadError, e: |
| 271 | branch.error = e | 281 | branch.error = e |
| @@ -309,14 +319,6 @@ Gerrit Code Review: http://code.google.com/p/gerrit/ | |||
| 309 | cc = _SplitEmails(opt.cc) | 319 | cc = _SplitEmails(opt.cc) |
| 310 | people = (reviewers,cc) | 320 | people = (reviewers,cc) |
| 311 | 321 | ||
| 312 | if opt.replace: | ||
| 313 | if len(project_list) != 1: | ||
| 314 | print >>sys.stderr, \ | ||
| 315 | 'error: --replace requires exactly one project' | ||
| 316 | sys.exit(1) | ||
| 317 | self._ReplaceBranch(project_list[0], people) | ||
| 318 | return | ||
| 319 | |||
| 320 | for project in project_list: | 322 | for project in project_list: |
| 321 | avail = project.GetUploadableBranches() | 323 | avail = project.GetUploadableBranches() |
| 322 | if avail: | 324 | if avail: |
| @@ -325,6 +327,6 @@ Gerrit Code Review: http://code.google.com/p/gerrit/ | |||
| 325 | if not pending: | 327 | if not pending: |
| 326 | print >>sys.stdout, "no branches ready for upload" | 328 | print >>sys.stdout, "no branches ready for upload" |
| 327 | elif len(pending) == 1 and len(pending[0][1]) == 1: | 329 | elif len(pending) == 1 and len(pending[0][1]) == 1: |
| 328 | self._SingleBranch(pending[0][1][0], people) | 330 | self._SingleBranch(opt, pending[0][1][0], people) |
| 329 | else: | 331 | else: |
| 330 | self._MultipleBranches(pending, people) | 332 | self._MultipleBranches(opt, pending, people) |
