comparison svnexternals.py @ 291:ba8e91a7c077

Add 'updateexternals' to synchronize externals with remote repo. To synchronize definitions in working copy .hgexternals with remote svn repository: $ hg svn updateexternals To synchronize them with .hgexternals at revision REV: $ hg svn updateexternals REV Last synchronized externals referenced are stored in .hg/svn/externals (a dump of the synchronized .hgexternals).
author Patrick Mezard <pmezard@gmail.com>
date Wed, 22 Apr 2009 23:24:58 +0200
parents 552deb1351ce
children 963d27a0b1c2
comparison
equal deleted inserted replaced
290:153266401676 291:ba8e91a7c077
1 import cStringIO 1 import cStringIO
2 2
3 import os, re, shutil, stat, subprocess
3 from mercurial import util as hgutil 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):
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