Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / lib / rewrite_git_alternates.py
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.
4
5 """Functionality for mangling repository checkouts that are shared
6
7 In particular, this in combination w/ enter_chroot's mount binding, allows
8 us to access the same repo from inside and outside a chroot at the same time
9 """
10
11 from __future__ import print_function
12
13 __all__ = ('RebuildRepoCheckout',)
14
15 import sys
16 import os
17 import shutil
18 import errno
19
20 _path = os.path.realpath(__file__)
21 _path = os.path.normpath(os.path.join(os.path.dirname(_path), '..', '..'))
22 sys.path.insert(0, _path)
23 del _path
24
25 from chromite.lib import cros_build_lib
26 from chromite.lib import git
27 from chromite.lib import osutils
28
29
30 _CACHE_NAME = '.cros_projects.list'
31
32
33 def _FilterNonExistentProjects(project_dir, projects):
34   for project in projects:
35     if os.path.exists(os.path.join(project_dir, project)):
36       yield project
37
38
39 def _CleanAlternates(projects, alt_root):
40   alt_root = os.path.normpath(alt_root)
41
42   projects = set(projects)
43   # Ignore our cache.
44   projects.add(_CACHE_NAME)
45   required_directories = set(os.path.dirname(x) for x in projects)
46
47   for abs_root, dirs, files in os.walk(alt_root):
48     rel_root = abs_root[len(alt_root):].strip('/')
49
50     if rel_root not in required_directories:
51       shutil.rmtree(abs_root)
52       dirs[:] = []
53       continue
54
55     if rel_root:
56       for filename in files:
57         if os.path.join(rel_root, filename) not in projects:
58           os.unlink(os.path.join(abs_root, filename))
59
60
61 def _UpdateAlternatesDir(alternates_root, reference_maps, projects):
62   for project in projects:
63     alt_path = os.path.join(alternates_root, project)
64     paths = []
65     for k, v in reference_maps.iteritems():
66       suffix = os.path.join('.repo', 'project-objects', project, 'objects')
67       if os.path.exists(os.path.join(k, suffix)):
68         paths.append(os.path.join(v, suffix))
69
70     osutils.SafeMakedirs(os.path.dirname(alt_path))
71     osutils.WriteFile(alt_path, '%s\n' % ('\n'.join(paths),), atomic=True)
72
73
74 def _UpdateGitAlternates(proj_root, projects):
75   for project in projects:
76
77     alt_path = os.path.join(proj_root, project, 'objects', 'info',
78                             'alternates')
79     tmp_path = '%s.tmp' % alt_path
80
81     # Clean out any tmp files that may have existed prior.
82     osutils.SafeUnlink(tmp_path)
83
84     # The pathway is written relative to the alternates files absolute path;
85     # literally, .repo/projects/chromite.git/objects/info/alternates.
86     relpath = '../' * (project.count('/') + 4)
87     relpath = os.path.join(relpath, 'alternates', project)
88
89     osutils.SafeMakedirs(os.path.dirname(tmp_path))
90     os.symlink(relpath , tmp_path)
91     os.rename(tmp_path, alt_path)
92
93
94 def _GetProjects(repo_root):
95   # Note that we cannot rely upon projects.list, nor repo list, nor repo forall
96   # here to be authoritive.
97   # if we rely on the manifest contents, the local tree may not yet be
98   # updated- thus if we drop the alternate for that project, that project is no
99   # longer usable (which can tick off repo sync).
100   # Thus, we just iterate over the raw underlying projects store, and generate
101   # alternates for that; we regenerate based on either the manifest changing,
102   # local_manifest having changed, or projects.list having changed (which
103   # occurs during partial local repo syncs; aka repo sync chromite for
104   # example).
105   # Finally, note we have to truncate our mtime awareness to just integers;
106   # this is required since utime isn't guaranteed to set floats, despite our
107   # being able to get a float back from stat'ing.
108
109   manifest_xml = os.path.join(repo_root, 'manifest.xml')
110   times = [os.lstat(manifest_xml).st_mtime,
111            os.stat(manifest_xml).st_mtime]
112   for path in ('local_manifest.xml', 'project.list'):
113     path = os.path.join(repo_root, path)
114     if os.path.exists(path):
115       times.append(os.stat(path).st_mtime)
116
117   manifest_time = long(max(times))
118
119   cache_path = os.path.join(repo_root, _CACHE_NAME)
120
121   try:
122     if long(os.stat(cache_path).st_mtime) == manifest_time:
123       return osutils.ReadFile(cache_path).split()
124   except EnvironmentError as e:
125     if e.errno != errno.ENOENT:
126       raise
127
128   # The -a ! section of this find invocation is to block descent
129   # into the actual git repository; for IO constrained systems,
130   # this avoids a fairly large amount of inode/dentry load up.
131   # TLDR; It's faster, don't remove it ;)
132   data = cros_build_lib.RunCommand(
133       ['find', './', '-type', 'd', '-name', '*.git', '-a',
134        '!', '-wholename', '*/*.git/*', '-prune'],
135       cwd=os.path.join(repo_root, 'project-objects'), capture_output=True)
136
137   # Drop the leading ./ and the trailing .git
138   data = [x[2:-4] for x in data.output.splitlines() if x]
139
140   with open(cache_path, 'w') as f:
141     f.write('\n'.join(sorted(data)))
142
143   # Finally, mark the cache with the time of the manifest.xml we examined.
144   os.utime(cache_path, (manifest_time, manifest_time))
145   return data
146
147
148 class Failed(Exception):
149   """Exception used to fail out for a bad environment."""
150
151
152 def _RebuildRepoCheckout(target_root, reference_map,
153                          alternates_dir):
154   repo_root = os.path.join(target_root, '.repo')
155   proj_root = os.path.join(repo_root, 'project-objects')
156
157   manifest_path = os.path.join(repo_root, 'manifest.xml')
158   if not os.path.exists(manifest_path):
159     raise Failed('%r does not exist, thus cannot be a repo checkout' %
160                  manifest_path)
161
162   projects = ['%s.git' % x for x in _GetProjects(repo_root)]
163   projects = _FilterNonExistentProjects(proj_root, projects)
164   projects = list(sorted(projects))
165
166   if not osutils.SafeMakedirs(alternates_dir, 0o775):
167     # We know the directory exists; thus cleanse out
168     # dead alternates.
169     _CleanAlternates(projects, alternates_dir)
170
171   _UpdateAlternatesDir(alternates_dir, reference_map, projects)
172   _UpdateGitAlternates(proj_root, projects)
173
174
175 def WalkReferences(repo_root, max_depth=5, suppress=()):
176   """Given a repo checkout root, find the repo's it references up to max_depth.
177
178   Args:
179     repo_root: The root of a repo checkout to start from
180     max_depth: Git internally limits the max alternates depth to 5;
181       this option exists to adjust how deep we're willing to look.
182     suppress: List of repos already seen (and so to ignore).
183
184   Returns:
185     List of repository roots required for this repo_root.
186   """
187
188   original_root = repo_root
189   seen = set(os.path.abspath(x) for x in suppress)
190
191   for _x in xrange(0, max_depth):
192     repo_root = os.path.abspath(repo_root)
193
194     if repo_root in seen:
195       # Cyclic reference graph; break out of it, if someone induced this the
196       # necessary objects should be in place.  If they aren't, really isn't
197       # much that can be done.
198       return
199
200     yield repo_root
201     seen.add(repo_root)
202     base = os.path.join(repo_root, '.repo', 'manifests.git')
203     result = git.RunGit(
204         base, ['config', 'repo.reference'], error_code_ok=True)
205
206     if result.returncode not in (0, 1):
207       raise Failed('Unexpected returncode %i from examining %s git '
208                    'repo.reference configuration' %
209                    (result.returncode, base))
210
211     repo_root = result.output.strip()
212     if not repo_root:
213       break
214
215   else:
216     raise Failed('While tracing out the references of %s, we recursed more '
217                  'than the allowed %i times ending at %s'
218                  % (original_root, max_depth, repo_root))
219
220
221 def RebuildRepoCheckout(repo_root, initial_reference,
222                         chroot_reference_root=None):
223   """Rebuild a repo checkout's 'alternate tree' rewriting the repo to use it
224
225   Args:
226     repo_root: absolute path to the root of a repository checkout
227     initial_reference: absolute path to the root of the repository that is
228       shared
229     chroot_reference_root: if given, repo_root will have it's chroot
230       alternates tree configured with this pathway, enabling repo access to
231       work from within the chroot.
232   """
233
234   reference_roots = list(WalkReferences(initial_reference,
235                                         suppress=[repo_root]))
236
237   # Always rebuild the external alternates for any operation; 1) we don't want
238   # external out of sync from chroot, 2) if this is the first conversion, if
239   # we only update chroot it'll break external access to the repo.
240   reference_map = dict((x, x) for x in reference_roots)
241   rebuilds = [('alternates', reference_map)]
242   if chroot_reference_root:
243     alternates_dir = 'chroot/alternates'
244     base = os.path.join(chroot_reference_root, '.repo', 'chroot', 'external')
245     reference_map = dict((x, '%s%i' % (base, idx + 1))
246                          for idx, x in enumerate(reference_roots))
247     rebuilds += [('chroot/alternates', reference_map)]
248
249   for alternates_dir, reference_map in rebuilds:
250     alternates_dir = os.path.join(repo_root, '.repo', alternates_dir)
251     _RebuildRepoCheckout(repo_root,
252                          reference_map,
253                          alternates_dir)
254   return reference_roots
255
256
257 if __name__ == '__main__':
258   chroot_root = None
259   if len(sys.argv) not in (3, 4):
260     sys.stderr.write('Usage: %s <repository_root> <referenced_repository> '
261                      '[path_from_within_the_chroot]\n' % (sys.argv[0],))
262     sys.exit(1)
263   if len(sys.argv) == 4:
264     chroot_root = sys.argv[3]
265   ret = RebuildRepoCheckout(sys.argv[1], sys.argv[2],
266                             chroot_reference_root=chroot_root)
267   print('\n'.join(ret))