##// END OF EJS Templates
largefiles: use the share source as the primary local store (issue4471)...
Matt Harbison -
r24631:2a3f2478 default
parent child Browse files
Show More
@@ -1,593 +1,606 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
11 11 import os
12 12 import platform
13 13 import shutil
14 14 import stat
15 15 import copy
16 16
17 17 from mercurial import dirstate, httpconnection, match as match_, util, scmutil
18 18 from mercurial.i18n import _
19 19 from mercurial import node
20 20
21 21 shortname = '.hglf'
22 22 shortnameslash = shortname + '/'
23 23 longname = 'largefiles'
24 24
25 25
26 26 # -- Private worker functions ------------------------------------------
27 27
28 28 def getminsize(ui, assumelfiles, opt, default=10):
29 29 lfsize = opt
30 30 if not lfsize and assumelfiles:
31 31 lfsize = ui.config(longname, 'minsize', default=default)
32 32 if lfsize:
33 33 try:
34 34 lfsize = float(lfsize)
35 35 except ValueError:
36 36 raise util.Abort(_('largefiles: size must be number (not %s)\n')
37 37 % lfsize)
38 38 if lfsize is None:
39 39 raise util.Abort(_('minimum size for largefiles must be specified'))
40 40 return lfsize
41 41
42 42 def link(src, dest):
43 43 util.makedirs(os.path.dirname(dest))
44 44 try:
45 45 util.oslink(src, dest)
46 46 except OSError:
47 47 # if hardlinks fail, fallback on atomic copy
48 48 dst = util.atomictempfile(dest)
49 49 for chunk in util.filechunkiter(open(src, 'rb')):
50 50 dst.write(chunk)
51 51 dst.close()
52 52 os.chmod(dest, os.stat(src).st_mode)
53 53
54 54 def usercachepath(ui, hash):
55 55 path = ui.configpath(longname, 'usercache', None)
56 56 if path:
57 57 path = os.path.join(path, hash)
58 58 else:
59 59 if os.name == 'nt':
60 60 appdata = os.getenv('LOCALAPPDATA', os.getenv('APPDATA'))
61 61 if appdata:
62 62 path = os.path.join(appdata, longname, hash)
63 63 elif platform.system() == 'Darwin':
64 64 home = os.getenv('HOME')
65 65 if home:
66 66 path = os.path.join(home, 'Library', 'Caches',
67 67 longname, hash)
68 68 elif os.name == 'posix':
69 69 path = os.getenv('XDG_CACHE_HOME')
70 70 if path:
71 71 path = os.path.join(path, longname, hash)
72 72 else:
73 73 home = os.getenv('HOME')
74 74 if home:
75 75 path = os.path.join(home, '.cache', longname, hash)
76 76 else:
77 77 raise util.Abort(_('unknown operating system: %s\n') % os.name)
78 78 return path
79 79
80 80 def inusercache(ui, hash):
81 81 path = usercachepath(ui, hash)
82 82 return path and os.path.exists(path)
83 83
84 84 def findfile(repo, hash):
85 if instore(repo, hash):
85 path, exists = findstorepath(repo, hash)
86 if exists:
86 87 repo.ui.note(_('found %s in store\n') % hash)
87 return storepath(repo, hash)
88 return path
88 89 elif inusercache(repo.ui, hash):
89 90 repo.ui.note(_('found %s in system cache\n') % hash)
90 91 path = storepath(repo, hash)
91 92 link(usercachepath(repo.ui, hash), path)
92 93 return path
93 94 return None
94 95
95 96 class largefilesdirstate(dirstate.dirstate):
96 97 def __getitem__(self, key):
97 98 return super(largefilesdirstate, self).__getitem__(unixpath(key))
98 99 def normal(self, f):
99 100 return super(largefilesdirstate, self).normal(unixpath(f))
100 101 def remove(self, f):
101 102 return super(largefilesdirstate, self).remove(unixpath(f))
102 103 def add(self, f):
103 104 return super(largefilesdirstate, self).add(unixpath(f))
104 105 def drop(self, f):
105 106 return super(largefilesdirstate, self).drop(unixpath(f))
106 107 def forget(self, f):
107 108 return super(largefilesdirstate, self).forget(unixpath(f))
108 109 def normallookup(self, f):
109 110 return super(largefilesdirstate, self).normallookup(unixpath(f))
110 111 def _ignore(self, f):
111 112 return False
112 113
113 114 def openlfdirstate(ui, repo, create=True):
114 115 '''
115 116 Return a dirstate object that tracks largefiles: i.e. its root is
116 117 the repo root, but it is saved in .hg/largefiles/dirstate.
117 118 '''
118 119 lfstoredir = repo.join(longname)
119 120 opener = scmutil.opener(lfstoredir)
120 121 lfdirstate = largefilesdirstate(opener, ui, repo.root,
121 122 repo.dirstate._validate)
122 123
123 124 # If the largefiles dirstate does not exist, populate and create
124 125 # it. This ensures that we create it on the first meaningful
125 126 # largefiles operation in a new clone.
126 127 if create and not os.path.exists(os.path.join(lfstoredir, 'dirstate')):
127 128 matcher = getstandinmatcher(repo)
128 129 standins = repo.dirstate.walk(matcher, [], False, False)
129 130
130 131 if len(standins) > 0:
131 132 util.makedirs(lfstoredir)
132 133
133 134 for standin in standins:
134 135 lfile = splitstandin(standin)
135 136 lfdirstate.normallookup(lfile)
136 137 return lfdirstate
137 138
138 139 def lfdirstatestatus(lfdirstate, repo):
139 140 wctx = repo['.']
140 141 match = match_.always(repo.root, repo.getcwd())
141 142 unsure, s = lfdirstate.status(match, [], False, False, False)
142 143 modified, clean = s.modified, s.clean
143 144 for lfile in unsure:
144 145 try:
145 146 fctx = wctx[standin(lfile)]
146 147 except LookupError:
147 148 fctx = None
148 149 if not fctx or fctx.data().strip() != hashfile(repo.wjoin(lfile)):
149 150 modified.append(lfile)
150 151 else:
151 152 clean.append(lfile)
152 153 lfdirstate.normal(lfile)
153 154 return s
154 155
155 156 def listlfiles(repo, rev=None, matcher=None):
156 157 '''return a list of largefiles in the working copy or the
157 158 specified changeset'''
158 159
159 160 if matcher is None:
160 161 matcher = getstandinmatcher(repo)
161 162
162 163 # ignore unknown files in working directory
163 164 return [splitstandin(f)
164 165 for f in repo[rev].walk(matcher)
165 166 if rev is not None or repo.dirstate[f] != '?']
166 167
167 def instore(repo, hash):
168 return os.path.exists(storepath(repo, hash))
168 def instore(repo, hash, forcelocal=False):
169 return os.path.exists(storepath(repo, hash, forcelocal))
169 170
170 def storepath(repo, hash):
171 def storepath(repo, hash, forcelocal=False):
172 if not forcelocal and repo.shared():
173 return repo.vfs.reljoin(repo.sharedpath, longname, hash)
171 174 return repo.join(longname, hash)
172 175
173 176 def findstorepath(repo, hash):
174 177 '''Search through the local store path(s) to find the file for the given
175 178 hash. If the file is not found, its path in the primary store is returned.
176 179 The return value is a tuple of (path, exists(path)).
177 180 '''
178 return (storepath(repo, hash), instore(repo, hash))
181 # For shared repos, the primary store is in the share source. But for
182 # backward compatibility, force a lookup in the local store if it wasn't
183 # found in the share source.
184 path = storepath(repo, hash, False)
185
186 if instore(repo, hash):
187 return (path, True)
188 elif repo.shared() and instore(repo, hash, True):
189 return storepath(repo, hash, True)
190
191 return (path, False)
179 192
180 193 def copyfromcache(repo, hash, filename):
181 194 '''Copy the specified largefile from the repo or system cache to
182 195 filename in the repository. Return true on success or false if the
183 196 file was not found in either cache (which should not happened:
184 197 this is meant to be called only after ensuring that the needed
185 198 largefile exists in the cache).'''
186 199 path = findfile(repo, hash)
187 200 if path is None:
188 201 return False
189 202 util.makedirs(os.path.dirname(repo.wjoin(filename)))
190 203 # The write may fail before the file is fully written, but we
191 204 # don't use atomic writes in the working copy.
192 205 shutil.copy(path, repo.wjoin(filename))
193 206 return True
194 207
195 208 def copytostore(repo, rev, file, uploaded=False):
196 209 hash = readstandin(repo, file, rev)
197 210 if instore(repo, hash):
198 211 return
199 212 copytostoreabsolute(repo, repo.wjoin(file), hash)
200 213
201 214 def copyalltostore(repo, node):
202 215 '''Copy all largefiles in a given revision to the store'''
203 216
204 217 ctx = repo[node]
205 218 for filename in ctx.files():
206 219 if isstandin(filename) and filename in ctx.manifest():
207 220 realfile = splitstandin(filename)
208 221 copytostore(repo, ctx.node(), realfile)
209 222
210 223
211 224 def copytostoreabsolute(repo, file, hash):
212 225 if inusercache(repo.ui, hash):
213 226 link(usercachepath(repo.ui, hash), storepath(repo, hash))
214 227 else:
215 228 util.makedirs(os.path.dirname(storepath(repo, hash)))
216 229 dst = util.atomictempfile(storepath(repo, hash),
217 230 createmode=repo.store.createmode)
218 231 for chunk in util.filechunkiter(open(file, 'rb')):
219 232 dst.write(chunk)
220 233 dst.close()
221 234 linktousercache(repo, hash)
222 235
223 236 def linktousercache(repo, hash):
224 237 path = usercachepath(repo.ui, hash)
225 238 if path:
226 239 link(storepath(repo, hash), path)
227 240
228 241 def getstandinmatcher(repo, pats=[], opts={}):
229 242 '''Return a match object that applies pats to the standin directory'''
230 243 standindir = repo.wjoin(shortname)
231 244 if pats:
232 245 pats = [os.path.join(standindir, pat) for pat in pats]
233 246 else:
234 247 # no patterns: relative to repo root
235 248 pats = [standindir]
236 249 # no warnings about missing files or directories
237 250 match = scmutil.match(repo[None], pats, opts)
238 251 match.bad = lambda f, msg: None
239 252 return match
240 253
241 254 def composestandinmatcher(repo, rmatcher):
242 255 '''Return a matcher that accepts standins corresponding to the
243 256 files accepted by rmatcher. Pass the list of files in the matcher
244 257 as the paths specified by the user.'''
245 258 smatcher = getstandinmatcher(repo, rmatcher.files())
246 259 isstandin = smatcher.matchfn
247 260 def composedmatchfn(f):
248 261 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
249 262 smatcher.matchfn = composedmatchfn
250 263
251 264 return smatcher
252 265
253 266 def standin(filename):
254 267 '''Return the repo-relative path to the standin for the specified big
255 268 file.'''
256 269 # Notes:
257 270 # 1) Some callers want an absolute path, but for instance addlargefiles
258 271 # needs it repo-relative so it can be passed to repo[None].add(). So
259 272 # leave it up to the caller to use repo.wjoin() to get an absolute path.
260 273 # 2) Join with '/' because that's what dirstate always uses, even on
261 274 # Windows. Change existing separator to '/' first in case we are
262 275 # passed filenames from an external source (like the command line).
263 276 return shortnameslash + util.pconvert(filename)
264 277
265 278 def isstandin(filename):
266 279 '''Return true if filename is a big file standin. filename must be
267 280 in Mercurial's internal form (slash-separated).'''
268 281 return filename.startswith(shortnameslash)
269 282
270 283 def splitstandin(filename):
271 284 # Split on / because that's what dirstate always uses, even on Windows.
272 285 # Change local separator to / first just in case we are passed filenames
273 286 # from an external source (like the command line).
274 287 bits = util.pconvert(filename).split('/', 1)
275 288 if len(bits) == 2 and bits[0] == shortname:
276 289 return bits[1]
277 290 else:
278 291 return None
279 292
280 293 def updatestandin(repo, standin):
281 294 file = repo.wjoin(splitstandin(standin))
282 295 if os.path.exists(file):
283 296 hash = hashfile(file)
284 297 executable = getexecutable(file)
285 298 writestandin(repo, standin, hash, executable)
286 299
287 300 def readstandin(repo, filename, node=None):
288 301 '''read hex hash from standin for filename at given node, or working
289 302 directory if no node is given'''
290 303 return repo[node][standin(filename)].data().strip()
291 304
292 305 def writestandin(repo, standin, hash, executable):
293 306 '''write hash to <repo.root>/<standin>'''
294 307 repo.wwrite(standin, hash + '\n', executable and 'x' or '')
295 308
296 309 def copyandhash(instream, outfile):
297 310 '''Read bytes from instream (iterable) and write them to outfile,
298 311 computing the SHA-1 hash of the data along the way. Return the hash.'''
299 312 hasher = util.sha1('')
300 313 for data in instream:
301 314 hasher.update(data)
302 315 outfile.write(data)
303 316 return hasher.hexdigest()
304 317
305 318 def hashrepofile(repo, file):
306 319 return hashfile(repo.wjoin(file))
307 320
308 321 def hashfile(file):
309 322 if not os.path.exists(file):
310 323 return ''
311 324 hasher = util.sha1('')
312 325 fd = open(file, 'rb')
313 326 for data in util.filechunkiter(fd, 128 * 1024):
314 327 hasher.update(data)
315 328 fd.close()
316 329 return hasher.hexdigest()
317 330
318 331 def getexecutable(filename):
319 332 mode = os.stat(filename).st_mode
320 333 return ((mode & stat.S_IXUSR) and
321 334 (mode & stat.S_IXGRP) and
322 335 (mode & stat.S_IXOTH))
323 336
324 337 def urljoin(first, second, *arg):
325 338 def join(left, right):
326 339 if not left.endswith('/'):
327 340 left += '/'
328 341 if right.startswith('/'):
329 342 right = right[1:]
330 343 return left + right
331 344
332 345 url = join(first, second)
333 346 for a in arg:
334 347 url = join(url, a)
335 348 return url
336 349
337 350 def hexsha1(data):
338 351 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
339 352 object data"""
340 353 h = util.sha1()
341 354 for chunk in util.filechunkiter(data):
342 355 h.update(chunk)
343 356 return h.hexdigest()
344 357
345 358 def httpsendfile(ui, filename):
346 359 return httpconnection.httpsendfile(ui, filename, 'rb')
347 360
348 361 def unixpath(path):
349 362 '''Return a version of path normalized for use with the lfdirstate.'''
350 363 return util.pconvert(os.path.normpath(path))
351 364
352 365 def islfilesrepo(repo):
353 366 if ('largefiles' in repo.requirements and
354 367 util.any(shortnameslash in f[0] for f in repo.store.datafiles())):
355 368 return True
356 369
357 370 return util.any(openlfdirstate(repo.ui, repo, False))
358 371
359 372 class storeprotonotcapable(Exception):
360 373 def __init__(self, storetypes):
361 374 self.storetypes = storetypes
362 375
363 376 def getstandinsstate(repo):
364 377 standins = []
365 378 matcher = getstandinmatcher(repo)
366 379 for standin in repo.dirstate.walk(matcher, [], False, False):
367 380 lfile = splitstandin(standin)
368 381 try:
369 382 hash = readstandin(repo, lfile)
370 383 except IOError:
371 384 hash = None
372 385 standins.append((lfile, hash))
373 386 return standins
374 387
375 388 def synclfdirstate(repo, lfdirstate, lfile, normallookup):
376 389 lfstandin = standin(lfile)
377 390 if lfstandin in repo.dirstate:
378 391 stat = repo.dirstate._map[lfstandin]
379 392 state, mtime = stat[0], stat[3]
380 393 else:
381 394 state, mtime = '?', -1
382 395 if state == 'n':
383 396 if normallookup or mtime < 0:
384 397 # state 'n' doesn't ensure 'clean' in this case
385 398 lfdirstate.normallookup(lfile)
386 399 else:
387 400 lfdirstate.normal(lfile)
388 401 elif state == 'm':
389 402 lfdirstate.normallookup(lfile)
390 403 elif state == 'r':
391 404 lfdirstate.remove(lfile)
392 405 elif state == 'a':
393 406 lfdirstate.add(lfile)
394 407 elif state == '?':
395 408 lfdirstate.drop(lfile)
396 409
397 410 def markcommitted(orig, ctx, node):
398 411 repo = ctx.repo()
399 412
400 413 orig(node)
401 414
402 415 # ATTENTION: "ctx.files()" may differ from "repo[node].files()"
403 416 # because files coming from the 2nd parent are omitted in the latter.
404 417 #
405 418 # The former should be used to get targets of "synclfdirstate",
406 419 # because such files:
407 420 # - are marked as "a" by "patch.patch()" (e.g. via transplant), and
408 421 # - have to be marked as "n" after commit, but
409 422 # - aren't listed in "repo[node].files()"
410 423
411 424 lfdirstate = openlfdirstate(repo.ui, repo)
412 425 for f in ctx.files():
413 426 if isstandin(f):
414 427 lfile = splitstandin(f)
415 428 synclfdirstate(repo, lfdirstate, lfile, False)
416 429 lfdirstate.write()
417 430
418 431 # As part of committing, copy all of the largefiles into the cache.
419 432 copyalltostore(repo, node)
420 433
421 434 def getlfilestoupdate(oldstandins, newstandins):
422 435 changedstandins = set(oldstandins).symmetric_difference(set(newstandins))
423 436 filelist = []
424 437 for f in changedstandins:
425 438 if f[0] not in filelist:
426 439 filelist.append(f[0])
427 440 return filelist
428 441
429 442 def getlfilestoupload(repo, missing, addfunc):
430 443 for i, n in enumerate(missing):
431 444 repo.ui.progress(_('finding outgoing largefiles'), i,
432 445 unit=_('revision'), total=len(missing))
433 446 parents = [p for p in repo.changelog.parents(n) if p != node.nullid]
434 447
435 448 oldlfstatus = repo.lfstatus
436 449 repo.lfstatus = False
437 450 try:
438 451 ctx = repo[n]
439 452 finally:
440 453 repo.lfstatus = oldlfstatus
441 454
442 455 files = set(ctx.files())
443 456 if len(parents) == 2:
444 457 mc = ctx.manifest()
445 458 mp1 = ctx.parents()[0].manifest()
446 459 mp2 = ctx.parents()[1].manifest()
447 460 for f in mp1:
448 461 if f not in mc:
449 462 files.add(f)
450 463 for f in mp2:
451 464 if f not in mc:
452 465 files.add(f)
453 466 for f in mc:
454 467 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
455 468 files.add(f)
456 469 for fn in files:
457 470 if isstandin(fn) and fn in ctx:
458 471 addfunc(fn, ctx[fn].data().strip())
459 472 repo.ui.progress(_('finding outgoing largefiles'), None)
460 473
461 474 def updatestandinsbymatch(repo, match):
462 475 '''Update standins in the working directory according to specified match
463 476
464 477 This returns (possibly modified) ``match`` object to be used for
465 478 subsequent commit process.
466 479 '''
467 480
468 481 ui = repo.ui
469 482
470 483 # Case 1: user calls commit with no specific files or
471 484 # include/exclude patterns: refresh and commit all files that
472 485 # are "dirty".
473 486 if match is None or match.always():
474 487 # Spend a bit of time here to get a list of files we know
475 488 # are modified so we can compare only against those.
476 489 # It can cost a lot of time (several seconds)
477 490 # otherwise to update all standins if the largefiles are
478 491 # large.
479 492 lfdirstate = openlfdirstate(ui, repo)
480 493 dirtymatch = match_.always(repo.root, repo.getcwd())
481 494 unsure, s = lfdirstate.status(dirtymatch, [], False, False,
482 495 False)
483 496 modifiedfiles = unsure + s.modified + s.added + s.removed
484 497 lfiles = listlfiles(repo)
485 498 # this only loops through largefiles that exist (not
486 499 # removed/renamed)
487 500 for lfile in lfiles:
488 501 if lfile in modifiedfiles:
489 502 if os.path.exists(
490 503 repo.wjoin(standin(lfile))):
491 504 # this handles the case where a rebase is being
492 505 # performed and the working copy is not updated
493 506 # yet.
494 507 if os.path.exists(repo.wjoin(lfile)):
495 508 updatestandin(repo,
496 509 standin(lfile))
497 510
498 511 return match
499 512
500 513 lfiles = listlfiles(repo)
501 514 match._files = repo._subdirlfs(match.files(), lfiles)
502 515
503 516 # Case 2: user calls commit with specified patterns: refresh
504 517 # any matching big files.
505 518 smatcher = composestandinmatcher(repo, match)
506 519 standins = repo.dirstate.walk(smatcher, [], False, False)
507 520
508 521 # No matching big files: get out of the way and pass control to
509 522 # the usual commit() method.
510 523 if not standins:
511 524 return match
512 525
513 526 # Refresh all matching big files. It's possible that the
514 527 # commit will end up failing, in which case the big files will
515 528 # stay refreshed. No harm done: the user modified them and
516 529 # asked to commit them, so sooner or later we're going to
517 530 # refresh the standins. Might as well leave them refreshed.
518 531 lfdirstate = openlfdirstate(ui, repo)
519 532 for fstandin in standins:
520 533 lfile = splitstandin(fstandin)
521 534 if lfdirstate[lfile] != 'r':
522 535 updatestandin(repo, fstandin)
523 536
524 537 # Cook up a new matcher that only matches regular files or
525 538 # standins corresponding to the big files requested by the
526 539 # user. Have to modify _files to prevent commit() from
527 540 # complaining "not tracked" for big files.
528 541 match = copy.copy(match)
529 542 origmatchfn = match.matchfn
530 543
531 544 # Check both the list of largefiles and the list of
532 545 # standins because if a largefile was removed, it
533 546 # won't be in the list of largefiles at this point
534 547 match._files += sorted(standins)
535 548
536 549 actualfiles = []
537 550 for f in match._files:
538 551 fstandin = standin(f)
539 552
540 553 # ignore known largefiles and standins
541 554 if f in lfiles or fstandin in standins:
542 555 continue
543 556
544 557 actualfiles.append(f)
545 558 match._files = actualfiles
546 559
547 560 def matchfn(f):
548 561 if origmatchfn(f):
549 562 return f not in lfiles
550 563 else:
551 564 return f in standins
552 565
553 566 match.matchfn = matchfn
554 567
555 568 return match
556 569
557 570 class automatedcommithook(object):
558 571 '''Stateful hook to update standins at the 1st commit of resuming
559 572
560 573 For efficiency, updating standins in the working directory should
561 574 be avoided while automated committing (like rebase, transplant and
562 575 so on), because they should be updated before committing.
563 576
564 577 But the 1st commit of resuming automated committing (e.g. ``rebase
565 578 --continue``) should update them, because largefiles may be
566 579 modified manually.
567 580 '''
568 581 def __init__(self, resuming):
569 582 self.resuming = resuming
570 583
571 584 def __call__(self, repo, match):
572 585 if self.resuming:
573 586 self.resuming = False # avoids updating at subsequent commits
574 587 return updatestandinsbymatch(repo, match)
575 588 else:
576 589 return match
577 590
578 591 def getstatuswriter(ui, repo, forcibly=None):
579 592 '''Return the function to write largefiles specific status out
580 593
581 594 If ``forcibly`` is ``None``, this returns the last element of
582 595 ``repo._lfstatuswriters`` as "default" writer function.
583 596
584 597 Otherwise, this returns the function to always write out (or
585 598 ignore if ``not forcibly``) status.
586 599 '''
587 600 if forcibly is None and util.safehasattr(repo, '_largefilesenabled'):
588 601 return repo._lfstatuswriters[-1]
589 602 else:
590 603 if forcibly:
591 604 return ui.status # forcibly WRITE OUT
592 605 else:
593 606 return lambda *msg, **opts: None # forcibly IGNORE
@@ -1,155 +1,181 b''
1 1 Create user cache directory
2 2
3 3 $ USERCACHE=`pwd`/cache; export USERCACHE
4 4 $ cat <<EOF >> ${HGRCPATH}
5 5 > [extensions]
6 6 > hgext.largefiles=
7 7 > [largefiles]
8 8 > usercache=${USERCACHE}
9 9 > EOF
10 10 $ mkdir -p ${USERCACHE}
11 11
12 12 Create source repo, and commit adding largefile.
13 13
14 14 $ hg init src
15 15 $ cd src
16 16 $ echo large > large
17 17 $ hg add --large large
18 18 $ hg commit -m 'add largefile'
19 19 $ hg rm large
20 20 $ hg commit -m 'branchhead without largefile'
21 21 $ hg up -qr 0
22 22 $ cd ..
23 23
24 24 Discard all cached largefiles in USERCACHE
25 25
26 26 $ rm -rf ${USERCACHE}
27 27
28 28 Create mirror repo, and pull from source without largefile:
29 29 "pull" is used instead of "clone" for suppression of (1) updating to
30 30 tip (= caching largefile from source repo), and (2) recording source
31 31 repo as "default" path in .hg/hgrc.
32 32
33 33 $ hg init mirror
34 34 $ cd mirror
35 35 $ hg pull ../src
36 36 pulling from ../src
37 37 requesting all changes
38 38 adding changesets
39 39 adding manifests
40 40 adding file changes
41 41 added 2 changesets with 1 changes to 1 files
42 42 (run 'hg update' to get a working copy)
43 43
44 44 Update working directory to "tip", which requires largefile("large"),
45 45 but there is no cache file for it. So, hg must treat it as
46 46 "missing"(!) file.
47 47
48 48 $ hg update -r0
49 49 getting changed largefiles
50 50 large: largefile 7f7097b041ccf68cc5561e9600da4655d21c6d18 not available from file:/*/$TESTTMP/mirror (glob)
51 51 0 largefiles updated, 0 removed
52 52 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
53 53 $ hg status
54 54 ! large
55 55
56 56 Update working directory to null: this cleanup .hg/largefiles/dirstate
57 57
58 58 $ hg update null
59 59 getting changed largefiles
60 60 0 largefiles updated, 0 removed
61 61 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
62 62
63 63 Update working directory to tip, again.
64 64
65 65 $ hg update -r0
66 66 getting changed largefiles
67 67 large: largefile 7f7097b041ccf68cc5561e9600da4655d21c6d18 not available from file:/*/$TESTTMP/mirror (glob)
68 68 0 largefiles updated, 0 removed
69 69 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
70 70 $ hg status
71 71 ! large
72 72 $ cd ..
73 73
74 74 Verify that largefiles from pulled branchheads are fetched, also to an empty repo
75 75
76 76 $ hg init mirror2
77 77 $ hg -R mirror2 pull src -r0
78 78 pulling from src
79 79 adding changesets
80 80 adding manifests
81 81 adding file changes
82 82 added 1 changesets with 1 changes to 1 files
83 83 (run 'hg update' to get a working copy)
84 84
85 85 #if unix-permissions
86 86
87 87 Portable way to print file permissions:
88 88
89 89 $ cat > ls-l.py <<EOF
90 90 > #!/usr/bin/env python
91 91 > import sys, os
92 92 > path = sys.argv[1]
93 93 > print '%03o' % (os.lstat(path).st_mode & 0777)
94 94 > EOF
95 95 $ chmod +x ls-l.py
96 96
97 97 Test that files in .hg/largefiles inherit mode from .hg/store, not
98 98 from file in working copy:
99 99
100 100 $ cd src
101 101 $ chmod 750 .hg/store
102 102 $ chmod 660 large
103 103 $ echo change >> large
104 104 $ hg commit -m change
105 105 created new head
106 106 $ ../ls-l.py .hg/largefiles/e151b474069de4ca6898f67ce2f2a7263adf8fea
107 107 640
108 108
109 109 Test permission of with files in .hg/largefiles created by update:
110 110
111 111 $ cd ../mirror
112 112 $ rm -r "$USERCACHE" .hg/largefiles # avoid links
113 113 $ chmod 750 .hg/store
114 114 $ hg pull ../src --update -q
115 115 $ ../ls-l.py .hg/largefiles/e151b474069de4ca6898f67ce2f2a7263adf8fea
116 116 640
117 117
118 118 Test permission of files created by push:
119 119
120 120 $ hg serve -R ../src -d -p $HGPORT --pid-file hg.pid \
121 121 > --config "web.allow_push=*" --config web.push_ssl=no
122 122 $ cat hg.pid >> $DAEMON_PIDS
123 123
124 124 $ echo change >> large
125 125 $ hg commit -m change
126 126
127 127 $ rm -r "$USERCACHE"
128 128
129 129 $ hg push -q http://localhost:$HGPORT/
130 130
131 131 $ ../ls-l.py ../src/.hg/largefiles/b734e14a0971e370408ab9bce8d56d8485e368a9
132 132 640
133 133
134 134 $ cd ..
135 135
136 136 #endif
137 137
138 138 Test issue 4053 (remove --after on a deleted, uncommitted file shouldn't say
139 139 it is missing, but a remove on a nonexistent unknown file still should. Same
140 140 for a forget.)
141 141
142 142 $ cd src
143 143 $ touch x
144 144 $ hg add x
145 145 $ mv x y
146 146 $ hg remove -A x y ENOENT
147 147 ENOENT: * (glob)
148 148 not removing y: file is untracked
149 149 [1]
150 150 $ hg add y
151 151 $ mv y z
152 152 $ hg forget y z ENOENT
153 153 ENOENT: * (glob)
154 154 not removing z: file is already untracked
155 155 [1]
156
157 Largefiles are accessible from the share's store
158 $ cd ..
159 $ hg share -q src share_dst --config extensions.share=
160 $ hg -R share_dst update -r0
161 getting changed largefiles
162 1 largefiles updated, 0 removed
163 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
164
165 $ echo modified > share_dst/large
166 $ hg -R share_dst ci -m modified
167 created new head
168
169 Only dirstate is in the local store for the share, and the largefile is in the
170 share source's local store. Avoid the extra largefiles added in the unix
171 conditional above.
172 $ hash=`hg -R share_dst cat share_dst/.hglf/large`
173 $ echo $hash
174 e2fb5f2139d086ded2cb600d5a91a196e76bf020
175
176 $ find share_dst/.hg/largefiles/* | sort
177 share_dst/.hg/largefiles/dirstate
178
179 $ find src/.hg/largefiles/* | egrep "(dirstate|$hash)" | sort
180 src/.hg/largefiles/dirstate
181 src/.hg/largefiles/e2fb5f2139d086ded2cb600d5a91a196e76bf020
General Comments 0
You need to be logged in to leave comments. Login now