Shawn O. Pearce | 0125ae2 | 2009-07-03 18:05:23 -0700 | [diff] [blame] | 1 | # |
| 2 | # Copyright (C) 2009 The Android Open Source Project |
| 3 | # |
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | # you may not use this file except in compliance with the License. |
| 6 | # You may obtain a copy of the License at |
| 7 | # |
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | # |
| 10 | # Unless required by applicable law or agreed to in writing, software |
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | # See the License for the specific language governing permissions and |
| 14 | # limitations under the License. |
| 15 | |
| 16 | import sys |
| 17 | import os |
| 18 | import shutil |
| 19 | |
| 20 | from error import GitError |
| 21 | from error import ManifestParseError |
| 22 | from git_command import GitCommand |
| 23 | from git_config import GitConfig |
| 24 | from git_config import IsId |
| 25 | from manifest import Manifest |
| 26 | from progress import Progress |
| 27 | from project import RemoteSpec |
| 28 | from project import Project |
| 29 | from project import MetaProject |
| 30 | from project import R_HEADS |
| 31 | from project import HEAD |
| 32 | from project import _lwrite |
| 33 | |
| 34 | import manifest_xml |
| 35 | |
| 36 | GITLINK = '160000' |
| 37 | |
| 38 | def _rmdir(dir, top): |
| 39 | while dir != top: |
| 40 | try: |
| 41 | os.rmdir(dir) |
| 42 | except OSError: |
| 43 | break |
| 44 | dir = os.path.dirname(dir) |
| 45 | |
| 46 | def _rmref(gitdir, ref): |
| 47 | os.remove(os.path.join(gitdir, ref)) |
| 48 | log = os.path.join(gitdir, 'logs', ref) |
| 49 | if os.path.exists(log): |
| 50 | os.remove(log) |
| 51 | _rmdir(os.path.dirname(log), gitdir) |
| 52 | |
| 53 | def _has_gitmodules(d): |
| 54 | return os.path.exists(os.path.join(d, '.gitmodules')) |
| 55 | |
| 56 | class SubmoduleManifest(Manifest): |
| 57 | """manifest from .gitmodules file""" |
| 58 | |
| 59 | @classmethod |
| 60 | def Is(cls, repodir): |
| 61 | return _has_gitmodules(os.path.dirname(repodir)) \ |
| 62 | or _has_gitmodules(os.path.join(repodir, 'manifest')) \ |
| 63 | or _has_gitmodules(os.path.join(repodir, 'manifests')) |
| 64 | |
| 65 | @classmethod |
| 66 | def IsBare(cls, p): |
| 67 | try: |
| 68 | p.bare_git.cat_file('-e', '%s:.gitmodules' % p.GetRevisionId()) |
| 69 | except GitError: |
| 70 | return False |
| 71 | return True |
| 72 | |
| 73 | def __init__(self, repodir): |
| 74 | Manifest.__init__(self, repodir) |
| 75 | |
| 76 | gitdir = os.path.join(repodir, 'manifest.git') |
| 77 | config = GitConfig.ForRepository(gitdir = gitdir) |
| 78 | |
| 79 | if config.GetBoolean('repo.mirror'): |
| 80 | worktree = os.path.join(repodir, 'manifest') |
| 81 | relpath = None |
| 82 | else: |
| 83 | worktree = self.topdir |
| 84 | relpath = '.' |
| 85 | |
| 86 | self.manifestProject = MetaProject(self, '__manifest__', |
| 87 | gitdir = gitdir, |
| 88 | worktree = worktree, |
| 89 | relpath = relpath) |
| 90 | self._modules = GitConfig(os.path.join(worktree, '.gitmodules'), |
| 91 | pickleFile = os.path.join( |
| 92 | repodir, '.repopickle_gitmodules' |
| 93 | )) |
| 94 | self._review = GitConfig(os.path.join(worktree, '.review'), |
| 95 | pickleFile = os.path.join( |
| 96 | repodir, '.repopickle_review' |
| 97 | )) |
| 98 | self._Unload() |
| 99 | |
| 100 | @property |
| 101 | def projects(self): |
| 102 | self._Load() |
| 103 | return self._projects |
| 104 | |
Shawn O. Pearce | 13f3da5 | 2010-12-07 10:31:19 -0800 | [diff] [blame] | 105 | @property |
| 106 | def notice(self): |
| 107 | return self._modules.GetString('repo.notice') |
| 108 | |
Shawn O. Pearce | 0125ae2 | 2009-07-03 18:05:23 -0700 | [diff] [blame] | 109 | def InitBranch(self): |
| 110 | m = self.manifestProject |
| 111 | if m.CurrentBranch is None: |
| 112 | b = m.revisionExpr |
| 113 | if b.startswith(R_HEADS): |
| 114 | b = b[len(R_HEADS):] |
| 115 | return m.StartBranch(b) |
| 116 | return True |
| 117 | |
| 118 | def SetMRefs(self, project): |
| 119 | if project.revisionId is None: |
| 120 | # Special project, e.g. the manifest or repo executable. |
| 121 | # |
| 122 | return |
| 123 | |
| 124 | ref = 'refs/remotes/m' |
| 125 | cur = project.bare_ref.get(ref) |
| 126 | exp = project.revisionId |
| 127 | if cur != exp: |
| 128 | msg = 'manifest set to %s' % exp |
| 129 | project.bare_git.UpdateRef(ref, exp, message = msg, detach = True) |
| 130 | |
| 131 | ref = 'refs/remotes/m-revision' |
| 132 | cur = project.bare_ref.symref(ref) |
| 133 | exp = project.revisionExpr |
| 134 | if exp is None: |
| 135 | if cur: |
| 136 | _rmref(project.gitdir, ref) |
| 137 | elif cur != exp: |
| 138 | remote = project.GetRemote(project.remote.name) |
| 139 | dst = remote.ToLocal(exp) |
| 140 | msg = 'manifest set to %s (%s)' % (exp, dst) |
| 141 | project.bare_git.symbolic_ref('-m', msg, ref, dst) |
| 142 | |
| 143 | def Upgrade_Local(self, old): |
| 144 | if isinstance(old, manifest_xml.XmlManifest): |
| 145 | self.FromXml_Local_1(old, checkout=True) |
| 146 | self.FromXml_Local_2(old) |
| 147 | else: |
| 148 | raise ManifestParseError, 'cannot upgrade manifest' |
| 149 | |
| 150 | def FromXml_Local_1(self, old, checkout): |
| 151 | os.rename(old.manifestProject.gitdir, |
| 152 | os.path.join(old.repodir, 'manifest.git')) |
| 153 | |
| 154 | oldmp = old.manifestProject |
| 155 | oldBranch = oldmp.CurrentBranch |
| 156 | b = oldmp.GetBranch(oldBranch).merge |
| 157 | if not b: |
| 158 | raise ManifestParseError, 'cannot upgrade manifest' |
| 159 | if b.startswith(R_HEADS): |
| 160 | b = b[len(R_HEADS):] |
| 161 | |
| 162 | newmp = self.manifestProject |
| 163 | self._CleanOldMRefs(newmp) |
| 164 | if oldBranch != b: |
| 165 | newmp.bare_git.branch('-m', oldBranch, b) |
| 166 | newmp.config.ClearCache() |
| 167 | |
| 168 | old_remote = newmp.GetBranch(b).remote.name |
| 169 | act_remote = self._GuessRemoteName(old) |
| 170 | if old_remote != act_remote: |
| 171 | newmp.bare_git.remote('rename', old_remote, act_remote) |
| 172 | newmp.config.ClearCache() |
| 173 | newmp.remote.name = act_remote |
| 174 | print >>sys.stderr, "Assuming remote named '%s'" % act_remote |
| 175 | |
| 176 | if checkout: |
| 177 | for p in old.projects.values(): |
| 178 | for c in p.copyfiles: |
| 179 | if os.path.exists(c.abs_dest): |
| 180 | os.remove(c.abs_dest) |
| 181 | newmp._InitWorkTree() |
| 182 | else: |
| 183 | newmp._LinkWorkTree() |
| 184 | |
| 185 | _lwrite(os.path.join(newmp.worktree,'.git',HEAD), |
| 186 | 'ref: refs/heads/%s\n' % b) |
| 187 | |
| 188 | def _GuessRemoteName(self, old): |
| 189 | used = {} |
| 190 | for p in old.projects.values(): |
| 191 | n = p.remote.name |
| 192 | used[n] = used.get(n, 0) + 1 |
| 193 | |
| 194 | remote_name = 'origin' |
| 195 | remote_used = 0 |
| 196 | for n in used.keys(): |
| 197 | if remote_used < used[n]: |
| 198 | remote_used = used[n] |
| 199 | remote_name = n |
| 200 | return remote_name |
| 201 | |
| 202 | def FromXml_Local_2(self, old): |
| 203 | shutil.rmtree(old.manifestProject.worktree) |
| 204 | os.remove(old._manifestFile) |
| 205 | |
| 206 | my_remote = self._Remote().name |
| 207 | new_base = os.path.join(self.repodir, 'projects') |
| 208 | old_base = os.path.join(self.repodir, 'projects.old') |
| 209 | os.rename(new_base, old_base) |
| 210 | os.makedirs(new_base) |
| 211 | |
| 212 | info = [] |
| 213 | pm = Progress('Converting projects', len(self.projects)) |
| 214 | for p in self.projects.values(): |
| 215 | pm.update() |
| 216 | |
| 217 | old_p = old.projects.get(p.name) |
| 218 | old_gitdir = os.path.join(old_base, '%s.git' % p.relpath) |
| 219 | if not os.path.isdir(old_gitdir): |
| 220 | continue |
| 221 | |
| 222 | parent = os.path.dirname(p.gitdir) |
| 223 | if not os.path.isdir(parent): |
| 224 | os.makedirs(parent) |
| 225 | os.rename(old_gitdir, p.gitdir) |
| 226 | _rmdir(os.path.dirname(old_gitdir), self.repodir) |
| 227 | |
| 228 | if not os.path.isdir(p.worktree): |
| 229 | os.makedirs(p.worktree) |
| 230 | |
| 231 | if os.path.isdir(os.path.join(p.worktree, '.git')): |
| 232 | p._LinkWorkTree(relink=True) |
| 233 | |
| 234 | self._CleanOldMRefs(p) |
| 235 | if old_p and old_p.remote.name != my_remote: |
| 236 | info.append("%s/: renamed remote '%s' to '%s'" \ |
| 237 | % (p.relpath, old_p.remote.name, my_remote)) |
| 238 | p.bare_git.remote('rename', old_p.remote.name, my_remote) |
| 239 | p.config.ClearCache() |
| 240 | |
| 241 | self.SetMRefs(p) |
| 242 | pm.end() |
| 243 | for i in info: |
| 244 | print >>sys.stderr, i |
| 245 | |
| 246 | def _CleanOldMRefs(self, p): |
| 247 | all_refs = p._allrefs |
| 248 | for ref in all_refs.keys(): |
| 249 | if ref.startswith(manifest_xml.R_M): |
| 250 | if p.bare_ref.symref(ref) != '': |
| 251 | _rmref(p.gitdir, ref) |
| 252 | else: |
| 253 | p.bare_git.DeleteRef(ref, all_refs[ref]) |
| 254 | |
| 255 | def FromXml_Definition(self, old): |
| 256 | """Convert another manifest representation to this one. |
| 257 | """ |
| 258 | mp = self.manifestProject |
| 259 | gm = self._modules |
| 260 | gr = self._review |
| 261 | |
| 262 | fd = open(os.path.join(mp.worktree, '.gitignore'), 'ab') |
| 263 | fd.write('/.repo\n') |
| 264 | fd.close() |
| 265 | |
| 266 | sort_projects = list(old.projects.keys()) |
| 267 | sort_projects.sort() |
| 268 | |
| 269 | b = mp.GetBranch(mp.CurrentBranch).merge |
| 270 | if b.startswith(R_HEADS): |
| 271 | b = b[len(R_HEADS):] |
| 272 | |
Shawn O. Pearce | 13f3da5 | 2010-12-07 10:31:19 -0800 | [diff] [blame] | 273 | if old.notice: |
| 274 | gm.SetString('repo.notice', old.notice) |
| 275 | |
Shawn O. Pearce | 0125ae2 | 2009-07-03 18:05:23 -0700 | [diff] [blame] | 276 | info = [] |
| 277 | pm = Progress('Converting manifest', len(sort_projects)) |
| 278 | for p in sort_projects: |
| 279 | pm.update() |
| 280 | p = old.projects[p] |
| 281 | |
| 282 | gm.SetString('submodule.%s.path' % p.name, p.relpath) |
| 283 | gm.SetString('submodule.%s.url' % p.name, p.remote.url) |
| 284 | |
| 285 | if gr.GetString('review.url') is None: |
| 286 | gr.SetString('review.url', p.remote.review) |
| 287 | elif gr.GetString('review.url') != p.remote.review: |
| 288 | gr.SetString('review.%s.url' % p.name, p.remote.review) |
| 289 | |
| 290 | r = p.revisionExpr |
| 291 | if r and not IsId(r): |
| 292 | if r.startswith(R_HEADS): |
| 293 | r = r[len(R_HEADS):] |
| 294 | if r == b: |
| 295 | r = '.' |
| 296 | gm.SetString('submodule.%s.revision' % p.name, r) |
| 297 | |
| 298 | for c in p.copyfiles: |
| 299 | info.append('Moved %s out of %s' % (c.src, p.relpath)) |
| 300 | c._Copy() |
| 301 | p.work_git.rm(c.src) |
| 302 | mp.work_git.add(c.dest) |
| 303 | |
| 304 | self.SetRevisionId(p.relpath, p.GetRevisionId()) |
| 305 | mp.work_git.add('.gitignore', '.gitmodules', '.review') |
| 306 | pm.end() |
| 307 | for i in info: |
| 308 | print >>sys.stderr, i |
| 309 | |
| 310 | def _Unload(self): |
| 311 | self._loaded = False |
| 312 | self._projects = {} |
| 313 | self._revisionIds = None |
| 314 | self.branch = None |
| 315 | |
| 316 | def _Load(self): |
| 317 | if not self._loaded: |
| 318 | f = os.path.join(self.repodir, manifest_xml.LOCAL_MANIFEST_NAME) |
| 319 | if os.path.exists(f): |
| 320 | print >>sys.stderr, 'warning: ignoring %s' % f |
| 321 | |
| 322 | m = self.manifestProject |
| 323 | b = m.CurrentBranch |
| 324 | if not b: |
| 325 | raise ManifestParseError, 'manifest cannot be on detached HEAD' |
| 326 | b = m.GetBranch(b).merge |
| 327 | if b.startswith(R_HEADS): |
| 328 | b = b[len(R_HEADS):] |
| 329 | self.branch = b |
| 330 | m.remote.name = self._Remote().name |
| 331 | |
| 332 | self._ParseModules() |
| 333 | |
| 334 | if self.IsMirror: |
| 335 | self._AddMetaProjectMirror(self.repoProject) |
| 336 | self._AddMetaProjectMirror(self.manifestProject) |
| 337 | |
| 338 | self._loaded = True |
| 339 | |
| 340 | def _ParseModules(self): |
| 341 | byPath = dict() |
| 342 | for name in self._modules.GetSubSections('submodule'): |
| 343 | p = self._ParseProject(name) |
| 344 | if self._projects.get(p.name): |
| 345 | raise ManifestParseError, 'duplicate project "%s"' % p.name |
| 346 | if byPath.get(p.relpath): |
| 347 | raise ManifestParseError, 'duplicate path "%s"' % p.relpath |
| 348 | self._projects[p.name] = p |
| 349 | byPath[p.relpath] = p |
| 350 | |
| 351 | for relpath in self._allRevisionIds.keys(): |
| 352 | if relpath not in byPath: |
| 353 | raise ManifestParseError, \ |
| 354 | 'project "%s" not in .gitmodules' \ |
| 355 | % relpath |
| 356 | |
| 357 | def _Remote(self): |
| 358 | m = self.manifestProject |
| 359 | b = m.GetBranch(m.CurrentBranch) |
| 360 | return b.remote |
| 361 | |
| 362 | def _ResolveUrl(self, url): |
| 363 | if url.startswith('./') or url.startswith('../'): |
| 364 | base = self._Remote().url |
| 365 | try: |
| 366 | base = base[:base.rindex('/')+1] |
| 367 | except ValueError: |
| 368 | base = base[:base.rindex(':')+1] |
| 369 | if url.startswith('./'): |
| 370 | url = url[2:] |
| 371 | while '/' in base and url.startswith('../'): |
| 372 | base = base[:base.rindex('/')+1] |
| 373 | url = url[3:] |
| 374 | return base + url |
| 375 | return url |
| 376 | |
| 377 | def _GetRevisionId(self, path): |
| 378 | return self._allRevisionIds.get(path) |
| 379 | |
| 380 | @property |
| 381 | def _allRevisionIds(self): |
| 382 | if self._revisionIds is None: |
| 383 | a = dict() |
| 384 | p = GitCommand(self.manifestProject, |
| 385 | ['ls-files','-z','--stage'], |
| 386 | capture_stdout = True) |
| 387 | for line in p.process.stdout.read().split('\0')[:-1]: |
| 388 | l_info, l_path = line.split('\t', 2) |
| 389 | l_mode, l_id, l_stage = l_info.split(' ', 2) |
| 390 | if l_mode == GITLINK and l_stage == '0': |
| 391 | a[l_path] = l_id |
| 392 | p.Wait() |
| 393 | self._revisionIds = a |
| 394 | return self._revisionIds |
| 395 | |
| 396 | def SetRevisionId(self, path, id): |
| 397 | self.manifestProject.work_git.update_index( |
| 398 | '--add','--cacheinfo', GITLINK, id, path) |
| 399 | |
| 400 | def _ParseProject(self, name): |
| 401 | gm = self._modules |
| 402 | gr = self._review |
| 403 | |
| 404 | path = gm.GetString('submodule.%s.path' % name) |
| 405 | if not path: |
| 406 | path = name |
| 407 | |
| 408 | revId = self._GetRevisionId(path) |
| 409 | if not revId: |
| 410 | raise ManifestParseError( |
| 411 | 'submodule "%s" has no revision at "%s"' \ |
| 412 | % (name, path)) |
| 413 | |
| 414 | url = gm.GetString('submodule.%s.url' % name) |
| 415 | if not url: |
| 416 | url = name |
| 417 | url = self._ResolveUrl(url) |
| 418 | |
| 419 | review = gr.GetString('review.%s.url' % name) |
| 420 | if not review: |
| 421 | review = gr.GetString('review.url') |
| 422 | if not review: |
| 423 | review = self._Remote().review |
| 424 | |
| 425 | remote = RemoteSpec(self._Remote().name, url, review) |
| 426 | revExpr = gm.GetString('submodule.%s.revision' % name) |
| 427 | if revExpr == '.': |
| 428 | revExpr = self.branch |
| 429 | |
| 430 | if self.IsMirror: |
| 431 | relpath = None |
| 432 | worktree = None |
| 433 | gitdir = os.path.join(self.topdir, '%s.git' % name) |
| 434 | else: |
| 435 | worktree = os.path.join(self.topdir, path) |
| 436 | gitdir = os.path.join(self.repodir, 'projects/%s.git' % name) |
| 437 | |
| 438 | return Project(manifest = self, |
| 439 | name = name, |
| 440 | remote = remote, |
| 441 | gitdir = gitdir, |
| 442 | worktree = worktree, |
| 443 | relpath = path, |
| 444 | revisionExpr = revExpr, |
| 445 | revisionId = revId) |
| 446 | |
| 447 | def _AddMetaProjectMirror(self, m): |
| 448 | m_url = m.GetRemote(m.remote.name).url |
| 449 | if m_url.endswith('/.git'): |
| 450 | raise ManifestParseError, 'refusing to mirror %s' % m_url |
| 451 | |
| 452 | name = self._GuessMetaName(m_url) |
| 453 | if name.endswith('.git'): |
| 454 | name = name[:-4] |
| 455 | |
| 456 | if name not in self._projects: |
| 457 | m.PreSync() |
| 458 | gitdir = os.path.join(self.topdir, '%s.git' % name) |
| 459 | project = Project(manifest = self, |
| 460 | name = name, |
| 461 | remote = RemoteSpec(self._Remote().name, m_url), |
| 462 | gitdir = gitdir, |
| 463 | worktree = None, |
| 464 | relpath = None, |
| 465 | revisionExpr = m.revisionExpr, |
| 466 | revisionId = None) |
| 467 | self._projects[project.name] = project |
| 468 | |
| 469 | def _GuessMetaName(self, m_url): |
| 470 | parts = m_url.split('/') |
| 471 | name = parts[-1] |
| 472 | parts = parts[0:-1] |
| 473 | s = len(parts) - 1 |
| 474 | while s > 0: |
| 475 | l = '/'.join(parts[0:s]) + '/' |
| 476 | r = '/'.join(parts[s:]) + '/' |
| 477 | for p in self._projects.values(): |
| 478 | if p.name.startswith(r) and p.remote.url.startswith(l): |
| 479 | return r + name |
| 480 | s -= 1 |
| 481 | return m_url[m_url.rindex('/') + 1:] |