##// END OF EJS Templates
largefiles: use progress helper...
Martin von Zweigbergk -
r38426:164306d3 default
parent child Browse files
Show More
@@ -1,164 +1,165 b''
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''base class for store implementations and store-related utility code'''
10 10 from __future__ import absolute_import
11 11
12 12 from mercurial.i18n import _
13 13
14 14 from mercurial import node, util
15 15
16 16 from . import lfutil
17 17
18 18 class StoreError(Exception):
19 19 '''Raised when there is a problem getting files from or putting
20 20 files to a central store.'''
21 21 def __init__(self, filename, hash, url, detail):
22 22 self.filename = filename
23 23 self.hash = hash
24 24 self.url = url
25 25 self.detail = detail
26 26
27 27 def longmessage(self):
28 28 return (_("error getting id %s from url %s for file %s: %s\n") %
29 29 (self.hash, util.hidepassword(self.url), self.filename,
30 30 self.detail))
31 31
32 32 def __str__(self):
33 33 return "%s: %s" % (util.hidepassword(self.url), self.detail)
34 34
35 35 class basestore(object):
36 36 def __init__(self, ui, repo, url):
37 37 self.ui = ui
38 38 self.repo = repo
39 39 self.url = url
40 40
41 41 def put(self, source, hash):
42 42 '''Put source file into the store so it can be retrieved by hash.'''
43 43 raise NotImplementedError('abstract method')
44 44
45 45 def exists(self, hashes):
46 46 '''Check to see if the store contains the given hashes. Given an
47 47 iterable of hashes it returns a mapping from hash to bool.'''
48 48 raise NotImplementedError('abstract method')
49 49
50 50 def get(self, files):
51 51 '''Get the specified largefiles from the store and write to local
52 52 files under repo.root. files is a list of (filename, hash)
53 53 tuples. Return (success, missing), lists of files successfully
54 54 downloaded and those not found in the store. success is a list
55 55 of (filename, hash) tuples; missing is a list of filenames that
56 56 we could not get. (The detailed error message will already have
57 57 been presented to the user, so missing is just supplied as a
58 58 summary.)'''
59 59 success = []
60 60 missing = []
61 61 ui = self.ui
62 62
63 63 at = 0
64 64 available = self.exists(set(hash for (_filename, hash) in files))
65 progress = ui.makeprogress(_('getting largefiles'), unit=_('files'),
66 total=len(files))
65 67 for filename, hash in files:
66 ui.progress(_('getting largefiles'), at, unit=_('files'),
67 total=len(files))
68 progress.update(at)
68 69 at += 1
69 70 ui.note(_('getting %s:%s\n') % (filename, hash))
70 71
71 72 if not available.get(hash):
72 73 ui.warn(_('%s: largefile %s not available from %s\n')
73 74 % (filename, hash, util.hidepassword(self.url)))
74 75 missing.append(filename)
75 76 continue
76 77
77 78 if self._gethash(filename, hash):
78 79 success.append((filename, hash))
79 80 else:
80 81 missing.append(filename)
81 82
82 ui.progress(_('getting largefiles'), None)
83 progress.complete()
83 84 return (success, missing)
84 85
85 86 def _gethash(self, filename, hash):
86 87 """Get file with the provided hash and store it in the local repo's
87 88 store and in the usercache.
88 89 filename is for informational messages only.
89 90 """
90 91 util.makedirs(lfutil.storepath(self.repo, ''))
91 92 storefilename = lfutil.storepath(self.repo, hash)
92 93
93 94 tmpname = storefilename + '.tmp'
94 95 with util.atomictempfile(tmpname,
95 96 createmode=self.repo.store.createmode) as tmpfile:
96 97 try:
97 98 gothash = self._getfile(tmpfile, filename, hash)
98 99 except StoreError as err:
99 100 self.ui.warn(err.longmessage())
100 101 gothash = ""
101 102
102 103 if gothash != hash:
103 104 if gothash != "":
104 105 self.ui.warn(_('%s: data corruption (expected %s, got %s)\n')
105 106 % (filename, hash, gothash))
106 107 util.unlink(tmpname)
107 108 return False
108 109
109 110 util.rename(tmpname, storefilename)
110 111 lfutil.linktousercache(self.repo, hash)
111 112 return True
112 113
113 114 def verify(self, revs, contents=False):
114 115 '''Verify the existence (and, optionally, contents) of every big
115 116 file revision referenced by every changeset in revs.
116 117 Return 0 if all is well, non-zero on any errors.'''
117 118
118 119 self.ui.status(_('searching %d changesets for largefiles\n') %
119 120 len(revs))
120 121 verified = set() # set of (filename, filenode) tuples
121 122 filestocheck = [] # list of (cset, filename, expectedhash)
122 123 for rev in revs:
123 124 cctx = self.repo[rev]
124 125 cset = "%d:%s" % (cctx.rev(), node.short(cctx.node()))
125 126
126 127 for standin in cctx:
127 128 filename = lfutil.splitstandin(standin)
128 129 if filename:
129 130 fctx = cctx[standin]
130 131 key = (filename, fctx.filenode())
131 132 if key not in verified:
132 133 verified.add(key)
133 134 expectedhash = lfutil.readasstandin(fctx)
134 135 filestocheck.append((cset, filename, expectedhash))
135 136
136 137 failed = self._verifyfiles(contents, filestocheck)
137 138
138 139 numrevs = len(verified)
139 140 numlfiles = len(set([fname for (fname, fnode) in verified]))
140 141 if contents:
141 142 self.ui.status(
142 143 _('verified contents of %d revisions of %d largefiles\n')
143 144 % (numrevs, numlfiles))
144 145 else:
145 146 self.ui.status(
146 147 _('verified existence of %d revisions of %d largefiles\n')
147 148 % (numrevs, numlfiles))
148 149 return int(failed)
149 150
150 151 def _getfile(self, tmpfile, filename, hash):
151 152 '''Fetch one revision of one file from the store and write it
152 153 to tmpfile. Compute the hash of the file on-the-fly as it
153 154 downloads and return the hash. Close tmpfile. Raise
154 155 StoreError if unable to download the file (e.g. it does not
155 156 exist in the store).'''
156 157 raise NotImplementedError('abstract method')
157 158
158 159 def _verifyfiles(self, contents, filestocheck):
159 160 '''Perform the actual verification of files in the store.
160 161 'contents' controls verification of content hash.
161 162 'filestocheck' is list of files to check.
162 163 Returns _true_ if any problems are found!
163 164 '''
164 165 raise NotImplementedError('abstract method')
@@ -1,604 +1,607 b''
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''High-level command function for lfconvert, plus the cmdtable.'''
10 10 from __future__ import absolute_import
11 11
12 12 import errno
13 13 import hashlib
14 14 import os
15 15 import shutil
16 16
17 17 from mercurial.i18n import _
18 18
19 19 from mercurial import (
20 20 cmdutil,
21 21 context,
22 22 error,
23 23 hg,
24 24 lock,
25 25 match as matchmod,
26 26 node,
27 27 pycompat,
28 28 registrar,
29 29 scmutil,
30 30 util,
31 31 )
32 32
33 33 from ..convert import (
34 34 convcmd,
35 35 filemap,
36 36 )
37 37
38 38 from . import (
39 39 lfutil,
40 40 storefactory
41 41 )
42 42
43 43 release = lock.release
44 44
45 45 # -- Commands ----------------------------------------------------------
46 46
47 47 cmdtable = {}
48 48 command = registrar.command(cmdtable)
49 49
50 50 @command('lfconvert',
51 51 [('s', 'size', '',
52 52 _('minimum size (MB) for files to be converted as largefiles'), 'SIZE'),
53 53 ('', 'to-normal', False,
54 54 _('convert from a largefiles repo to a normal repo')),
55 55 ],
56 56 _('hg lfconvert SOURCE DEST [FILE ...]'),
57 57 norepo=True,
58 58 inferrepo=True)
59 59 def lfconvert(ui, src, dest, *pats, **opts):
60 60 '''convert a normal repository to a largefiles repository
61 61
62 62 Convert repository SOURCE to a new repository DEST, identical to
63 63 SOURCE except that certain files will be converted as largefiles:
64 64 specifically, any file that matches any PATTERN *or* whose size is
65 65 above the minimum size threshold is converted as a largefile. The
66 66 size used to determine whether or not to track a file as a
67 67 largefile is the size of the first version of the file. The
68 68 minimum size can be specified either with --size or in
69 69 configuration as ``largefiles.size``.
70 70
71 71 After running this command you will need to make sure that
72 72 largefiles is enabled anywhere you intend to push the new
73 73 repository.
74 74
75 75 Use --to-normal to convert largefiles back to normal files; after
76 76 this, the DEST repository can be used without largefiles at all.'''
77 77
78 78 opts = pycompat.byteskwargs(opts)
79 79 if opts['to_normal']:
80 80 tolfile = False
81 81 else:
82 82 tolfile = True
83 83 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
84 84
85 85 if not hg.islocal(src):
86 86 raise error.Abort(_('%s is not a local Mercurial repo') % src)
87 87 if not hg.islocal(dest):
88 88 raise error.Abort(_('%s is not a local Mercurial repo') % dest)
89 89
90 90 rsrc = hg.repository(ui, src)
91 91 ui.status(_('initializing destination %s\n') % dest)
92 92 rdst = hg.repository(ui, dest, create=True)
93 93
94 94 success = False
95 95 dstwlock = dstlock = None
96 96 try:
97 97 # Get a list of all changesets in the source. The easy way to do this
98 98 # is to simply walk the changelog, using changelog.nodesbetween().
99 99 # Take a look at mercurial/revlog.py:639 for more details.
100 100 # Use a generator instead of a list to decrease memory usage
101 101 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
102 102 rsrc.heads())[0])
103 103 revmap = {node.nullid: node.nullid}
104 104 if tolfile:
105 105 # Lock destination to prevent modification while it is converted to.
106 106 # Don't need to lock src because we are just reading from its
107 107 # history which can't change.
108 108 dstwlock = rdst.wlock()
109 109 dstlock = rdst.lock()
110 110
111 111 lfiles = set()
112 112 normalfiles = set()
113 113 if not pats:
114 114 pats = ui.configlist(lfutil.longname, 'patterns')
115 115 if pats:
116 116 matcher = matchmod.match(rsrc.root, '', list(pats))
117 117 else:
118 118 matcher = None
119 119
120 120 lfiletohash = {}
121 progress = ui.makeprogress(_('converting revisions'),
122 unit=_('revisions'),
123 total=rsrc['tip'].rev())
121 124 for ctx in ctxs:
122 ui.progress(_('converting revisions'), ctx.rev(),
123 unit=_('revisions'), total=rsrc['tip'].rev())
125 progress.update(ctx.rev())
124 126 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
125 127 lfiles, normalfiles, matcher, size, lfiletohash)
126 ui.progress(_('converting revisions'), None)
128 progress.complete()
127 129
128 130 if rdst.wvfs.exists(lfutil.shortname):
129 131 rdst.wvfs.rmtree(lfutil.shortname)
130 132
131 133 for f in lfiletohash.keys():
132 134 if rdst.wvfs.isfile(f):
133 135 rdst.wvfs.unlink(f)
134 136 try:
135 137 rdst.wvfs.removedirs(rdst.wvfs.dirname(f))
136 138 except OSError:
137 139 pass
138 140
139 141 # If there were any files converted to largefiles, add largefiles
140 142 # to the destination repository's requirements.
141 143 if lfiles:
142 144 rdst.requirements.add('largefiles')
143 145 rdst._writerequirements()
144 146 else:
145 147 class lfsource(filemap.filemap_source):
146 148 def __init__(self, ui, source):
147 149 super(lfsource, self).__init__(ui, source, None)
148 150 self.filemapper.rename[lfutil.shortname] = '.'
149 151
150 152 def getfile(self, name, rev):
151 153 realname, realrev = rev
152 154 f = super(lfsource, self).getfile(name, rev)
153 155
154 156 if (not realname.startswith(lfutil.shortnameslash)
155 157 or f[0] is None):
156 158 return f
157 159
158 160 # Substitute in the largefile data for the hash
159 161 hash = f[0].strip()
160 162 path = lfutil.findfile(rsrc, hash)
161 163
162 164 if path is None:
163 165 raise error.Abort(_("missing largefile for '%s' in %s")
164 166 % (realname, realrev))
165 167 return util.readfile(path), f[1]
166 168
167 169 class converter(convcmd.converter):
168 170 def __init__(self, ui, source, dest, revmapfile, opts):
169 171 src = lfsource(ui, source)
170 172
171 173 super(converter, self).__init__(ui, src, dest, revmapfile,
172 174 opts)
173 175
174 176 found, missing = downloadlfiles(ui, rsrc)
175 177 if missing != 0:
176 178 raise error.Abort(_("all largefiles must be present locally"))
177 179
178 180 orig = convcmd.converter
179 181 convcmd.converter = converter
180 182
181 183 try:
182 184 convcmd.convert(ui, src, dest, source_type='hg', dest_type='hg')
183 185 finally:
184 186 convcmd.converter = orig
185 187 success = True
186 188 finally:
187 189 if tolfile:
188 190 rdst.dirstate.clear()
189 191 release(dstlock, dstwlock)
190 192 if not success:
191 193 # we failed, remove the new directory
192 194 shutil.rmtree(rdst.root)
193 195
194 196 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
195 197 matcher, size, lfiletohash):
196 198 # Convert src parents to dst parents
197 199 parents = _convertparents(ctx, revmap)
198 200
199 201 # Generate list of changed files
200 202 files = _getchangedfiles(ctx, parents)
201 203
202 204 dstfiles = []
203 205 for f in files:
204 206 if f not in lfiles and f not in normalfiles:
205 207 islfile = _islfile(f, ctx, matcher, size)
206 208 # If this file was renamed or copied then copy
207 209 # the largefile-ness of its predecessor
208 210 if f in ctx.manifest():
209 211 fctx = ctx.filectx(f)
210 212 renamed = fctx.renamed()
211 213 renamedlfile = renamed and renamed[0] in lfiles
212 214 islfile |= renamedlfile
213 215 if 'l' in fctx.flags():
214 216 if renamedlfile:
215 217 raise error.Abort(
216 218 _('renamed/copied largefile %s becomes symlink')
217 219 % f)
218 220 islfile = False
219 221 if islfile:
220 222 lfiles.add(f)
221 223 else:
222 224 normalfiles.add(f)
223 225
224 226 if f in lfiles:
225 227 fstandin = lfutil.standin(f)
226 228 dstfiles.append(fstandin)
227 229 # largefile in manifest if it has not been removed/renamed
228 230 if f in ctx.manifest():
229 231 fctx = ctx.filectx(f)
230 232 if 'l' in fctx.flags():
231 233 renamed = fctx.renamed()
232 234 if renamed and renamed[0] in lfiles:
233 235 raise error.Abort(_('largefile %s becomes symlink') % f)
234 236
235 237 # largefile was modified, update standins
236 238 m = hashlib.sha1('')
237 239 m.update(ctx[f].data())
238 240 hash = m.hexdigest()
239 241 if f not in lfiletohash or lfiletohash[f] != hash:
240 242 rdst.wwrite(f, ctx[f].data(), ctx[f].flags())
241 243 executable = 'x' in ctx[f].flags()
242 244 lfutil.writestandin(rdst, fstandin, hash,
243 245 executable)
244 246 lfiletohash[f] = hash
245 247 else:
246 248 # normal file
247 249 dstfiles.append(f)
248 250
249 251 def getfilectx(repo, memctx, f):
250 252 srcfname = lfutil.splitstandin(f)
251 253 if srcfname is not None:
252 254 # if the file isn't in the manifest then it was removed
253 255 # or renamed, return None to indicate this
254 256 try:
255 257 fctx = ctx.filectx(srcfname)
256 258 except error.LookupError:
257 259 return None
258 260 renamed = fctx.renamed()
259 261 if renamed:
260 262 # standin is always a largefile because largefile-ness
261 263 # doesn't change after rename or copy
262 264 renamed = lfutil.standin(renamed[0])
263 265
264 266 return context.memfilectx(repo, memctx, f,
265 267 lfiletohash[srcfname] + '\n',
266 268 'l' in fctx.flags(), 'x' in fctx.flags(),
267 269 renamed)
268 270 else:
269 271 return _getnormalcontext(repo, ctx, f, revmap)
270 272
271 273 # Commit
272 274 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
273 275
274 276 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
275 277 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
276 278 getfilectx, ctx.user(), ctx.date(), ctx.extra())
277 279 ret = rdst.commitctx(mctx)
278 280 lfutil.copyalltostore(rdst, ret)
279 281 rdst.setparents(ret)
280 282 revmap[ctx.node()] = rdst.changelog.tip()
281 283
282 284 # Generate list of changed files
283 285 def _getchangedfiles(ctx, parents):
284 286 files = set(ctx.files())
285 287 if node.nullid not in parents:
286 288 mc = ctx.manifest()
287 289 mp1 = ctx.parents()[0].manifest()
288 290 mp2 = ctx.parents()[1].manifest()
289 291 files |= (set(mp1) | set(mp2)) - set(mc)
290 292 for f in mc:
291 293 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
292 294 files.add(f)
293 295 return files
294 296
295 297 # Convert src parents to dst parents
296 298 def _convertparents(ctx, revmap):
297 299 parents = []
298 300 for p in ctx.parents():
299 301 parents.append(revmap[p.node()])
300 302 while len(parents) < 2:
301 303 parents.append(node.nullid)
302 304 return parents
303 305
304 306 # Get memfilectx for a normal file
305 307 def _getnormalcontext(repo, ctx, f, revmap):
306 308 try:
307 309 fctx = ctx.filectx(f)
308 310 except error.LookupError:
309 311 return None
310 312 renamed = fctx.renamed()
311 313 if renamed:
312 314 renamed = renamed[0]
313 315
314 316 data = fctx.data()
315 317 if f == '.hgtags':
316 318 data = _converttags (repo.ui, revmap, data)
317 319 return context.memfilectx(repo, ctx, f, data, 'l' in fctx.flags(),
318 320 'x' in fctx.flags(), renamed)
319 321
320 322 # Remap tag data using a revision map
321 323 def _converttags(ui, revmap, data):
322 324 newdata = []
323 325 for line in data.splitlines():
324 326 try:
325 327 id, name = line.split(' ', 1)
326 328 except ValueError:
327 329 ui.warn(_('skipping incorrectly formatted tag %s\n')
328 330 % line)
329 331 continue
330 332 try:
331 333 newid = node.bin(id)
332 334 except TypeError:
333 335 ui.warn(_('skipping incorrectly formatted id %s\n')
334 336 % id)
335 337 continue
336 338 try:
337 339 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
338 340 name))
339 341 except KeyError:
340 342 ui.warn(_('no mapping for id %s\n') % id)
341 343 continue
342 344 return ''.join(newdata)
343 345
344 346 def _islfile(file, ctx, matcher, size):
345 347 '''Return true if file should be considered a largefile, i.e.
346 348 matcher matches it or it is larger than size.'''
347 349 # never store special .hg* files as largefiles
348 350 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
349 351 return False
350 352 if matcher and matcher(file):
351 353 return True
352 354 try:
353 355 return ctx.filectx(file).size() >= size * 1024 * 1024
354 356 except error.LookupError:
355 357 return False
356 358
357 359 def uploadlfiles(ui, rsrc, rdst, files):
358 360 '''upload largefiles to the central store'''
359 361
360 362 if not files:
361 363 return
362 364
363 365 store = storefactory.openstore(rsrc, rdst, put=True)
364 366
365 367 at = 0
366 368 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
367 369 retval = store.exists(files)
368 370 files = [h for h in files if not retval[h]]
369 371 ui.debug("%d largefiles need to be uploaded\n" % len(files))
370 372
373 progress = ui.makeprogress(_('uploading largefiles'), unit=_('files'),
374 total=len(files))
371 375 for hash in files:
372 ui.progress(_('uploading largefiles'), at, unit=_('files'),
373 total=len(files))
376 progress.update(at)
374 377 source = lfutil.findfile(rsrc, hash)
375 378 if not source:
376 379 raise error.Abort(_('largefile %s missing from store'
377 380 ' (needs to be uploaded)') % hash)
378 381 # XXX check for errors here
379 382 store.put(source, hash)
380 383 at += 1
381 ui.progress(_('uploading largefiles'), None)
384 progress.complete()
382 385
383 386 def verifylfiles(ui, repo, all=False, contents=False):
384 387 '''Verify that every largefile revision in the current changeset
385 388 exists in the central store. With --contents, also verify that
386 389 the contents of each local largefile file revision are correct (SHA-1 hash
387 390 matches the revision ID). With --all, check every changeset in
388 391 this repository.'''
389 392 if all:
390 393 revs = repo.revs('all()')
391 394 else:
392 395 revs = ['.']
393 396
394 397 store = storefactory.openstore(repo)
395 398 return store.verify(revs, contents=contents)
396 399
397 400 def cachelfiles(ui, repo, node, filelist=None):
398 401 '''cachelfiles ensures that all largefiles needed by the specified revision
399 402 are present in the repository's largefile cache.
400 403
401 404 returns a tuple (cached, missing). cached is the list of files downloaded
402 405 by this operation; missing is the list of files that were needed but could
403 406 not be found.'''
404 407 lfiles = lfutil.listlfiles(repo, node)
405 408 if filelist:
406 409 lfiles = set(lfiles) & set(filelist)
407 410 toget = []
408 411
409 412 ctx = repo[node]
410 413 for lfile in lfiles:
411 414 try:
412 415 expectedhash = lfutil.readasstandin(ctx[lfutil.standin(lfile)])
413 416 except IOError as err:
414 417 if err.errno == errno.ENOENT:
415 418 continue # node must be None and standin wasn't found in wctx
416 419 raise
417 420 if not lfutil.findfile(repo, expectedhash):
418 421 toget.append((lfile, expectedhash))
419 422
420 423 if toget:
421 424 store = storefactory.openstore(repo)
422 425 ret = store.get(toget)
423 426 return ret
424 427
425 428 return ([], [])
426 429
427 430 def downloadlfiles(ui, repo, rev=None):
428 431 match = scmutil.match(repo[None], [repo.wjoin(lfutil.shortname)], {})
429 432 def prepare(ctx, fns):
430 433 pass
431 434 totalsuccess = 0
432 435 totalmissing = 0
433 436 if rev != []: # walkchangerevs on empty list would return all revs
434 437 for ctx in cmdutil.walkchangerevs(repo, match, {'rev' : rev},
435 438 prepare):
436 439 success, missing = cachelfiles(ui, repo, ctx.node())
437 440 totalsuccess += len(success)
438 441 totalmissing += len(missing)
439 442 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
440 443 if totalmissing > 0:
441 444 ui.status(_("%d largefiles failed to download\n") % totalmissing)
442 445 return totalsuccess, totalmissing
443 446
444 447 def updatelfiles(ui, repo, filelist=None, printmessage=None,
445 448 normallookup=False):
446 449 '''Update largefiles according to standins in the working directory
447 450
448 451 If ``printmessage`` is other than ``None``, it means "print (or
449 452 ignore, for false) message forcibly".
450 453 '''
451 454 statuswriter = lfutil.getstatuswriter(ui, repo, printmessage)
452 455 with repo.wlock():
453 456 lfdirstate = lfutil.openlfdirstate(ui, repo)
454 457 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
455 458
456 459 if filelist is not None:
457 460 filelist = set(filelist)
458 461 lfiles = [f for f in lfiles if f in filelist]
459 462
460 463 update = {}
461 464 dropped = set()
462 465 updated, removed = 0, 0
463 466 wvfs = repo.wvfs
464 467 wctx = repo[None]
465 468 for lfile in lfiles:
466 469 rellfile = lfile
467 470 rellfileorig = os.path.relpath(
468 471 scmutil.origpath(ui, repo, wvfs.join(rellfile)),
469 472 start=repo.root)
470 473 relstandin = lfutil.standin(lfile)
471 474 relstandinorig = os.path.relpath(
472 475 scmutil.origpath(ui, repo, wvfs.join(relstandin)),
473 476 start=repo.root)
474 477 if wvfs.exists(relstandin):
475 478 if (wvfs.exists(relstandinorig) and
476 479 wvfs.exists(rellfile)):
477 480 shutil.copyfile(wvfs.join(rellfile),
478 481 wvfs.join(rellfileorig))
479 482 wvfs.unlinkpath(relstandinorig)
480 483 expecthash = lfutil.readasstandin(wctx[relstandin])
481 484 if expecthash != '':
482 485 if lfile not in wctx: # not switched to normal file
483 486 if repo.dirstate[relstandin] != '?':
484 487 wvfs.unlinkpath(rellfile, ignoremissing=True)
485 488 else:
486 489 dropped.add(rellfile)
487 490
488 491 # use normallookup() to allocate an entry in largefiles
489 492 # dirstate to prevent lfilesrepo.status() from reporting
490 493 # missing files as removed.
491 494 lfdirstate.normallookup(lfile)
492 495 update[lfile] = expecthash
493 496 else:
494 497 # Remove lfiles for which the standin is deleted, unless the
495 498 # lfile is added to the repository again. This happens when a
496 499 # largefile is converted back to a normal file: the standin
497 500 # disappears, but a new (normal) file appears as the lfile.
498 501 if (wvfs.exists(rellfile) and
499 502 repo.dirstate.normalize(lfile) not in wctx):
500 503 wvfs.unlinkpath(rellfile)
501 504 removed += 1
502 505
503 506 # largefile processing might be slow and be interrupted - be prepared
504 507 lfdirstate.write()
505 508
506 509 if lfiles:
507 510 lfiles = [f for f in lfiles if f not in dropped]
508 511
509 512 for f in dropped:
510 513 repo.wvfs.unlinkpath(lfutil.standin(f))
511 514
512 515 # This needs to happen for dropped files, otherwise they stay in
513 516 # the M state.
514 517 lfutil.synclfdirstate(repo, lfdirstate, f, normallookup)
515 518
516 519 statuswriter(_('getting changed largefiles\n'))
517 520 cachelfiles(ui, repo, None, lfiles)
518 521
519 522 for lfile in lfiles:
520 523 update1 = 0
521 524
522 525 expecthash = update.get(lfile)
523 526 if expecthash:
524 527 if not lfutil.copyfromcache(repo, expecthash, lfile):
525 528 # failed ... but already removed and set to normallookup
526 529 continue
527 530 # Synchronize largefile dirstate to the last modified
528 531 # time of the file
529 532 lfdirstate.normal(lfile)
530 533 update1 = 1
531 534
532 535 # copy the exec mode of largefile standin from the repository's
533 536 # dirstate to its state in the lfdirstate.
534 537 rellfile = lfile
535 538 relstandin = lfutil.standin(lfile)
536 539 if wvfs.exists(relstandin):
537 540 # exec is decided by the users permissions using mask 0o100
538 541 standinexec = wvfs.stat(relstandin).st_mode & 0o100
539 542 st = wvfs.stat(rellfile)
540 543 mode = st.st_mode
541 544 if standinexec != mode & 0o100:
542 545 # first remove all X bits, then shift all R bits to X
543 546 mode &= ~0o111
544 547 if standinexec:
545 548 mode |= (mode >> 2) & 0o111 & ~util.umask
546 549 wvfs.chmod(rellfile, mode)
547 550 update1 = 1
548 551
549 552 updated += update1
550 553
551 554 lfutil.synclfdirstate(repo, lfdirstate, lfile, normallookup)
552 555
553 556 lfdirstate.write()
554 557 if lfiles:
555 558 statuswriter(_('%d largefiles updated, %d removed\n') % (updated,
556 559 removed))
557 560
558 561 @command('lfpull',
559 562 [('r', 'rev', [], _('pull largefiles for these revisions'))
560 563 ] + cmdutil.remoteopts,
561 564 _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]'))
562 565 def lfpull(ui, repo, source="default", **opts):
563 566 """pull largefiles for the specified revisions from the specified source
564 567
565 568 Pull largefiles that are referenced from local changesets but missing
566 569 locally, pulling from a remote repository to the local cache.
567 570
568 571 If SOURCE is omitted, the 'default' path will be used.
569 572 See :hg:`help urls` for more information.
570 573
571 574 .. container:: verbose
572 575
573 576 Some examples:
574 577
575 578 - pull largefiles for all branch heads::
576 579
577 580 hg lfpull -r "head() and not closed()"
578 581
579 582 - pull largefiles on the default branch::
580 583
581 584 hg lfpull -r "branch(default)"
582 585 """
583 586 repo.lfpullsource = source
584 587
585 588 revs = opts.get(r'rev', [])
586 589 if not revs:
587 590 raise error.Abort(_('no revisions specified'))
588 591 revs = scmutil.revrange(repo, revs)
589 592
590 593 numcached = 0
591 594 for rev in revs:
592 595 ui.note(_('pulling largefiles for revision %d\n') % rev)
593 596 (cached, missing) = cachelfiles(ui, repo, rev)
594 597 numcached += len(cached)
595 598 ui.status(_("%d largefiles cached\n") % numcached)
596 599
597 600 @command('debuglfput',
598 601 [] + cmdutil.remoteopts,
599 602 _('FILE'))
600 603 def debuglfput(ui, repo, filepath, **kwargs):
601 604 hash = lfutil.hashfile(filepath)
602 605 storefactory.openstore(repo).put(filepath, hash)
603 606 ui.write('%s\n' % hash)
604 607 return 0
@@ -1,674 +1,675 b''
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''largefiles utility code: must not import other modules in this package.'''
10 10 from __future__ import absolute_import
11 11
12 12 import copy
13 13 import hashlib
14 14 import os
15 15 import stat
16 16
17 17 from mercurial.i18n import _
18 18 from mercurial.node import hex
19 19
20 20 from mercurial import (
21 21 dirstate,
22 22 encoding,
23 23 error,
24 24 httpconnection,
25 25 match as matchmod,
26 26 node,
27 27 pycompat,
28 28 scmutil,
29 29 sparse,
30 30 util,
31 31 vfs as vfsmod,
32 32 )
33 33
34 34 shortname = '.hglf'
35 35 shortnameslash = shortname + '/'
36 36 longname = 'largefiles'
37 37
38 38 # -- Private worker functions ------------------------------------------
39 39
40 40 def getminsize(ui, assumelfiles, opt, default=10):
41 41 lfsize = opt
42 42 if not lfsize and assumelfiles:
43 43 lfsize = ui.config(longname, 'minsize', default=default)
44 44 if lfsize:
45 45 try:
46 46 lfsize = float(lfsize)
47 47 except ValueError:
48 48 raise error.Abort(_('largefiles: size must be number (not %s)\n')
49 49 % lfsize)
50 50 if lfsize is None:
51 51 raise error.Abort(_('minimum size for largefiles must be specified'))
52 52 return lfsize
53 53
54 54 def link(src, dest):
55 55 """Try to create hardlink - if that fails, efficiently make a copy."""
56 56 util.makedirs(os.path.dirname(dest))
57 57 try:
58 58 util.oslink(src, dest)
59 59 except OSError:
60 60 # if hardlinks fail, fallback on atomic copy
61 61 with open(src, 'rb') as srcf, util.atomictempfile(dest) as dstf:
62 62 for chunk in util.filechunkiter(srcf):
63 63 dstf.write(chunk)
64 64 os.chmod(dest, os.stat(src).st_mode)
65 65
66 66 def usercachepath(ui, hash):
67 67 '''Return the correct location in the "global" largefiles cache for a file
68 68 with the given hash.
69 69 This cache is used for sharing of largefiles across repositories - both
70 70 to preserve download bandwidth and storage space.'''
71 71 return os.path.join(_usercachedir(ui), hash)
72 72
73 73 def _usercachedir(ui, name=longname):
74 74 '''Return the location of the "global" largefiles cache.'''
75 75 path = ui.configpath(name, 'usercache')
76 76 if path:
77 77 return path
78 78 if pycompat.iswindows:
79 79 appdata = encoding.environ.get('LOCALAPPDATA',\
80 80 encoding.environ.get('APPDATA'))
81 81 if appdata:
82 82 return os.path.join(appdata, name)
83 83 elif pycompat.isdarwin:
84 84 home = encoding.environ.get('HOME')
85 85 if home:
86 86 return os.path.join(home, 'Library', 'Caches', name)
87 87 elif pycompat.isposix:
88 88 path = encoding.environ.get('XDG_CACHE_HOME')
89 89 if path:
90 90 return os.path.join(path, name)
91 91 home = encoding.environ.get('HOME')
92 92 if home:
93 93 return os.path.join(home, '.cache', name)
94 94 else:
95 95 raise error.Abort(_('unknown operating system: %s\n')
96 96 % pycompat.osname)
97 97 raise error.Abort(_('unknown %s usercache location') % name)
98 98
99 99 def inusercache(ui, hash):
100 100 path = usercachepath(ui, hash)
101 101 return os.path.exists(path)
102 102
103 103 def findfile(repo, hash):
104 104 '''Return store path of the largefile with the specified hash.
105 105 As a side effect, the file might be linked from user cache.
106 106 Return None if the file can't be found locally.'''
107 107 path, exists = findstorepath(repo, hash)
108 108 if exists:
109 109 repo.ui.note(_('found %s in store\n') % hash)
110 110 return path
111 111 elif inusercache(repo.ui, hash):
112 112 repo.ui.note(_('found %s in system cache\n') % hash)
113 113 path = storepath(repo, hash)
114 114 link(usercachepath(repo.ui, hash), path)
115 115 return path
116 116 return None
117 117
118 118 class largefilesdirstate(dirstate.dirstate):
119 119 def __getitem__(self, key):
120 120 return super(largefilesdirstate, self).__getitem__(unixpath(key))
121 121 def normal(self, f):
122 122 return super(largefilesdirstate, self).normal(unixpath(f))
123 123 def remove(self, f):
124 124 return super(largefilesdirstate, self).remove(unixpath(f))
125 125 def add(self, f):
126 126 return super(largefilesdirstate, self).add(unixpath(f))
127 127 def drop(self, f):
128 128 return super(largefilesdirstate, self).drop(unixpath(f))
129 129 def forget(self, f):
130 130 return super(largefilesdirstate, self).forget(unixpath(f))
131 131 def normallookup(self, f):
132 132 return super(largefilesdirstate, self).normallookup(unixpath(f))
133 133 def _ignore(self, f):
134 134 return False
135 135 def write(self, tr=False):
136 136 # (1) disable PENDING mode always
137 137 # (lfdirstate isn't yet managed as a part of the transaction)
138 138 # (2) avoid develwarn 'use dirstate.write with ....'
139 139 super(largefilesdirstate, self).write(None)
140 140
141 141 def openlfdirstate(ui, repo, create=True):
142 142 '''
143 143 Return a dirstate object that tracks largefiles: i.e. its root is
144 144 the repo root, but it is saved in .hg/largefiles/dirstate.
145 145 '''
146 146 vfs = repo.vfs
147 147 lfstoredir = longname
148 148 opener = vfsmod.vfs(vfs.join(lfstoredir))
149 149 lfdirstate = largefilesdirstate(opener, ui, repo.root,
150 150 repo.dirstate._validate,
151 151 lambda: sparse.matcher(repo))
152 152
153 153 # If the largefiles dirstate does not exist, populate and create
154 154 # it. This ensures that we create it on the first meaningful
155 155 # largefiles operation in a new clone.
156 156 if create and not vfs.exists(vfs.join(lfstoredir, 'dirstate')):
157 157 matcher = getstandinmatcher(repo)
158 158 standins = repo.dirstate.walk(matcher, subrepos=[], unknown=False,
159 159 ignored=False)
160 160
161 161 if len(standins) > 0:
162 162 vfs.makedirs(lfstoredir)
163 163
164 164 for standin in standins:
165 165 lfile = splitstandin(standin)
166 166 lfdirstate.normallookup(lfile)
167 167 return lfdirstate
168 168
169 169 def lfdirstatestatus(lfdirstate, repo):
170 170 pctx = repo['.']
171 171 match = matchmod.always(repo.root, repo.getcwd())
172 172 unsure, s = lfdirstate.status(match, subrepos=[], ignored=False,
173 173 clean=False, unknown=False)
174 174 modified, clean = s.modified, s.clean
175 175 for lfile in unsure:
176 176 try:
177 177 fctx = pctx[standin(lfile)]
178 178 except LookupError:
179 179 fctx = None
180 180 if not fctx or readasstandin(fctx) != hashfile(repo.wjoin(lfile)):
181 181 modified.append(lfile)
182 182 else:
183 183 clean.append(lfile)
184 184 lfdirstate.normal(lfile)
185 185 return s
186 186
187 187 def listlfiles(repo, rev=None, matcher=None):
188 188 '''return a list of largefiles in the working copy or the
189 189 specified changeset'''
190 190
191 191 if matcher is None:
192 192 matcher = getstandinmatcher(repo)
193 193
194 194 # ignore unknown files in working directory
195 195 return [splitstandin(f)
196 196 for f in repo[rev].walk(matcher)
197 197 if rev is not None or repo.dirstate[f] != '?']
198 198
199 199 def instore(repo, hash, forcelocal=False):
200 200 '''Return true if a largefile with the given hash exists in the store'''
201 201 return os.path.exists(storepath(repo, hash, forcelocal))
202 202
203 203 def storepath(repo, hash, forcelocal=False):
204 204 '''Return the correct location in the repository largefiles store for a
205 205 file with the given hash.'''
206 206 if not forcelocal and repo.shared():
207 207 return repo.vfs.reljoin(repo.sharedpath, longname, hash)
208 208 return repo.vfs.join(longname, hash)
209 209
210 210 def findstorepath(repo, hash):
211 211 '''Search through the local store path(s) to find the file for the given
212 212 hash. If the file is not found, its path in the primary store is returned.
213 213 The return value is a tuple of (path, exists(path)).
214 214 '''
215 215 # For shared repos, the primary store is in the share source. But for
216 216 # backward compatibility, force a lookup in the local store if it wasn't
217 217 # found in the share source.
218 218 path = storepath(repo, hash, False)
219 219
220 220 if instore(repo, hash):
221 221 return (path, True)
222 222 elif repo.shared() and instore(repo, hash, True):
223 223 return storepath(repo, hash, True), True
224 224
225 225 return (path, False)
226 226
227 227 def copyfromcache(repo, hash, filename):
228 228 '''Copy the specified largefile from the repo or system cache to
229 229 filename in the repository. Return true on success or false if the
230 230 file was not found in either cache (which should not happened:
231 231 this is meant to be called only after ensuring that the needed
232 232 largefile exists in the cache).'''
233 233 wvfs = repo.wvfs
234 234 path = findfile(repo, hash)
235 235 if path is None:
236 236 return False
237 237 wvfs.makedirs(wvfs.dirname(wvfs.join(filename)))
238 238 # The write may fail before the file is fully written, but we
239 239 # don't use atomic writes in the working copy.
240 240 with open(path, 'rb') as srcfd, wvfs(filename, 'wb') as destfd:
241 241 gothash = copyandhash(
242 242 util.filechunkiter(srcfd), destfd)
243 243 if gothash != hash:
244 244 repo.ui.warn(_('%s: data corruption in %s with hash %s\n')
245 245 % (filename, path, gothash))
246 246 wvfs.unlink(filename)
247 247 return False
248 248 return True
249 249
250 250 def copytostore(repo, ctx, file, fstandin):
251 251 wvfs = repo.wvfs
252 252 hash = readasstandin(ctx[fstandin])
253 253 if instore(repo, hash):
254 254 return
255 255 if wvfs.exists(file):
256 256 copytostoreabsolute(repo, wvfs.join(file), hash)
257 257 else:
258 258 repo.ui.warn(_("%s: largefile %s not available from local store\n") %
259 259 (file, hash))
260 260
261 261 def copyalltostore(repo, node):
262 262 '''Copy all largefiles in a given revision to the store'''
263 263
264 264 ctx = repo[node]
265 265 for filename in ctx.files():
266 266 realfile = splitstandin(filename)
267 267 if realfile is not None and filename in ctx.manifest():
268 268 copytostore(repo, ctx, realfile, filename)
269 269
270 270 def copytostoreabsolute(repo, file, hash):
271 271 if inusercache(repo.ui, hash):
272 272 link(usercachepath(repo.ui, hash), storepath(repo, hash))
273 273 else:
274 274 util.makedirs(os.path.dirname(storepath(repo, hash)))
275 275 with open(file, 'rb') as srcf:
276 276 with util.atomictempfile(storepath(repo, hash),
277 277 createmode=repo.store.createmode) as dstf:
278 278 for chunk in util.filechunkiter(srcf):
279 279 dstf.write(chunk)
280 280 linktousercache(repo, hash)
281 281
282 282 def linktousercache(repo, hash):
283 283 '''Link / copy the largefile with the specified hash from the store
284 284 to the cache.'''
285 285 path = usercachepath(repo.ui, hash)
286 286 link(storepath(repo, hash), path)
287 287
288 288 def getstandinmatcher(repo, rmatcher=None):
289 289 '''Return a match object that applies rmatcher to the standin directory'''
290 290 wvfs = repo.wvfs
291 291 standindir = shortname
292 292
293 293 # no warnings about missing files or directories
294 294 badfn = lambda f, msg: None
295 295
296 296 if rmatcher and not rmatcher.always():
297 297 pats = [wvfs.join(standindir, pat) for pat in rmatcher.files()]
298 298 if not pats:
299 299 pats = [wvfs.join(standindir)]
300 300 match = scmutil.match(repo[None], pats, badfn=badfn)
301 301 else:
302 302 # no patterns: relative to repo root
303 303 match = scmutil.match(repo[None], [wvfs.join(standindir)], badfn=badfn)
304 304 return match
305 305
306 306 def composestandinmatcher(repo, rmatcher):
307 307 '''Return a matcher that accepts standins corresponding to the
308 308 files accepted by rmatcher. Pass the list of files in the matcher
309 309 as the paths specified by the user.'''
310 310 smatcher = getstandinmatcher(repo, rmatcher)
311 311 isstandin = smatcher.matchfn
312 312 def composedmatchfn(f):
313 313 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
314 314 smatcher.matchfn = composedmatchfn
315 315
316 316 return smatcher
317 317
318 318 def standin(filename):
319 319 '''Return the repo-relative path to the standin for the specified big
320 320 file.'''
321 321 # Notes:
322 322 # 1) Some callers want an absolute path, but for instance addlargefiles
323 323 # needs it repo-relative so it can be passed to repo[None].add(). So
324 324 # leave it up to the caller to use repo.wjoin() to get an absolute path.
325 325 # 2) Join with '/' because that's what dirstate always uses, even on
326 326 # Windows. Change existing separator to '/' first in case we are
327 327 # passed filenames from an external source (like the command line).
328 328 return shortnameslash + util.pconvert(filename)
329 329
330 330 def isstandin(filename):
331 331 '''Return true if filename is a big file standin. filename must be
332 332 in Mercurial's internal form (slash-separated).'''
333 333 return filename.startswith(shortnameslash)
334 334
335 335 def splitstandin(filename):
336 336 # Split on / because that's what dirstate always uses, even on Windows.
337 337 # Change local separator to / first just in case we are passed filenames
338 338 # from an external source (like the command line).
339 339 bits = util.pconvert(filename).split('/', 1)
340 340 if len(bits) == 2 and bits[0] == shortname:
341 341 return bits[1]
342 342 else:
343 343 return None
344 344
345 345 def updatestandin(repo, lfile, standin):
346 346 """Re-calculate hash value of lfile and write it into standin
347 347
348 348 This assumes that "lfutil.standin(lfile) == standin", for efficiency.
349 349 """
350 350 file = repo.wjoin(lfile)
351 351 if repo.wvfs.exists(lfile):
352 352 hash = hashfile(file)
353 353 executable = getexecutable(file)
354 354 writestandin(repo, standin, hash, executable)
355 355 else:
356 356 raise error.Abort(_('%s: file not found!') % lfile)
357 357
358 358 def readasstandin(fctx):
359 359 '''read hex hash from given filectx of standin file
360 360
361 361 This encapsulates how "standin" data is stored into storage layer.'''
362 362 return fctx.data().strip()
363 363
364 364 def writestandin(repo, standin, hash, executable):
365 365 '''write hash to <repo.root>/<standin>'''
366 366 repo.wwrite(standin, hash + '\n', executable and 'x' or '')
367 367
368 368 def copyandhash(instream, outfile):
369 369 '''Read bytes from instream (iterable) and write them to outfile,
370 370 computing the SHA-1 hash of the data along the way. Return the hash.'''
371 371 hasher = hashlib.sha1('')
372 372 for data in instream:
373 373 hasher.update(data)
374 374 outfile.write(data)
375 375 return hex(hasher.digest())
376 376
377 377 def hashfile(file):
378 378 if not os.path.exists(file):
379 379 return ''
380 380 with open(file, 'rb') as fd:
381 381 return hexsha1(fd)
382 382
383 383 def getexecutable(filename):
384 384 mode = os.stat(filename).st_mode
385 385 return ((mode & stat.S_IXUSR) and
386 386 (mode & stat.S_IXGRP) and
387 387 (mode & stat.S_IXOTH))
388 388
389 389 def urljoin(first, second, *arg):
390 390 def join(left, right):
391 391 if not left.endswith('/'):
392 392 left += '/'
393 393 if right.startswith('/'):
394 394 right = right[1:]
395 395 return left + right
396 396
397 397 url = join(first, second)
398 398 for a in arg:
399 399 url = join(url, a)
400 400 return url
401 401
402 402 def hexsha1(fileobj):
403 403 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
404 404 object data"""
405 405 h = hashlib.sha1()
406 406 for chunk in util.filechunkiter(fileobj):
407 407 h.update(chunk)
408 408 return hex(h.digest())
409 409
410 410 def httpsendfile(ui, filename):
411 411 return httpconnection.httpsendfile(ui, filename, 'rb')
412 412
413 413 def unixpath(path):
414 414 '''Return a version of path normalized for use with the lfdirstate.'''
415 415 return util.pconvert(os.path.normpath(path))
416 416
417 417 def islfilesrepo(repo):
418 418 '''Return true if the repo is a largefile repo.'''
419 419 if ('largefiles' in repo.requirements and
420 420 any(shortnameslash in f[0] for f in repo.store.datafiles())):
421 421 return True
422 422
423 423 return any(openlfdirstate(repo.ui, repo, False))
424 424
425 425 class storeprotonotcapable(Exception):
426 426 def __init__(self, storetypes):
427 427 self.storetypes = storetypes
428 428
429 429 def getstandinsstate(repo):
430 430 standins = []
431 431 matcher = getstandinmatcher(repo)
432 432 wctx = repo[None]
433 433 for standin in repo.dirstate.walk(matcher, subrepos=[], unknown=False,
434 434 ignored=False):
435 435 lfile = splitstandin(standin)
436 436 try:
437 437 hash = readasstandin(wctx[standin])
438 438 except IOError:
439 439 hash = None
440 440 standins.append((lfile, hash))
441 441 return standins
442 442
443 443 def synclfdirstate(repo, lfdirstate, lfile, normallookup):
444 444 lfstandin = standin(lfile)
445 445 if lfstandin in repo.dirstate:
446 446 stat = repo.dirstate._map[lfstandin]
447 447 state, mtime = stat[0], stat[3]
448 448 else:
449 449 state, mtime = '?', -1
450 450 if state == 'n':
451 451 if (normallookup or mtime < 0 or
452 452 not repo.wvfs.exists(lfile)):
453 453 # state 'n' doesn't ensure 'clean' in this case
454 454 lfdirstate.normallookup(lfile)
455 455 else:
456 456 lfdirstate.normal(lfile)
457 457 elif state == 'm':
458 458 lfdirstate.normallookup(lfile)
459 459 elif state == 'r':
460 460 lfdirstate.remove(lfile)
461 461 elif state == 'a':
462 462 lfdirstate.add(lfile)
463 463 elif state == '?':
464 464 lfdirstate.drop(lfile)
465 465
466 466 def markcommitted(orig, ctx, node):
467 467 repo = ctx.repo()
468 468
469 469 orig(node)
470 470
471 471 # ATTENTION: "ctx.files()" may differ from "repo[node].files()"
472 472 # because files coming from the 2nd parent are omitted in the latter.
473 473 #
474 474 # The former should be used to get targets of "synclfdirstate",
475 475 # because such files:
476 476 # - are marked as "a" by "patch.patch()" (e.g. via transplant), and
477 477 # - have to be marked as "n" after commit, but
478 478 # - aren't listed in "repo[node].files()"
479 479
480 480 lfdirstate = openlfdirstate(repo.ui, repo)
481 481 for f in ctx.files():
482 482 lfile = splitstandin(f)
483 483 if lfile is not None:
484 484 synclfdirstate(repo, lfdirstate, lfile, False)
485 485 lfdirstate.write()
486 486
487 487 # As part of committing, copy all of the largefiles into the cache.
488 488 #
489 489 # Using "node" instead of "ctx" implies additional "repo[node]"
490 490 # lookup while copyalltostore(), but can omit redundant check for
491 491 # files comming from the 2nd parent, which should exist in store
492 492 # at merging.
493 493 copyalltostore(repo, node)
494 494
495 495 def getlfilestoupdate(oldstandins, newstandins):
496 496 changedstandins = set(oldstandins).symmetric_difference(set(newstandins))
497 497 filelist = []
498 498 for f in changedstandins:
499 499 if f[0] not in filelist:
500 500 filelist.append(f[0])
501 501 return filelist
502 502
503 503 def getlfilestoupload(repo, missing, addfunc):
504 progress = repo.ui.makeprogress(_('finding outgoing largefiles'),
505 unit=_('revisions'), total=len(missing))
504 506 for i, n in enumerate(missing):
505 repo.ui.progress(_('finding outgoing largefiles'), i,
506 unit=_('revisions'), total=len(missing))
507 progress.update(i)
507 508 parents = [p for p in repo[n].parents() if p != node.nullid]
508 509
509 510 oldlfstatus = repo.lfstatus
510 511 repo.lfstatus = False
511 512 try:
512 513 ctx = repo[n]
513 514 finally:
514 515 repo.lfstatus = oldlfstatus
515 516
516 517 files = set(ctx.files())
517 518 if len(parents) == 2:
518 519 mc = ctx.manifest()
519 520 mp1 = ctx.parents()[0].manifest()
520 521 mp2 = ctx.parents()[1].manifest()
521 522 for f in mp1:
522 523 if f not in mc:
523 524 files.add(f)
524 525 for f in mp2:
525 526 if f not in mc:
526 527 files.add(f)
527 528 for f in mc:
528 529 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
529 530 files.add(f)
530 531 for fn in files:
531 532 if isstandin(fn) and fn in ctx:
532 533 addfunc(fn, readasstandin(ctx[fn]))
533 repo.ui.progress(_('finding outgoing largefiles'), None)
534 progress.complete()
534 535
535 536 def updatestandinsbymatch(repo, match):
536 537 '''Update standins in the working directory according to specified match
537 538
538 539 This returns (possibly modified) ``match`` object to be used for
539 540 subsequent commit process.
540 541 '''
541 542
542 543 ui = repo.ui
543 544
544 545 # Case 1: user calls commit with no specific files or
545 546 # include/exclude patterns: refresh and commit all files that
546 547 # are "dirty".
547 548 if match is None or match.always():
548 549 # Spend a bit of time here to get a list of files we know
549 550 # are modified so we can compare only against those.
550 551 # It can cost a lot of time (several seconds)
551 552 # otherwise to update all standins if the largefiles are
552 553 # large.
553 554 lfdirstate = openlfdirstate(ui, repo)
554 555 dirtymatch = matchmod.always(repo.root, repo.getcwd())
555 556 unsure, s = lfdirstate.status(dirtymatch, subrepos=[], ignored=False,
556 557 clean=False, unknown=False)
557 558 modifiedfiles = unsure + s.modified + s.added + s.removed
558 559 lfiles = listlfiles(repo)
559 560 # this only loops through largefiles that exist (not
560 561 # removed/renamed)
561 562 for lfile in lfiles:
562 563 if lfile in modifiedfiles:
563 564 fstandin = standin(lfile)
564 565 if repo.wvfs.exists(fstandin):
565 566 # this handles the case where a rebase is being
566 567 # performed and the working copy is not updated
567 568 # yet.
568 569 if repo.wvfs.exists(lfile):
569 570 updatestandin(repo, lfile, fstandin)
570 571
571 572 return match
572 573
573 574 lfiles = listlfiles(repo)
574 575 match._files = repo._subdirlfs(match.files(), lfiles)
575 576
576 577 # Case 2: user calls commit with specified patterns: refresh
577 578 # any matching big files.
578 579 smatcher = composestandinmatcher(repo, match)
579 580 standins = repo.dirstate.walk(smatcher, subrepos=[], unknown=False,
580 581 ignored=False)
581 582
582 583 # No matching big files: get out of the way and pass control to
583 584 # the usual commit() method.
584 585 if not standins:
585 586 return match
586 587
587 588 # Refresh all matching big files. It's possible that the
588 589 # commit will end up failing, in which case the big files will
589 590 # stay refreshed. No harm done: the user modified them and
590 591 # asked to commit them, so sooner or later we're going to
591 592 # refresh the standins. Might as well leave them refreshed.
592 593 lfdirstate = openlfdirstate(ui, repo)
593 594 for fstandin in standins:
594 595 lfile = splitstandin(fstandin)
595 596 if lfdirstate[lfile] != 'r':
596 597 updatestandin(repo, lfile, fstandin)
597 598
598 599 # Cook up a new matcher that only matches regular files or
599 600 # standins corresponding to the big files requested by the
600 601 # user. Have to modify _files to prevent commit() from
601 602 # complaining "not tracked" for big files.
602 603 match = copy.copy(match)
603 604 origmatchfn = match.matchfn
604 605
605 606 # Check both the list of largefiles and the list of
606 607 # standins because if a largefile was removed, it
607 608 # won't be in the list of largefiles at this point
608 609 match._files += sorted(standins)
609 610
610 611 actualfiles = []
611 612 for f in match._files:
612 613 fstandin = standin(f)
613 614
614 615 # For largefiles, only one of the normal and standin should be
615 616 # committed (except if one of them is a remove). In the case of a
616 617 # standin removal, drop the normal file if it is unknown to dirstate.
617 618 # Thus, skip plain largefile names but keep the standin.
618 619 if f in lfiles or fstandin in standins:
619 620 if repo.dirstate[fstandin] != 'r':
620 621 if repo.dirstate[f] != 'r':
621 622 continue
622 623 elif repo.dirstate[f] == '?':
623 624 continue
624 625
625 626 actualfiles.append(f)
626 627 match._files = actualfiles
627 628
628 629 def matchfn(f):
629 630 if origmatchfn(f):
630 631 return f not in lfiles
631 632 else:
632 633 return f in standins
633 634
634 635 match.matchfn = matchfn
635 636
636 637 return match
637 638
638 639 class automatedcommithook(object):
639 640 '''Stateful hook to update standins at the 1st commit of resuming
640 641
641 642 For efficiency, updating standins in the working directory should
642 643 be avoided while automated committing (like rebase, transplant and
643 644 so on), because they should be updated before committing.
644 645
645 646 But the 1st commit of resuming automated committing (e.g. ``rebase
646 647 --continue``) should update them, because largefiles may be
647 648 modified manually.
648 649 '''
649 650 def __init__(self, resuming):
650 651 self.resuming = resuming
651 652
652 653 def __call__(self, repo, match):
653 654 if self.resuming:
654 655 self.resuming = False # avoids updating at subsequent commits
655 656 return updatestandinsbymatch(repo, match)
656 657 else:
657 658 return match
658 659
659 660 def getstatuswriter(ui, repo, forcibly=None):
660 661 '''Return the function to write largefiles specific status out
661 662
662 663 If ``forcibly`` is ``None``, this returns the last element of
663 664 ``repo._lfstatuswriters`` as "default" writer function.
664 665
665 666 Otherwise, this returns the function to always write out (or
666 667 ignore if ``not forcibly``) status.
667 668 '''
668 669 if forcibly is None and util.safehasattr(repo, '_largefilesenabled'):
669 670 return repo._lfstatuswriters[-1]
670 671 else:
671 672 if forcibly:
672 673 return ui.status # forcibly WRITE OUT
673 674 else:
674 675 return lambda *msg, **opts: None # forcibly IGNORE
General Comments 0
You need to be logged in to leave comments. Login now