summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.pylintrc301
-rw-r--r--SUBMITTING_PATCHES12
-rw-r--r--command.py76
-rw-r--r--docs/manifest-format.txt15
-rw-r--r--manifest_xml.py110
-rw-r--r--project.py180
-rw-r--r--subcmds/forall.py4
-rw-r--r--subcmds/list.py38
-rw-r--r--subcmds/sync.py21
9 files changed, 691 insertions, 66 deletions
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 00000000..9f81ee15
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,301 @@
1# lint Python modules using external checkers.
2#
3# This is the main checker controling the other ones and the reports
4# generation. It is itself both a raw checker and an astng checker in order
5# to:
6# * handle message activation / deactivation at the module level
7# * handle some basic but necessary stats'data (number of classes, methods...)
8#
9[MASTER]
10
11# Specify a configuration file.
12#rcfile=
13
14# Python code to execute, usually for sys.path manipulation such as
15# pygtk.require().
16#init-hook=
17
18# Profiled execution.
19profile=no
20
21# Add <file or directory> to the black list. It should be a base name, not a
22# path. You may set this option multiple times.
23ignore=SVN
24
25# Pickle collected data for later comparisons.
26persistent=yes
27
28# Set the cache size for astng objects.
29cache-size=500
30
31# List of plugins (as comma separated values of python modules names) to load,
32# usually to register additional checkers.
33load-plugins=
34
35
36[MESSAGES CONTROL]
37
38# Enable only checker(s) with the given id(s). This option conflicts with the
39# disable-checker option
40#enable-checker=
41
42# Enable all checker(s) except those with the given id(s). This option
43# conflicts with the enable-checker option
44#disable-checker=
45
46# Enable all messages in the listed categories.
47#enable-msg-cat=
48
49# Disable all messages in the listed categories.
50#disable-msg-cat=
51
52# Enable the message(s) with the given id(s).
53enable=RP0004
54
55# Disable the message(s) with the given id(s).
56disable=R0903,R0912,R0913,R0914,R0915,W0141,C0111,C0103,C0323,C0322,C0324,W0603,W0703,R0911,C0301,C0302,R0902,R0904,W0142,W0212,E1101,E1103,R0201,W0201,W0122,W0232,W0311,RP0001,RP0003,RP0101,RP0002,RP0401,RP0701,RP0801
57
58[REPORTS]
59
60# set the output format. Available formats are text, parseable, colorized, msvs
61# (visual studio) and html
62output-format=text
63
64# Include message's id in output
65include-ids=yes
66
67# Put messages in a separate file for each module / package specified on the
68# command line instead of printing them on stdout. Reports (if any) will be
69# written in a file name "pylint_global.[txt|html]".
70files-output=no
71
72# Tells whether to display a full report or only the messages
73reports=yes
74
75# Python expression which should return a note less than 10 (10 is the highest
76# note).You have access to the variables errors warning, statement which
77# respectivly contain the number of errors / warnings messages and the total
78# number of statements analyzed. This is used by the global evaluation report
79# (R0004).
80evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
81
82# Add a comment according to your evaluation note. This is used by the global
83# evaluation report (R0004).
84comment=no
85
86# checks for
87# * unused variables / imports
88# * undefined variables
89# * redefinition of variable from builtins or from an outer scope
90# * use of variable before assigment
91#
92[VARIABLES]
93
94# Tells whether we should check for unused import in __init__ files.
95init-import=no
96
97# A regular expression matching names used for dummy variables (i.e. not used).
98dummy-variables-rgx=_|dummy
99
100# List of additional names supposed to be defined in builtins. Remember that
101# you should avoid to define new builtins when possible.
102additional-builtins=
103
104
105# try to find bugs in the code using type inference
106#
107[TYPECHECK]
108
109# Tells whether missing members accessed in mixin class should be ignored. A
110# mixin class is detected if its name ends with "mixin" (case insensitive).
111ignore-mixin-members=yes
112
113# List of classes names for which member attributes should not be checked
114# (useful for classes with attributes dynamicaly set).
115ignored-classes=SQLObject
116
117# When zope mode is activated, consider the acquired-members option to ignore
118# access to some undefined attributes.
119zope=no
120
121# List of members which are usually get through zope's acquisition mecanism and
122# so shouldn't trigger E0201 when accessed (need zope=yes to be considered).
123acquired-members=REQUEST,acl_users,aq_parent
124
125
126# checks for :
127# * doc strings
128# * modules / classes / functions / methods / arguments / variables name
129# * number of arguments, local variables, branchs, returns and statements in
130# functions, methods
131# * required module attributes
132# * dangerous default values as arguments
133# * redefinition of function / method / class
134# * uses of the global statement
135#
136[BASIC]
137
138# Required attributes for module, separated by a comma
139required-attributes=
140
141# Regular expression which should only match functions or classes name which do
142# not require a docstring
143no-docstring-rgx=_main|__.*__
144
145# Regular expression which should only match correct module names
146module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
147
148# Regular expression which should only match correct module level names
149const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))|(log)$
150
151# Regular expression which should only match correct class names
152class-rgx=[A-Z_][a-zA-Z0-9]+$
153
154# Regular expression which should only match correct function names
155function-rgx=[a-z_][a-z0-9_]{2,30}$
156
157# Regular expression which should only match correct method names
158method-rgx=[a-z_][a-z0-9_]{2,30}$
159
160# Regular expression which should only match correct instance attribute names
161attr-rgx=[a-z_][a-z0-9_]{2,30}$
162
163# Regular expression which should only match correct argument names
164argument-rgx=[a-z_][a-z0-9_]{2,30}$
165
166# Regular expression which should only match correct variable names
167variable-rgx=[a-z_][a-z0-9_]{2,30}$
168
169# Regular expression which should only match correct list comprehension /
170# generator expression variable names
171inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
172
173# Good variable names which should always be accepted, separated by a comma
174good-names=i,j,k,ex,Run,_,e,d1,d2,v,f,l,d
175
176# Bad variable names which should always be refused, separated by a comma
177bad-names=foo,bar,baz,toto,tutu,tata
178
179# List of builtins function names that should not be used, separated by a comma
180bad-functions=map,filter,apply,input
181
182
183# checks for sign of poor/misdesign:
184# * number of methods, attributes, local variables...
185# * size, complexity of functions, methods
186#
187[DESIGN]
188
189# Maximum number of arguments for function / method
190max-args=5
191
192# Maximum number of locals for function / method body
193max-locals=15
194
195# Maximum number of return / yield for function / method body
196max-returns=6
197
198# Maximum number of branch for function / method body
199max-branchs=12
200
201# Maximum number of statements in function / method body
202max-statements=50
203
204# Maximum number of parents for a class (see R0901).
205max-parents=7
206
207# Maximum number of attributes for a class (see R0902).
208max-attributes=20
209
210# Minimum number of public methods for a class (see R0903).
211min-public-methods=2
212
213# Maximum number of public methods for a class (see R0904).
214max-public-methods=30
215
216
217# checks for
218# * external modules dependencies
219# * relative / wildcard imports
220# * cyclic imports
221# * uses of deprecated modules
222#
223[IMPORTS]
224
225# Deprecated modules which should not be used, separated by a comma
226deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
227
228# Create a graph of every (i.e. internal and external) dependencies in the
229# given file (report R0402 must not be disabled)
230import-graph=
231
232# Create a graph of external dependencies in the given file (report R0402 must
233# not be disabled)
234ext-import-graph=
235
236# Create a graph of internal dependencies in the given file (report R0402 must
237# not be disabled)
238int-import-graph=
239
240
241# checks for :
242# * methods without self as first argument
243# * overridden methods signature
244# * access only to existant members via self
245# * attributes not defined in the __init__ method
246# * supported interfaces implementation
247# * unreachable code
248#
249[CLASSES]
250
251# List of interface methods to ignore, separated by a comma. This is used for
252# instance to not check methods defines in Zope's Interface base class.
253ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
254
255# List of method names used to declare (i.e. assign) instance attributes.
256defining-attr-methods=__init__,__new__,setUp
257
258
259# checks for similarities and duplicated code. This computation may be
260# memory / CPU intensive, so you should disable it if you experiments some
261# problems.
262#
263[SIMILARITIES]
264
265# Minimum lines number of a similarity.
266min-similarity-lines=4
267
268# Ignore comments when computing similarities.
269ignore-comments=yes
270
271# Ignore docstrings when computing similarities.
272ignore-docstrings=yes
273
274
275# checks for:
276# * warning notes in the code like FIXME, XXX
277# * PEP 263: source code with non ascii character but no encoding declaration
278#
279[MISCELLANEOUS]
280
281# List of note tags to take in consideration, separated by a comma.
282notes=FIXME,XXX,TODO
283
284
285# checks for :
286# * unauthorized constructions
287# * strict indentation
288# * line length
289# * use of <> instead of !=
290#
291[FORMAT]
292
293# Maximum number of characters on a single line.
294max-line-length=80
295
296# Maximum number of lines in a module
297max-module-lines=1000
298
299# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
300# tab). In repo it is 2 spaces.
301indent-string=' '
diff --git a/SUBMITTING_PATCHES b/SUBMITTING_PATCHES
index cba67416..50e2cf77 100644
--- a/SUBMITTING_PATCHES
+++ b/SUBMITTING_PATCHES
@@ -2,6 +2,7 @@ Short Version:
2 2
3 - Make small logical changes. 3 - Make small logical changes.
4 - Provide a meaningful commit message. 4 - Provide a meaningful commit message.
5 - Check for coding errors with pylint
5 - Make sure all code is under the Apache License, 2.0. 6 - Make sure all code is under the Apache License, 2.0.
6 - Publish your changes for review: 7 - Publish your changes for review:
7 8
@@ -33,7 +34,14 @@ If your description starts to get too long, that's a sign that you
33probably need to split up your commit to finer grained pieces. 34probably need to split up your commit to finer grained pieces.
34 35
35 36
36(2) Check the license 37(2) Check for coding errors with pylint
38
39Run pylint on changed modules using the provided configuration:
40
41 pylint --rcfile=.pylintrc file.py
42
43
44(3) Check the license
37 45
38repo is licensed under the Apache License, 2.0. 46repo is licensed under the Apache License, 2.0.
39 47
@@ -49,7 +57,7 @@ your patch. It is virtually impossible to remove a patch once it
49has been applied and pushed out. 57has been applied and pushed out.
50 58
51 59
52(3) Sending your patches. 60(4) Sending your patches.
53 61
54Do not email your patches to anyone. 62Do not email your patches to anyone.
55 63
diff --git a/command.py b/command.py
index 5a5f468f..d543e3a8 100644
--- a/command.py
+++ b/command.py
@@ -60,6 +60,32 @@ class Command(object):
60 """ 60 """
61 raise NotImplementedError 61 raise NotImplementedError
62 62
63 def _ResetPathToProjectMap(self, projects):
64 self._by_path = dict((p.worktree, p) for p in projects)
65
66 def _UpdatePathToProjectMap(self, project):
67 self._by_path[project.worktree] = project
68
69 def _GetProjectByPath(self, path):
70 project = None
71 if os.path.exists(path):
72 oldpath = None
73 while path \
74 and path != oldpath \
75 and path != self.manifest.topdir:
76 try:
77 project = self._by_path[path]
78 break
79 except KeyError:
80 oldpath = path
81 path = os.path.dirname(path)
82 else:
83 try:
84 project = self._by_path[path]
85 except KeyError:
86 pass
87 return project
88
63 def GetProjects(self, args, missing_ok=False): 89 def GetProjects(self, args, missing_ok=False):
64 """A list of projects that match the arguments. 90 """A list of projects that match the arguments.
65 """ 91 """
@@ -74,40 +100,38 @@ class Command(object):
74 groups = [x for x in re.split('[,\s]+', groups) if x] 100 groups = [x for x in re.split('[,\s]+', groups) if x]
75 101
76 if not args: 102 if not args:
77 for project in all_projects.values(): 103 all_projects_list = all_projects.values()
104 derived_projects = []
105 for project in all_projects_list:
106 if project.Registered:
107 # Do not search registered subproject for derived projects
108 # since its parent has been searched already
109 continue
110 derived_projects.extend(project.GetDerivedSubprojects())
111 all_projects_list.extend(derived_projects)
112 for project in all_projects_list:
78 if ((missing_ok or project.Exists) and 113 if ((missing_ok or project.Exists) and
79 project.MatchesGroups(groups)): 114 project.MatchesGroups(groups)):
80 result.append(project) 115 result.append(project)
81 else: 116 else:
82 by_path = None 117 self._ResetPathToProjectMap(all_projects.values())
83 118
84 for arg in args: 119 for arg in args:
85 project = all_projects.get(arg) 120 project = all_projects.get(arg)
86 121
87 if not project: 122 if not project:
88 path = os.path.abspath(arg).replace('\\', '/') 123 path = os.path.abspath(arg).replace('\\', '/')
89 124 project = self._GetProjectByPath(path)
90 if not by_path: 125
91 by_path = dict() 126 # If it's not a derived project, update path->project mapping and
92 for p in all_projects.values(): 127 # search again, as arg might actually point to a derived subproject.
93 by_path[p.worktree] = p 128 if project and not project.Derived:
94 129 search_again = False
95 if os.path.exists(path): 130 for subproject in project.GetDerivedSubprojects():
96 oldpath = None 131 self._UpdatePathToProjectMap(subproject)
97 while path \ 132 search_again = True
98 and path != oldpath \ 133 if search_again:
99 and path != self.manifest.topdir: 134 project = self._GetProjectByPath(path) or project
100 try:
101 project = by_path[path]
102 break
103 except KeyError:
104 oldpath = path
105 path = os.path.dirname(path)
106 else:
107 try:
108 project = by_path[path]
109 except KeyError:
110 pass
111 135
112 if not project: 136 if not project:
113 raise NoSuchProjectError(arg) 137 raise NoSuchProjectError(arg)
@@ -123,7 +147,7 @@ class Command(object):
123 result.sort(key=_getpath) 147 result.sort(key=_getpath)
124 return result 148 return result
125 149
126# pylint: disable-msg=W0223 150# pylint: disable=W0223
127# Pylint warns that the `InteractiveCommand` and `PagedCommand` classes do not 151# Pylint warns that the `InteractiveCommand` and `PagedCommand` classes do not
128# override method `Execute` which is abstract in `Command`. Since that method 152# override method `Execute` which is abstract in `Command`. Since that method
129# is always implemented in classes derived from `InteractiveCommand` and 153# is always implemented in classes derived from `InteractiveCommand` and
@@ -142,7 +166,7 @@ class PagedCommand(Command):
142 def WantPager(self, opt): 166 def WantPager(self, opt):
143 return True 167 return True
144 168
145# pylint: enable-msg=W0223 169# pylint: enable=W0223
146 170
147class MirrorSafeCommand(object): 171class MirrorSafeCommand(object):
148 """Command permits itself to run within a mirror, 172 """Command permits itself to run within a mirror,
diff --git a/docs/manifest-format.txt b/docs/manifest-format.txt
index f499868c..a36af67c 100644
--- a/docs/manifest-format.txt
+++ b/docs/manifest-format.txt
@@ -45,7 +45,8 @@ following DTD:
45 <!ELEMENT manifest-server (EMPTY)> 45 <!ELEMENT manifest-server (EMPTY)>
46 <!ATTLIST url CDATA #REQUIRED> 46 <!ATTLIST url CDATA #REQUIRED>
47 47
48 <!ELEMENT project (annotation?)> 48 <!ELEMENT project (annotation?,
49 project*)>
49 <!ATTLIST project name CDATA #REQUIRED> 50 <!ATTLIST project name CDATA #REQUIRED>
50 <!ATTLIST project path CDATA #IMPLIED> 51 <!ATTLIST project path CDATA #IMPLIED>
51 <!ATTLIST project remote IDREF #IMPLIED> 52 <!ATTLIST project remote IDREF #IMPLIED>
@@ -152,7 +153,10 @@ Element project
152 153
153One or more project elements may be specified. Each element 154One or more project elements may be specified. Each element
154describes a single Git repository to be cloned into the repo 155describes a single Git repository to be cloned into the repo
155client workspace. 156client workspace. You may specify Git-submodules by creating a
157nested project. Git-submodules will be automatically
158recognized and inherit their parent's attributes, but those
159may be overridden by an explicitly specified project element.
156 160
157Attribute `name`: A unique name for this project. The project's 161Attribute `name`: A unique name for this project. The project's
158name is appended onto its remote's fetch URL to generate the actual 162name is appended onto its remote's fetch URL to generate the actual
@@ -163,7 +167,8 @@ URL to configure the Git remote with. The URL gets formed as:
163where ${remote_fetch} is the remote's fetch attribute and 167where ${remote_fetch} is the remote's fetch attribute and
164${project_name} is the project's name attribute. The suffix ".git" 168${project_name} is the project's name attribute. The suffix ".git"
165is always appended as repo assumes the upstream is a forest of 169is always appended as repo assumes the upstream is a forest of
166bare Git repositories. 170bare Git repositories. If the project has a parent element, its
171name will be prefixed by the parent's.
167 172
168The project name must match the name Gerrit knows, if Gerrit is 173The project name must match the name Gerrit knows, if Gerrit is
169being used for code reviews. 174being used for code reviews.
@@ -171,6 +176,8 @@ being used for code reviews.
171Attribute `path`: An optional path relative to the top directory 176Attribute `path`: An optional path relative to the top directory
172of the repo client where the Git working directory for this project 177of the repo client where the Git working directory for this project
173should be placed. If not supplied the project name is used. 178should be placed. If not supplied the project name is used.
179If the project has a parent element, its path will be prefixed
180by the parent's.
174 181
175Attribute `remote`: Name of a previously defined remote element. 182Attribute `remote`: Name of a previously defined remote element.
176If not supplied the remote given by the default element is used. 183If not supplied the remote given by the default element is used.
@@ -190,6 +197,8 @@ its name:`name` and path:`path`. E.g. for
190definition is implicitly in the following manifest groups: 197definition is implicitly in the following manifest groups:
191default, name:monkeys, and path:barrel-of. If you place a project in the 198default, name:monkeys, and path:barrel-of. If you place a project in the
192group "notdefault", it will not be automatically downloaded by repo. 199group "notdefault", it will not be automatically downloaded by repo.
200If the project has a parent element, the `name` and `path` here
201are the prefixed ones.
193 202
194Element annotation 203Element annotation
195------------------ 204------------------
diff --git a/manifest_xml.py b/manifest_xml.py
index ab842f3f..11e4ee54 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -180,20 +180,25 @@ class XmlManifest(object):
180 root.appendChild(e) 180 root.appendChild(e)
181 root.appendChild(doc.createTextNode('')) 181 root.appendChild(doc.createTextNode(''))
182 182
183 sort_projects = list(self.projects.keys()) 183 def output_projects(parent, parent_node, projects):
184 sort_projects.sort() 184 for p in projects:
185 185 output_project(parent, parent_node, self.projects[p])
186 for p in sort_projects:
187 p = self.projects[p]
188 186
187 def output_project(parent, parent_node, p):
189 if not p.MatchesGroups(groups): 188 if not p.MatchesGroups(groups):
190 continue 189 return
190
191 name = p.name
192 relpath = p.relpath
193 if parent:
194 name = self._UnjoinName(parent.name, name)
195 relpath = self._UnjoinRelpath(parent.relpath, relpath)
191 196
192 e = doc.createElement('project') 197 e = doc.createElement('project')
193 root.appendChild(e) 198 parent_node.appendChild(e)
194 e.setAttribute('name', p.name) 199 e.setAttribute('name', name)
195 if p.relpath != p.name: 200 if relpath != name:
196 e.setAttribute('path', p.relpath) 201 e.setAttribute('path', relpath)
197 if not d.remote or p.remote.name != d.remote.name: 202 if not d.remote or p.remote.name != d.remote.name:
198 e.setAttribute('remote', p.remote.name) 203 e.setAttribute('remote', p.remote.name)
199 if peg_rev: 204 if peg_rev:
@@ -231,6 +236,16 @@ class XmlManifest(object):
231 if p.sync_c: 236 if p.sync_c:
232 e.setAttribute('sync-c', 'true') 237 e.setAttribute('sync-c', 'true')
233 238
239 if p.subprojects:
240 sort_projects = [subp.name for subp in p.subprojects]
241 sort_projects.sort()
242 output_projects(p, e, sort_projects)
243
244 sort_projects = [key for key in self.projects.keys()
245 if not self.projects[key].parent]
246 sort_projects.sort()
247 output_projects(None, root, sort_projects)
248
234 if self._repo_hooks_project: 249 if self._repo_hooks_project:
235 root.appendChild(doc.createTextNode('')) 250 root.appendChild(doc.createTextNode(''))
236 e = doc.createElement('repo-hooks') 251 e = doc.createElement('repo-hooks')
@@ -321,7 +336,7 @@ class XmlManifest(object):
321 raise ManifestParseError("no <manifest> in %s" % (path,)) 336 raise ManifestParseError("no <manifest> in %s" % (path,))
322 337
323 nodes = [] 338 nodes = []
324 for node in manifest.childNodes: # pylint:disable-msg=W0631 339 for node in manifest.childNodes: # pylint:disable=W0631
325 # We only get here if manifest is initialised 340 # We only get here if manifest is initialised
326 if node.nodeName == 'include': 341 if node.nodeName == 'include':
327 name = self._reqatt(node, 'name') 342 name = self._reqatt(node, 'name')
@@ -383,11 +398,15 @@ class XmlManifest(object):
383 for node in itertools.chain(*node_list): 398 for node in itertools.chain(*node_list):
384 if node.nodeName == 'project': 399 if node.nodeName == 'project':
385 project = self._ParseProject(node) 400 project = self._ParseProject(node)
386 if self._projects.get(project.name): 401 def recursively_add_projects(project):
387 raise ManifestParseError( 402 if self._projects.get(project.name):
388 'duplicate project %s in %s' % 403 raise ManifestParseError(
389 (project.name, self.manifestFile)) 404 'duplicate project %s in %s' %
390 self._projects[project.name] = project 405 (project.name, self.manifestFile))
406 self._projects[project.name] = project
407 for subproject in project.subprojects:
408 recursively_add_projects(subproject)
409 recursively_add_projects(project)
391 if node.nodeName == 'repo-hooks': 410 if node.nodeName == 'repo-hooks':
392 # Get the name of the project and the (space-separated) list of enabled. 411 # Get the name of the project and the (space-separated) list of enabled.
393 repo_hooks_project = self._reqatt(node, 'in-project') 412 repo_hooks_project = self._reqatt(node, 'in-project')
@@ -537,11 +556,19 @@ class XmlManifest(object):
537 556
538 return '\n'.join(cleanLines) 557 return '\n'.join(cleanLines)
539 558
540 def _ParseProject(self, node): 559 def _JoinName(self, parent_name, name):
560 return os.path.join(parent_name, name)
561
562 def _UnjoinName(self, parent_name, name):
563 return os.path.relpath(name, parent_name)
564
565 def _ParseProject(self, node, parent = None):
541 """ 566 """
542 reads a <project> element from the manifest file 567 reads a <project> element from the manifest file
543 """ 568 """
544 name = self._reqatt(node, 'name') 569 name = self._reqatt(node, 'name')
570 if parent:
571 name = self._JoinName(parent.name, name)
545 572
546 remote = self._get_remote(node) 573 remote = self._get_remote(node)
547 if remote is None: 574 if remote is None:
@@ -586,37 +613,66 @@ class XmlManifest(object):
586 groups = node.getAttribute('groups') 613 groups = node.getAttribute('groups')
587 groups = [x for x in re.split('[,\s]+', groups) if x] 614 groups = [x for x in re.split('[,\s]+', groups) if x]
588 615
589 default_groups = ['all', 'name:%s' % name, 'path:%s' % path] 616 if parent is None:
590 groups.extend(set(default_groups).difference(groups)) 617 relpath, worktree, gitdir = self.GetProjectPaths(name, path)
591
592 if self.IsMirror:
593 worktree = None
594 gitdir = os.path.join(self.topdir, '%s.git' % name)
595 else: 618 else:
596 worktree = os.path.join(self.topdir, path).replace('\\', '/') 619 relpath, worktree, gitdir = self.GetSubprojectPaths(parent, path)
597 gitdir = os.path.join(self.repodir, 'projects/%s.git' % path) 620
621 default_groups = ['all', 'name:%s' % name, 'path:%s' % relpath]
622 groups.extend(set(default_groups).difference(groups))
598 623
599 project = Project(manifest = self, 624 project = Project(manifest = self,
600 name = name, 625 name = name,
601 remote = remote.ToRemoteSpec(name), 626 remote = remote.ToRemoteSpec(name),
602 gitdir = gitdir, 627 gitdir = gitdir,
603 worktree = worktree, 628 worktree = worktree,
604 relpath = path, 629 relpath = relpath,
605 revisionExpr = revisionExpr, 630 revisionExpr = revisionExpr,
606 revisionId = None, 631 revisionId = None,
607 rebase = rebase, 632 rebase = rebase,
608 groups = groups, 633 groups = groups,
609 sync_c = sync_c, 634 sync_c = sync_c,
610 upstream = upstream) 635 upstream = upstream,
636 parent = parent)
611 637
612 for n in node.childNodes: 638 for n in node.childNodes:
613 if n.nodeName == 'copyfile': 639 if n.nodeName == 'copyfile':
614 self._ParseCopyFile(project, n) 640 self._ParseCopyFile(project, n)
615 if n.nodeName == 'annotation': 641 if n.nodeName == 'annotation':
616 self._ParseAnnotation(project, n) 642 self._ParseAnnotation(project, n)
643 if n.nodeName == 'project':
644 project.subprojects.append(self._ParseProject(n, parent = project))
617 645
618 return project 646 return project
619 647
648 def GetProjectPaths(self, name, path):
649 relpath = path
650 if self.IsMirror:
651 worktree = None
652 gitdir = os.path.join(self.topdir, '%s.git' % name)
653 else:
654 worktree = os.path.join(self.topdir, path).replace('\\', '/')
655 gitdir = os.path.join(self.repodir, 'projects', '%s.git' % path)
656 return relpath, worktree, gitdir
657
658 def GetSubprojectName(self, parent, submodule_path):
659 return os.path.join(parent.name, submodule_path)
660
661 def _JoinRelpath(self, parent_relpath, relpath):
662 return os.path.join(parent_relpath, relpath)
663
664 def _UnjoinRelpath(self, parent_relpath, relpath):
665 return os.path.relpath(relpath, parent_relpath)
666
667 def GetSubprojectPaths(self, parent, path):
668 relpath = self._JoinRelpath(parent.relpath, path)
669 gitdir = os.path.join(parent.gitdir, 'subprojects', '%s.git' % path)
670 if self.IsMirror:
671 worktree = None
672 else:
673 worktree = os.path.join(parent.worktree, path).replace('\\', '/')
674 return relpath, worktree, gitdir
675
620 def _ParseCopyFile(self, project, node): 676 def _ParseCopyFile(self, project, node):
621 src = self._reqatt(node, 'src') 677 src = self._reqatt(node, 'src')
622 dest = self._reqatt(node, 'dest') 678 dest = self._reqatt(node, 'dest')
diff --git a/project.py b/project.py
index 96feb94b..a0889b3f 100644
--- a/project.py
+++ b/project.py
@@ -22,6 +22,7 @@ import shutil
22import stat 22import stat
23import subprocess 23import subprocess
24import sys 24import sys
25import tempfile
25import time 26import time
26 27
27from color import Coloring 28from color import Coloring
@@ -484,7 +485,28 @@ class Project(object):
484 rebase = True, 485 rebase = True,
485 groups = None, 486 groups = None,
486 sync_c = False, 487 sync_c = False,
487 upstream = None): 488 upstream = None,
489 parent = None,
490 is_derived = False):
491 """Init a Project object.
492
493 Args:
494 manifest: The XmlManifest object.
495 name: The `name` attribute of manifest.xml's project element.
496 remote: RemoteSpec object specifying its remote's properties.
497 gitdir: Absolute path of git directory.
498 worktree: Absolute path of git working tree.
499 relpath: Relative path of git working tree to repo's top directory.
500 revisionExpr: The `revision` attribute of manifest.xml's project element.
501 revisionId: git commit id for checking out.
502 rebase: The `rebase` attribute of manifest.xml's project element.
503 groups: The `groups` attribute of manifest.xml's project element.
504 sync_c: The `sync-c` attribute of manifest.xml's project element.
505 upstream: The `upstream` attribute of manifest.xml's project element.
506 parent: The parent Project object.
507 is_derived: False if the project was explicitly defined in the manifest;
508 True if the project is a discovered submodule.
509 """
488 self.manifest = manifest 510 self.manifest = manifest
489 self.name = name 511 self.name = name
490 self.remote = remote 512 self.remote = remote
@@ -507,6 +529,9 @@ class Project(object):
507 self.groups = groups 529 self.groups = groups
508 self.sync_c = sync_c 530 self.sync_c = sync_c
509 self.upstream = upstream 531 self.upstream = upstream
532 self.parent = parent
533 self.is_derived = is_derived
534 self.subprojects = []
510 535
511 self.snapshots = {} 536 self.snapshots = {}
512 self.copyfiles = [] 537 self.copyfiles = []
@@ -527,6 +552,14 @@ class Project(object):
527 self.enabled_repo_hooks = [] 552 self.enabled_repo_hooks = []
528 553
529 @property 554 @property
555 def Registered(self):
556 return self.parent and not self.is_derived
557
558 @property
559 def Derived(self):
560 return self.is_derived
561
562 @property
530 def Exists(self): 563 def Exists(self):
531 return os.path.isdir(self.gitdir) 564 return os.path.isdir(self.gitdir)
532 565
@@ -1370,6 +1403,151 @@ class Project(object):
1370 return kept 1403 return kept
1371 1404
1372 1405
1406## Submodule Management ##
1407
1408 def GetRegisteredSubprojects(self):
1409 result = []
1410 def rec(subprojects):
1411 if not subprojects:
1412 return
1413 result.extend(subprojects)
1414 for p in subprojects:
1415 rec(p.subprojects)
1416 rec(self.subprojects)
1417 return result
1418
1419 def _GetSubmodules(self):
1420 # Unfortunately we cannot call `git submodule status --recursive` here
1421 # because the working tree might not exist yet, and it cannot be used
1422 # without a working tree in its current implementation.
1423
1424 def get_submodules(gitdir, rev, path):
1425 # Parse .gitmodules for submodule sub_paths and sub_urls
1426 sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
1427 if not sub_paths:
1428 return []
1429 # Run `git ls-tree` to read SHAs of submodule object, which happen to be
1430 # revision of submodule repository
1431 sub_revs = git_ls_tree(gitdir, rev, sub_paths)
1432 submodules = []
1433 for sub_path, sub_url in zip(sub_paths, sub_urls):
1434 try:
1435 sub_rev = sub_revs[sub_path]
1436 except KeyError:
1437 # Ignore non-exist submodules
1438 continue
1439 sub_gitdir = self.manifest.GetSubprojectPaths(self, sub_path)[2]
1440 submodules.append((sub_rev, sub_path, sub_url))
1441 return submodules
1442
1443 re_path = re.compile(r'submodule.(\w+).path')
1444 re_url = re.compile(r'submodule.(\w+).url')
1445 def parse_gitmodules(gitdir, rev):
1446 cmd = ['cat-file', 'blob', '%s:.gitmodules' % rev]
1447 try:
1448 p = GitCommand(None, cmd, capture_stdout = True, capture_stderr = True,
1449 bare = True, gitdir = gitdir)
1450 except GitError as e:
1451 return [], []
1452 if p.Wait() != 0:
1453 return [], []
1454
1455 gitmodules_lines = []
1456 fd, temp_gitmodules_path = tempfile.mkstemp()
1457 try:
1458 os.write(fd, p.stdout)
1459 os.close(fd)
1460 cmd = ['config', '--file', temp_gitmodules_path, '--list']
1461 p = GitCommand(None, cmd, capture_stdout = True, capture_stderr = True,
1462 bare = True, gitdir = gitdir)
1463 if p.Wait() != 0:
1464 return [], []
1465 gitmodules_lines = p.stdout.split('\n')
1466 except GitError as e:
1467 return [], []
1468 finally:
1469 os.remove(temp_gitmodules_path)
1470
1471 names = set()
1472 paths = {}
1473 urls = {}
1474 for line in gitmodules_lines:
1475 if not line:
1476 continue
1477 key, value = line.split('=')
1478 m = re_path.match(key)
1479 if m:
1480 names.add(m.group(1))
1481 paths[m.group(1)] = value
1482 continue
1483 m = re_url.match(key)
1484 if m:
1485 names.add(m.group(1))
1486 urls[m.group(1)] = value
1487 continue
1488 names = sorted(names)
1489 return [paths[name] for name in names], [urls[name] for name in names]
1490
1491 def git_ls_tree(gitdir, rev, paths):
1492 cmd = ['ls-tree', rev, '--']
1493 cmd.extend(paths)
1494 try:
1495 p = GitCommand(None, cmd, capture_stdout = True, capture_stderr = True,
1496 bare = True, gitdir = gitdir)
1497 except GitError:
1498 return []
1499 if p.Wait() != 0:
1500 return []
1501 objects = {}
1502 for line in p.stdout.split('\n'):
1503 if not line.strip():
1504 continue
1505 object_rev, object_path = line.split()[2:4]
1506 objects[object_path] = object_rev
1507 return objects
1508
1509 try:
1510 rev = self.GetRevisionId()
1511 except GitError:
1512 return []
1513 return get_submodules(self.gitdir, rev, '')
1514
1515 def GetDerivedSubprojects(self):
1516 result = []
1517 if not self.Exists:
1518 # If git repo does not exist yet, querying its submodules will
1519 # mess up its states; so return here.
1520 return result
1521 for rev, path, url in self._GetSubmodules():
1522 name = self.manifest.GetSubprojectName(self, path)
1523 project = self.manifest.projects.get(name)
1524 if project and project.Registered:
1525 # If it has been registered, skip it because we are searching
1526 # derived subprojects, but search for its derived subprojects.
1527 result.extend(project.GetDerivedSubprojects())
1528 continue
1529 relpath, worktree, gitdir = self.manifest.GetSubprojectPaths(self, path)
1530 remote = RemoteSpec(self.remote.name,
1531 url = url,
1532 review = self.remote.review)
1533 subproject = Project(manifest = self.manifest,
1534 name = name,
1535 remote = remote,
1536 gitdir = gitdir,
1537 worktree = worktree,
1538 relpath = relpath,
1539 revisionExpr = self.revisionExpr,
1540 revisionId = rev,
1541 rebase = self.rebase,
1542 groups = self.groups,
1543 sync_c = self.sync_c,
1544 parent = self,
1545 is_derived = True)
1546 result.append(subproject)
1547 result.extend(subproject.GetDerivedSubprojects())
1548 return result
1549
1550
1373## Direct Git Commands ## 1551## Direct Git Commands ##
1374 1552
1375 def _RemoteFetch(self, name=None, 1553 def _RemoteFetch(self, name=None,
diff --git a/subcmds/forall.py b/subcmds/forall.py
index 2ece95ed..b633b7d4 100644
--- a/subcmds/forall.py
+++ b/subcmds/forall.py
@@ -143,14 +143,14 @@ terminal and are not redirected.
143 break 143 break
144 else: 144 else:
145 cn = None 145 cn = None
146 # pylint: disable-msg=W0631 146 # pylint: disable=W0631
147 if cn and cn in _CAN_COLOR: 147 if cn and cn in _CAN_COLOR:
148 class ColorCmd(Coloring): 148 class ColorCmd(Coloring):
149 def __init__(self, config, cmd): 149 def __init__(self, config, cmd):
150 Coloring.__init__(self, config, cmd) 150 Coloring.__init__(self, config, cmd)
151 if ColorCmd(self.manifest.manifestProject.config, cn).is_on: 151 if ColorCmd(self.manifest.manifestProject.config, cn).is_on:
152 cmd.insert(cmd.index(cn) + 1, '--color') 152 cmd.insert(cmd.index(cn) + 1, '--color')
153 # pylint: enable-msg=W0631 153 # pylint: enable=W0631
154 154
155 mirror = self.manifest.IsMirror 155 mirror = self.manifest.IsMirror
156 out = ForallColoring(self.manifest.manifestProject.config) 156 out = ForallColoring(self.manifest.manifestProject.config)
diff --git a/subcmds/list.py b/subcmds/list.py
index 2be82570..6058a755 100644
--- a/subcmds/list.py
+++ b/subcmds/list.py
@@ -13,13 +13,16 @@
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 re
17
16from command import Command, MirrorSafeCommand 18from command import Command, MirrorSafeCommand
17 19
18class List(Command, MirrorSafeCommand): 20class List(Command, MirrorSafeCommand):
19 common = True 21 common = True
20 helpSummary = "List projects and their associated directories" 22 helpSummary = "List projects and their associated directories"
21 helpUsage = """ 23 helpUsage = """
22%prog [<project>...] 24%prog [-f] [<project>...]
25%prog [-f] -r str1 [str2]..."
23""" 26"""
24 helpDescription = """ 27 helpDescription = """
25List all projects; pass '.' to list the project for the cwd. 28List all projects; pass '.' to list the project for the cwd.
@@ -27,6 +30,14 @@ List all projects; pass '.' to list the project for the cwd.
27This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'. 30This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
28""" 31"""
29 32
33 def _Options(self, p, show_smart=True):
34 p.add_option('-r', '--regex',
35 dest='regex', action='store_true',
36 help="Filter the project list based on regex or wildcard matching of strings")
37 p.add_option('-f', '--fullpath',
38 dest='fullpath', action='store_true',
39 help="Display the full work tree path instead of the relative path")
40
30 def Execute(self, opt, args): 41 def Execute(self, opt, args):
31 """List all projects and the associated directories. 42 """List all projects and the associated directories.
32 43
@@ -35,14 +46,33 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
35 discoverable. 46 discoverable.
36 47
37 Args: 48 Args:
38 opt: The options. We don't take any. 49 opt: The options.
39 args: Positional args. Can be a list of projects to list, or empty. 50 args: Positional args. Can be a list of projects to list, or empty.
40 """ 51 """
41 projects = self.GetProjects(args) 52 if not opt.regex:
53 projects = self.GetProjects(args)
54 else:
55 projects = self.FindProjects(args)
56
57 def _getpath(x):
58 if opt.fullpath:
59 return x.worktree
60 return x.relpath
42 61
43 lines = [] 62 lines = []
44 for project in projects: 63 for project in projects:
45 lines.append("%s : %s" % (project.relpath, project.name)) 64 lines.append("%s : %s" % (_getpath(project), project.name))
46 65
47 lines.sort() 66 lines.sort()
48 print '\n'.join(lines) 67 print '\n'.join(lines)
68
69 def FindProjects(self, args):
70 result = []
71 for project in self.GetProjects(''):
72 for arg in args:
73 pattern = re.compile(r'%s' % arg, re.IGNORECASE)
74 if pattern.search(project.name) or pattern.search(project.relpath):
75 result.append(project)
76 break
77 result.sort(key=lambda project: project.relpath)
78 return result
diff --git a/subcmds/sync.py b/subcmds/sync.py
index d16605ff..28c154a0 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -503,12 +503,31 @@ uncommitted changes are present' % project.relpath
503 to_fetch.append(rp) 503 to_fetch.append(rp)
504 to_fetch.extend(all_projects) 504 to_fetch.extend(all_projects)
505 505
506 self._Fetch(to_fetch, opt) 506 fetched = self._Fetch(to_fetch, opt)
507 _PostRepoFetch(rp, opt.no_repo_verify) 507 _PostRepoFetch(rp, opt.no_repo_verify)
508 if opt.network_only: 508 if opt.network_only:
509 # bail out now; the rest touches the working tree 509 # bail out now; the rest touches the working tree
510 return 510 return
511 511
512 # Iteratively fetch missing and/or nested unregistered submodules
513 previously_missing_set = set()
514 while True:
515 self.manifest._Unload()
516 all = self.GetProjects(args, missing_ok=True)
517 missing = []
518 for project in all:
519 if project.gitdir not in fetched:
520 missing.append(project)
521 if not missing:
522 break
523 # Stop us from non-stopped fetching actually-missing repos: If set of
524 # missing repos has not been changed from last fetch, we break.
525 missing_set = set(p.name for p in missing)
526 if previously_missing_set == missing_set:
527 break
528 previously_missing_set = missing_set
529 fetched.update(self._Fetch(missing, opt))
530
512 if self.manifest.IsMirror: 531 if self.manifest.IsMirror:
513 # bail out now, we have no working tree 532 # bail out now, we have no working tree
514 return 533 return