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