summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/manifest_submodule.txt6
-rw-r--r--docs/manifest_xml.txt30
-rw-r--r--editor.py2
-rw-r--r--git_command.py33
-rw-r--r--git_config.py67
-rwxr-xr-xgit_ssh2
-rw-r--r--manifest.py8
-rw-r--r--manifest_submodule.py7
-rw-r--r--manifest_xml.py94
-rw-r--r--progress.py7
-rw-r--r--project.py177
-rwxr-xr-xrepo3
-rw-r--r--subcmds/branches.py12
-rw-r--r--subcmds/grep.py8
-rw-r--r--subcmds/init.py29
-rw-r--r--subcmds/rebase.py107
-rw-r--r--subcmds/smartsync.py33
-rw-r--r--subcmds/sync.py196
-rw-r--r--subcmds/upload.py164
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
77This key is not supported by repo. If set, it will be ignored. 77This key is not supported by repo. If set, it will be ignored.
78 78
79repo.notice
80-----------
81
82A 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
20following DTD: 20following 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
89revision attribute will use this revision. 96revision attribute will use this revision.
90 97
91 98
99Element manifest-server
100-----------------------
101
102At most one manifest-server may be specified. The url attribute
103is used to specify the URL of a manifest server, which is an
104XML RPC service that will return a manifest in which each project
105is pegged to a known good revision for the current branch and
106target.
107
108The manifest server should implement:
109
110 GetApprovedManifest(branch, target)
111
112The target to use is defined by environment variables TARGET_PRODUCT
113and TARGET_BUILD_VARIANT. These variables are used to create a string
114of the form $TARGET_PRODUCT-$TARGET_BUILD_VARIANT, e.g. passion-userdebug.
115If one of those variables or both are not present, the program will call
116GetApprovedManifest without the target paramater and the manifest server
117should choose a reasonable default target.
118
119
92Element project 120Element project
93--------------- 121---------------
94 122
diff --git a/editor.py b/editor.py
index 23aab542..62afbb91 100644
--- a/editor.py
+++ b/editor.py
@@ -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
17import sys 17import sys
18import subprocess 18import subprocess
19import tempfile 19import tempfile
20from signal import SIGTERM
20from error import GitError 21from error import GitError
21from trace import REPO_TRACE, IsTrace, Trace 22from 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
33def _ssh_sock(create=True): 35def 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
56def _add_ssh_client(p):
57 _ssh_clients.append(p)
58
59def _remove_ssh_client(p):
60 try:
61 _ssh_clients.remove(p)
62 except ValueError:
63 pass
64
65def 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
55class _GitCall(object): 75class _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
25from urllib2 import urlopen, HTTPError 25from urllib2 import urlopen, HTTPError
26from error import GitError, UploadError 26from error import GitError, UploadError
27from trace import Trace 27from trace import Trace
28from git_command import GitCommand, _ssh_sock 28
29from git_command import GitCommand
30from git_command import ssh_sock
31from git_command import terminate_ssh_clients
29 32
30R_HEADS = 'refs/heads/' 33R_HEADS = 'refs/heads/'
31R_TAGS = 'refs/tags/' 34R_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
371def _open_ssh(host, port=None): 375def _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
412def close_ssh(): 443def 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)
diff --git a/git_ssh b/git_ssh
index 63aa63c2..b1ab521e 100755
--- a/git_ssh
+++ b/git_ssh
@@ -1,2 +1,2 @@
1#!/bin/sh 1#!/bin/sh
2exec ssh -o "ControlPath $REPO_SSH_SOCK" "$@" 2exec 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
16import os
16import sys 17import sys
17from time import time 18from time import time
18from trace import IsTrace 19from trace import IsTrace
19 20
21_NOT_TTY = not os.isatty(2)
22
20class Progress(object): 23class 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:
diff --git a/project.py b/project.py
index 1cea959e..fde98ad7 100644
--- a/project.py
+++ b/project.py
@@ -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:
diff --git a/repo b/repo
index 3a545cc6..cb6f6349 100755
--- a/repo
+++ b/repo
@@ -123,6 +123,9 @@ group.add_option('-m', '--manifest-name',
123group.add_option('--mirror', 123group.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')
126group.add_option('--reference',
127 dest='reference',
128 help='location of mirror directory', metavar='DIR')
126 129
127# Tool 130# Tool
128group = init_optparse.add_option_group('repo Version options') 131group = 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.
40The optional -b argument can be used to select the manifest branch 40The optional -b argument can be used to select the manifest branch
41to checkout and use. If no branch is specified, master is assumed. 41to checkout and use. If no branch is specified, master is assumed.
42 42
43The optional -m argument can be used to specify an alternate manifest
44to be used. If no manifest is specified, the manifest default.xml
45will be used.
46
47The --reference option can be used to point to a directory that
48has the content of a --mirror sync. This will make the working
49directory use as much data as possible from the local reference
50directory when fetching from the server. This will make the sync
51go a lot faster by reducing data traffic on the network.
52
53
43Switching Manifest Branches 54Switching 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
16import sys
17
18from command import Command
19from git_command import GitCommand
20from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB
21from error import GitError
22
23class 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
31the HEAD of the upstream history, useful when you have made commits in a topic
32branch 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
16from sync import Sync
17
18class Smartsync(Sync):
19 common = True
20 helpSummary = "Update working tree to the latest known good revision"
21 helpUsage = """
22%prog [<project>...]
23"""
24 helpDescription = """
25The '%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
17import os 17import os
18import re 18import re
19import shutil 19import shutil
20import socket
20import subprocess 21import subprocess
21import sys 22import sys
22import time 23import time
24import xmlrpclib
25
26try:
27 import threading as _threading
28except ImportError:
29 import dummy_threading as _threading
23 30
24from git_command import GIT 31from git_command import GIT
32from git_refs import R_HEADS
25from project import HEAD 33from project import HEAD
26from project import Project 34from project import Project
27from project import RemoteSpec 35from project import RemoteSpec
@@ -32,6 +40,7 @@ from project import SyncBuffer
32from progress import Progress 40from progress import Progress
33 41
34class Sync(Command, MirrorSafeCommand): 42class 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
57if the project is currently on a topic branch, but the manifest 66if the project is currently on a topic branch, but the manifest
58revision is temporarily needed. 67revision is temporarily needed.
59 68
69The -s/--smart-sync option can be used to sync to a known good
70build as specified by the manifest-server element in the current
71manifest.
72
73The -f/--force-broken option can be used to proceed with syncing
74other projects if a project sync fails.
75
60SSH Connections 76SSH 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": \
155uncommitted changes are present' % project.relpath 227uncommitted 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
262def _PostRepoUpgrade(manifest): 384def _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
16import copy
16import re 17import re
17import sys 18import sys
18 19
@@ -20,6 +21,17 @@ from command import InteractiveCommand
20from editor import Editor 21from editor import Editor
21from error import UploadError 22from error import UploadError
22 23
24UNUSUAL_COMMIT_THRESHOLD = 5
25
26def _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
23def _die(fmt, *args): 35def _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 = """
41The '%prog' command is used to send changes to the Gerrit Code 53The '%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
55new users. Users passed as --reviewers must already be registered 67new users. Users passed as --reviewers must already be registered
56with the code review system, or the upload will fail. 68with the code review system, or the upload will fail.
57 69
58If the --replace option (deprecated) is passed the user can designate
59which existing change(s) in Gerrit match up to the commits in the
60branch being uploaded. For each matched pair of change,commit the
61commit will be added as a new patch set, completely replacing the
62set of files and description associated with the change in Gerrit.
63
64Configuration 70Configuration
65------------- 71-------------
66 72
@@ -72,6 +78,19 @@ to "true" then repo will assume you always answer "y" at the prompt,
72and will not prompt you further. If it is set to "false" then repo 78and will not prompt you further. If it is set to "false" then repo
73will assume you always answer "n", and will abort. 79will assume you always answer "n", and will abort.
74 80
81review.URL.autocopy:
82
83To automatically copy a user or mailing list to all uploaded reviews,
84you can set a per-project or global Git option to do so. Specifically,
85review.URL.autocopy can be set to a comma separated list of reviewers
86who you always want copied on all uploads with a non-empty --re
87argument.
88
89review.URL.username:
90
91Override the username used to connect to Gerrit Code Review.
92By default the local part of the email address is used.
93
75The URL must match the review URL listed in the manifest XML file, 94The URL must match the review URL listed in the manifest XML file,
76or in the .git/config within the project. For example: 95or 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
85References 105References
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)