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