##// END OF EJS Templates
largefiles: restore the original converter class after lfconvert --to-normal...
Matt Harbison -
r25560:2b2108c3 default
parent child Browse files
Show More
@@ -1,545 +1,550 b''
1 # Copyright 2009-2010 Gregory P. Ward
1 # Copyright 2009-2010 Gregory P. Ward
2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 # Copyright 2010-2011 Fog Creek Software
3 # Copyright 2010-2011 Fog Creek Software
4 # Copyright 2010-2011 Unity Technologies
4 # Copyright 2010-2011 Unity Technologies
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 '''High-level command function for lfconvert, plus the cmdtable.'''
9 '''High-level command function for lfconvert, plus the cmdtable.'''
10
10
11 import os, errno
11 import os, errno
12 import shutil
12 import shutil
13
13
14 from mercurial import util, match as match_, hg, node, context, error, \
14 from mercurial import util, match as match_, hg, node, context, error, \
15 cmdutil, scmutil, commands
15 cmdutil, scmutil, commands
16 from mercurial.i18n import _
16 from mercurial.i18n import _
17 from mercurial.lock import release
17 from mercurial.lock import release
18
18
19 from hgext.convert import convcmd
19 from hgext.convert import convcmd
20 from hgext.convert import filemap
20 from hgext.convert import filemap
21
21
22 import lfutil
22 import lfutil
23 import basestore
23 import basestore
24
24
25 # -- Commands ----------------------------------------------------------
25 # -- Commands ----------------------------------------------------------
26
26
27 cmdtable = {}
27 cmdtable = {}
28 command = cmdutil.command(cmdtable)
28 command = cmdutil.command(cmdtable)
29
29
30 @command('lfconvert',
30 @command('lfconvert',
31 [('s', 'size', '',
31 [('s', 'size', '',
32 _('minimum size (MB) for files to be converted as largefiles'), 'SIZE'),
32 _('minimum size (MB) for files to be converted as largefiles'), 'SIZE'),
33 ('', 'to-normal', False,
33 ('', 'to-normal', False,
34 _('convert from a largefiles repo to a normal repo')),
34 _('convert from a largefiles repo to a normal repo')),
35 ],
35 ],
36 _('hg lfconvert SOURCE DEST [FILE ...]'),
36 _('hg lfconvert SOURCE DEST [FILE ...]'),
37 norepo=True,
37 norepo=True,
38 inferrepo=True)
38 inferrepo=True)
39 def lfconvert(ui, src, dest, *pats, **opts):
39 def lfconvert(ui, src, dest, *pats, **opts):
40 '''convert a normal repository to a largefiles repository
40 '''convert a normal repository to a largefiles repository
41
41
42 Convert repository SOURCE to a new repository DEST, identical to
42 Convert repository SOURCE to a new repository DEST, identical to
43 SOURCE except that certain files will be converted as largefiles:
43 SOURCE except that certain files will be converted as largefiles:
44 specifically, any file that matches any PATTERN *or* whose size is
44 specifically, any file that matches any PATTERN *or* whose size is
45 above the minimum size threshold is converted as a largefile. The
45 above the minimum size threshold is converted as a largefile. The
46 size used to determine whether or not to track a file as a
46 size used to determine whether or not to track a file as a
47 largefile is the size of the first version of the file. The
47 largefile is the size of the first version of the file. The
48 minimum size can be specified either with --size or in
48 minimum size can be specified either with --size or in
49 configuration as ``largefiles.size``.
49 configuration as ``largefiles.size``.
50
50
51 After running this command you will need to make sure that
51 After running this command you will need to make sure that
52 largefiles is enabled anywhere you intend to push the new
52 largefiles is enabled anywhere you intend to push the new
53 repository.
53 repository.
54
54
55 Use --to-normal to convert largefiles back to normal files; after
55 Use --to-normal to convert largefiles back to normal files; after
56 this, the DEST repository can be used without largefiles at all.'''
56 this, the DEST repository can be used without largefiles at all.'''
57
57
58 if opts['to_normal']:
58 if opts['to_normal']:
59 tolfile = False
59 tolfile = False
60 else:
60 else:
61 tolfile = True
61 tolfile = True
62 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
62 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
63
63
64 if not hg.islocal(src):
64 if not hg.islocal(src):
65 raise util.Abort(_('%s is not a local Mercurial repo') % src)
65 raise util.Abort(_('%s is not a local Mercurial repo') % src)
66 if not hg.islocal(dest):
66 if not hg.islocal(dest):
67 raise util.Abort(_('%s is not a local Mercurial repo') % dest)
67 raise util.Abort(_('%s is not a local Mercurial repo') % dest)
68
68
69 rsrc = hg.repository(ui, src)
69 rsrc = hg.repository(ui, src)
70 ui.status(_('initializing destination %s\n') % dest)
70 ui.status(_('initializing destination %s\n') % dest)
71 rdst = hg.repository(ui, dest, create=True)
71 rdst = hg.repository(ui, dest, create=True)
72
72
73 success = False
73 success = False
74 dstwlock = dstlock = None
74 dstwlock = dstlock = None
75 try:
75 try:
76 # Get a list of all changesets in the source. The easy way to do this
76 # Get a list of all changesets in the source. The easy way to do this
77 # is to simply walk the changelog, using changelog.nodesbetween().
77 # is to simply walk the changelog, using changelog.nodesbetween().
78 # Take a look at mercurial/revlog.py:639 for more details.
78 # Take a look at mercurial/revlog.py:639 for more details.
79 # Use a generator instead of a list to decrease memory usage
79 # Use a generator instead of a list to decrease memory usage
80 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
80 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
81 rsrc.heads())[0])
81 rsrc.heads())[0])
82 revmap = {node.nullid: node.nullid}
82 revmap = {node.nullid: node.nullid}
83 if tolfile:
83 if tolfile:
84 # Lock destination to prevent modification while it is converted to.
84 # Lock destination to prevent modification while it is converted to.
85 # Don't need to lock src because we are just reading from its
85 # Don't need to lock src because we are just reading from its
86 # history which can't change.
86 # history which can't change.
87 dstwlock = rdst.wlock()
87 dstwlock = rdst.wlock()
88 dstlock = rdst.lock()
88 dstlock = rdst.lock()
89
89
90 lfiles = set()
90 lfiles = set()
91 normalfiles = set()
91 normalfiles = set()
92 if not pats:
92 if not pats:
93 pats = ui.configlist(lfutil.longname, 'patterns', default=[])
93 pats = ui.configlist(lfutil.longname, 'patterns', default=[])
94 if pats:
94 if pats:
95 matcher = match_.match(rsrc.root, '', list(pats))
95 matcher = match_.match(rsrc.root, '', list(pats))
96 else:
96 else:
97 matcher = None
97 matcher = None
98
98
99 lfiletohash = {}
99 lfiletohash = {}
100 for ctx in ctxs:
100 for ctx in ctxs:
101 ui.progress(_('converting revisions'), ctx.rev(),
101 ui.progress(_('converting revisions'), ctx.rev(),
102 unit=_('revision'), total=rsrc['tip'].rev())
102 unit=_('revision'), total=rsrc['tip'].rev())
103 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
103 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
104 lfiles, normalfiles, matcher, size, lfiletohash)
104 lfiles, normalfiles, matcher, size, lfiletohash)
105 ui.progress(_('converting revisions'), None)
105 ui.progress(_('converting revisions'), None)
106
106
107 if os.path.exists(rdst.wjoin(lfutil.shortname)):
107 if os.path.exists(rdst.wjoin(lfutil.shortname)):
108 shutil.rmtree(rdst.wjoin(lfutil.shortname))
108 shutil.rmtree(rdst.wjoin(lfutil.shortname))
109
109
110 for f in lfiletohash.keys():
110 for f in lfiletohash.keys():
111 if os.path.isfile(rdst.wjoin(f)):
111 if os.path.isfile(rdst.wjoin(f)):
112 os.unlink(rdst.wjoin(f))
112 os.unlink(rdst.wjoin(f))
113 try:
113 try:
114 os.removedirs(os.path.dirname(rdst.wjoin(f)))
114 os.removedirs(os.path.dirname(rdst.wjoin(f)))
115 except OSError:
115 except OSError:
116 pass
116 pass
117
117
118 # If there were any files converted to largefiles, add largefiles
118 # If there were any files converted to largefiles, add largefiles
119 # to the destination repository's requirements.
119 # to the destination repository's requirements.
120 if lfiles:
120 if lfiles:
121 rdst.requirements.add('largefiles')
121 rdst.requirements.add('largefiles')
122 rdst._writerequirements()
122 rdst._writerequirements()
123 else:
123 else:
124 class lfsource(filemap.filemap_source):
124 class lfsource(filemap.filemap_source):
125 def __init__(self, ui, source):
125 def __init__(self, ui, source):
126 super(lfsource, self).__init__(ui, source, None)
126 super(lfsource, self).__init__(ui, source, None)
127 self.filemapper.rename[lfutil.shortname] = '.'
127 self.filemapper.rename[lfutil.shortname] = '.'
128
128
129 def getfile(self, name, rev):
129 def getfile(self, name, rev):
130 realname, realrev = rev
130 realname, realrev = rev
131 f = super(lfsource, self).getfile(name, rev)
131 f = super(lfsource, self).getfile(name, rev)
132
132
133 if (not realname.startswith(lfutil.shortnameslash)
133 if (not realname.startswith(lfutil.shortnameslash)
134 or f[0] is None):
134 or f[0] is None):
135 return f
135 return f
136
136
137 # Substitute in the largefile data for the hash
137 # Substitute in the largefile data for the hash
138 hash = f[0].strip()
138 hash = f[0].strip()
139 path = lfutil.findfile(rsrc, hash)
139 path = lfutil.findfile(rsrc, hash)
140
140
141 if path is None:
141 if path is None:
142 raise util.Abort(_("missing largefile for \'%s\' in %s")
142 raise util.Abort(_("missing largefile for \'%s\' in %s")
143 % (realname, realrev))
143 % (realname, realrev))
144 fp = open(path, 'rb')
144 fp = open(path, 'rb')
145
145
146 try:
146 try:
147 return (fp.read(), f[1])
147 return (fp.read(), f[1])
148 finally:
148 finally:
149 fp.close()
149 fp.close()
150
150
151 class converter(convcmd.converter):
151 class converter(convcmd.converter):
152 def __init__(self, ui, source, dest, revmapfile, opts):
152 def __init__(self, ui, source, dest, revmapfile, opts):
153 src = lfsource(ui, source)
153 src = lfsource(ui, source)
154
154
155 super(converter, self).__init__(ui, src, dest, revmapfile,
155 super(converter, self).__init__(ui, src, dest, revmapfile,
156 opts)
156 opts)
157
157
158 found, missing = downloadlfiles(ui, rsrc)
158 found, missing = downloadlfiles(ui, rsrc)
159 if missing != 0:
159 if missing != 0:
160 raise util.Abort(_("all largefiles must be present locally"))
160 raise util.Abort(_("all largefiles must be present locally"))
161
161
162 orig = convcmd.converter
162 convcmd.converter = converter
163 convcmd.converter = converter
163 convcmd.convert(ui, src, dest)
164
165 try:
166 convcmd.convert(ui, src, dest)
167 finally:
168 convcmd.converter = orig
164 success = True
169 success = True
165 finally:
170 finally:
166 if tolfile:
171 if tolfile:
167 rdst.dirstate.clear()
172 rdst.dirstate.clear()
168 release(dstlock, dstwlock)
173 release(dstlock, dstwlock)
169 if not success:
174 if not success:
170 # we failed, remove the new directory
175 # we failed, remove the new directory
171 shutil.rmtree(rdst.root)
176 shutil.rmtree(rdst.root)
172
177
173 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
178 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
174 matcher, size, lfiletohash):
179 matcher, size, lfiletohash):
175 # Convert src parents to dst parents
180 # Convert src parents to dst parents
176 parents = _convertparents(ctx, revmap)
181 parents = _convertparents(ctx, revmap)
177
182
178 # Generate list of changed files
183 # Generate list of changed files
179 files = _getchangedfiles(ctx, parents)
184 files = _getchangedfiles(ctx, parents)
180
185
181 dstfiles = []
186 dstfiles = []
182 for f in files:
187 for f in files:
183 if f not in lfiles and f not in normalfiles:
188 if f not in lfiles and f not in normalfiles:
184 islfile = _islfile(f, ctx, matcher, size)
189 islfile = _islfile(f, ctx, matcher, size)
185 # If this file was renamed or copied then copy
190 # If this file was renamed or copied then copy
186 # the largefile-ness of its predecessor
191 # the largefile-ness of its predecessor
187 if f in ctx.manifest():
192 if f in ctx.manifest():
188 fctx = ctx.filectx(f)
193 fctx = ctx.filectx(f)
189 renamed = fctx.renamed()
194 renamed = fctx.renamed()
190 renamedlfile = renamed and renamed[0] in lfiles
195 renamedlfile = renamed and renamed[0] in lfiles
191 islfile |= renamedlfile
196 islfile |= renamedlfile
192 if 'l' in fctx.flags():
197 if 'l' in fctx.flags():
193 if renamedlfile:
198 if renamedlfile:
194 raise util.Abort(
199 raise util.Abort(
195 _('renamed/copied largefile %s becomes symlink')
200 _('renamed/copied largefile %s becomes symlink')
196 % f)
201 % f)
197 islfile = False
202 islfile = False
198 if islfile:
203 if islfile:
199 lfiles.add(f)
204 lfiles.add(f)
200 else:
205 else:
201 normalfiles.add(f)
206 normalfiles.add(f)
202
207
203 if f in lfiles:
208 if f in lfiles:
204 dstfiles.append(lfutil.standin(f))
209 dstfiles.append(lfutil.standin(f))
205 # largefile in manifest if it has not been removed/renamed
210 # largefile in manifest if it has not been removed/renamed
206 if f in ctx.manifest():
211 if f in ctx.manifest():
207 fctx = ctx.filectx(f)
212 fctx = ctx.filectx(f)
208 if 'l' in fctx.flags():
213 if 'l' in fctx.flags():
209 renamed = fctx.renamed()
214 renamed = fctx.renamed()
210 if renamed and renamed[0] in lfiles:
215 if renamed and renamed[0] in lfiles:
211 raise util.Abort(_('largefile %s becomes symlink') % f)
216 raise util.Abort(_('largefile %s becomes symlink') % f)
212
217
213 # largefile was modified, update standins
218 # largefile was modified, update standins
214 m = util.sha1('')
219 m = util.sha1('')
215 m.update(ctx[f].data())
220 m.update(ctx[f].data())
216 hash = m.hexdigest()
221 hash = m.hexdigest()
217 if f not in lfiletohash or lfiletohash[f] != hash:
222 if f not in lfiletohash or lfiletohash[f] != hash:
218 rdst.wwrite(f, ctx[f].data(), ctx[f].flags())
223 rdst.wwrite(f, ctx[f].data(), ctx[f].flags())
219 executable = 'x' in ctx[f].flags()
224 executable = 'x' in ctx[f].flags()
220 lfutil.writestandin(rdst, lfutil.standin(f), hash,
225 lfutil.writestandin(rdst, lfutil.standin(f), hash,
221 executable)
226 executable)
222 lfiletohash[f] = hash
227 lfiletohash[f] = hash
223 else:
228 else:
224 # normal file
229 # normal file
225 dstfiles.append(f)
230 dstfiles.append(f)
226
231
227 def getfilectx(repo, memctx, f):
232 def getfilectx(repo, memctx, f):
228 if lfutil.isstandin(f):
233 if lfutil.isstandin(f):
229 # if the file isn't in the manifest then it was removed
234 # if the file isn't in the manifest then it was removed
230 # or renamed, raise IOError to indicate this
235 # or renamed, raise IOError to indicate this
231 srcfname = lfutil.splitstandin(f)
236 srcfname = lfutil.splitstandin(f)
232 try:
237 try:
233 fctx = ctx.filectx(srcfname)
238 fctx = ctx.filectx(srcfname)
234 except error.LookupError:
239 except error.LookupError:
235 return None
240 return None
236 renamed = fctx.renamed()
241 renamed = fctx.renamed()
237 if renamed:
242 if renamed:
238 # standin is always a largefile because largefile-ness
243 # standin is always a largefile because largefile-ness
239 # doesn't change after rename or copy
244 # doesn't change after rename or copy
240 renamed = lfutil.standin(renamed[0])
245 renamed = lfutil.standin(renamed[0])
241
246
242 return context.memfilectx(repo, f, lfiletohash[srcfname] + '\n',
247 return context.memfilectx(repo, f, lfiletohash[srcfname] + '\n',
243 'l' in fctx.flags(), 'x' in fctx.flags(),
248 'l' in fctx.flags(), 'x' in fctx.flags(),
244 renamed)
249 renamed)
245 else:
250 else:
246 return _getnormalcontext(repo, ctx, f, revmap)
251 return _getnormalcontext(repo, ctx, f, revmap)
247
252
248 # Commit
253 # Commit
249 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
254 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
250
255
251 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
256 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
252 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
257 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
253 getfilectx, ctx.user(), ctx.date(), ctx.extra())
258 getfilectx, ctx.user(), ctx.date(), ctx.extra())
254 ret = rdst.commitctx(mctx)
259 ret = rdst.commitctx(mctx)
255 lfutil.copyalltostore(rdst, ret)
260 lfutil.copyalltostore(rdst, ret)
256 rdst.setparents(ret)
261 rdst.setparents(ret)
257 revmap[ctx.node()] = rdst.changelog.tip()
262 revmap[ctx.node()] = rdst.changelog.tip()
258
263
259 # Generate list of changed files
264 # Generate list of changed files
260 def _getchangedfiles(ctx, parents):
265 def _getchangedfiles(ctx, parents):
261 files = set(ctx.files())
266 files = set(ctx.files())
262 if node.nullid not in parents:
267 if node.nullid not in parents:
263 mc = ctx.manifest()
268 mc = ctx.manifest()
264 mp1 = ctx.parents()[0].manifest()
269 mp1 = ctx.parents()[0].manifest()
265 mp2 = ctx.parents()[1].manifest()
270 mp2 = ctx.parents()[1].manifest()
266 files |= (set(mp1) | set(mp2)) - set(mc)
271 files |= (set(mp1) | set(mp2)) - set(mc)
267 for f in mc:
272 for f in mc:
268 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
273 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
269 files.add(f)
274 files.add(f)
270 return files
275 return files
271
276
272 # Convert src parents to dst parents
277 # Convert src parents to dst parents
273 def _convertparents(ctx, revmap):
278 def _convertparents(ctx, revmap):
274 parents = []
279 parents = []
275 for p in ctx.parents():
280 for p in ctx.parents():
276 parents.append(revmap[p.node()])
281 parents.append(revmap[p.node()])
277 while len(parents) < 2:
282 while len(parents) < 2:
278 parents.append(node.nullid)
283 parents.append(node.nullid)
279 return parents
284 return parents
280
285
281 # Get memfilectx for a normal file
286 # Get memfilectx for a normal file
282 def _getnormalcontext(repo, ctx, f, revmap):
287 def _getnormalcontext(repo, ctx, f, revmap):
283 try:
288 try:
284 fctx = ctx.filectx(f)
289 fctx = ctx.filectx(f)
285 except error.LookupError:
290 except error.LookupError:
286 return None
291 return None
287 renamed = fctx.renamed()
292 renamed = fctx.renamed()
288 if renamed:
293 if renamed:
289 renamed = renamed[0]
294 renamed = renamed[0]
290
295
291 data = fctx.data()
296 data = fctx.data()
292 if f == '.hgtags':
297 if f == '.hgtags':
293 data = _converttags (repo.ui, revmap, data)
298 data = _converttags (repo.ui, revmap, data)
294 return context.memfilectx(repo, f, data, 'l' in fctx.flags(),
299 return context.memfilectx(repo, f, data, 'l' in fctx.flags(),
295 'x' in fctx.flags(), renamed)
300 'x' in fctx.flags(), renamed)
296
301
297 # Remap tag data using a revision map
302 # Remap tag data using a revision map
298 def _converttags(ui, revmap, data):
303 def _converttags(ui, revmap, data):
299 newdata = []
304 newdata = []
300 for line in data.splitlines():
305 for line in data.splitlines():
301 try:
306 try:
302 id, name = line.split(' ', 1)
307 id, name = line.split(' ', 1)
303 except ValueError:
308 except ValueError:
304 ui.warn(_('skipping incorrectly formatted tag %s\n')
309 ui.warn(_('skipping incorrectly formatted tag %s\n')
305 % line)
310 % line)
306 continue
311 continue
307 try:
312 try:
308 newid = node.bin(id)
313 newid = node.bin(id)
309 except TypeError:
314 except TypeError:
310 ui.warn(_('skipping incorrectly formatted id %s\n')
315 ui.warn(_('skipping incorrectly formatted id %s\n')
311 % id)
316 % id)
312 continue
317 continue
313 try:
318 try:
314 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
319 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
315 name))
320 name))
316 except KeyError:
321 except KeyError:
317 ui.warn(_('no mapping for id %s\n') % id)
322 ui.warn(_('no mapping for id %s\n') % id)
318 continue
323 continue
319 return ''.join(newdata)
324 return ''.join(newdata)
320
325
321 def _islfile(file, ctx, matcher, size):
326 def _islfile(file, ctx, matcher, size):
322 '''Return true if file should be considered a largefile, i.e.
327 '''Return true if file should be considered a largefile, i.e.
323 matcher matches it or it is larger than size.'''
328 matcher matches it or it is larger than size.'''
324 # never store special .hg* files as largefiles
329 # never store special .hg* files as largefiles
325 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
330 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
326 return False
331 return False
327 if matcher and matcher(file):
332 if matcher and matcher(file):
328 return True
333 return True
329 try:
334 try:
330 return ctx.filectx(file).size() >= size * 1024 * 1024
335 return ctx.filectx(file).size() >= size * 1024 * 1024
331 except error.LookupError:
336 except error.LookupError:
332 return False
337 return False
333
338
334 def uploadlfiles(ui, rsrc, rdst, files):
339 def uploadlfiles(ui, rsrc, rdst, files):
335 '''upload largefiles to the central store'''
340 '''upload largefiles to the central store'''
336
341
337 if not files:
342 if not files:
338 return
343 return
339
344
340 store = basestore._openstore(rsrc, rdst, put=True)
345 store = basestore._openstore(rsrc, rdst, put=True)
341
346
342 at = 0
347 at = 0
343 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
348 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
344 retval = store.exists(files)
349 retval = store.exists(files)
345 files = filter(lambda h: not retval[h], files)
350 files = filter(lambda h: not retval[h], files)
346 ui.debug("%d largefiles need to be uploaded\n" % len(files))
351 ui.debug("%d largefiles need to be uploaded\n" % len(files))
347
352
348 for hash in files:
353 for hash in files:
349 ui.progress(_('uploading largefiles'), at, unit='largefile',
354 ui.progress(_('uploading largefiles'), at, unit='largefile',
350 total=len(files))
355 total=len(files))
351 source = lfutil.findfile(rsrc, hash)
356 source = lfutil.findfile(rsrc, hash)
352 if not source:
357 if not source:
353 raise util.Abort(_('largefile %s missing from store'
358 raise util.Abort(_('largefile %s missing from store'
354 ' (needs to be uploaded)') % hash)
359 ' (needs to be uploaded)') % hash)
355 # XXX check for errors here
360 # XXX check for errors here
356 store.put(source, hash)
361 store.put(source, hash)
357 at += 1
362 at += 1
358 ui.progress(_('uploading largefiles'), None)
363 ui.progress(_('uploading largefiles'), None)
359
364
360 def verifylfiles(ui, repo, all=False, contents=False):
365 def verifylfiles(ui, repo, all=False, contents=False):
361 '''Verify that every largefile revision in the current changeset
366 '''Verify that every largefile revision in the current changeset
362 exists in the central store. With --contents, also verify that
367 exists in the central store. With --contents, also verify that
363 the contents of each local largefile file revision are correct (SHA-1 hash
368 the contents of each local largefile file revision are correct (SHA-1 hash
364 matches the revision ID). With --all, check every changeset in
369 matches the revision ID). With --all, check every changeset in
365 this repository.'''
370 this repository.'''
366 if all:
371 if all:
367 revs = repo.revs('all()')
372 revs = repo.revs('all()')
368 else:
373 else:
369 revs = ['.']
374 revs = ['.']
370
375
371 store = basestore._openstore(repo)
376 store = basestore._openstore(repo)
372 return store.verify(revs, contents=contents)
377 return store.verify(revs, contents=contents)
373
378
374 def cachelfiles(ui, repo, node, filelist=None):
379 def cachelfiles(ui, repo, node, filelist=None):
375 '''cachelfiles ensures that all largefiles needed by the specified revision
380 '''cachelfiles ensures that all largefiles needed by the specified revision
376 are present in the repository's largefile cache.
381 are present in the repository's largefile cache.
377
382
378 returns a tuple (cached, missing). cached is the list of files downloaded
383 returns a tuple (cached, missing). cached is the list of files downloaded
379 by this operation; missing is the list of files that were needed but could
384 by this operation; missing is the list of files that were needed but could
380 not be found.'''
385 not be found.'''
381 lfiles = lfutil.listlfiles(repo, node)
386 lfiles = lfutil.listlfiles(repo, node)
382 if filelist:
387 if filelist:
383 lfiles = set(lfiles) & set(filelist)
388 lfiles = set(lfiles) & set(filelist)
384 toget = []
389 toget = []
385
390
386 for lfile in lfiles:
391 for lfile in lfiles:
387 try:
392 try:
388 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
393 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
389 except IOError, err:
394 except IOError, err:
390 if err.errno == errno.ENOENT:
395 if err.errno == errno.ENOENT:
391 continue # node must be None and standin wasn't found in wctx
396 continue # node must be None and standin wasn't found in wctx
392 raise
397 raise
393 if not lfutil.findfile(repo, expectedhash):
398 if not lfutil.findfile(repo, expectedhash):
394 toget.append((lfile, expectedhash))
399 toget.append((lfile, expectedhash))
395
400
396 if toget:
401 if toget:
397 store = basestore._openstore(repo)
402 store = basestore._openstore(repo)
398 ret = store.get(toget)
403 ret = store.get(toget)
399 return ret
404 return ret
400
405
401 return ([], [])
406 return ([], [])
402
407
403 def downloadlfiles(ui, repo, rev=None):
408 def downloadlfiles(ui, repo, rev=None):
404 matchfn = scmutil.match(repo[None],
409 matchfn = scmutil.match(repo[None],
405 [repo.wjoin(lfutil.shortname)], {})
410 [repo.wjoin(lfutil.shortname)], {})
406 def prepare(ctx, fns):
411 def prepare(ctx, fns):
407 pass
412 pass
408 totalsuccess = 0
413 totalsuccess = 0
409 totalmissing = 0
414 totalmissing = 0
410 if rev != []: # walkchangerevs on empty list would return all revs
415 if rev != []: # walkchangerevs on empty list would return all revs
411 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
416 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
412 prepare):
417 prepare):
413 success, missing = cachelfiles(ui, repo, ctx.node())
418 success, missing = cachelfiles(ui, repo, ctx.node())
414 totalsuccess += len(success)
419 totalsuccess += len(success)
415 totalmissing += len(missing)
420 totalmissing += len(missing)
416 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
421 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
417 if totalmissing > 0:
422 if totalmissing > 0:
418 ui.status(_("%d largefiles failed to download\n") % totalmissing)
423 ui.status(_("%d largefiles failed to download\n") % totalmissing)
419 return totalsuccess, totalmissing
424 return totalsuccess, totalmissing
420
425
421 def updatelfiles(ui, repo, filelist=None, printmessage=None,
426 def updatelfiles(ui, repo, filelist=None, printmessage=None,
422 normallookup=False):
427 normallookup=False):
423 '''Update largefiles according to standins in the working directory
428 '''Update largefiles according to standins in the working directory
424
429
425 If ``printmessage`` is other than ``None``, it means "print (or
430 If ``printmessage`` is other than ``None``, it means "print (or
426 ignore, for false) message forcibly".
431 ignore, for false) message forcibly".
427 '''
432 '''
428 statuswriter = lfutil.getstatuswriter(ui, repo, printmessage)
433 statuswriter = lfutil.getstatuswriter(ui, repo, printmessage)
429 wlock = repo.wlock()
434 wlock = repo.wlock()
430 try:
435 try:
431 lfdirstate = lfutil.openlfdirstate(ui, repo)
436 lfdirstate = lfutil.openlfdirstate(ui, repo)
432 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
437 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
433
438
434 if filelist is not None:
439 if filelist is not None:
435 filelist = set(filelist)
440 filelist = set(filelist)
436 lfiles = [f for f in lfiles if f in filelist]
441 lfiles = [f for f in lfiles if f in filelist]
437
442
438 update = {}
443 update = {}
439 updated, removed = 0, 0
444 updated, removed = 0, 0
440 for lfile in lfiles:
445 for lfile in lfiles:
441 abslfile = repo.wjoin(lfile)
446 abslfile = repo.wjoin(lfile)
442 absstandin = repo.wjoin(lfutil.standin(lfile))
447 absstandin = repo.wjoin(lfutil.standin(lfile))
443 if os.path.exists(absstandin):
448 if os.path.exists(absstandin):
444 if (os.path.exists(absstandin + '.orig') and
449 if (os.path.exists(absstandin + '.orig') and
445 os.path.exists(abslfile)):
450 os.path.exists(abslfile)):
446 shutil.copyfile(abslfile, abslfile + '.orig')
451 shutil.copyfile(abslfile, abslfile + '.orig')
447 util.unlinkpath(absstandin + '.orig')
452 util.unlinkpath(absstandin + '.orig')
448 expecthash = lfutil.readstandin(repo, lfile)
453 expecthash = lfutil.readstandin(repo, lfile)
449 if expecthash != '':
454 if expecthash != '':
450 if lfile not in repo[None]: # not switched to normal file
455 if lfile not in repo[None]: # not switched to normal file
451 util.unlinkpath(abslfile, ignoremissing=True)
456 util.unlinkpath(abslfile, ignoremissing=True)
452 # use normallookup() to allocate an entry in largefiles
457 # use normallookup() to allocate an entry in largefiles
453 # dirstate to prevent lfilesrepo.status() from reporting
458 # dirstate to prevent lfilesrepo.status() from reporting
454 # missing files as removed.
459 # missing files as removed.
455 lfdirstate.normallookup(lfile)
460 lfdirstate.normallookup(lfile)
456 update[lfile] = expecthash
461 update[lfile] = expecthash
457 else:
462 else:
458 # Remove lfiles for which the standin is deleted, unless the
463 # Remove lfiles for which the standin is deleted, unless the
459 # lfile is added to the repository again. This happens when a
464 # lfile is added to the repository again. This happens when a
460 # largefile is converted back to a normal file: the standin
465 # largefile is converted back to a normal file: the standin
461 # disappears, but a new (normal) file appears as the lfile.
466 # disappears, but a new (normal) file appears as the lfile.
462 if (os.path.exists(abslfile) and
467 if (os.path.exists(abslfile) and
463 repo.dirstate.normalize(lfile) not in repo[None]):
468 repo.dirstate.normalize(lfile) not in repo[None]):
464 util.unlinkpath(abslfile)
469 util.unlinkpath(abslfile)
465 removed += 1
470 removed += 1
466
471
467 # largefile processing might be slow and be interrupted - be prepared
472 # largefile processing might be slow and be interrupted - be prepared
468 lfdirstate.write()
473 lfdirstate.write()
469
474
470 if lfiles:
475 if lfiles:
471 statuswriter(_('getting changed largefiles\n'))
476 statuswriter(_('getting changed largefiles\n'))
472 cachelfiles(ui, repo, None, lfiles)
477 cachelfiles(ui, repo, None, lfiles)
473
478
474 for lfile in lfiles:
479 for lfile in lfiles:
475 update1 = 0
480 update1 = 0
476
481
477 expecthash = update.get(lfile)
482 expecthash = update.get(lfile)
478 if expecthash:
483 if expecthash:
479 if not lfutil.copyfromcache(repo, expecthash, lfile):
484 if not lfutil.copyfromcache(repo, expecthash, lfile):
480 # failed ... but already removed and set to normallookup
485 # failed ... but already removed and set to normallookup
481 continue
486 continue
482 # Synchronize largefile dirstate to the last modified
487 # Synchronize largefile dirstate to the last modified
483 # time of the file
488 # time of the file
484 lfdirstate.normal(lfile)
489 lfdirstate.normal(lfile)
485 update1 = 1
490 update1 = 1
486
491
487 # copy the state of largefile standin from the repository's
492 # copy the state of largefile standin from the repository's
488 # dirstate to its state in the lfdirstate.
493 # dirstate to its state in the lfdirstate.
489 abslfile = repo.wjoin(lfile)
494 abslfile = repo.wjoin(lfile)
490 absstandin = repo.wjoin(lfutil.standin(lfile))
495 absstandin = repo.wjoin(lfutil.standin(lfile))
491 if os.path.exists(absstandin):
496 if os.path.exists(absstandin):
492 mode = os.stat(absstandin).st_mode
497 mode = os.stat(absstandin).st_mode
493 if mode != os.stat(abslfile).st_mode:
498 if mode != os.stat(abslfile).st_mode:
494 os.chmod(abslfile, mode)
499 os.chmod(abslfile, mode)
495 update1 = 1
500 update1 = 1
496
501
497 updated += update1
502 updated += update1
498
503
499 lfutil.synclfdirstate(repo, lfdirstate, lfile, normallookup)
504 lfutil.synclfdirstate(repo, lfdirstate, lfile, normallookup)
500
505
501 lfdirstate.write()
506 lfdirstate.write()
502 if lfiles:
507 if lfiles:
503 statuswriter(_('%d largefiles updated, %d removed\n') % (updated,
508 statuswriter(_('%d largefiles updated, %d removed\n') % (updated,
504 removed))
509 removed))
505 finally:
510 finally:
506 wlock.release()
511 wlock.release()
507
512
508 @command('lfpull',
513 @command('lfpull',
509 [('r', 'rev', [], _('pull largefiles for these revisions'))
514 [('r', 'rev', [], _('pull largefiles for these revisions'))
510 ] + commands.remoteopts,
515 ] + commands.remoteopts,
511 _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]'))
516 _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]'))
512 def lfpull(ui, repo, source="default", **opts):
517 def lfpull(ui, repo, source="default", **opts):
513 """pull largefiles for the specified revisions from the specified source
518 """pull largefiles for the specified revisions from the specified source
514
519
515 Pull largefiles that are referenced from local changesets but missing
520 Pull largefiles that are referenced from local changesets but missing
516 locally, pulling from a remote repository to the local cache.
521 locally, pulling from a remote repository to the local cache.
517
522
518 If SOURCE is omitted, the 'default' path will be used.
523 If SOURCE is omitted, the 'default' path will be used.
519 See :hg:`help urls` for more information.
524 See :hg:`help urls` for more information.
520
525
521 .. container:: verbose
526 .. container:: verbose
522
527
523 Some examples:
528 Some examples:
524
529
525 - pull largefiles for all branch heads::
530 - pull largefiles for all branch heads::
526
531
527 hg lfpull -r "head() and not closed()"
532 hg lfpull -r "head() and not closed()"
528
533
529 - pull largefiles on the default branch::
534 - pull largefiles on the default branch::
530
535
531 hg lfpull -r "branch(default)"
536 hg lfpull -r "branch(default)"
532 """
537 """
533 repo.lfpullsource = source
538 repo.lfpullsource = source
534
539
535 revs = opts.get('rev', [])
540 revs = opts.get('rev', [])
536 if not revs:
541 if not revs:
537 raise util.Abort(_('no revisions specified'))
542 raise util.Abort(_('no revisions specified'))
538 revs = scmutil.revrange(repo, revs)
543 revs = scmutil.revrange(repo, revs)
539
544
540 numcached = 0
545 numcached = 0
541 for rev in revs:
546 for rev in revs:
542 ui.note(_('pulling largefiles for revision %s\n') % rev)
547 ui.note(_('pulling largefiles for revision %s\n') % rev)
543 (cached, missing) = cachelfiles(ui, repo, rev)
548 (cached, missing) = cachelfiles(ui, repo, rev)
544 numcached += len(cached)
549 numcached += len(cached)
545 ui.status(_("%d largefiles cached\n") % numcached)
550 ui.status(_("%d largefiles cached\n") % numcached)
General Comments 0
You need to be logged in to leave comments. Login now