Mercurial > hgsubversion
comparison cmdutil.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 | 75d4fde9aa2e |
children | b6a9cdee2f68 |
comparison
equal
deleted
inserted
replaced
303:f423a8780832 | 304:ce676eff002b |
---|---|
1 #!/usr/bin/python | |
2 import re | |
3 import os | |
4 import urllib | |
5 | |
6 from mercurial import util as hgutil | |
7 | |
8 from svn import core | |
9 | |
10 import util | |
11 import svnwrap | |
12 import svnexternals | |
13 | |
14 | |
15 b_re = re.compile(r'^\+\+\+ b\/([^\n]*)', re.MULTILINE) | |
16 a_re = re.compile(r'^--- a\/([^\n]*)', re.MULTILINE) | |
17 devnull_re = re.compile(r'^([-+]{3}) /dev/null', re.MULTILINE) | |
18 header_re = re.compile(r'^diff --git .* b\/(.*)', re.MULTILINE) | |
19 newfile_devnull_re = re.compile(r'^--- /dev/null\n\+\+\+ b/([^\n]*)', | |
20 re.MULTILINE) | |
21 | |
22 | |
23 class NoFilesException(Exception): | |
24 """Exception raised when you try and commit without files. | |
25 """ | |
26 | |
27 def formatrev(rev): | |
28 if rev == -1: | |
29 return '\t(working copy)' | |
30 return '\t(revision %d)' % rev | |
31 | |
32 | |
33 def filterdiff(diff, oldrev, newrev): | |
34 diff = newfile_devnull_re.sub(r'--- \1\t(revision 0)' '\n' | |
35 r'+++ \1\t(working copy)', | |
36 diff) | |
37 oldrev = formatrev(oldrev) | |
38 newrev = formatrev(newrev) | |
39 diff = a_re.sub(r'--- \1'+ oldrev, diff) | |
40 diff = b_re.sub(r'+++ \1' + newrev, diff) | |
41 diff = devnull_re.sub(r'\1 /dev/null\t(working copy)', diff) | |
42 diff = header_re.sub(r'Index: \1' + '\n' + ('=' * 67), diff) | |
43 return diff | |
44 | |
45 | |
46 def parentrev(ui, repo, hge, svn_commit_hashes): | |
47 """Find the svn parent revision of the repo's dirstate. | |
48 """ | |
49 workingctx = repo.parents()[0] | |
50 outrev = util.outgoing_revisions(ui, repo, hge, svn_commit_hashes, | |
51 workingctx.node()) | |
52 if outrev: | |
53 workingctx = repo[outrev[-1]].parents()[0] | |
54 return workingctx | |
55 | |
56 | |
57 def replay_convert_rev(hg_editor, svn, r): | |
58 hg_editor.set_current_rev(r) | |
59 svn.get_replay(r.revnum, hg_editor) | |
60 i = 1 | |
61 if hg_editor.missing_plaintexts: | |
62 hg_editor.ui.debug('Fetching %s files that could not use replay.\n' % | |
63 len(hg_editor.missing_plaintexts)) | |
64 files_to_grab = set() | |
65 rootpath = svn.subdir and svn.subdir[1:] or '' | |
66 for p in hg_editor.missing_plaintexts: | |
67 hg_editor.ui.note('.') | |
68 hg_editor.ui.flush() | |
69 if p[-1] == '/': | |
70 dirpath = p[len(rootpath):] | |
71 files_to_grab.update([dirpath + f for f,k in | |
72 svn.list_files(dirpath, r.revnum) | |
73 if k == 'f']) | |
74 else: | |
75 files_to_grab.add(p[len(rootpath):]) | |
76 hg_editor.ui.note('\nFetching files...\n') | |
77 for p in files_to_grab: | |
78 hg_editor.ui.note('.') | |
79 hg_editor.ui.flush() | |
80 if i % 50 == 0: | |
81 svn.init_ra_and_client() | |
82 i += 1 | |
83 data, mode = svn.get_file(p, r.revnum) | |
84 hg_editor.set_file(p, data, 'x' in mode, 'l' in mode) | |
85 hg_editor.missing_plaintexts = set() | |
86 hg_editor.ui.note('\n') | |
87 hg_editor.commit_current_delta() | |
88 | |
89 | |
90 def _isdir(svn, branchpath, svndir): | |
91 try: | |
92 svn.list_dir('%s/%s' % (branchpath, svndir)) | |
93 return True | |
94 except core.SubversionException: | |
95 return False | |
96 | |
97 | |
98 def _getdirchanges(svn, branchpath, parentctx, ctx, changedfiles, extchanges): | |
99 """Compute directories to add or delete when moving from parentctx | |
100 to ctx, assuming only 'changedfiles' files changed, and 'extchanges' | |
101 external references changed (as returned by svnexternals.diff()). | |
102 | |
103 Return (added, deleted) where 'added' is the list of all added | |
104 directories and 'deleted' the list of deleted directories. | |
105 Intermediate directories are included: if a/b/c is new and requires | |
106 the addition of a/b and a, those will be listed too. Intermediate | |
107 deleted directories are also listed, but item order of undefined | |
108 in either list. | |
109 """ | |
110 def finddirs(path, includeself=False): | |
111 if includeself: | |
112 yield path | |
113 pos = path.rfind('/') | |
114 while pos != -1: | |
115 yield path[:pos] | |
116 pos = path.rfind('/', 0, pos) | |
117 | |
118 def getctxdirs(ctx, keptdirs, extdirs): | |
119 dirs = {} | |
120 for f in ctx.manifest(): | |
121 for d in finddirs(f): | |
122 if d in dirs: | |
123 break | |
124 if d in keptdirs: | |
125 dirs[d] = 1 | |
126 for extdir in extdirs: | |
127 for d in finddirs(extdir, True): | |
128 dirs[d] = 1 | |
129 return dirs | |
130 | |
131 deleted, added = [], [] | |
132 changeddirs = {} | |
133 for f in changedfiles: | |
134 if f in parentctx and f in ctx: | |
135 # Updated files cannot cause directories to be created | |
136 # or removed. | |
137 continue | |
138 for d in finddirs(f): | |
139 changeddirs[d] = 1 | |
140 for e in extchanges: | |
141 if not e[1] or not e[2]: | |
142 for d in finddirs(e[0], True): | |
143 changeddirs[d] = 1 | |
144 if not changeddirs: | |
145 return added, deleted | |
146 olddirs = getctxdirs(parentctx, changeddirs, | |
147 [e[0] for e in extchanges if e[1]]) | |
148 newdirs = getctxdirs(ctx, changeddirs, | |
149 [e[0] for e in extchanges if e[2]]) | |
150 | |
151 for d in newdirs: | |
152 if d not in olddirs and not _isdir(svn, branchpath, d): | |
153 added.append(d) | |
154 | |
155 for d in olddirs: | |
156 if d not in newdirs and _isdir(svn, branchpath, d): | |
157 deleted.append(d) | |
158 | |
159 return added, deleted | |
160 | |
161 | |
162 def _externals(ctx): | |
163 ext = svnexternals.externalsfile() | |
164 if '.hgsvnexternals' in ctx: | |
165 ext.read(ctx['.hgsvnexternals'].data()) | |
166 return ext | |
167 | |
168 | |
169 def commit_from_rev(ui, repo, rev_ctx, hg_editor, svn_url, base_revision, | |
170 username, password): | |
171 """Build and send a commit from Mercurial to Subversion. | |
172 """ | |
173 file_data = {} | |
174 svn = svnwrap.SubversionRepo(svn_url, username, password) | |
175 parent = rev_ctx.parents()[0] | |
176 parent_branch = rev_ctx.parents()[0].branch() | |
177 branch_path = 'trunk' | |
178 | |
179 if parent_branch and parent_branch != 'default': | |
180 branch_path = 'branches/%s' % parent_branch | |
181 | |
182 extchanges = list(svnexternals.diff(_externals(parent), | |
183 _externals(rev_ctx))) | |
184 addeddirs, deleteddirs = _getdirchanges(svn, branch_path, parent, rev_ctx, | |
185 rev_ctx.files(), extchanges) | |
186 deleteddirs = set(deleteddirs) | |
187 | |
188 props = {} | |
189 copies = {} | |
190 for file in rev_ctx.files(): | |
191 if file == '.hgsvnexternals': | |
192 continue | |
193 new_data = base_data = '' | |
194 action = '' | |
195 if file in rev_ctx: | |
196 fctx = rev_ctx.filectx(file) | |
197 new_data = fctx.data() | |
198 | |
199 if 'x' in fctx.flags(): | |
200 props.setdefault(file, {})['svn:executable'] = '*' | |
201 if 'l' in fctx.flags(): | |
202 props.setdefault(file, {})['svn:special'] = '*' | |
203 | |
204 if file not in parent: | |
205 renamed = fctx.renamed() | |
206 if renamed: | |
207 # TODO current model (and perhaps svn model) does not support | |
208 # this kind of renames: a -> b, b -> c | |
209 copies[file] = renamed[0] | |
210 base_data = parent[renamed[0]].data() | |
211 | |
212 action = 'add' | |
213 dirname = '/'.join(file.split('/')[:-1] + ['']) | |
214 else: | |
215 base_data = parent.filectx(file).data() | |
216 if ('x' in parent.filectx(file).flags() | |
217 and 'x' not in rev_ctx.filectx(file).flags()): | |
218 props.setdefault(file, {})['svn:executable'] = None | |
219 if ('l' in parent.filectx(file).flags() | |
220 and 'l' not in rev_ctx.filectx(file).flags()): | |
221 props.setdefault(file, {})['svn:special'] = None | |
222 action = 'modify' | |
223 else: | |
224 pos = file.rfind('/') | |
225 if pos >= 0: | |
226 if file[:pos] in deleteddirs: | |
227 # This file will be removed when its directory is removed | |
228 continue | |
229 action = 'delete' | |
230 file_data[file] = base_data, new_data, action | |
231 | |
232 def svnpath(p): | |
233 return '%s/%s' % (branch_path, p) | |
234 | |
235 changeddirs = [] | |
236 for d, v1, v2 in extchanges: | |
237 props.setdefault(svnpath(d), {})['svn:externals'] = v2 | |
238 if d not in deleteddirs and d not in addeddirs: | |
239 changeddirs.append(svnpath(d)) | |
240 | |
241 # Now we are done with files, we can prune deleted directories | |
242 # against themselves: ignore a/b if a/ is already removed | |
243 deleteddirs2 = list(deleteddirs) | |
244 deleteddirs2.sort(reverse=True) | |
245 for d in deleteddirs2: | |
246 pos = d.rfind('/') | |
247 if pos >= 0 and d[:pos] in deleteddirs: | |
248 deleteddirs.remove(d[:pos]) | |
249 | |
250 newcopies = {} | |
251 for source, dest in copies.iteritems(): | |
252 newcopies[svnpath(source)] = (svnpath(dest), base_revision) | |
253 | |
254 new_target_files = [svnpath(f) for f in file_data] | |
255 for tf, ntf in zip(file_data, new_target_files): | |
256 if tf in file_data: | |
257 file_data[ntf] = file_data[tf] | |
258 if tf in props: | |
259 props[ntf] = props[tf] | |
260 del props[tf] | |
261 if hgutil.binary(file_data[ntf][1]): | |
262 props.setdefault(ntf, {}).update(props.get(ntf, {})) | |
263 props.setdefault(ntf, {})['svn:mime-type'] = 'application/octet-stream' | |
264 del file_data[tf] | |
265 | |
266 addeddirs = [svnpath(d) for d in addeddirs] | |
267 deleteddirs = [svnpath(d) for d in deleteddirs] | |
268 new_target_files += addeddirs + deleteddirs + changeddirs | |
269 if not new_target_files: | |
270 raise NoFilesException() | |
271 try: | |
272 svn.commit(new_target_files, rev_ctx.description(), file_data, | |
273 base_revision, set(addeddirs), set(deleteddirs), | |
274 props, newcopies) | |
275 except core.SubversionException, e: | |
276 if hasattr(e, 'apr_err') and (e.apr_err == core.SVN_ERR_FS_TXN_OUT_OF_DATE | |
277 or e.apr_err == core.SVN_ERR_FS_CONFLICT): | |
278 raise hgutil.Abort('Base text was out of date, maybe rebase?') | |
279 else: | |
280 raise | |
281 | |
282 return True | |
283 | |
284 def islocalrepo(url): | |
285 if not url.startswith('file:///'): | |
286 return False | |
287 if '#' in url.split('/')[-1]: # strip off #anchor | |
288 url = url[:url.rfind('#')] | |
289 path = url[len('file://'):] | |
290 path = urllib.url2pathname(path).replace(os.sep, '/') | |
291 while '/' in path: | |
292 if reduce(lambda x,y: x and y, | |
293 map(lambda p: os.path.exists(os.path.join(path, p)), | |
294 ('hooks', 'format', 'db', ))): | |
295 return True | |
296 path = path.rsplit('/', 1)[0] | |
297 return False | |
298 | |
299 def issvnurl(url): | |
300 return url.startswith('svn') or islocalrepo(url) |