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)