##// END OF EJS Templates
largefiles: use repo.wwrite for writing standins (issue3909)
Mads Kiilerich -
r19089:0509ae08 stable
parent child Browse files
Show More
@@ -1,587 +1,579 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)
219 util.makedirs(os.path.dirname(fullpath))
220 m = util.sha1('')
218 m = util.sha1('')
221 m.update(ctx[f].data())
219 m.update(ctx[f].data())
222 hash = m.hexdigest()
220 hash = m.hexdigest()
223 if f not in lfiletohash or lfiletohash[f] != hash:
221 if f not in lfiletohash or lfiletohash[f] != hash:
224 try:
222 rdst.wwrite(f, ctx[f].data(), ctx[f].flags())
225 fd = open(fullpath, 'wb')
226 fd.write(ctx[f].data())
227 finally:
228 if fd:
229 fd.close()
230 executable = 'x' in ctx[f].flags()
223 executable = 'x' in ctx[f].flags()
231 os.chmod(fullpath, lfutil.getmode(executable))
232 lfutil.writestandin(rdst, lfutil.standin(f), hash,
224 lfutil.writestandin(rdst, lfutil.standin(f), hash,
233 executable)
225 executable)
234 lfiletohash[f] = hash
226 lfiletohash[f] = hash
235 else:
227 else:
236 # normal file
228 # normal file
237 dstfiles.append(f)
229 dstfiles.append(f)
238
230
239 def getfilectx(repo, memctx, f):
231 def getfilectx(repo, memctx, f):
240 if lfutil.isstandin(f):
232 if lfutil.isstandin(f):
241 # if the file isn't in the manifest then it was removed
233 # if the file isn't in the manifest then it was removed
242 # or renamed, raise IOError to indicate this
234 # or renamed, raise IOError to indicate this
243 srcfname = lfutil.splitstandin(f)
235 srcfname = lfutil.splitstandin(f)
244 try:
236 try:
245 fctx = ctx.filectx(srcfname)
237 fctx = ctx.filectx(srcfname)
246 except error.LookupError:
238 except error.LookupError:
247 raise IOError
239 raise IOError
248 renamed = fctx.renamed()
240 renamed = fctx.renamed()
249 if renamed:
241 if renamed:
250 # standin is always a largefile because largefile-ness
242 # standin is always a largefile because largefile-ness
251 # doesn't change after rename or copy
243 # doesn't change after rename or copy
252 renamed = lfutil.standin(renamed[0])
244 renamed = lfutil.standin(renamed[0])
253
245
254 return context.memfilectx(f, lfiletohash[srcfname] + '\n', 'l' in
246 return context.memfilectx(f, lfiletohash[srcfname] + '\n', 'l' in
255 fctx.flags(), 'x' in fctx.flags(), renamed)
247 fctx.flags(), 'x' in fctx.flags(), renamed)
256 else:
248 else:
257 return _getnormalcontext(repo.ui, ctx, f, revmap)
249 return _getnormalcontext(repo.ui, ctx, f, revmap)
258
250
259 # Commit
251 # Commit
260 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
252 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
261
253
262 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
254 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
263 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
255 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
264 getfilectx, ctx.user(), ctx.date(), ctx.extra())
256 getfilectx, ctx.user(), ctx.date(), ctx.extra())
265 ret = rdst.commitctx(mctx)
257 ret = rdst.commitctx(mctx)
266 rdst.setparents(ret)
258 rdst.setparents(ret)
267 revmap[ctx.node()] = rdst.changelog.tip()
259 revmap[ctx.node()] = rdst.changelog.tip()
268
260
269 # Generate list of changed files
261 # Generate list of changed files
270 def _getchangedfiles(ctx, parents):
262 def _getchangedfiles(ctx, parents):
271 files = set(ctx.files())
263 files = set(ctx.files())
272 if node.nullid not in parents:
264 if node.nullid not in parents:
273 mc = ctx.manifest()
265 mc = ctx.manifest()
274 mp1 = ctx.parents()[0].manifest()
266 mp1 = ctx.parents()[0].manifest()
275 mp2 = ctx.parents()[1].manifest()
267 mp2 = ctx.parents()[1].manifest()
276 files |= (set(mp1) | set(mp2)) - set(mc)
268 files |= (set(mp1) | set(mp2)) - set(mc)
277 for f in mc:
269 for f in mc:
278 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
270 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
279 files.add(f)
271 files.add(f)
280 return files
272 return files
281
273
282 # Convert src parents to dst parents
274 # Convert src parents to dst parents
283 def _convertparents(ctx, revmap):
275 def _convertparents(ctx, revmap):
284 parents = []
276 parents = []
285 for p in ctx.parents():
277 for p in ctx.parents():
286 parents.append(revmap[p.node()])
278 parents.append(revmap[p.node()])
287 while len(parents) < 2:
279 while len(parents) < 2:
288 parents.append(node.nullid)
280 parents.append(node.nullid)
289 return parents
281 return parents
290
282
291 # Get memfilectx for a normal file
283 # Get memfilectx for a normal file
292 def _getnormalcontext(ui, ctx, f, revmap):
284 def _getnormalcontext(ui, ctx, f, revmap):
293 try:
285 try:
294 fctx = ctx.filectx(f)
286 fctx = ctx.filectx(f)
295 except error.LookupError:
287 except error.LookupError:
296 raise IOError
288 raise IOError
297 renamed = fctx.renamed()
289 renamed = fctx.renamed()
298 if renamed:
290 if renamed:
299 renamed = renamed[0]
291 renamed = renamed[0]
300
292
301 data = fctx.data()
293 data = fctx.data()
302 if f == '.hgtags':
294 if f == '.hgtags':
303 data = _converttags (ui, revmap, data)
295 data = _converttags (ui, revmap, data)
304 return context.memfilectx(f, data, 'l' in fctx.flags(),
296 return context.memfilectx(f, data, 'l' in fctx.flags(),
305 'x' in fctx.flags(), renamed)
297 'x' in fctx.flags(), renamed)
306
298
307 # Remap tag data using a revision map
299 # Remap tag data using a revision map
308 def _converttags(ui, revmap, data):
300 def _converttags(ui, revmap, data):
309 newdata = []
301 newdata = []
310 for line in data.splitlines():
302 for line in data.splitlines():
311 try:
303 try:
312 id, name = line.split(' ', 1)
304 id, name = line.split(' ', 1)
313 except ValueError:
305 except ValueError:
314 ui.warn(_('skipping incorrectly formatted tag %s\n'
306 ui.warn(_('skipping incorrectly formatted tag %s\n'
315 % line))
307 % line))
316 continue
308 continue
317 try:
309 try:
318 newid = node.bin(id)
310 newid = node.bin(id)
319 except TypeError:
311 except TypeError:
320 ui.warn(_('skipping incorrectly formatted id %s\n'
312 ui.warn(_('skipping incorrectly formatted id %s\n'
321 % id))
313 % id))
322 continue
314 continue
323 try:
315 try:
324 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
316 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
325 name))
317 name))
326 except KeyError:
318 except KeyError:
327 ui.warn(_('no mapping for id %s\n') % id)
319 ui.warn(_('no mapping for id %s\n') % id)
328 continue
320 continue
329 return ''.join(newdata)
321 return ''.join(newdata)
330
322
331 def _islfile(file, ctx, matcher, size):
323 def _islfile(file, ctx, matcher, size):
332 '''Return true if file should be considered a largefile, i.e.
324 '''Return true if file should be considered a largefile, i.e.
333 matcher matches it or it is larger than size.'''
325 matcher matches it or it is larger than size.'''
334 # never store special .hg* files as largefiles
326 # never store special .hg* files as largefiles
335 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
327 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
336 return False
328 return False
337 if matcher and matcher(file):
329 if matcher and matcher(file):
338 return True
330 return True
339 try:
331 try:
340 return ctx.filectx(file).size() >= size * 1024 * 1024
332 return ctx.filectx(file).size() >= size * 1024 * 1024
341 except error.LookupError:
333 except error.LookupError:
342 return False
334 return False
343
335
344 def uploadlfiles(ui, rsrc, rdst, files):
336 def uploadlfiles(ui, rsrc, rdst, files):
345 '''upload largefiles to the central store'''
337 '''upload largefiles to the central store'''
346
338
347 if not files:
339 if not files:
348 return
340 return
349
341
350 store = basestore._openstore(rsrc, rdst, put=True)
342 store = basestore._openstore(rsrc, rdst, put=True)
351
343
352 at = 0
344 at = 0
353 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
345 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
354 retval = store.exists(files)
346 retval = store.exists(files)
355 files = filter(lambda h: not retval[h], files)
347 files = filter(lambda h: not retval[h], files)
356 ui.debug("%d largefiles need to be uploaded\n" % len(files))
348 ui.debug("%d largefiles need to be uploaded\n" % len(files))
357
349
358 for hash in files:
350 for hash in files:
359 ui.progress(_('uploading largefiles'), at, unit='largefile',
351 ui.progress(_('uploading largefiles'), at, unit='largefile',
360 total=len(files))
352 total=len(files))
361 source = lfutil.findfile(rsrc, hash)
353 source = lfutil.findfile(rsrc, hash)
362 if not source:
354 if not source:
363 raise util.Abort(_('largefile %s missing from store'
355 raise util.Abort(_('largefile %s missing from store'
364 ' (needs to be uploaded)') % hash)
356 ' (needs to be uploaded)') % hash)
365 # XXX check for errors here
357 # XXX check for errors here
366 store.put(source, hash)
358 store.put(source, hash)
367 at += 1
359 at += 1
368 ui.progress(_('uploading largefiles'), None)
360 ui.progress(_('uploading largefiles'), None)
369
361
370 def verifylfiles(ui, repo, all=False, contents=False):
362 def verifylfiles(ui, repo, all=False, contents=False):
371 '''Verify that every largefile revision in the current changeset
363 '''Verify that every largefile revision in the current changeset
372 exists in the central store. With --contents, also verify that
364 exists in the central store. With --contents, also verify that
373 the contents of each local largefile file revision are correct (SHA-1 hash
365 the contents of each local largefile file revision are correct (SHA-1 hash
374 matches the revision ID). With --all, check every changeset in
366 matches the revision ID). With --all, check every changeset in
375 this repository.'''
367 this repository.'''
376 if all:
368 if all:
377 # Pass a list to the function rather than an iterator because we know a
369 # Pass a list to the function rather than an iterator because we know a
378 # list will work.
370 # list will work.
379 revs = range(len(repo))
371 revs = range(len(repo))
380 else:
372 else:
381 revs = ['.']
373 revs = ['.']
382
374
383 store = basestore._openstore(repo)
375 store = basestore._openstore(repo)
384 return store.verify(revs, contents=contents)
376 return store.verify(revs, contents=contents)
385
377
386 def debugdirstate(ui, repo):
378 def debugdirstate(ui, repo):
387 '''Show basic information for the largefiles dirstate'''
379 '''Show basic information for the largefiles dirstate'''
388 lfdirstate = lfutil.openlfdirstate(ui, repo)
380 lfdirstate = lfutil.openlfdirstate(ui, repo)
389 for file_, ent in sorted(lfdirstate._map.iteritems()):
381 for file_, ent in sorted(lfdirstate._map.iteritems()):
390 mode = '%3o' % (ent[1] & 0777 & ~util.umask)
382 mode = '%3o' % (ent[1] & 0777 & ~util.umask)
391 ui.write("%c %s %10d %s\n" % (ent[0], mode, ent[2], file_))
383 ui.write("%c %s %10d %s\n" % (ent[0], mode, ent[2], file_))
392
384
393 def cachelfiles(ui, repo, node, filelist=None):
385 def cachelfiles(ui, repo, node, filelist=None):
394 '''cachelfiles ensures that all largefiles needed by the specified revision
386 '''cachelfiles ensures that all largefiles needed by the specified revision
395 are present in the repository's largefile cache.
387 are present in the repository's largefile cache.
396
388
397 returns a tuple (cached, missing). cached is the list of files downloaded
389 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
390 by this operation; missing is the list of files that were needed but could
399 not be found.'''
391 not be found.'''
400 lfiles = lfutil.listlfiles(repo, node)
392 lfiles = lfutil.listlfiles(repo, node)
401 if filelist:
393 if filelist:
402 lfiles = set(lfiles) & set(filelist)
394 lfiles = set(lfiles) & set(filelist)
403 toget = []
395 toget = []
404
396
405 for lfile in lfiles:
397 for lfile in lfiles:
406 try:
398 try:
407 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
399 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
408 except IOError, err:
400 except IOError, err:
409 if err.errno == errno.ENOENT:
401 if err.errno == errno.ENOENT:
410 continue # node must be None and standin wasn't found in wctx
402 continue # node must be None and standin wasn't found in wctx
411 raise
403 raise
412 if not lfutil.findfile(repo, expectedhash):
404 if not lfutil.findfile(repo, expectedhash):
413 toget.append((lfile, expectedhash))
405 toget.append((lfile, expectedhash))
414
406
415 if toget:
407 if toget:
416 store = basestore._openstore(repo)
408 store = basestore._openstore(repo)
417 ret = store.get(toget)
409 ret = store.get(toget)
418 return ret
410 return ret
419
411
420 return ([], [])
412 return ([], [])
421
413
422 def downloadlfiles(ui, repo, rev=None):
414 def downloadlfiles(ui, repo, rev=None):
423 matchfn = scmutil.match(repo[None],
415 matchfn = scmutil.match(repo[None],
424 [repo.wjoin(lfutil.shortname)], {})
416 [repo.wjoin(lfutil.shortname)], {})
425 def prepare(ctx, fns):
417 def prepare(ctx, fns):
426 pass
418 pass
427 totalsuccess = 0
419 totalsuccess = 0
428 totalmissing = 0
420 totalmissing = 0
429 if rev != []: # walkchangerevs on empty list would return all revs
421 if rev != []: # walkchangerevs on empty list would return all revs
430 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
422 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
431 prepare):
423 prepare):
432 success, missing = cachelfiles(ui, repo, ctx.node())
424 success, missing = cachelfiles(ui, repo, ctx.node())
433 totalsuccess += len(success)
425 totalsuccess += len(success)
434 totalmissing += len(missing)
426 totalmissing += len(missing)
435 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
427 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
436 if totalmissing > 0:
428 if totalmissing > 0:
437 ui.status(_("%d largefiles failed to download\n") % totalmissing)
429 ui.status(_("%d largefiles failed to download\n") % totalmissing)
438 return totalsuccess, totalmissing
430 return totalsuccess, totalmissing
439
431
440 def updatelfiles(ui, repo, filelist=None, printmessage=True):
432 def updatelfiles(ui, repo, filelist=None, printmessage=True):
441 wlock = repo.wlock()
433 wlock = repo.wlock()
442 try:
434 try:
443 lfdirstate = lfutil.openlfdirstate(ui, repo)
435 lfdirstate = lfutil.openlfdirstate(ui, repo)
444 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
436 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
445
437
446 if filelist is not None:
438 if filelist is not None:
447 lfiles = [f for f in lfiles if f in filelist]
439 lfiles = [f for f in lfiles if f in filelist]
448
440
449 printed = False
441 printed = False
450 if printmessage and lfiles:
442 if printmessage and lfiles:
451 ui.status(_('getting changed largefiles\n'))
443 ui.status(_('getting changed largefiles\n'))
452 printed = True
444 printed = True
453 cachelfiles(ui, repo, None, lfiles)
445 cachelfiles(ui, repo, None, lfiles)
454
446
455 updated, removed = 0, 0
447 updated, removed = 0, 0
456 for f in lfiles:
448 for f in lfiles:
457 i = _updatelfile(repo, lfdirstate, f)
449 i = _updatelfile(repo, lfdirstate, f)
458 if i:
450 if i:
459 if i > 0:
451 if i > 0:
460 updated += i
452 updated += i
461 else:
453 else:
462 removed -= i
454 removed -= i
463 if printmessage and (removed or updated) and not printed:
455 if printmessage and (removed or updated) and not printed:
464 ui.status(_('getting changed largefiles\n'))
456 ui.status(_('getting changed largefiles\n'))
465 printed = True
457 printed = True
466
458
467 lfdirstate.write()
459 lfdirstate.write()
468 if printed and printmessage:
460 if printed and printmessage:
469 ui.status(_('%d largefiles updated, %d removed\n') % (updated,
461 ui.status(_('%d largefiles updated, %d removed\n') % (updated,
470 removed))
462 removed))
471 finally:
463 finally:
472 wlock.release()
464 wlock.release()
473
465
474 def _updatelfile(repo, lfdirstate, lfile):
466 def _updatelfile(repo, lfdirstate, lfile):
475 '''updates a single largefile and copies the state of its standin from
467 '''updates a single largefile and copies the state of its standin from
476 the repository's dirstate to its state in the lfdirstate.
468 the repository's dirstate to its state in the lfdirstate.
477
469
478 returns 1 if the file was modified, -1 if the file was removed, 0 if the
470 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
471 file was unchanged, and None if the needed largefile was missing from the
480 cache.'''
472 cache.'''
481 ret = 0
473 ret = 0
482 abslfile = repo.wjoin(lfile)
474 abslfile = repo.wjoin(lfile)
483 absstandin = repo.wjoin(lfutil.standin(lfile))
475 absstandin = repo.wjoin(lfutil.standin(lfile))
484 if os.path.exists(absstandin):
476 if os.path.exists(absstandin):
485 if os.path.exists(absstandin + '.orig') and os.path.exists(abslfile):
477 if os.path.exists(absstandin + '.orig') and os.path.exists(abslfile):
486 shutil.copyfile(abslfile, abslfile + '.orig')
478 shutil.copyfile(abslfile, abslfile + '.orig')
487 expecthash = lfutil.readstandin(repo, lfile)
479 expecthash = lfutil.readstandin(repo, lfile)
488 if (expecthash != '' and
480 if (expecthash != '' and
489 (not os.path.exists(abslfile) or
481 (not os.path.exists(abslfile) or
490 expecthash != lfutil.hashfile(abslfile))):
482 expecthash != lfutil.hashfile(abslfile))):
491 if not lfutil.copyfromcache(repo, expecthash, lfile):
483 if not lfutil.copyfromcache(repo, expecthash, lfile):
492 # use normallookup() to allocate entry in largefiles dirstate,
484 # use normallookup() to allocate entry in largefiles dirstate,
493 # because lack of it misleads lfilesrepo.status() into
485 # because lack of it misleads lfilesrepo.status() into
494 # recognition that such cache missing files are REMOVED.
486 # recognition that such cache missing files are REMOVED.
495 if lfile not in repo[None]: # not switched to normal file
487 if lfile not in repo[None]: # not switched to normal file
496 util.unlinkpath(abslfile, ignoremissing=True)
488 util.unlinkpath(abslfile, ignoremissing=True)
497 lfdirstate.normallookup(lfile)
489 lfdirstate.normallookup(lfile)
498 return None # don't try to set the mode
490 return None # don't try to set the mode
499 else:
491 else:
500 # Synchronize largefile dirstate to the last modified time of
492 # Synchronize largefile dirstate to the last modified time of
501 # the file
493 # the file
502 lfdirstate.normal(lfile)
494 lfdirstate.normal(lfile)
503 ret = 1
495 ret = 1
504 mode = os.stat(absstandin).st_mode
496 mode = os.stat(absstandin).st_mode
505 if mode != os.stat(abslfile).st_mode:
497 if mode != os.stat(abslfile).st_mode:
506 os.chmod(abslfile, mode)
498 os.chmod(abslfile, mode)
507 ret = 1
499 ret = 1
508 else:
500 else:
509 # Remove lfiles for which the standin is deleted, unless the
501 # Remove lfiles for which the standin is deleted, unless the
510 # lfile is added to the repository again. This happens when a
502 # lfile is added to the repository again. This happens when a
511 # largefile is converted back to a normal file: the standin
503 # largefile is converted back to a normal file: the standin
512 # disappears, but a new (normal) file appears as the lfile.
504 # disappears, but a new (normal) file appears as the lfile.
513 if os.path.exists(abslfile) and lfile not in repo[None]:
505 if os.path.exists(abslfile) and lfile not in repo[None]:
514 util.unlinkpath(abslfile)
506 util.unlinkpath(abslfile)
515 ret = -1
507 ret = -1
516 state = repo.dirstate[lfutil.standin(lfile)]
508 state = repo.dirstate[lfutil.standin(lfile)]
517 if state == 'n':
509 if state == 'n':
518 # When rebasing, we need to synchronize the standin and the largefile,
510 # When rebasing, we need to synchronize the standin and the largefile,
519 # because otherwise the largefile will get reverted. But for commit's
511 # because otherwise the largefile will get reverted. But for commit's
520 # sake, we have to mark the file as unclean.
512 # sake, we have to mark the file as unclean.
521 if getattr(repo, "_isrebasing", False):
513 if getattr(repo, "_isrebasing", False):
522 lfdirstate.normallookup(lfile)
514 lfdirstate.normallookup(lfile)
523 else:
515 else:
524 lfdirstate.normal(lfile)
516 lfdirstate.normal(lfile)
525 elif state == 'r':
517 elif state == 'r':
526 lfdirstate.remove(lfile)
518 lfdirstate.remove(lfile)
527 elif state == 'a':
519 elif state == 'a':
528 lfdirstate.add(lfile)
520 lfdirstate.add(lfile)
529 elif state == '?':
521 elif state == '?':
530 lfdirstate.drop(lfile)
522 lfdirstate.drop(lfile)
531 return ret
523 return ret
532
524
533 def lfpull(ui, repo, source="default", **opts):
525 def lfpull(ui, repo, source="default", **opts):
534 """pull largefiles for the specified revisions from the specified source
526 """pull largefiles for the specified revisions from the specified source
535
527
536 Pull largefiles that are referenced from local changesets but missing
528 Pull largefiles that are referenced from local changesets but missing
537 locally, pulling from a remote repository to the local cache.
529 locally, pulling from a remote repository to the local cache.
538
530
539 If SOURCE is omitted, the 'default' path will be used.
531 If SOURCE is omitted, the 'default' path will be used.
540 See :hg:`help urls` for more information.
532 See :hg:`help urls` for more information.
541
533
542 .. container:: verbose
534 .. container:: verbose
543
535
544 Some examples:
536 Some examples:
545
537
546 - pull largefiles for all branch heads::
538 - pull largefiles for all branch heads::
547
539
548 hg lfpull -r "head() and not closed()"
540 hg lfpull -r "head() and not closed()"
549
541
550 - pull largefiles on the default branch::
542 - pull largefiles on the default branch::
551
543
552 hg lfpull -r "branch(default)"
544 hg lfpull -r "branch(default)"
553 """
545 """
554 repo.lfpullsource = source
546 repo.lfpullsource = source
555
547
556 revs = opts.get('rev', [])
548 revs = opts.get('rev', [])
557 if not revs:
549 if not revs:
558 raise util.Abort(_('no revisions specified'))
550 raise util.Abort(_('no revisions specified'))
559 revs = scmutil.revrange(repo, revs)
551 revs = scmutil.revrange(repo, revs)
560
552
561 numcached = 0
553 numcached = 0
562 for rev in revs:
554 for rev in revs:
563 ui.note(_('pulling largefiles for revision %s\n') % rev)
555 ui.note(_('pulling largefiles for revision %s\n') % rev)
564 (cached, missing) = cachelfiles(ui, repo, rev)
556 (cached, missing) = cachelfiles(ui, repo, rev)
565 numcached += len(cached)
557 numcached += len(cached)
566 ui.status(_("%d largefiles cached\n") % numcached)
558 ui.status(_("%d largefiles cached\n") % numcached)
567
559
568 # -- hg commands declarations ------------------------------------------------
560 # -- hg commands declarations ------------------------------------------------
569
561
570 cmdtable = {
562 cmdtable = {
571 'lfconvert': (lfconvert,
563 'lfconvert': (lfconvert,
572 [('s', 'size', '',
564 [('s', 'size', '',
573 _('minimum size (MB) for files to be converted '
565 _('minimum size (MB) for files to be converted '
574 'as largefiles'),
566 'as largefiles'),
575 'SIZE'),
567 'SIZE'),
576 ('', 'to-normal', False,
568 ('', 'to-normal', False,
577 _('convert from a largefiles repo to a normal repo')),
569 _('convert from a largefiles repo to a normal repo')),
578 ],
570 ],
579 _('hg lfconvert SOURCE DEST [FILE ...]')),
571 _('hg lfconvert SOURCE DEST [FILE ...]')),
580 'lfpull': (lfpull,
572 'lfpull': (lfpull,
581 [('r', 'rev', [], _('pull largefiles for these revisions'))
573 [('r', 'rev', [], _('pull largefiles for these revisions'))
582 ] + commands.remoteopts,
574 ] + commands.remoteopts,
583 _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]')
575 _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]')
584 ),
576 ),
585 }
577 }
586
578
587 commands.inferrepo += " lfconvert"
579 commands.inferrepo += " lfconvert"
@@ -1,378 +1,367 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 '''largefiles utility code: must not import other modules in this package.'''
9 '''largefiles utility code: must not import other modules in this package.'''
10
10
11 import os
11 import os
12 import platform
12 import platform
13 import shutil
13 import shutil
14 import stat
14 import stat
15
15
16 from mercurial import dirstate, httpconnection, match as match_, util, scmutil
16 from mercurial import dirstate, httpconnection, match as match_, util, scmutil
17 from mercurial.i18n import _
17 from mercurial.i18n import _
18
18
19 shortname = '.hglf'
19 shortname = '.hglf'
20 shortnameslash = shortname + '/'
20 shortnameslash = shortname + '/'
21 longname = 'largefiles'
21 longname = 'largefiles'
22
22
23
23
24 # -- Private worker functions ------------------------------------------
24 # -- Private worker functions ------------------------------------------
25
25
26 def getminsize(ui, assumelfiles, opt, default=10):
26 def getminsize(ui, assumelfiles, opt, default=10):
27 lfsize = opt
27 lfsize = opt
28 if not lfsize and assumelfiles:
28 if not lfsize and assumelfiles:
29 lfsize = ui.config(longname, 'minsize', default=default)
29 lfsize = ui.config(longname, 'minsize', default=default)
30 if lfsize:
30 if lfsize:
31 try:
31 try:
32 lfsize = float(lfsize)
32 lfsize = float(lfsize)
33 except ValueError:
33 except ValueError:
34 raise util.Abort(_('largefiles: size must be number (not %s)\n')
34 raise util.Abort(_('largefiles: size must be number (not %s)\n')
35 % lfsize)
35 % lfsize)
36 if lfsize is None:
36 if lfsize is None:
37 raise util.Abort(_('minimum size for largefiles must be specified'))
37 raise util.Abort(_('minimum size for largefiles must be specified'))
38 return lfsize
38 return lfsize
39
39
40 def link(src, dest):
40 def link(src, dest):
41 util.makedirs(os.path.dirname(dest))
41 util.makedirs(os.path.dirname(dest))
42 try:
42 try:
43 util.oslink(src, dest)
43 util.oslink(src, dest)
44 except OSError:
44 except OSError:
45 # if hardlinks fail, fallback on atomic copy
45 # if hardlinks fail, fallback on atomic copy
46 dst = util.atomictempfile(dest)
46 dst = util.atomictempfile(dest)
47 for chunk in util.filechunkiter(open(src, 'rb')):
47 for chunk in util.filechunkiter(open(src, 'rb')):
48 dst.write(chunk)
48 dst.write(chunk)
49 dst.close()
49 dst.close()
50 os.chmod(dest, os.stat(src).st_mode)
50 os.chmod(dest, os.stat(src).st_mode)
51
51
52 def usercachepath(ui, hash):
52 def usercachepath(ui, hash):
53 path = ui.configpath(longname, 'usercache', None)
53 path = ui.configpath(longname, 'usercache', None)
54 if path:
54 if path:
55 path = os.path.join(path, hash)
55 path = os.path.join(path, hash)
56 else:
56 else:
57 if os.name == 'nt':
57 if os.name == 'nt':
58 appdata = os.getenv('LOCALAPPDATA', os.getenv('APPDATA'))
58 appdata = os.getenv('LOCALAPPDATA', os.getenv('APPDATA'))
59 if appdata:
59 if appdata:
60 path = os.path.join(appdata, longname, hash)
60 path = os.path.join(appdata, longname, hash)
61 elif platform.system() == 'Darwin':
61 elif platform.system() == 'Darwin':
62 home = os.getenv('HOME')
62 home = os.getenv('HOME')
63 if home:
63 if home:
64 path = os.path.join(home, 'Library', 'Caches',
64 path = os.path.join(home, 'Library', 'Caches',
65 longname, hash)
65 longname, hash)
66 elif os.name == 'posix':
66 elif os.name == 'posix':
67 path = os.getenv('XDG_CACHE_HOME')
67 path = os.getenv('XDG_CACHE_HOME')
68 if path:
68 if path:
69 path = os.path.join(path, longname, hash)
69 path = os.path.join(path, longname, hash)
70 else:
70 else:
71 home = os.getenv('HOME')
71 home = os.getenv('HOME')
72 if home:
72 if home:
73 path = os.path.join(home, '.cache', longname, hash)
73 path = os.path.join(home, '.cache', longname, hash)
74 else:
74 else:
75 raise util.Abort(_('unknown operating system: %s\n') % os.name)
75 raise util.Abort(_('unknown operating system: %s\n') % os.name)
76 return path
76 return path
77
77
78 def inusercache(ui, hash):
78 def inusercache(ui, hash):
79 path = usercachepath(ui, hash)
79 path = usercachepath(ui, hash)
80 return path and os.path.exists(path)
80 return path and os.path.exists(path)
81
81
82 def findfile(repo, hash):
82 def findfile(repo, hash):
83 if instore(repo, hash):
83 if instore(repo, hash):
84 repo.ui.note(_('found %s in store\n') % hash)
84 repo.ui.note(_('found %s in store\n') % hash)
85 return storepath(repo, hash)
85 return storepath(repo, hash)
86 elif inusercache(repo.ui, hash):
86 elif inusercache(repo.ui, hash):
87 repo.ui.note(_('found %s in system cache\n') % hash)
87 repo.ui.note(_('found %s in system cache\n') % hash)
88 path = storepath(repo, hash)
88 path = storepath(repo, hash)
89 link(usercachepath(repo.ui, hash), path)
89 link(usercachepath(repo.ui, hash), path)
90 return path
90 return path
91 return None
91 return None
92
92
93 class largefilesdirstate(dirstate.dirstate):
93 class largefilesdirstate(dirstate.dirstate):
94 def __getitem__(self, key):
94 def __getitem__(self, key):
95 return super(largefilesdirstate, self).__getitem__(unixpath(key))
95 return super(largefilesdirstate, self).__getitem__(unixpath(key))
96 def normal(self, f):
96 def normal(self, f):
97 return super(largefilesdirstate, self).normal(unixpath(f))
97 return super(largefilesdirstate, self).normal(unixpath(f))
98 def remove(self, f):
98 def remove(self, f):
99 return super(largefilesdirstate, self).remove(unixpath(f))
99 return super(largefilesdirstate, self).remove(unixpath(f))
100 def add(self, f):
100 def add(self, f):
101 return super(largefilesdirstate, self).add(unixpath(f))
101 return super(largefilesdirstate, self).add(unixpath(f))
102 def drop(self, f):
102 def drop(self, f):
103 return super(largefilesdirstate, self).drop(unixpath(f))
103 return super(largefilesdirstate, self).drop(unixpath(f))
104 def forget(self, f):
104 def forget(self, f):
105 return super(largefilesdirstate, self).forget(unixpath(f))
105 return super(largefilesdirstate, self).forget(unixpath(f))
106 def normallookup(self, f):
106 def normallookup(self, f):
107 return super(largefilesdirstate, self).normallookup(unixpath(f))
107 return super(largefilesdirstate, self).normallookup(unixpath(f))
108 def _ignore(self):
108 def _ignore(self):
109 return False
109 return False
110
110
111 def openlfdirstate(ui, repo, create=True):
111 def openlfdirstate(ui, repo, create=True):
112 '''
112 '''
113 Return a dirstate object that tracks largefiles: i.e. its root is
113 Return a dirstate object that tracks largefiles: i.e. its root is
114 the repo root, but it is saved in .hg/largefiles/dirstate.
114 the repo root, but it is saved in .hg/largefiles/dirstate.
115 '''
115 '''
116 lfstoredir = repo.join(longname)
116 lfstoredir = repo.join(longname)
117 opener = scmutil.opener(lfstoredir)
117 opener = scmutil.opener(lfstoredir)
118 lfdirstate = largefilesdirstate(opener, ui, repo.root,
118 lfdirstate = largefilesdirstate(opener, ui, repo.root,
119 repo.dirstate._validate)
119 repo.dirstate._validate)
120
120
121 # If the largefiles dirstate does not exist, populate and create
121 # If the largefiles dirstate does not exist, populate and create
122 # it. This ensures that we create it on the first meaningful
122 # it. This ensures that we create it on the first meaningful
123 # largefiles operation in a new clone.
123 # largefiles operation in a new clone.
124 if create and not os.path.exists(os.path.join(lfstoredir, 'dirstate')):
124 if create and not os.path.exists(os.path.join(lfstoredir, 'dirstate')):
125 util.makedirs(lfstoredir)
125 util.makedirs(lfstoredir)
126 matcher = getstandinmatcher(repo)
126 matcher = getstandinmatcher(repo)
127 for standin in repo.dirstate.walk(matcher, [], False, False):
127 for standin in repo.dirstate.walk(matcher, [], False, False):
128 lfile = splitstandin(standin)
128 lfile = splitstandin(standin)
129 lfdirstate.normallookup(lfile)
129 lfdirstate.normallookup(lfile)
130 return lfdirstate
130 return lfdirstate
131
131
132 def lfdirstatestatus(lfdirstate, repo, rev):
132 def lfdirstatestatus(lfdirstate, repo, rev):
133 match = match_.always(repo.root, repo.getcwd())
133 match = match_.always(repo.root, repo.getcwd())
134 s = lfdirstate.status(match, [], False, False, False)
134 s = lfdirstate.status(match, [], False, False, False)
135 unsure, modified, added, removed, missing, unknown, ignored, clean = s
135 unsure, modified, added, removed, missing, unknown, ignored, clean = s
136 for lfile in unsure:
136 for lfile in unsure:
137 try:
137 try:
138 fctx = repo[rev][standin(lfile)]
138 fctx = repo[rev][standin(lfile)]
139 except LookupError:
139 except LookupError:
140 fctx = None
140 fctx = None
141 if not fctx or fctx.data().strip() != hashfile(repo.wjoin(lfile)):
141 if not fctx or fctx.data().strip() != hashfile(repo.wjoin(lfile)):
142 modified.append(lfile)
142 modified.append(lfile)
143 else:
143 else:
144 clean.append(lfile)
144 clean.append(lfile)
145 lfdirstate.normal(lfile)
145 lfdirstate.normal(lfile)
146 return (modified, added, removed, missing, unknown, ignored, clean)
146 return (modified, added, removed, missing, unknown, ignored, clean)
147
147
148 def listlfiles(repo, rev=None, matcher=None):
148 def listlfiles(repo, rev=None, matcher=None):
149 '''return a list of largefiles in the working copy or the
149 '''return a list of largefiles in the working copy or the
150 specified changeset'''
150 specified changeset'''
151
151
152 if matcher is None:
152 if matcher is None:
153 matcher = getstandinmatcher(repo)
153 matcher = getstandinmatcher(repo)
154
154
155 # ignore unknown files in working directory
155 # ignore unknown files in working directory
156 return [splitstandin(f)
156 return [splitstandin(f)
157 for f in repo[rev].walk(matcher)
157 for f in repo[rev].walk(matcher)
158 if rev is not None or repo.dirstate[f] != '?']
158 if rev is not None or repo.dirstate[f] != '?']
159
159
160 def instore(repo, hash):
160 def instore(repo, hash):
161 return os.path.exists(storepath(repo, hash))
161 return os.path.exists(storepath(repo, hash))
162
162
163 def storepath(repo, hash):
163 def storepath(repo, hash):
164 return repo.join(os.path.join(longname, hash))
164 return repo.join(os.path.join(longname, hash))
165
165
166 def copyfromcache(repo, hash, filename):
166 def copyfromcache(repo, hash, filename):
167 '''Copy the specified largefile from the repo or system cache to
167 '''Copy the specified largefile from the repo or system cache to
168 filename in the repository. Return true on success or false if the
168 filename in the repository. Return true on success or false if the
169 file was not found in either cache (which should not happened:
169 file was not found in either cache (which should not happened:
170 this is meant to be called only after ensuring that the needed
170 this is meant to be called only after ensuring that the needed
171 largefile exists in the cache).'''
171 largefile exists in the cache).'''
172 path = findfile(repo, hash)
172 path = findfile(repo, hash)
173 if path is None:
173 if path is None:
174 return False
174 return False
175 util.makedirs(os.path.dirname(repo.wjoin(filename)))
175 util.makedirs(os.path.dirname(repo.wjoin(filename)))
176 # The write may fail before the file is fully written, but we
176 # The write may fail before the file is fully written, but we
177 # don't use atomic writes in the working copy.
177 # don't use atomic writes in the working copy.
178 shutil.copy(path, repo.wjoin(filename))
178 shutil.copy(path, repo.wjoin(filename))
179 return True
179 return True
180
180
181 def copytostore(repo, rev, file, uploaded=False):
181 def copytostore(repo, rev, file, uploaded=False):
182 hash = readstandin(repo, file, rev)
182 hash = readstandin(repo, file, rev)
183 if instore(repo, hash):
183 if instore(repo, hash):
184 return
184 return
185 copytostoreabsolute(repo, repo.wjoin(file), hash)
185 copytostoreabsolute(repo, repo.wjoin(file), hash)
186
186
187 def copyalltostore(repo, node):
187 def copyalltostore(repo, node):
188 '''Copy all largefiles in a given revision to the store'''
188 '''Copy all largefiles in a given revision to the store'''
189
189
190 ctx = repo[node]
190 ctx = repo[node]
191 for filename in ctx.files():
191 for filename in ctx.files():
192 if isstandin(filename) and filename in ctx.manifest():
192 if isstandin(filename) and filename in ctx.manifest():
193 realfile = splitstandin(filename)
193 realfile = splitstandin(filename)
194 copytostore(repo, ctx.node(), realfile)
194 copytostore(repo, ctx.node(), realfile)
195
195
196
196
197 def copytostoreabsolute(repo, file, hash):
197 def copytostoreabsolute(repo, file, hash):
198 if inusercache(repo.ui, hash):
198 if inusercache(repo.ui, hash):
199 link(usercachepath(repo.ui, hash), storepath(repo, hash))
199 link(usercachepath(repo.ui, hash), storepath(repo, hash))
200 elif not getattr(repo, "_isconverting", False):
200 elif not getattr(repo, "_isconverting", False):
201 util.makedirs(os.path.dirname(storepath(repo, hash)))
201 util.makedirs(os.path.dirname(storepath(repo, hash)))
202 dst = util.atomictempfile(storepath(repo, hash),
202 dst = util.atomictempfile(storepath(repo, hash),
203 createmode=repo.store.createmode)
203 createmode=repo.store.createmode)
204 for chunk in util.filechunkiter(open(file, 'rb')):
204 for chunk in util.filechunkiter(open(file, 'rb')):
205 dst.write(chunk)
205 dst.write(chunk)
206 dst.close()
206 dst.close()
207 linktousercache(repo, hash)
207 linktousercache(repo, hash)
208
208
209 def linktousercache(repo, hash):
209 def linktousercache(repo, hash):
210 path = usercachepath(repo.ui, hash)
210 path = usercachepath(repo.ui, hash)
211 if path:
211 if path:
212 link(storepath(repo, hash), path)
212 link(storepath(repo, hash), path)
213
213
214 def getstandinmatcher(repo, pats=[], opts={}):
214 def getstandinmatcher(repo, pats=[], opts={}):
215 '''Return a match object that applies pats to the standin directory'''
215 '''Return a match object that applies pats to the standin directory'''
216 standindir = repo.wjoin(shortname)
216 standindir = repo.wjoin(shortname)
217 if pats:
217 if pats:
218 pats = [os.path.join(standindir, pat) for pat in pats]
218 pats = [os.path.join(standindir, pat) for pat in pats]
219 else:
219 else:
220 # no patterns: relative to repo root
220 # no patterns: relative to repo root
221 pats = [standindir]
221 pats = [standindir]
222 # no warnings about missing files or directories
222 # no warnings about missing files or directories
223 match = scmutil.match(repo[None], pats, opts)
223 match = scmutil.match(repo[None], pats, opts)
224 match.bad = lambda f, msg: None
224 match.bad = lambda f, msg: None
225 return match
225 return match
226
226
227 def composestandinmatcher(repo, rmatcher):
227 def composestandinmatcher(repo, rmatcher):
228 '''Return a matcher that accepts standins corresponding to the
228 '''Return a matcher that accepts standins corresponding to the
229 files accepted by rmatcher. Pass the list of files in the matcher
229 files accepted by rmatcher. Pass the list of files in the matcher
230 as the paths specified by the user.'''
230 as the paths specified by the user.'''
231 smatcher = getstandinmatcher(repo, rmatcher.files())
231 smatcher = getstandinmatcher(repo, rmatcher.files())
232 isstandin = smatcher.matchfn
232 isstandin = smatcher.matchfn
233 def composedmatchfn(f):
233 def composedmatchfn(f):
234 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
234 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
235 smatcher.matchfn = composedmatchfn
235 smatcher.matchfn = composedmatchfn
236
236
237 return smatcher
237 return smatcher
238
238
239 def standin(filename):
239 def standin(filename):
240 '''Return the repo-relative path to the standin for the specified big
240 '''Return the repo-relative path to the standin for the specified big
241 file.'''
241 file.'''
242 # Notes:
242 # Notes:
243 # 1) Some callers want an absolute path, but for instance addlargefiles
243 # 1) Some callers want an absolute path, but for instance addlargefiles
244 # needs it repo-relative so it can be passed to repo[None].add(). So
244 # needs it repo-relative so it can be passed to repo[None].add(). So
245 # leave it up to the caller to use repo.wjoin() to get an absolute path.
245 # leave it up to the caller to use repo.wjoin() to get an absolute path.
246 # 2) Join with '/' because that's what dirstate always uses, even on
246 # 2) Join with '/' because that's what dirstate always uses, even on
247 # Windows. Change existing separator to '/' first in case we are
247 # Windows. Change existing separator to '/' first in case we are
248 # passed filenames from an external source (like the command line).
248 # passed filenames from an external source (like the command line).
249 return shortnameslash + util.pconvert(filename)
249 return shortnameslash + util.pconvert(filename)
250
250
251 def isstandin(filename):
251 def isstandin(filename):
252 '''Return true if filename is a big file standin. filename must be
252 '''Return true if filename is a big file standin. filename must be
253 in Mercurial's internal form (slash-separated).'''
253 in Mercurial's internal form (slash-separated).'''
254 return filename.startswith(shortnameslash)
254 return filename.startswith(shortnameslash)
255
255
256 def splitstandin(filename):
256 def splitstandin(filename):
257 # Split on / because that's what dirstate always uses, even on Windows.
257 # Split on / because that's what dirstate always uses, even on Windows.
258 # Change local separator to / first just in case we are passed filenames
258 # Change local separator to / first just in case we are passed filenames
259 # from an external source (like the command line).
259 # from an external source (like the command line).
260 bits = util.pconvert(filename).split('/', 1)
260 bits = util.pconvert(filename).split('/', 1)
261 if len(bits) == 2 and bits[0] == shortname:
261 if len(bits) == 2 and bits[0] == shortname:
262 return bits[1]
262 return bits[1]
263 else:
263 else:
264 return None
264 return None
265
265
266 def updatestandin(repo, standin):
266 def updatestandin(repo, standin):
267 file = repo.wjoin(splitstandin(standin))
267 file = repo.wjoin(splitstandin(standin))
268 if os.path.exists(file):
268 if os.path.exists(file):
269 hash = hashfile(file)
269 hash = hashfile(file)
270 executable = getexecutable(file)
270 executable = getexecutable(file)
271 writestandin(repo, standin, hash, executable)
271 writestandin(repo, standin, hash, executable)
272
272
273 def readstandin(repo, filename, node=None):
273 def readstandin(repo, filename, node=None):
274 '''read hex hash from standin for filename at given node, or working
274 '''read hex hash from standin for filename at given node, or working
275 directory if no node is given'''
275 directory if no node is given'''
276 return repo[node][standin(filename)].data().strip()
276 return repo[node][standin(filename)].data().strip()
277
277
278 def writestandin(repo, standin, hash, executable):
278 def writestandin(repo, standin, hash, executable):
279 '''write hash to <repo.root>/<standin>'''
279 '''write hash to <repo.root>/<standin>'''
280 writehash(hash, repo.wjoin(standin), executable)
280 repo.wwrite(standin, hash + '\n', executable and 'x' or '')
281
281
282 def copyandhash(instream, outfile):
282 def copyandhash(instream, outfile):
283 '''Read bytes from instream (iterable) and write them to outfile,
283 '''Read bytes from instream (iterable) and write them to outfile,
284 computing the SHA-1 hash of the data along the way. Return the hash.'''
284 computing the SHA-1 hash of the data along the way. Return the hash.'''
285 hasher = util.sha1('')
285 hasher = util.sha1('')
286 for data in instream:
286 for data in instream:
287 hasher.update(data)
287 hasher.update(data)
288 outfile.write(data)
288 outfile.write(data)
289 return hasher.hexdigest()
289 return hasher.hexdigest()
290
290
291 def hashrepofile(repo, file):
291 def hashrepofile(repo, file):
292 return hashfile(repo.wjoin(file))
292 return hashfile(repo.wjoin(file))
293
293
294 def hashfile(file):
294 def hashfile(file):
295 if not os.path.exists(file):
295 if not os.path.exists(file):
296 return ''
296 return ''
297 hasher = util.sha1('')
297 hasher = util.sha1('')
298 fd = open(file, 'rb')
298 fd = open(file, 'rb')
299 for data in util.filechunkiter(fd, 128 * 1024):
299 for data in util.filechunkiter(fd, 128 * 1024):
300 hasher.update(data)
300 hasher.update(data)
301 fd.close()
301 fd.close()
302 return hasher.hexdigest()
302 return hasher.hexdigest()
303
303
304 def writehash(hash, filename, executable):
305 util.makedirs(os.path.dirname(filename))
306 util.writefile(filename, hash + '\n')
307 os.chmod(filename, getmode(executable))
308
309 def getexecutable(filename):
304 def getexecutable(filename):
310 mode = os.stat(filename).st_mode
305 mode = os.stat(filename).st_mode
311 return ((mode & stat.S_IXUSR) and
306 return ((mode & stat.S_IXUSR) and
312 (mode & stat.S_IXGRP) and
307 (mode & stat.S_IXGRP) and
313 (mode & stat.S_IXOTH))
308 (mode & stat.S_IXOTH))
314
309
315 def getmode(executable):
316 if executable:
317 return 0755
318 else:
319 return 0644
320
321 def urljoin(first, second, *arg):
310 def urljoin(first, second, *arg):
322 def join(left, right):
311 def join(left, right):
323 if not left.endswith('/'):
312 if not left.endswith('/'):
324 left += '/'
313 left += '/'
325 if right.startswith('/'):
314 if right.startswith('/'):
326 right = right[1:]
315 right = right[1:]
327 return left + right
316 return left + right
328
317
329 url = join(first, second)
318 url = join(first, second)
330 for a in arg:
319 for a in arg:
331 url = join(url, a)
320 url = join(url, a)
332 return url
321 return url
333
322
334 def hexsha1(data):
323 def hexsha1(data):
335 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
324 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
336 object data"""
325 object data"""
337 h = util.sha1()
326 h = util.sha1()
338 for chunk in util.filechunkiter(data):
327 for chunk in util.filechunkiter(data):
339 h.update(chunk)
328 h.update(chunk)
340 return h.hexdigest()
329 return h.hexdigest()
341
330
342 def httpsendfile(ui, filename):
331 def httpsendfile(ui, filename):
343 return httpconnection.httpsendfile(ui, filename, 'rb')
332 return httpconnection.httpsendfile(ui, filename, 'rb')
344
333
345 def unixpath(path):
334 def unixpath(path):
346 '''Return a version of path normalized for use with the lfdirstate.'''
335 '''Return a version of path normalized for use with the lfdirstate.'''
347 return util.pconvert(os.path.normpath(path))
336 return util.pconvert(os.path.normpath(path))
348
337
349 def islfilesrepo(repo):
338 def islfilesrepo(repo):
350 if ('largefiles' in repo.requirements and
339 if ('largefiles' in repo.requirements and
351 util.any(shortnameslash in f[0] for f in repo.store.datafiles())):
340 util.any(shortnameslash in f[0] for f in repo.store.datafiles())):
352 return True
341 return True
353
342
354 return util.any(openlfdirstate(repo.ui, repo, False))
343 return util.any(openlfdirstate(repo.ui, repo, False))
355
344
356 class storeprotonotcapable(Exception):
345 class storeprotonotcapable(Exception):
357 def __init__(self, storetypes):
346 def __init__(self, storetypes):
358 self.storetypes = storetypes
347 self.storetypes = storetypes
359
348
360 def getstandinsstate(repo):
349 def getstandinsstate(repo):
361 standins = []
350 standins = []
362 matcher = getstandinmatcher(repo)
351 matcher = getstandinmatcher(repo)
363 for standin in repo.dirstate.walk(matcher, [], False, False):
352 for standin in repo.dirstate.walk(matcher, [], False, False):
364 lfile = splitstandin(standin)
353 lfile = splitstandin(standin)
365 try:
354 try:
366 hash = readstandin(repo, lfile)
355 hash = readstandin(repo, lfile)
367 except IOError:
356 except IOError:
368 hash = None
357 hash = None
369 standins.append((lfile, hash))
358 standins.append((lfile, hash))
370 return standins
359 return standins
371
360
372 def getlfilestoupdate(oldstandins, newstandins):
361 def getlfilestoupdate(oldstandins, newstandins):
373 changedstandins = set(oldstandins).symmetric_difference(set(newstandins))
362 changedstandins = set(oldstandins).symmetric_difference(set(newstandins))
374 filelist = []
363 filelist = []
375 for f in changedstandins:
364 for f in changedstandins:
376 if f[0] not in filelist:
365 if f[0] not in filelist:
377 filelist.append(f[0])
366 filelist.append(f[0])
378 return filelist
367 return filelist
General Comments 0
You need to be logged in to leave comments. Login now