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 manifest_repo_url: 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.
140 repo_url: URL to fetch repo tool from.
141 repo_branch: Branch to check out the repo tool at.
143 # Use our own repo, in case android.kernel.org (the default location) is down.
144 _INIT_CMD = ['repo', 'init']
146 # If a repo hasn't been used in the last 5 runs, wipe it.
149 def __init__(self, manifest_repo_url, directory, branch=None,
150 referenced_repo=None, manifest=constants.DEFAULT_MANIFEST,
151 depth=None, repo_url=constants.REPO_URL, repo_branch=None):
152 self.manifest_repo_url = manifest_repo_url
153 self.repo_url = repo_url
154 self.repo_branch = repo_branch
155 self.directory = directory
158 # It's perfectly acceptable to pass in a reference pathway that isn't
159 # usable. Detect it, and suppress the setting so that any depth
160 # settings aren't disabled due to it.
161 if referenced_repo is not None:
162 if depth is not None:
163 raise ValueError("referenced_repo and depth are mutually exclusive "
164 "options; please pick one or the other.")
165 if not IsARepoRoot(referenced_repo):
166 referenced_repo = None
167 self._referenced_repo = referenced_repo
168 self._manifest = manifest
170 # If the repo exists already, force a selfupdate as the first step.
171 self._repo_update_needed = IsARepoRoot(self.directory)
172 if not self._repo_update_needed and git.FindRepoDir(self.directory):
173 raise ValueError('Given directory %s is not the root of a repository.'
176 self._depth = int(depth) if depth is not None else None
178 def _SwitchToLocalManifest(self, local_manifest):
179 """Reinitializes the repository if the manifest has changed."""
180 logging.debug('Moving to manifest defined by %s', local_manifest)
181 # TODO: use upstream repo's manifest logic when we bump repo version.
182 manifest_path = self.GetRelativePath('.repo/manifest.xml')
183 os.unlink(manifest_path)
184 shutil.copyfile(local_manifest, manifest_path)
186 def Initialize(self, local_manifest=None, extra_args=()):
187 """Initializes a repository. Optionally forces a local manifest.
190 local_manifest: The absolute path to a custom manifest to use. This will
191 replace .repo/manifest.xml.
192 extra_args: Extra args to pass to 'repo init'
195 # Do a sanity check on the repo; if it exists and we can't pull a
196 # manifest from it, we know it's fairly screwed up and needs a fresh
198 if os.path.exists(os.path.join(self.directory, '.repo', 'manifest.xml')):
200 cros_build_lib.RunCommand(
201 ['repo', 'manifest'], cwd=self.directory, capture_output=True)
202 except cros_build_lib.RunCommandError:
203 cros_build_lib.Warning("Wiping %r due to `repo manifest` failure",
205 paths = [os.path.join(self.directory, '.repo', x) for x in
206 ('manifest.xml', 'manifests.git', 'manifests', 'repo')]
207 cros_build_lib.SudoRunCommand(['rm', '-rf'] + paths)
208 self._repo_update_needed = False
210 # Wipe local_manifest.xml if it exists- it can interfere w/ things in
211 # bad ways (duplicate projects, etc); we control this repository, thus
213 osutils.SafeUnlink(os.path.join(self.directory, 'local_manifest.xml'))
215 # Force a repo self update first; during reinit, repo doesn't do the
216 # update itself, but we could be doing the init on a repo version less
217 # then v1.9.4, which didn't have proper support for doing reinit that
218 # involved changing the manifest branch in use; thus selfupdate.
219 # Additionally, if the self update fails for *any* reason, wipe the repo
220 # innards and force repo init to redownload it; same end result, just
222 # Additionally, note that this method may be called multiple times;
223 # thus code appropriately.
224 if self._repo_update_needed:
226 cros_build_lib.RunCommand(['repo', 'selfupdate'], cwd=self.directory)
227 except cros_build_lib.RunCommandError:
228 osutils.RmDir(os.path.join(self.directory, '.repo', 'repo'),
230 self._repo_update_needed = False
232 init_cmd = self._INIT_CMD + ['--repo-url', self.repo_url,
233 '--manifest-url', self.manifest_repo_url]
234 if self._referenced_repo:
235 init_cmd.extend(['--reference', self._referenced_repo])
237 init_cmd.extend(['--manifest-name', self._manifest])
238 if self._depth is not None:
239 init_cmd.extend(['--depth', str(self._depth)])
240 init_cmd.extend(extra_args)
241 # Handle branch / manifest options.
243 init_cmd.extend(['--manifest-branch', self.branch])
245 init_cmd.extend(['--repo-branch', self.repo_branch])
247 cros_build_lib.RunCommand(init_cmd, cwd=self.directory, input='\n\ny\n')
248 if local_manifest and local_manifest != self._manifest:
249 self._SwitchToLocalManifest(local_manifest)
252 def _ManifestConfig(self):
253 return os.path.join(self.directory, '.repo', 'manifests.git', 'config')
255 def _EnsureMirroring(self, post_sync=False):
256 """Ensure git is usable from w/in the chroot if --references is enabled
258 repo init --references hardcodes the abspath to parent; this pathway
259 however isn't usable from the chroot (it doesn't exist). As such the
260 pathway is rewritten to use relative pathways pointing at the root of
261 the repo, which via I84988630 enter_chroot sets up a helper bind mount
262 allowing git/repo to access the actual referenced repo.
264 This has to be invoked prior to a repo sync of the target trybot to
265 fix any pathways that may have been broken by the parent repo moving
266 on disk, and needs to be invoked after the sync has completed to rewrite
267 any new project's abspath to relative.
269 if not self._referenced_repo:
272 proj_root = os.path.join(self.directory, '.repo', 'project-objects')
273 if not os.path.exists(proj_root):
274 # Not yet synced, nothing to be done.
277 rewrite_git_alternates.RebuildRepoCheckout(self.directory,
278 self._referenced_repo)
281 chroot_path = os.path.join(self._referenced_repo, '.repo', 'chroot',
283 chroot_path = git.ReinterpretPathForChroot(chroot_path)
284 rewrite_git_alternates.RebuildRepoCheckout(
285 self.directory, self._referenced_repo, chroot_path)
287 # Finally, force the git config marker that enter_chroot looks for
288 # to know when to do bind mounting trickery; this normally will exist,
289 # but if we're converting a pre-existing repo checkout, it's possible
290 # that it was invoked w/out the reference arg. Note this must be
291 # an absolute path to the source repo- enter_chroot uses that to know
292 # what to bind mount into the chroot.
293 cmd = ['config', '--file', self._ManifestConfig, 'repo.reference',
294 self._referenced_repo]
298 """Detach projects back to manifest versions. Effectively a 'reset'."""
299 cros_build_lib.RunCommand(['repo', '--time', 'sync', '-d'],
302 def Sync(self, local_manifest=None, jobs=None, all_branches=True,
304 """Sync/update the source. Changes manifest if specified.
307 local_manifest: If true, checks out source to manifest. DEFAULT_MANIFEST
308 may be used to set it back to the default manifest.
309 jobs: May be set to override the default sync parallelism defined by
311 all_branches: If False, a repo sync -c is performed; this saves on
312 sync'ing via grabbing only what is needed for the manifest specified
313 branch. Defaults to True. TODO(davidjames): Set the default back to
314 False once we've fixed http://crbug.com/368722 .
315 network_only: If true, perform only the network half of the sync; skip
316 the checkout. Primarily of use to validate a manifest (although
317 if the manifest has bad copyfile statements, via skipping checkout
318 the broken copyfile tag won't be spotted), or of use when the
319 invoking code is fine w/ operating on bare repos, ie .repo/projects/*.
322 # Always re-initialize to the current branch.
323 self.Initialize(local_manifest)
324 # Fix existing broken mirroring configurations.
325 self._EnsureMirroring()
327 cmd = ['repo', '--time', 'sync']
329 cmd += ['--jobs', str(jobs)]
332 # Do the network half of the sync; retry as necessary to get the content.
333 retry_util.RunCommandWithRetries(constants.SYNC_RETRIES, cmd + ['-n'],
339 # Do the local sync; note that there is a couple of corner cases where
340 # the new manifest cannot transition from the old checkout cleanly-
341 # primarily involving git submodules. Thus we intercept, and do
342 # a forced wipe, then a retry.
344 cros_build_lib.RunCommand(cmd + ['-l'], cwd=self.directory)
345 except cros_build_lib.RunCommandError:
346 manifest = git.ManifestCheckout.Cached(self.directory)
347 targets = set(project['path'].split('/', 1)[0]
348 for project in manifest.ListCheckouts())
350 # No directories to wipe, thus nothing we can fix.
353 cros_build_lib.SudoRunCommand(['rm', '-rf'] + sorted(targets),
356 # Retry the sync now; if it fails, let the exception propagate.
357 cros_build_lib.RunCommand(cmd + ['-l'], cwd=self.directory)
359 # We do a second run to fix any new repositories created by repo to
360 # use relative object pathways. Note that cros_sdk also triggers the
361 # same cleanup- we however kick it erring on the side of caution.
362 self._EnsureMirroring(True)
365 except cros_build_lib.RunCommandError as e:
366 err_msg = e.Stringify(error=False, output=False)
367 logging.error(err_msg)
368 raise SrcCheckOutException(err_msg)
370 def _DoCleanup(self):
371 """Wipe unused repositories."""
373 # Find all projects, even if they're not in the manifest. Note the find
374 # trickery this is done to keep it as fast as possible.
375 repo_path = os.path.join(self.directory, '.repo', 'projects')
376 current = set(cros_build_lib.RunCommand(
377 ['find', repo_path, '-type', 'd', '-name', '*.git', '-printf', '%P\n',
378 '-a', '!', '-wholename', '*.git/*', '-prune'],
379 print_cmd=False, capture_output=True).output.splitlines())
380 data = {}.fromkeys(current, 0)
382 path = os.path.join(self.directory, '.repo', 'project.lru')
383 if os.path.exists(path):
384 existing = [x.strip().split(None, 1)
385 for x in osutils.ReadFile(path).splitlines()]
386 data.update((k, int(v)) for k, v in existing if k in current)
388 # Increment it all...
389 data.update((k, v + 1) for k, v in data.iteritems())
390 # Zero out what is now used.
391 checkouts = git.ManifestCheckout.Cached(self.directory).ListCheckouts()
392 data.update(('%s.git' % x['path'], 0) for x in checkouts)
394 # Finally... wipe anything that's greater than our threshold.
395 wipes = [k for k, v in data.iteritems() if v > self.LRU_THRESHOLD]
397 cros_build_lib.SudoRunCommand(
398 ['rm', '-rf'] + [os.path.join(repo_path, proj) for proj in wipes])
401 osutils.WriteFile(path, "\n".join('%s %i' % x for x in data.iteritems()))
403 def GetRelativePath(self, path):
404 """Returns full path including source directory of path in repo."""
405 return os.path.join(self.directory, path)
407 def ExportManifest(self, mark_revision=False, revisions=True):
408 """Export the revision locked manifest
411 mark_revision: If True, then the sha1 of manifest.git is recorded
412 into the resultant manifest tag as a version attribute.
413 Specifically, if manifests.git is at 1234, <manifest> becomes
414 <manifest revision="1234">.
415 revisions: If True, then rewrite all branches/tags into a specific
416 sha1 revision. If False, don't.
419 The manifest as a string.
421 cmd = ['repo', 'manifest', '-o', '-']
424 output = cros_build_lib.RunCommand(
425 cmd, cwd=self.directory, print_cmd=False, capture_output=True,
426 extra_env={'PAGER':'cat'}).output
428 if not mark_revision:
430 modified = git.RunGit(os.path.join(self.directory, '.repo/manifests'),
431 ['rev-list', '-n1', 'HEAD'])
432 assert modified.output
433 return output.replace("<manifest>", '<manifest revision="%s">' %
434 modified.output.strip())
436 def IsManifestDifferent(self, other_manifest):
437 """Checks whether this manifest is different than another.
439 May blacklists certain repos as part of the diff.
442 other_manifest: Second manifest file to compare against.
445 True: If the manifests are different
446 False: If the manifests are same
448 logging.debug('Calling IsManifestDifferent against %s', other_manifest)
450 black_list = ['="chromium/']
451 blacklist_pattern = re.compile(r'|'.join(black_list))
452 manifest_revision_pattern = re.compile(r'<manifest revision="[a-f0-9]+">',
455 current = self.ExportManifest()
456 with open(other_manifest, 'r') as manifest2_fh:
457 for (line1, line2) in zip(current.splitlines(), manifest2_fh):
458 line1 = line1.strip()
459 line2 = line2.strip()
460 if blacklist_pattern.search(line1):
461 logging.debug('%s ignored %s', line1, line2)
465 logging.debug('Current and other manifest differ.')
466 logging.debug('current: "%s"', line1)
467 logging.debug('other : "%s"', line2)
469 # Ignore revision differences on the manifest line. The revision of
470 # the manifest.git repo is uninteresting when determining if the
471 # current manifest describes the same sources as the other manifest.
472 if manifest_revision_pattern.search(line2):
473 logging.debug('Ignoring difference in manifest revision.')