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