1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Repository module to handle different types of repositories."""
7 from __future__ import print_function
15 from chromite.lib import cros_build_lib
16 from chromite.lib import git
17 from chromite.lib import osutils
18 from chromite.lib import rewrite_git_alternates
19 from chromite.lib import retry_util
21 # File that marks a buildroot as being used by a trybot
22 _TRYBOT_MARKER = '.trybot'
25 class SrcCheckOutException(Exception):
26 """Exception gets thrown for failure to sync sources"""
30 def IsARepoRoot(directory):
31 """Returns True if directory is the root of a repo checkout."""
32 return os.path.exists(os.path.join(directory, '.repo'))
35 def IsInternalRepoCheckout(root):
36 """Returns whether root houses an internal 'repo' checkout."""
37 manifest_dir = os.path.join(root, '.repo', 'manifests')
38 manifest_url = git.RunGit(
39 manifest_dir, ['config', 'remote.origin.url']).output.strip()
40 return (os.path.splitext(os.path.basename(manifest_url))[0]
41 == os.path.splitext(os.path.basename(constants.MANIFEST_INT_URL))[0])
44 def CloneGitRepo(working_dir, repo_url, reference=None, bare=False,
45 mirror=False, depth=None):
46 """Clone given git repo
49 working_dir: location where it should be cloned to
50 repo_url: git repo to clone
51 reference: If given, pathway to a git repository to access git objects
52 from. Note that the reference must exist as long as the newly created
54 bare: Clone a bare checkout.
55 mirror: Clone a mirror checkout.
56 depth: If given, do a shallow clone limiting the objects pulled to just
57 that # of revs of history. This option is mutually exclusive to
60 osutils.SafeMakedirs(working_dir)
61 cmd = ['clone', repo_url, working_dir]
64 raise ValueError("reference and depth are mutually exclusive "
65 "options; please pick one or the other.")
66 cmd += ['--reference', reference]
72 cmd += ['--depth', str(int(depth))]
73 git.RunGit(working_dir, cmd)
76 def UpdateGitRepo(working_dir, repo_url, **kwargs):
77 """Update the given git repo, blowing away any local changes.
79 If the repo does not exist, clone it from scratch.
82 working_dir: location where it should be cloned to
83 repo_url: git repo to clone
84 **kwargs: See CloneGitRepo.
86 assert not kwargs.get('bare'), 'Bare checkouts are not supported'
87 if git.IsGitRepo(working_dir):
89 git.CleanAndCheckoutUpstream(working_dir)
90 except cros_build_lib.RunCommandError:
91 cros_build_lib.Warning('Could not update %s', working_dir, exc_info=True)
92 shutil.rmtree(working_dir)
93 CloneGitRepo(working_dir, repo_url, **kwargs)
95 CloneGitRepo(working_dir, repo_url, **kwargs)
98 def GetTrybotMarkerPath(buildroot):
99 """Get path to trybot marker file given the buildroot."""
100 return os.path.join(buildroot, _TRYBOT_MARKER)
103 def CreateTrybotMarker(buildroot):
104 """Create the file that identifies a buildroot as being used by a trybot."""
105 osutils.WriteFile(GetTrybotMarkerPath(buildroot), '')
107 def ClearBuildRoot(buildroot, preserve_paths=()):
108 """Remove and recreate the buildroot while preserving the trybot marker."""
109 trybot_root = os.path.exists(GetTrybotMarkerPath(buildroot))
110 if os.path.exists(buildroot):
111 cmd = ['find', buildroot, '-mindepth', '1', '-maxdepth', '1']
114 for path in preserve_paths:
117 ignores += ['!', '-name', path]
120 cmd += ['-exec', 'rm', '-rf', '{}', '+']
121 cros_build_lib.SudoRunCommand(cmd)
123 os.makedirs(buildroot)
125 CreateTrybotMarker(buildroot)
128 class RepoRepository(object):
129 """A Class that encapsulates a repo repository.
132 repo_url: gitserver URL to fetch repo manifest from.
133 directory: local path where to checkout the repository.
134 branch: Branch to check out the manifest at.
135 referenced_repo: Repository to reference for git objects, if possible.
136 manifest: Which manifest.xml within the branch to use. Effectively
137 default.xml if not given.
138 depth: Mutually exclusive option to referenced_repo; this limits the
139 checkout to a max commit history of the given integer.
141 # Use our own repo, in case android.kernel.org (the default location) is down.
142 _INIT_CMD = ['repo', 'init', '--repo-url', constants.REPO_URL]
144 # If a repo hasn't been used in the last 5 runs, wipe it.
147 def __init__(self, repo_url, directory, branch=None, referenced_repo=None,
148 manifest=constants.DEFAULT_MANIFEST, depth=None):
149 self.repo_url = repo_url
150 self.directory = directory
153 # It's perfectly acceptable to pass in a reference pathway that isn't
154 # usable. Detect it, and suppress the setting so that any depth
155 # settings aren't disabled due to it.
156 if referenced_repo is not None:
157 if depth is not None:
158 raise ValueError("referenced_repo and depth are mutually exclusive "
159 "options; please pick one or the other.")
160 if not IsARepoRoot(referenced_repo):
161 referenced_repo = None
162 self._referenced_repo = referenced_repo
163 self._manifest = manifest
165 # If the repo exists already, force a selfupdate as the first step.
166 self._repo_update_needed = IsARepoRoot(self.directory)
167 if not self._repo_update_needed and git.FindRepoDir(self.directory):
168 raise ValueError('Given directory %s is not the root of a repository.'
171 self._depth = int(depth) if depth is not None else None
173 def _SwitchToLocalManifest(self, local_manifest):
174 """Reinitializes the repository if the manifest has changed."""
175 logging.debug('Moving to manifest defined by %s', local_manifest)
176 # TODO: use upstream repo's manifest logic when we bump repo version.
177 manifest_path = self.GetRelativePath('.repo/manifest.xml')
178 os.unlink(manifest_path)
179 shutil.copyfile(local_manifest, manifest_path)
181 def Initialize(self, local_manifest=None, extra_args=()):
182 """Initializes a repository. Optionally forces a local manifest.
185 local_manifest: The absolute path to a custom manifest to use. This will
186 replace .repo/manifest.xml.
187 extra_args: Extra args to pass to 'repo init'
190 # Do a sanity check on the repo; if it exists and we can't pull a
191 # manifest from it, we know it's fairly screwed up and needs a fresh
193 if os.path.exists(os.path.join(self.directory, '.repo', 'manifest.xml')):
195 cros_build_lib.RunCommand(
196 ['repo', 'manifest'], cwd=self.directory, capture_output=True)
197 except cros_build_lib.RunCommandError:
198 cros_build_lib.Warning("Wiping %r due to `repo manifest` failure",
200 paths = [os.path.join(self.directory, '.repo', x) for x in
201 ('manifest.xml', 'manifests.git', 'manifests', 'repo')]
202 cros_build_lib.SudoRunCommand(['rm', '-rf'] + paths)
203 self._repo_update_needed = False
205 # Wipe local_manifest.xml if it exists- it can interfere w/ things in
206 # bad ways (duplicate projects, etc); we control this repository, thus
208 osutils.SafeUnlink(os.path.join(self.directory, 'local_manifest.xml'))
210 # Force a repo self update first; during reinit, repo doesn't do the
211 # update itself, but we could be doing the init on a repo version less
212 # then v1.9.4, which didn't have proper support for doing reinit that
213 # involved changing the manifest branch in use; thus selfupdate.
214 # Additionally, if the self update fails for *any* reason, wipe the repo
215 # innards and force repo init to redownload it; same end result, just
217 # Additionally, note that this method may be called multiple times;
218 # thus code appropriately.
219 if self._repo_update_needed:
221 cros_build_lib.RunCommand(['repo', 'selfupdate'], cwd=self.directory)
222 except cros_build_lib.RunCommandError:
223 osutils.RmDir(os.path.join(self.directory, '.repo', 'repo'),
225 self._repo_update_needed = False
227 init_cmd = self._INIT_CMD + ['--manifest-url', self.repo_url]
228 if self._referenced_repo:
229 init_cmd.extend(['--reference', self._referenced_repo])
231 init_cmd.extend(['--manifest-name', self._manifest])
232 if self._depth is not None:
233 init_cmd.extend(['--depth', str(self._depth)])
234 init_cmd.extend(extra_args)
235 # Handle branch / manifest options.
237 init_cmd.extend(['--manifest-branch', self.branch])
239 cros_build_lib.RunCommand(init_cmd, cwd=self.directory, input='\n\ny\n')
240 if local_manifest and local_manifest != self._manifest:
241 self._SwitchToLocalManifest(local_manifest)
244 def _ManifestConfig(self):
245 return os.path.join(self.directory, '.repo', 'manifests.git', 'config')
247 def _EnsureMirroring(self, post_sync=False):
248 """Ensure git is usable from w/in the chroot if --references is enabled
250 repo init --references hardcodes the abspath to parent; this pathway
251 however isn't usable from the chroot (it doesn't exist). As such the
252 pathway is rewritten to use relative pathways pointing at the root of
253 the repo, which via I84988630 enter_chroot sets up a helper bind mount
254 allowing git/repo to access the actual referenced repo.
256 This has to be invoked prior to a repo sync of the target trybot to
257 fix any pathways that may have been broken by the parent repo moving
258 on disk, and needs to be invoked after the sync has completed to rewrite
259 any new project's abspath to relative.
261 if not self._referenced_repo:
264 proj_root = os.path.join(self.directory, '.repo', 'project-objects')
265 if not os.path.exists(proj_root):
266 # Not yet synced, nothing to be done.
269 rewrite_git_alternates.RebuildRepoCheckout(self.directory,
270 self._referenced_repo)
273 chroot_path = os.path.join(self._referenced_repo, '.repo', 'chroot',
275 chroot_path = git.ReinterpretPathForChroot(chroot_path)
276 rewrite_git_alternates.RebuildRepoCheckout(
277 self.directory, self._referenced_repo, chroot_path)
279 # Finally, force the git config marker that enter_chroot looks for
280 # to know when to do bind mounting trickery; this normally will exist,
281 # but if we're converting a pre-existing repo checkout, it's possible
282 # that it was invoked w/out the reference arg. Note this must be
283 # an absolute path to the source repo- enter_chroot uses that to know
284 # what to bind mount into the chroot.
285 cmd = ['config', '--file', self._ManifestConfig, 'repo.reference',
286 self._referenced_repo]
290 """Detach projects back to manifest versions. Effectively a 'reset'."""
291 cros_build_lib.RunCommand(['repo', '--time', 'sync', '-d'],
294 def Sync(self, local_manifest=None, jobs=None, all_branches=True,
296 """Sync/update the source. Changes manifest if specified.
299 local_manifest: If true, checks out source to manifest. DEFAULT_MANIFEST
300 may be used to set it back to the default manifest.
301 jobs: May be set to override the default sync parallelism defined by
303 all_branches: If False, a repo sync -c is performed; this saves on
304 sync'ing via grabbing only what is needed for the manifest specified
305 branch. Defaults to True. TODO(davidjames): Set the default back to
306 False once we've fixed http://crbug.com/368722 .
307 network_only: If true, perform only the network half of the sync; skip
308 the checkout. Primarily of use to validate a manifest (although
309 if the manifest has bad copyfile statements, via skipping checkout
310 the broken copyfile tag won't be spotted), or of use when the
311 invoking code is fine w/ operating on bare repos, ie .repo/projects/*.
314 # Always re-initialize to the current branch.
315 self.Initialize(local_manifest)
316 # Fix existing broken mirroring configurations.
317 self._EnsureMirroring()
319 cmd = ['repo', '--time', 'sync']
321 cmd += ['--jobs', str(jobs)]
324 # Do the network half of the sync; retry as necessary to get the content.
325 retry_util.RunCommandWithRetries(constants.SYNC_RETRIES, cmd + ['-n'],
331 # Do the local sync; note that there is a couple of corner cases where
332 # the new manifest cannot transition from the old checkout cleanly-
333 # primarily involving git submodules. Thus we intercept, and do
334 # a forced wipe, then a retry.
336 cros_build_lib.RunCommand(cmd + ['-l'], cwd=self.directory)
337 except cros_build_lib.RunCommandError:
338 manifest = git.ManifestCheckout.Cached(self.directory)
339 targets = set(project['path'].split('/', 1)[0]
340 for project in manifest.ListCheckouts())
342 # No directories to wipe, thus nothing we can fix.
345 cros_build_lib.SudoRunCommand(['rm', '-rf'] + sorted(targets),
348 # Retry the sync now; if it fails, let the exception propagate.
349 cros_build_lib.RunCommand(cmd + ['-l'], cwd=self.directory)
351 # We do a second run to fix any new repositories created by repo to
352 # use relative object pathways. Note that cros_sdk also triggers the
353 # same cleanup- we however kick it erring on the side of caution.
354 self._EnsureMirroring(True)
357 except cros_build_lib.RunCommandError as e:
358 err_msg = e.Stringify(error=False, output=False)
359 logging.error(err_msg)
360 raise SrcCheckOutException(err_msg)
362 def _DoCleanup(self):
363 """Wipe unused repositories."""
365 # Find all projects, even if they're not in the manifest. Note the find
366 # trickery this is done to keep it as fast as possible.
367 repo_path = os.path.join(self.directory, '.repo', 'projects')
368 current = set(cros_build_lib.RunCommand(
369 ['find', repo_path, '-type', 'd', '-name', '*.git', '-printf', '%P\n',
370 '-a', '!', '-wholename', '*.git/*', '-prune'],
371 print_cmd=False, capture_output=True).output.splitlines())
372 data = {}.fromkeys(current, 0)
374 path = os.path.join(self.directory, '.repo', 'project.lru')
375 if os.path.exists(path):
376 existing = [x.strip().split(None, 1)
377 for x in osutils.ReadFile(path).splitlines()]
378 data.update((k, int(v)) for k, v in existing if k in current)
380 # Increment it all...
381 data.update((k, v + 1) for k, v in data.iteritems())
382 # Zero out what is now used.
383 checkouts = git.ManifestCheckout.Cached(self.directory).ListCheckouts()
384 data.update(('%s.git' % x['path'], 0) for x in checkouts)
386 # Finally... wipe anything that's greater than our threshold.
387 wipes = [k for k, v in data.iteritems() if v > self.LRU_THRESHOLD]
389 cros_build_lib.SudoRunCommand(
390 ['rm', '-rf'] + [os.path.join(repo_path, proj) for proj in wipes])
393 osutils.WriteFile(path, "\n".join('%s %i' % x for x in data.iteritems()))
395 def GetRelativePath(self, path):
396 """Returns full path including source directory of path in repo."""
397 return os.path.join(self.directory, path)
399 def ExportManifest(self, mark_revision=False, revisions=True):
400 """Export the revision locked manifest
403 mark_revision: If True, then the sha1 of manifest.git is recorded
404 into the resultant manifest tag as a version attribute.
405 Specifically, if manifests.git is at 1234, <manifest> becomes
406 <manifest revision="1234">.
407 revisions: If True, then rewrite all branches/tags into a specific
408 sha1 revision. If False, don't.
411 The manifest as a string.
413 cmd = ['repo', 'manifest', '-o', '-']
416 output = cros_build_lib.RunCommand(
417 cmd, cwd=self.directory, print_cmd=False, capture_output=True,
418 extra_env={'PAGER':'cat'}).output
420 if not mark_revision:
422 modified = git.RunGit(os.path.join(self.directory, '.repo/manifests'),
423 ['rev-list', '-n1', 'HEAD'])
424 assert modified.output
425 return output.replace("<manifest>", '<manifest revision="%s">' %
426 modified.output.strip())
428 def IsManifestDifferent(self, other_manifest):
429 """Checks whether this manifest is different than another.
431 May blacklists certain repos as part of the diff.
434 other_manifest: Second manifest file to compare against.
437 True: If the manifests are different
438 False: If the manifests are same
440 logging.debug('Calling IsManifestDifferent against %s', other_manifest)
442 black_list = ['="chromium/']
443 blacklist_pattern = re.compile(r'|'.join(black_list))
444 manifest_revision_pattern = re.compile(r'<manifest revision="[a-f0-9]+">',
447 current = self.ExportManifest()
448 with open(other_manifest, 'r') as manifest2_fh:
449 for (line1, line2) in zip(current.splitlines(), manifest2_fh):
450 line1 = line1.strip()
451 line2 = line2.strip()
452 if blacklist_pattern.search(line1):
453 logging.debug('%s ignored %s', line1, line2)
457 logging.debug('Current and other manifest differ.')
458 logging.debug('current: "%s"', line1)
459 logging.debug('other : "%s"', line2)
461 # Ignore revision differences on the manifest line. The revision of
462 # the manifest.git repo is uninteresting when determining if the
463 # current manifest describes the same sources as the other manifest.
464 if manifest_revision_pattern.search(line2):
465 logging.debug('Ignoring difference in manifest revision.')