Mercurial > hgsubversion
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 |