Mercurial > hgsubversion
comparison svnexternals.py @ 304:ce676eff002b
First merge, totally untested.
| author | Dan Villiom Podlaski Christiansen <danchr@gmail.com> |
|---|---|
| date | Fri, 01 May 2009 10:28:59 +0200 |
| parents | ba8e91a7c077 |
| children | 963d27a0b1c2 |
comparison
equal
deleted
inserted
replaced
| 303:f423a8780832 | 304:ce676eff002b |
|---|---|
| 1 import cStringIO | 1 import cStringIO |
| 2 | 2 |
| 3 from mercurial import util as merc_util | 3 import os, re, shutil, stat, subprocess |
| 4 from mercurial import util as hgutil | |
| 5 from mercurial.i18n import _ | |
| 4 | 6 |
| 5 class externalsfile(dict): | 7 class externalsfile(dict): |
| 6 """Map svn directories to lists of externals entries. | 8 """Map svn directories to lists of externals entries. |
| 7 """ | 9 """ |
| 8 def __init__(self): | 10 def __init__(self): |
| 22 else: | 24 else: |
| 23 super(externalsfile, self).__setitem__(key, value) | 25 super(externalsfile, self).__setitem__(key, value) |
| 24 | 26 |
| 25 def write(self): | 27 def write(self): |
| 26 fp = cStringIO.StringIO() | 28 fp = cStringIO.StringIO() |
| 27 for target in merc_util.sort(self): | 29 for target in sorted(self): |
| 28 lines = self[target] | 30 lines = self[target] |
| 29 if not lines: | 31 if not lines: |
| 30 continue | 32 continue |
| 31 if not target: | 33 if not target: |
| 32 target = '.' | 34 target = '.' |
| 45 if not line.strip(): | 47 if not line.strip(): |
| 46 continue | 48 continue |
| 47 if line.startswith('['): | 49 if line.startswith('['): |
| 48 line = line.strip() | 50 line = line.strip() |
| 49 if line[-1] != ']': | 51 if line[-1] != ']': |
| 50 raise merc_util.Abort('invalid externals section name: %s' % line) | 52 raise hgutil.Abort('invalid externals section name: %s' % line) |
| 51 target = line[1:-1] | 53 target = line[1:-1] |
| 52 if target == '.': | 54 if target == '.': |
| 53 target = '' | 55 target = '' |
| 54 elif line.startswith(' '): | 56 elif line.startswith(' '): |
| 55 line = line.rstrip('\n') | 57 line = line.rstrip('\n') |
| 56 if target is None or not line: | 58 if target is None or not line: |
| 57 continue | 59 continue |
| 58 self.setdefault(target, []).append(line[1:]) | 60 self.setdefault(target, []).append(line[1:]) |
| 59 | 61 |
| 60 def diff(ext1, ext2): | 62 def diff(ext1, ext2): |
| 61 """Compare 2 externalsfile and yield tuples like (dir, value1, value2) | 63 """Compare 2 externalsfile and yield tuples like (dir, value1, value2) |
| 62 where value1 is the external value in ext1 for dir or None, and | 64 where value1 is the external value in ext1 for dir or None, and |
| 63 value2 the same in ext2. | 65 value2 the same in ext2. |
| 64 """ | 66 """ |
| 68 elif ext1[d] != ext2[d]: | 70 elif ext1[d] != ext2[d]: |
| 69 yield d, '\n'.join(ext1[d]), '\n'.join(ext2[d]) | 71 yield d, '\n'.join(ext1[d]), '\n'.join(ext2[d]) |
| 70 for d in ext2: | 72 for d in ext2: |
| 71 if d not in ext1: | 73 if d not in ext1: |
| 72 yield d, None, '\n'.join(ext2[d]) | 74 yield d, None, '\n'.join(ext2[d]) |
| 75 | |
| 76 class BadDefinition(Exception): | |
| 77 pass | |
| 78 | |
| 79 re_defold = re.compile(r'^(.*?)\s+(?:-r\s*(\d+)\s+)?([a-zA-Z]+://.*)$') | |
| 80 re_defnew = re.compile(r'^(?:-r\s*(\d+)\s+)?((?:[a-zA-Z]+://|\^/).*)\s+(.*)$') | |
| 81 re_pegrev = re.compile(r'^(.*)@(\d+)$') | |
| 82 re_scheme = re.compile(r'^[a-zA-Z]+://') | |
| 83 | |
| 84 def parsedefinition(line): | |
| 85 """Parse an external definition line, return a tuple (path, rev, source) | |
| 86 or raise BadDefinition. | |
| 87 """ | |
| 88 # The parsing is probably not correct wrt path with whitespaces or | |
| 89 # potential quotes. svn documentation is not really talkative about | |
| 90 # these either. | |
| 91 line = line.strip() | |
| 92 m = re_defnew.search(line) | |
| 93 if m: | |
| 94 rev, source, path = m.group(1, 2, 3) | |
| 95 else: | |
| 96 m = re_defold.search(line) | |
| 97 if not m: | |
| 98 raise BadDefinition() | |
| 99 path, rev, source = m.group(1, 2, 3) | |
| 100 # Look for peg revisions | |
| 101 m = re_pegrev.search(source) | |
| 102 if m: | |
| 103 source, rev = m.group(1, 2) | |
| 104 return (path, rev, source) | |
| 105 | |
| 106 def parsedefinitions(ui, repo, svnroot, exts): | |
| 107 """Return (targetdir, revision, source) tuples. Fail if nested | |
| 108 targetdirs are detected. source is an svn project URL. | |
| 109 """ | |
| 110 defs = [] | |
| 111 for base in sorted(exts): | |
| 112 for line in exts[base]: | |
| 113 try: | |
| 114 path, rev, source = parsedefinition(line) | |
| 115 except BadDefinition: | |
| 116 ui.warn(_('ignoring invalid external definition: %r' % line)) | |
| 117 continue | |
| 118 if re_scheme.search(source): | |
| 119 pass | |
| 120 elif source.startswith('^/'): | |
| 121 source = svnroot + source[1:] | |
| 122 else: | |
| 123 ui.warn(_('ignoring unsupported non-fully qualified external: %r' % source)) | |
| 124 continue | |
| 125 wpath = hgutil.pconvert(os.path.join(base, path)) | |
| 126 wpath = hgutil.canonpath(repo.root, '', wpath) | |
| 127 defs.append((wpath, rev, source)) | |
| 128 # Check target dirs are not nested | |
| 129 defs.sort() | |
| 130 for i, d in enumerate(defs): | |
| 131 for d2 in defs[i+1:]: | |
| 132 if d2[0].startswith(d[0] + '/'): | |
| 133 raise hgutil.Abort(_('external directories cannot nest:\n%s\n%s') | |
| 134 % (d[0], d2[0])) | |
| 135 return defs | |
| 136 | |
| 137 def computeactions(ui, repo, svnroot, ext1, ext2): | |
| 138 | |
| 139 def listdefs(data): | |
| 140 defs = {} | |
| 141 exts = externalsfile() | |
| 142 exts.read(data) | |
| 143 for d in parsedefinitions(ui, repo, svnroot, exts): | |
| 144 defs[d[0]] = d | |
| 145 return defs | |
| 146 | |
| 147 ext1 = listdefs(ext1) | |
| 148 ext2 = listdefs(ext2) | |
| 149 for wp1 in ext1: | |
| 150 if wp1 in ext2: | |
| 151 yield 'u', ext2[wp1] | |
| 152 else: | |
| 153 yield 'd', ext1[wp1] | |
| 154 for wp2 in ext2: | |
| 155 if wp2 not in ext1: | |
| 156 yield 'u', ext2[wp2] | |
| 157 | |
| 158 def getsvninfo(svnurl): | |
| 159 """Return a tuple (url, root) for supplied svn URL or working | |
| 160 directory path. | |
| 161 """ | |
| 162 # Yes, this is ugly, but good enough for now | |
| 163 args = ['svn', 'info', '--xml', svnurl] | |
| 164 shell = os.name == 'nt' | |
| 165 p = subprocess.Popen(args, stdout=subprocess.PIPE, shell=shell) | |
| 166 stdout = p.communicate()[0] | |
| 167 if p.returncode: | |
| 168 raise hgutil.Abort(_('cannot get information about %s') | |
| 169 % svnurl) | |
| 170 m = re.search(r'<root>(.*)</root>', stdout, re.S) | |
| 171 if not m: | |
| 172 raise hgutil.Abort(_('cannot find SVN repository root from %s') | |
| 173 % svnurl) | |
| 174 root = m.group(1).rstrip('/') | |
| 175 | |
| 176 m = re.search(r'<url>(.*)</url>', stdout, re.S) | |
| 177 if not m: | |
| 178 raise hgutil.Abort(_('cannot find SVN repository URL from %s') % svnurl) | |
| 179 url = m.group(1) | |
| 180 | |
| 181 m = re.search(r'<entry[^>]+revision="([^"]+)"', stdout, re.S) | |
| 182 if not m: | |
| 183 raise hgutil.Abort(_('cannot find SVN revision from %s') % svnurl) | |
| 184 rev = m.group(1) | |
| 185 return url, root, rev | |
| 186 | |
| 187 class externalsupdater: | |
| 188 def __init__(self, ui, repo): | |
| 189 self.repo = repo | |
| 190 self.ui = ui | |
| 191 | |
| 192 def update(self, wpath, rev, source): | |
| 193 path = self.repo.wjoin(wpath) | |
| 194 revspec = [] | |
| 195 if rev: | |
| 196 revspec = ['-r', rev] | |
| 197 if os.path.isdir(path): | |
| 198 exturl, extroot, extrev = getsvninfo(path) | |
| 199 if source == exturl: | |
| 200 if extrev != rev: | |
| 201 self.ui.status(_('updating external on %s@%s\n') % | |
| 202 (wpath, rev or 'HEAD')) | |
| 203 cwd = os.path.join(self.repo.root, path) | |
| 204 self.svn(['update'] + revspec, cwd) | |
| 205 return | |
| 206 self.delete(wpath) | |
| 207 cwd, dest = os.path.split(path) | |
| 208 cwd = os.path.join(self.repo.root, cwd) | |
| 209 if not os.path.isdir(cwd): | |
| 210 os.makedirs(cwd) | |
| 211 self.ui.status(_('fetching external %s@%s\n') % (wpath, rev or 'HEAD')) | |
| 212 self.svn(['co'] + revspec + [source, dest], cwd) | |
| 213 | |
| 214 def delete(self, wpath): | |
| 215 path = self.repo.wjoin(wpath) | |
| 216 if os.path.isdir(path): | |
| 217 self.ui.status(_('removing external %s\n') % wpath) | |
| 218 | |
| 219 def onerror(function, path, excinfo): | |
| 220 if function is not os.remove: | |
| 221 raise | |
| 222 # read-only files cannot be unlinked under Windows | |
| 223 s = os.stat(path) | |
| 224 if (s.st_mode & stat.S_IWRITE) != 0: | |
| 225 raise | |
| 226 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE) | |
| 227 os.remove(path) | |
| 228 | |
| 229 shutil.rmtree(path, onerror=onerror) | |
| 230 return 1 | |
| 231 | |
| 232 def svn(self, args, cwd): | |
| 233 args = ['svn'] + args | |
| 234 self.ui.debug(_('updating externals: %r, cwd=%s\n') % (args, cwd)) | |
| 235 shell = os.name == 'nt' | |
| 236 subprocess.check_call(args, cwd=cwd, shell=shell) | |
| 237 | |
| 238 def updateexternals(ui, args, repo, **opts): | |
| 239 """update repository externals | |
| 240 """ | |
| 241 if len(args) > 1: | |
| 242 raise hgutil.Abort(_('updateexternals expects at most one changeset')) | |
| 243 node = None | |
| 244 if args: | |
| 245 node = args[0] | |
| 246 | |
| 247 try: | |
| 248 svnurl = file(repo.join('svn/url'), 'rb').read() | |
| 249 except: | |
| 250 raise hgutil.Abort(_('failed to retrieve original svn URL')) | |
| 251 svnroot = getsvninfo(svnurl)[1] | |
| 252 | |
| 253 # Retrieve current externals status | |
| 254 try: | |
| 255 oldext = file(repo.join('svn/externals'), 'rb').read() | |
| 256 except IOError: | |
| 257 oldext = '' | |
| 258 newext = '' | |
| 259 ctx = repo[node] | |
| 260 if '.hgsvnexternals' in ctx: | |
| 261 newext = ctx['.hgsvnexternals'].data() | |
| 262 | |
| 263 updater = externalsupdater(ui, repo) | |
| 264 actions = computeactions(ui, repo, svnroot, oldext, newext) | |
| 265 for action, ext in actions: | |
| 266 if action == 'u': | |
| 267 updater.update(ext[0], ext[1], ext[2]) | |
| 268 elif action == 'd': | |
| 269 updater.delete(ext[0]) | |
| 270 else: | |
| 271 raise hgutil.Abort(_('unknown update actions: %r') % action) | |
| 272 | |
| 273 file(repo.join('svn/externals'), 'wb').write(newext) | |
| 274 |
