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