summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Frysinger <vapier@google.com>2020-09-06 14:53:18 -0400
committerMike Frysinger <vapier@google.com>2020-11-18 19:10:57 +0000
commit8c1e9cbef161f2ff12dadbacf26affd23876fde9 (patch)
treefcfdc404568a2d8dbc9edd9ec99ecdd758f8c424
parenta488af5ea5c53dd7cf2c90a751e77cc4ba87b7c3 (diff)
downloadgit-repo-8c1e9cbef161f2ff12dadbacf26affd23876fde9.tar.gz
manifest_xml: refactor manifest parsing from client management
We conflate the manifest & parsing logic with the management of the repo client checkout in a single class. This makes testing just one part (the manifest parsing) hard as it requires a full checkout too. Start splitting the two apart into separate classes to make it easy to reason about & test. Change-Id: Iaf897c93db9c724baba6044bfe7a589c024523b2 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/288682 Reviewed-by: Michael Mortensen <mmortensen@google.com> Tested-by: Mike Frysinger <vapier@google.com>
-rwxr-xr-xmain.py13
-rw-r--r--manifest_xml.py80
-rw-r--r--project.py6
-rw-r--r--subcmds/diffmanifests.py8
-rw-r--r--subcmds/help.py4
-rw-r--r--subcmds/info.py2
-rw-r--r--subcmds/init.py4
-rw-r--r--subcmds/status.py2
-rw-r--r--tests/test_manifest_xml.py89
9 files changed, 164 insertions, 44 deletions
diff --git a/main.py b/main.py
index cd62793d..e152de4f 100755
--- a/main.py
+++ b/main.py
@@ -63,7 +63,7 @@ from error import NoManifestException
63from error import NoSuchProjectError 63from error import NoSuchProjectError
64from error import RepoChangedException 64from error import RepoChangedException
65import gitc_utils 65import gitc_utils
66from manifest_xml import GitcManifest, XmlManifest 66from manifest_xml import GitcClient, RepoClient
67from pager import RunPager, TerminatePager 67from pager import RunPager, TerminatePager
68from wrapper import WrapperPath, Wrapper 68from wrapper import WrapperPath, Wrapper
69 69
@@ -212,14 +212,15 @@ class _Repo(object):
212 return 1 212 return 1
213 213
214 cmd.repodir = self.repodir 214 cmd.repodir = self.repodir
215 cmd.manifest = XmlManifest(cmd.repodir) 215 cmd.client = RepoClient(cmd.repodir)
216 cmd.manifest = cmd.client.manifest
216 cmd.gitc_manifest = None 217 cmd.gitc_manifest = None
217 gitc_client_name = gitc_utils.parse_clientdir(os.getcwd()) 218 gitc_client_name = gitc_utils.parse_clientdir(os.getcwd())
218 if gitc_client_name: 219 if gitc_client_name:
219 cmd.gitc_manifest = GitcManifest(cmd.repodir, gitc_client_name) 220 cmd.gitc_manifest = GitcClient(cmd.repodir, gitc_client_name)
220 cmd.manifest.isGitcClient = True 221 cmd.client.isGitcClient = True
221 222
222 Editor.globalConfig = cmd.manifest.globalConfig 223 Editor.globalConfig = cmd.client.globalConfig
223 224
224 if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror: 225 if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:
225 print("fatal: '%s' requires a working directory" % name, 226 print("fatal: '%s' requires a working directory" % name,
@@ -247,7 +248,7 @@ class _Repo(object):
247 return 1 248 return 1
248 249
249 if gopts.pager is not False and not isinstance(cmd, InteractiveCommand): 250 if gopts.pager is not False and not isinstance(cmd, InteractiveCommand):
250 config = cmd.manifest.globalConfig 251 config = cmd.client.globalConfig
251 if gopts.pager: 252 if gopts.pager:
252 use_pager = True 253 use_pager = True
253 else: 254 else:
diff --git a/manifest_xml.py b/manifest_xml.py
index e1ef330f..95c67d73 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -187,12 +187,24 @@ class _XmlRemote(object):
187class XmlManifest(object): 187class XmlManifest(object):
188 """manages the repo configuration file""" 188 """manages the repo configuration file"""
189 189
190 def __init__(self, repodir): 190 def __init__(self, repodir, manifest_file, local_manifests=None):
191 """Initialize.
192
193 Args:
194 repodir: Path to the .repo/ dir for holding all internal checkout state.
195 It must be in the top directory of the repo client checkout.
196 manifest_file: Full path to the manifest file to parse. This will usually
197 be |repodir|/|MANIFEST_FILE_NAME|.
198 local_manifests: Full path to the directory of local override manifests.
199 This will usually be |repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
200 """
201 # TODO(vapier): Move this out of this class.
202 self.globalConfig = GitConfig.ForUser()
203
191 self.repodir = os.path.abspath(repodir) 204 self.repodir = os.path.abspath(repodir)
192 self.topdir = os.path.dirname(self.repodir) 205 self.topdir = os.path.dirname(self.repodir)
193 self.manifestFile = os.path.join(self.repodir, MANIFEST_FILE_NAME) 206 self.manifestFile = manifest_file
194 self.globalConfig = GitConfig.ForUser() 207 self.local_manifests = local_manifests
195 self.isGitcClient = False
196 self._load_local_manifests = True 208 self._load_local_manifests = True
197 209
198 self.repoProject = MetaProject(self, 'repo', 210 self.repoProject = MetaProject(self, 'repo',
@@ -602,20 +614,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
602 nodes.append(self._ParseManifestXml(self.manifestFile, 614 nodes.append(self._ParseManifestXml(self.manifestFile,
603 self.manifestProject.worktree)) 615 self.manifestProject.worktree))
604 616
605 if self._load_local_manifests: 617 if self._load_local_manifests and self.local_manifests:
606 if os.path.exists(os.path.join(self.repodir, LOCAL_MANIFEST_NAME)):
607 print('error: %s is not supported; put local manifests in `%s`'
608 'instead' % (LOCAL_MANIFEST_NAME,
609 os.path.join(self.repodir, LOCAL_MANIFESTS_DIR_NAME)),
610 file=sys.stderr)
611 sys.exit(1)
612
613 local_dir = os.path.abspath(os.path.join(self.repodir,
614 LOCAL_MANIFESTS_DIR_NAME))
615 try: 618 try:
616 for local_file in sorted(platform_utils.listdir(local_dir)): 619 for local_file in sorted(platform_utils.listdir(self.local_manifests)):
617 if local_file.endswith('.xml'): 620 if local_file.endswith('.xml'):
618 local = os.path.join(local_dir, local_file) 621 local = os.path.join(self.local_manifests, local_file)
619 nodes.append(self._ParseManifestXml(local, self.repodir)) 622 nodes.append(self._ParseManifestXml(local, self.repodir))
620 except OSError: 623 except OSError:
621 pass 624 pass
@@ -1253,15 +1256,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
1253 1256
1254 1257
1255class GitcManifest(XmlManifest): 1258class GitcManifest(XmlManifest):
1256 1259 """Parser for GitC (git-in-the-cloud) manifests."""
1257 def __init__(self, repodir, gitc_client_name):
1258 """Initialize the GitcManifest object."""
1259 super(GitcManifest, self).__init__(repodir)
1260 self.isGitcClient = True
1261 self.gitc_client_name = gitc_client_name
1262 self.gitc_client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(),
1263 gitc_client_name)
1264 self.manifestFile = os.path.join(self.gitc_client_dir, '.manifest')
1265 1260
1266 def _ParseProject(self, node, parent=None): 1261 def _ParseProject(self, node, parent=None):
1267 """Override _ParseProject and add support for GITC specific attributes.""" 1262 """Override _ParseProject and add support for GITC specific attributes."""
@@ -1272,3 +1267,38 @@ class GitcManifest(XmlManifest):
1272 """Output GITC Specific Project attributes""" 1267 """Output GITC Specific Project attributes"""
1273 if p.old_revision: 1268 if p.old_revision:
1274 e.setAttribute('old-revision', str(p.old_revision)) 1269 e.setAttribute('old-revision', str(p.old_revision))
1270
1271
1272class RepoClient(XmlManifest):
1273 """Manages a repo client checkout."""
1274
1275 def __init__(self, repodir, manifest_file=None):
1276 self.isGitcClient = False
1277
1278 if os.path.exists(os.path.join(repodir, LOCAL_MANIFEST_NAME)):
1279 print('error: %s is not supported; put local manifests in `%s` instead' %
1280 (LOCAL_MANIFEST_NAME, os.path.join(repodir, LOCAL_MANIFESTS_DIR_NAME)),
1281 file=sys.stderr)
1282 sys.exit(1)
1283
1284 if manifest_file is None:
1285 manifest_file = os.path.join(repodir, MANIFEST_FILE_NAME)
1286 local_manifests = os.path.abspath(os.path.join(repodir, LOCAL_MANIFESTS_DIR_NAME))
1287 super(RepoClient, self).__init__(repodir, manifest_file, local_manifests)
1288
1289 # TODO: Completely separate manifest logic out of the client.
1290 self.manifest = self
1291
1292
1293class GitcClient(RepoClient, GitcManifest):
1294 """Manages a GitC client checkout."""
1295
1296 def __init__(self, repodir, gitc_client_name):
1297 """Initialize the GitcManifest object."""
1298 self.gitc_client_name = gitc_client_name
1299 self.gitc_client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(),
1300 gitc_client_name)
1301
1302 super(GitcManifest, self).__init__(
1303 repodir, os.path.join(self.gitc_client_dir, '.manifest'))
1304 self.isGitcClient = True
diff --git a/project.py b/project.py
index 50bb53c3..29f6b1e8 100644
--- a/project.py
+++ b/project.py
@@ -510,7 +510,7 @@ class Project(object):
510 with exponential backoff and jitter. 510 with exponential backoff and jitter.
511 old_revision: saved git commit id for open GITC projects. 511 old_revision: saved git commit id for open GITC projects.
512 """ 512 """
513 self.manifest = manifest 513 self.client = self.manifest = manifest
514 self.name = name 514 self.name = name
515 self.remote = remote 515 self.remote = remote
516 self.gitdir = gitdir.replace('\\', '/') 516 self.gitdir = gitdir.replace('\\', '/')
@@ -551,7 +551,7 @@ class Project(object):
551 self.linkfiles = [] 551 self.linkfiles = []
552 self.annotations = [] 552 self.annotations = []
553 self.config = GitConfig.ForRepository(gitdir=self.gitdir, 553 self.config = GitConfig.ForRepository(gitdir=self.gitdir,
554 defaults=self.manifest.globalConfig) 554 defaults=self.client.globalConfig)
555 555
556 if self.worktree: 556 if self.worktree:
557 self.work_git = self._GitGetByExec(self, bare=False, gitdir=gitdir) 557 self.work_git = self._GitGetByExec(self, bare=False, gitdir=gitdir)
@@ -1168,7 +1168,7 @@ class Project(object):
1168 self._InitHooks() 1168 self._InitHooks()
1169 1169
1170 def _CopyAndLinkFiles(self): 1170 def _CopyAndLinkFiles(self):
1171 if self.manifest.isGitcClient: 1171 if self.client.isGitcClient:
1172 return 1172 return
1173 for copyfile in self.copyfiles: 1173 for copyfile in self.copyfiles:
1174 copyfile._Copy() 1174 copyfile._Copy()
diff --git a/subcmds/diffmanifests.py b/subcmds/diffmanifests.py
index 72c441ac..409bbdac 100644
--- a/subcmds/diffmanifests.py
+++ b/subcmds/diffmanifests.py
@@ -16,7 +16,7 @@
16 16
17from color import Coloring 17from color import Coloring
18from command import PagedCommand 18from command import PagedCommand
19from manifest_xml import XmlManifest 19from manifest_xml import RepoClient
20 20
21 21
22class _Coloring(Coloring): 22class _Coloring(Coloring):
@@ -183,7 +183,7 @@ synced and their revisions won't be found.
183 self.OptionParser.error('missing manifests to diff') 183 self.OptionParser.error('missing manifests to diff')
184 184
185 def Execute(self, opt, args): 185 def Execute(self, opt, args):
186 self.out = _Coloring(self.manifest.globalConfig) 186 self.out = _Coloring(self.client.globalConfig)
187 self.printText = self.out.nofmt_printer('text') 187 self.printText = self.out.nofmt_printer('text')
188 if opt.color: 188 if opt.color:
189 self.printProject = self.out.nofmt_printer('project', attr='bold') 189 self.printProject = self.out.nofmt_printer('project', attr='bold')
@@ -193,12 +193,12 @@ synced and their revisions won't be found.
193 else: 193 else:
194 self.printProject = self.printAdded = self.printRemoved = self.printRevision = self.printText 194 self.printProject = self.printAdded = self.printRemoved = self.printRevision = self.printText
195 195
196 manifest1 = XmlManifest(self.manifest.repodir) 196 manifest1 = RepoClient(self.manifest.repodir)
197 manifest1.Override(args[0], load_local_manifests=False) 197 manifest1.Override(args[0], load_local_manifests=False)
198 if len(args) == 1: 198 if len(args) == 1:
199 manifest2 = self.manifest 199 manifest2 = self.manifest
200 else: 200 else:
201 manifest2 = XmlManifest(self.manifest.repodir) 201 manifest2 = RepoClient(self.manifest.repodir)
202 manifest2.Override(args[1], load_local_manifests=False) 202 manifest2.Override(args[1], load_local_manifests=False)
203 203
204 diff = manifest1.projectsDiff(manifest2) 204 diff = manifest1.projectsDiff(manifest2)
diff --git a/subcmds/help.py b/subcmds/help.py
index 1e16019a..c219a763 100644
--- a/subcmds/help.py
+++ b/subcmds/help.py
@@ -65,7 +65,7 @@ Displays detailed usage information about a command.
65 def gitc_supported(cmd): 65 def gitc_supported(cmd):
66 if not isinstance(cmd, GitcAvailableCommand) and not isinstance(cmd, GitcClientCommand): 66 if not isinstance(cmd, GitcAvailableCommand) and not isinstance(cmd, GitcClientCommand):
67 return True 67 return True
68 if self.manifest.isGitcClient: 68 if self.client.isGitcClient:
69 return True 69 return True
70 if isinstance(cmd, GitcClientCommand): 70 if isinstance(cmd, GitcClientCommand):
71 return False 71 return False
@@ -127,7 +127,7 @@ Displays detailed usage information about a command.
127 self.wrap.end_paragraph(1) 127 self.wrap.end_paragraph(1)
128 self.wrap.end_paragraph(0) 128 self.wrap.end_paragraph(0)
129 129
130 out = _Out(self.manifest.globalConfig) 130 out = _Out(self.client.globalConfig)
131 out._PrintSection('Summary', 'helpSummary') 131 out._PrintSection('Summary', 'helpSummary')
132 cmd.OptionParser.print_help() 132 cmd.OptionParser.print_help()
133 out._PrintSection('Description', 'helpDescription') 133 out._PrintSection('Description', 'helpDescription')
diff --git a/subcmds/info.py b/subcmds/info.py
index f4d6f98c..60149975 100644
--- a/subcmds/info.py
+++ b/subcmds/info.py
@@ -44,7 +44,7 @@ class Info(PagedCommand):
44 help="Disable all remote operations") 44 help="Disable all remote operations")
45 45
46 def Execute(self, opt, args): 46 def Execute(self, opt, args):
47 self.out = _Coloring(self.manifest.globalConfig) 47 self.out = _Coloring(self.client.globalConfig)
48 self.heading = self.out.printer('heading', attr='bold') 48 self.heading = self.out.printer('heading', attr='bold')
49 self.headtext = self.out.nofmt_printer('headtext', fg='yellow') 49 self.headtext = self.out.nofmt_printer('headtext', fg='yellow')
50 self.redtext = self.out.printer('redtext', fg='red') 50 self.redtext = self.out.printer('redtext', fg='red')
diff --git a/subcmds/init.py b/subcmds/init.py
index 5ba0d074..f46babfe 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -365,7 +365,7 @@ to update the working directory files.
365 return a 365 return a
366 366
367 def _ShouldConfigureUser(self, opt): 367 def _ShouldConfigureUser(self, opt):
368 gc = self.manifest.globalConfig 368 gc = self.client.globalConfig
369 mp = self.manifest.manifestProject 369 mp = self.manifest.manifestProject
370 370
371 # If we don't have local settings, get from global. 371 # If we don't have local settings, get from global.
@@ -414,7 +414,7 @@ to update the working directory files.
414 return False 414 return False
415 415
416 def _ConfigureColor(self): 416 def _ConfigureColor(self):
417 gc = self.manifest.globalConfig 417 gc = self.client.globalConfig
418 if self._HasColorSet(gc): 418 if self._HasColorSet(gc):
419 return 419 return
420 420
diff --git a/subcmds/status.py b/subcmds/status.py
index c380dba3..dfa974e9 100644
--- a/subcmds/status.py
+++ b/subcmds/status.py
@@ -165,7 +165,7 @@ the following meanings:
165 proj_dirs, proj_dirs_parents, outstring) 165 proj_dirs, proj_dirs_parents, outstring)
166 166
167 if outstring: 167 if outstring:
168 output = StatusColoring(self.manifest.globalConfig) 168 output = StatusColoring(self.client.globalConfig)
169 output.project('Objects not within a project (orphans)') 169 output.project('Objects not within a project (orphans)')
170 output.nl() 170 output.nl()
171 for entry in outstring: 171 for entry in outstring:
diff --git a/tests/test_manifest_xml.py b/tests/test_manifest_xml.py
index aa6cb7df..40385cce 100644
--- a/tests/test_manifest_xml.py
+++ b/tests/test_manifest_xml.py
@@ -19,6 +19,8 @@
19from __future__ import print_function 19from __future__ import print_function
20 20
21import os 21import os
22import shutil
23import tempfile
22import unittest 24import unittest
23import xml.dom.minidom 25import xml.dom.minidom
24 26
@@ -146,3 +148,90 @@ class ValueTests(unittest.TestCase):
146 with self.assertRaises(error.ManifestParseError): 148 with self.assertRaises(error.ManifestParseError):
147 node = self._get_node('<node a="xx"/>') 149 node = self._get_node('<node a="xx"/>')
148 manifest_xml.XmlInt(node, 'a') 150 manifest_xml.XmlInt(node, 'a')
151
152
153class XmlManifestTests(unittest.TestCase):
154 """Check manifest processing."""
155
156 def setUp(self):
157 self.tempdir = tempfile.mkdtemp(prefix='repo_tests')
158 self.repodir = os.path.join(self.tempdir, '.repo')
159 self.manifest_dir = os.path.join(self.repodir, 'manifests')
160 self.manifest_file = os.path.join(
161 self.repodir, manifest_xml.MANIFEST_FILE_NAME)
162 self.local_manifest_dir = os.path.join(
163 self.repodir, manifest_xml.LOCAL_MANIFESTS_DIR_NAME)
164 os.mkdir(self.repodir)
165 os.mkdir(self.manifest_dir)
166
167 # The manifest parsing really wants a git repo currently.
168 gitdir = os.path.join(self.repodir, 'manifests.git')
169 os.mkdir(gitdir)
170 with open(os.path.join(gitdir, 'config'), 'w') as fp:
171 fp.write("""[remote "origin"]
172 url = https://localhost:0/manifest
173""")
174
175 def tearDown(self):
176 shutil.rmtree(self.tempdir, ignore_errors=True)
177
178 def getXmlManifest(self, data):
179 """Helper to initialize a manifest for testing."""
180 with open(self.manifest_file, 'w') as fp:
181 fp.write(data)
182 return manifest_xml.XmlManifest(self.repodir, self.manifest_file)
183
184 def test_empty(self):
185 """Parse an 'empty' manifest file."""
186 manifest = self.getXmlManifest(
187 '<?xml version="1.0" encoding="UTF-8"?>'
188 '<manifest></manifest>')
189 self.assertEqual(manifest.remotes, {})
190 self.assertEqual(manifest.projects, [])
191
192 def test_link(self):
193 """Verify Link handling with new names."""
194 manifest = manifest_xml.XmlManifest(self.repodir, self.manifest_file)
195 with open(os.path.join(self.manifest_dir, 'foo.xml'), 'w') as fp:
196 fp.write('<manifest></manifest>')
197 manifest.Link('foo.xml')
198 with open(self.manifest_file) as fp:
199 self.assertIn('<include name="foo.xml" />', fp.read())
200
201 def test_toxml_empty(self):
202 """Verify the ToXml() helper."""
203 manifest = self.getXmlManifest(
204 '<?xml version="1.0" encoding="UTF-8"?>'
205 '<manifest></manifest>')
206 self.assertEqual(manifest.ToXml().toxml(), '<?xml version="1.0" ?><manifest/>')
207
208 def test_todict_empty(self):
209 """Verify the ToDict() helper."""
210 manifest = self.getXmlManifest(
211 '<?xml version="1.0" encoding="UTF-8"?>'
212 '<manifest></manifest>')
213 self.assertEqual(manifest.ToDict(), {})
214
215 def test_project_group(self):
216 """Check project group settings."""
217 manifest = self.getXmlManifest("""
218<manifest>
219 <remote name="test-remote" fetch="http://localhost" />
220 <default remote="test-remote" revision="refs/heads/main" />
221 <project name="test-name" path="test-path"/>
222 <project name="extras" path="path" groups="g1,g2,g1"/>
223</manifest>
224""")
225 self.assertEqual(len(manifest.projects), 2)
226 # Ordering isn't guaranteed.
227 result = {
228 manifest.projects[0].name: manifest.projects[0].groups,
229 manifest.projects[1].name: manifest.projects[1].groups,
230 }
231 project = manifest.projects[0]
232 self.assertCountEqual(
233 result['test-name'],
234 ['name:test-name', 'all', 'path:test-path'])
235 self.assertCountEqual(
236 result['extras'],
237 ['g1', 'g2', 'g1', 'name:extras', 'all', 'path:path'])