##// END OF EJS Templates
lfs: respect narrowmatcher when testing to add 'lfs' requirement (issue5794)...
Matt Harbison -
r37156:4d63f3bc default
parent child Browse files
Show More
@@ -1,387 +1,389 b''
1 1 # lfs - hash-preserving large file support using Git-LFS protocol
2 2 #
3 3 # Copyright 2017 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """lfs - large file support (EXPERIMENTAL)
9 9
10 10 This extension allows large files to be tracked outside of the normal
11 11 repository storage and stored on a centralized server, similar to the
12 12 ``largefiles`` extension. The ``git-lfs`` protocol is used when
13 13 communicating with the server, so existing git infrastructure can be
14 14 harnessed. Even though the files are stored outside of the repository,
15 15 they are still integrity checked in the same manner as normal files.
16 16
17 17 The files stored outside of the repository are downloaded on demand,
18 18 which reduces the time to clone, and possibly the local disk usage.
19 19 This changes fundamental workflows in a DVCS, so careful thought
20 20 should be given before deploying it. :hg:`convert` can be used to
21 21 convert LFS repositories to normal repositories that no longer
22 22 require this extension, and do so without changing the commit hashes.
23 23 This allows the extension to be disabled if the centralized workflow
24 24 becomes burdensome. However, the pre and post convert clones will
25 25 not be able to communicate with each other unless the extension is
26 26 enabled on both.
27 27
28 28 To start a new repository, or to add LFS files to an existing one, just
29 29 create an ``.hglfs`` file as described below in the root directory of
30 30 the repository. Typically, this file should be put under version
31 31 control, so that the settings will propagate to other repositories with
32 32 push and pull. During any commit, Mercurial will consult this file to
33 33 determine if an added or modified file should be stored externally. The
34 34 type of storage depends on the characteristics of the file at each
35 35 commit. A file that is near a size threshold may switch back and forth
36 36 between LFS and normal storage, as needed.
37 37
38 38 Alternately, both normal repositories and largefile controlled
39 39 repositories can be converted to LFS by using :hg:`convert` and the
40 40 ``lfs.track`` config option described below. The ``.hglfs`` file
41 41 should then be created and added, to control subsequent LFS selection.
42 42 The hashes are also unchanged in this case. The LFS and non-LFS
43 43 repositories can be distinguished because the LFS repository will
44 44 abort any command if this extension is disabled.
45 45
46 46 Committed LFS files are held locally, until the repository is pushed.
47 47 Prior to pushing the normal repository data, the LFS files that are
48 48 tracked by the outgoing commits are automatically uploaded to the
49 49 configured central server. No LFS files are transferred on
50 50 :hg:`pull` or :hg:`clone`. Instead, the files are downloaded on
51 51 demand as they need to be read, if a cached copy cannot be found
52 52 locally. Both committing and downloading an LFS file will link the
53 53 file to a usercache, to speed up future access. See the `usercache`
54 54 config setting described below.
55 55
56 56 .hglfs::
57 57
58 58 The extension reads its configuration from a versioned ``.hglfs``
59 59 configuration file found in the root of the working directory. The
60 60 ``.hglfs`` file uses the same syntax as all other Mercurial
61 61 configuration files. It uses a single section, ``[track]``.
62 62
63 63 The ``[track]`` section specifies which files are stored as LFS (or
64 64 not). Each line is keyed by a file pattern, with a predicate value.
65 65 The first file pattern match is used, so put more specific patterns
66 66 first. The available predicates are ``all()``, ``none()``, and
67 67 ``size()``. See "hg help filesets.size" for the latter.
68 68
69 69 Example versioned ``.hglfs`` file::
70 70
71 71 [track]
72 72 # No Makefile or python file, anywhere, will be LFS
73 73 **Makefile = none()
74 74 **.py = none()
75 75
76 76 **.zip = all()
77 77 **.exe = size(">1MB")
78 78
79 79 # Catchall for everything not matched above
80 80 ** = size(">10MB")
81 81
82 82 Configs::
83 83
84 84 [lfs]
85 85 # Remote endpoint. Multiple protocols are supported:
86 86 # - http(s)://user:pass@example.com/path
87 87 # git-lfs endpoint
88 88 # - file:///tmp/path
89 89 # local filesystem, usually for testing
90 90 # if unset, lfs will prompt setting this when it must use this value.
91 91 # (default: unset)
92 92 url = https://example.com/repo.git/info/lfs
93 93
94 94 # Which files to track in LFS. Path tests are "**.extname" for file
95 95 # extensions, and "path:under/some/directory" for path prefix. Both
96 96 # are relative to the repository root.
97 97 # File size can be tested with the "size()" fileset, and tests can be
98 98 # joined with fileset operators. (See "hg help filesets.operators".)
99 99 #
100 100 # Some examples:
101 101 # - all() # everything
102 102 # - none() # nothing
103 103 # - size(">20MB") # larger than 20MB
104 104 # - !**.txt # anything not a *.txt file
105 105 # - **.zip | **.tar.gz | **.7z # some types of compressed files
106 106 # - path:bin # files under "bin" in the project root
107 107 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
108 108 # | (path:bin & !path:/bin/README) | size(">1GB")
109 109 # (default: none())
110 110 #
111 111 # This is ignored if there is a tracked '.hglfs' file, and this setting
112 112 # will eventually be deprecated and removed.
113 113 track = size(">10M")
114 114
115 115 # how many times to retry before giving up on transferring an object
116 116 retry = 5
117 117
118 118 # the local directory to store lfs files for sharing across local clones.
119 119 # If not set, the cache is located in an OS specific cache location.
120 120 usercache = /path/to/global/cache
121 121 """
122 122
123 123 from __future__ import absolute_import
124 124
125 125 from mercurial.i18n import _
126 126
127 127 from mercurial import (
128 128 bundle2,
129 129 changegroup,
130 130 cmdutil,
131 131 config,
132 132 context,
133 133 error,
134 134 exchange,
135 135 extensions,
136 136 filelog,
137 137 fileset,
138 138 hg,
139 139 localrepo,
140 140 minifileset,
141 141 node,
142 142 pycompat,
143 143 registrar,
144 144 revlog,
145 145 scmutil,
146 146 templateutil,
147 147 upgrade,
148 148 util,
149 149 vfs as vfsmod,
150 150 wireproto,
151 151 )
152 152
153 153 from . import (
154 154 blobstore,
155 155 wrapper,
156 156 )
157 157
158 158 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
159 159 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
160 160 # be specifying the version(s) of Mercurial they are tested with, or
161 161 # leave the attribute unspecified.
162 162 testedwith = 'ships-with-hg-core'
163 163
164 164 configtable = {}
165 165 configitem = registrar.configitem(configtable)
166 166
167 167 configitem('experimental', 'lfs.user-agent',
168 168 default=None,
169 169 )
170 170 configitem('experimental', 'lfs.worker-enable',
171 171 default=False,
172 172 )
173 173
174 174 configitem('lfs', 'url',
175 175 default=None,
176 176 )
177 177 configitem('lfs', 'usercache',
178 178 default=None,
179 179 )
180 180 # Deprecated
181 181 configitem('lfs', 'threshold',
182 182 default=None,
183 183 )
184 184 configitem('lfs', 'track',
185 185 default='none()',
186 186 )
187 187 configitem('lfs', 'retry',
188 188 default=5,
189 189 )
190 190
191 191 cmdtable = {}
192 192 command = registrar.command(cmdtable)
193 193
194 194 templatekeyword = registrar.templatekeyword()
195 195 filesetpredicate = registrar.filesetpredicate()
196 196
197 197 def featuresetup(ui, supported):
198 198 # don't die on seeing a repo with the lfs requirement
199 199 supported |= {'lfs'}
200 200
201 201 def uisetup(ui):
202 202 localrepo.featuresetupfuncs.add(featuresetup)
203 203
204 204 def reposetup(ui, repo):
205 205 # Nothing to do with a remote repo
206 206 if not repo.local():
207 207 return
208 208
209 209 repo.svfs.lfslocalblobstore = blobstore.local(repo)
210 210 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
211 211
212 212 class lfsrepo(repo.__class__):
213 213 @localrepo.unfilteredmethod
214 214 def commitctx(self, ctx, error=False):
215 215 repo.svfs.options['lfstrack'] = _trackedmatcher(self)
216 216 return super(lfsrepo, self).commitctx(ctx, error)
217 217
218 218 repo.__class__ = lfsrepo
219 219
220 220 if 'lfs' not in repo.requirements:
221 221 def checkrequireslfs(ui, repo, **kwargs):
222 222 if 'lfs' not in repo.requirements:
223 223 last = kwargs.get(r'node_last')
224 224 _bin = node.bin
225 225 if last:
226 226 s = repo.set('%n:%n', _bin(kwargs[r'node']), _bin(last))
227 227 else:
228 228 s = repo.set('%n', _bin(kwargs[r'node']))
229 match = repo.narrowmatch()
229 230 for ctx in s:
230 231 # TODO: is there a way to just walk the files in the commit?
231 if any(ctx[f].islfs() for f in ctx.files() if f in ctx):
232 if any(ctx[f].islfs() for f in ctx.files()
233 if f in ctx and match(f)):
232 234 repo.requirements.add('lfs')
233 235 repo._writerequirements()
234 236 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
235 237 break
236 238
237 239 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
238 240 ui.setconfig('hooks', 'pretxnchangegroup.lfs', checkrequireslfs, 'lfs')
239 241 else:
240 242 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
241 243
242 244 def _trackedmatcher(repo):
243 245 """Return a function (path, size) -> bool indicating whether or not to
244 246 track a given file with lfs."""
245 247 if not repo.wvfs.exists('.hglfs'):
246 248 # No '.hglfs' in wdir. Fallback to config for now.
247 249 trackspec = repo.ui.config('lfs', 'track')
248 250
249 251 # deprecated config: lfs.threshold
250 252 threshold = repo.ui.configbytes('lfs', 'threshold')
251 253 if threshold:
252 254 fileset.parse(trackspec) # make sure syntax errors are confined
253 255 trackspec = "(%s) | size('>%d')" % (trackspec, threshold)
254 256
255 257 return minifileset.compile(trackspec)
256 258
257 259 data = repo.wvfs.tryread('.hglfs')
258 260 if not data:
259 261 return lambda p, s: False
260 262
261 263 # Parse errors here will abort with a message that points to the .hglfs file
262 264 # and line number.
263 265 cfg = config.config()
264 266 cfg.parse('.hglfs', data)
265 267
266 268 try:
267 269 rules = [(minifileset.compile(pattern), minifileset.compile(rule))
268 270 for pattern, rule in cfg.items('track')]
269 271 except error.ParseError as e:
270 272 # The original exception gives no indicator that the error is in the
271 273 # .hglfs file, so add that.
272 274
273 275 # TODO: See if the line number of the file can be made available.
274 276 raise error.Abort(_('parse error in .hglfs: %s') % e)
275 277
276 278 def _match(path, size):
277 279 for pat, rule in rules:
278 280 if pat(path, size):
279 281 return rule(path, size)
280 282
281 283 return False
282 284
283 285 return _match
284 286
285 287 def wrapfilelog(filelog):
286 288 wrapfunction = extensions.wrapfunction
287 289
288 290 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
289 291 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
290 292 wrapfunction(filelog, 'size', wrapper.filelogsize)
291 293
292 294 def extsetup(ui):
293 295 wrapfilelog(filelog.filelog)
294 296
295 297 wrapfunction = extensions.wrapfunction
296 298
297 299 wrapfunction(cmdutil, '_updatecatformatter', wrapper._updatecatformatter)
298 300 wrapfunction(scmutil, 'wrapconvertsink', wrapper.convertsink)
299 301
300 302 wrapfunction(upgrade, '_finishdatamigration',
301 303 wrapper.upgradefinishdatamigration)
302 304
303 305 wrapfunction(upgrade, 'preservedrequirements',
304 306 wrapper.upgraderequirements)
305 307
306 308 wrapfunction(upgrade, 'supporteddestrequirements',
307 309 wrapper.upgraderequirements)
308 310
309 311 wrapfunction(changegroup,
310 312 'allsupportedversions',
311 313 wrapper.allsupportedversions)
312 314
313 315 wrapfunction(exchange, 'push', wrapper.push)
314 316 wrapfunction(wireproto, '_capabilities', wrapper._capabilities)
315 317
316 318 wrapfunction(context.basefilectx, 'cmp', wrapper.filectxcmp)
317 319 wrapfunction(context.basefilectx, 'isbinary', wrapper.filectxisbinary)
318 320 context.basefilectx.islfs = wrapper.filectxislfs
319 321
320 322 revlog.addflagprocessor(
321 323 revlog.REVIDX_EXTSTORED,
322 324 (
323 325 wrapper.readfromstore,
324 326 wrapper.writetostore,
325 327 wrapper.bypasscheckhash,
326 328 ),
327 329 )
328 330
329 331 wrapfunction(hg, 'clone', wrapper.hgclone)
330 332 wrapfunction(hg, 'postshare', wrapper.hgpostshare)
331 333
332 334 scmutil.fileprefetchhooks.add('lfs', wrapper._prefetchfiles)
333 335
334 336 # Make bundle choose changegroup3 instead of changegroup2. This affects
335 337 # "hg bundle" command. Note: it does not cover all bundle formats like
336 338 # "packed1". Using "packed1" with lfs will likely cause trouble.
337 339 names = [k for k, v in exchange._bundlespeccgversions.items() if v == '02']
338 340 for k in names:
339 341 exchange._bundlespeccgversions[k] = '03'
340 342
341 343 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
342 344 # options and blob stores are passed from othervfs to the new readonlyvfs.
343 345 wrapfunction(vfsmod.readonlyvfs, '__init__', wrapper.vfsinit)
344 346
345 347 # when writing a bundle via "hg bundle" command, upload related LFS blobs
346 348 wrapfunction(bundle2, 'writenewbundle', wrapper.writenewbundle)
347 349
348 350 @filesetpredicate('lfs()', callstatus=True)
349 351 def lfsfileset(mctx, x):
350 352 """File that uses LFS storage."""
351 353 # i18n: "lfs" is a keyword
352 354 fileset.getargs(x, 0, 0, _("lfs takes no arguments"))
353 355 return [f for f in mctx.subset
354 356 if wrapper.pointerfromctx(mctx.ctx, f, removed=True) is not None]
355 357
356 358 @templatekeyword('lfs_files', requires={'ctx'})
357 359 def lfsfiles(context, mapping):
358 360 """List of strings. All files modified, added, or removed by this
359 361 changeset."""
360 362 ctx = context.resource(mapping, 'ctx')
361 363
362 364 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
363 365 files = sorted(pointers.keys())
364 366
365 367 def pointer(v):
366 368 # In the file spec, version is first and the other keys are sorted.
367 369 sortkeyfunc = lambda x: (x[0] != 'version', x)
368 370 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
369 371 return util.sortdict(items)
370 372
371 373 makemap = lambda v: {
372 374 'file': v,
373 375 'lfsoid': pointers[v].oid() if pointers[v] else None,
374 376 'lfspointer': templateutil.hybriddict(pointer(v)),
375 377 }
376 378
377 379 # TODO: make the separator ', '?
378 380 f = templateutil._showcompatlist(context, mapping, 'lfs_file', files)
379 381 return templateutil.hybrid(f, files, makemap, pycompat.identity)
380 382
381 383 @command('debuglfsupload',
382 384 [('r', 'rev', [], _('upload large files introduced by REV'))])
383 385 def debuglfsupload(ui, repo, **opts):
384 386 """upload lfs blobs added by the working copy parent or given revisions"""
385 387 revs = opts.get(r'rev', [])
386 388 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
387 389 wrapper.uploadblobs(repo, pointers)
@@ -1,102 +1,106 b''
1 1 #testcases flat tree
2 2
3 3 $ . "$TESTDIR/narrow-library.sh"
4 4
5 5 #if tree
6 6 $ cat << EOF >> $HGRCPATH
7 7 > [experimental]
8 8 > treemanifest = 1
9 9 > EOF
10 10 #endif
11 11
12 12 create full repo
13 13
14 14 $ hg init master
15 15 $ cd master
16 16
17 17 $ mkdir inside
18 18 $ echo inside > inside/f1
19 19 $ mkdir outside
20 20 $ echo outside > outside/f1
21 21 $ hg ci -Aqm 'initial'
22 22
23 23 $ echo modified > inside/f1
24 24 $ hg ci -qm 'modify inside'
25 25
26 26 $ echo modified > outside/f1
27 27 $ hg ci -qm 'modify outside'
28 28
29 29 $ cd ..
30 30
31 $ hg clone --narrow ssh://user@dummy/master narrow --include inside
31 (The lfs extension does nothing here, but this test ensures that its hook that
32 determines whether to add the lfs requirement, respects the narrow boundaries.)
33
34 $ hg --config extensions.lfs= clone --narrow ssh://user@dummy/master narrow \
35 > --include inside
32 36 requesting all changes
33 37 adding changesets
34 38 adding manifests
35 39 adding file changes
36 40 added 3 changesets with 2 changes to 1 files
37 41 new changesets *:* (glob)
38 42 updating to branch default
39 43 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
40 44 $ cd narrow
41 45
42 46 $ hg update -q 0
43 47
44 48 Can not modify dirstate outside
45 49
46 50 $ mkdir outside
47 51 $ touch outside/f1
48 52 $ hg debugwalk -I 'relglob:f1'
49 53 matcher: <includematcher includes='(?:(?:|.*/)f1(?:/|$))'>
50 54 f inside/f1 inside/f1
51 55 $ hg add outside/f1
52 56 abort: cannot track 'outside/f1' - it is outside the narrow clone
53 57 [255]
54 58 $ touch outside/f3
55 59 $ hg add outside/f3
56 60 abort: cannot track 'outside/f3' - it is outside the narrow clone
57 61 [255]
58 62
59 63 But adding a truly excluded file shouldn't count
60 64
61 65 $ hg add outside/f3 -X outside/f3
62 66
63 67 $ rm -r outside
64 68
65 69 Can modify dirstate inside
66 70
67 71 $ echo modified > inside/f1
68 72 $ touch inside/f3
69 73 $ hg add inside/f3
70 74 $ hg status
71 75 M inside/f1
72 76 A inside/f3
73 77 $ hg revert -qC .
74 78 $ rm inside/f3
75 79
76 80 Can commit changes inside. Leaves outside unchanged.
77 81
78 82 $ hg update -q 'desc("initial")'
79 83 $ echo modified2 > inside/f1
80 84 $ hg manifest --debug
81 85 4d6a634d5ba06331a60c29ee0db8412490a54fcd 644 inside/f1
82 86 7fb3bb6356d28d4dc352c5ba52d7350a81b6bd46 644 outside/f1 (flat !)
83 87 d0f2f706468ab0e8bec7af87446835fb1b13511b 755 d outside/ (tree !)
84 88 $ hg commit -m 'modify inside/f1'
85 89 created new head
86 90 $ hg files -r .
87 91 inside/f1
88 92 outside/f1 (flat !)
89 93 outside/ (tree !)
90 94 $ hg manifest --debug
91 95 3f4197b4a11b9016e77ebc47fe566944885fd11b 644 inside/f1
92 96 7fb3bb6356d28d4dc352c5ba52d7350a81b6bd46 644 outside/f1 (flat !)
93 97 d0f2f706468ab0e8bec7af87446835fb1b13511b 755 d outside/ (tree !)
94 98 Some filesystems (notably FAT/exFAT only store timestamps with 2
95 99 seconds of precision, so by sleeping for 3 seconds, we can ensure that
96 100 the timestamps of files stored by dirstate will appear older than the
97 101 dirstate file, and therefore we'll be able to get stable output from
98 102 debugdirstate. If we don't do this, the test can be slightly flaky.
99 103 $ sleep 3
100 104 $ hg status
101 105 $ hg debugdirstate --nodates
102 106 n 644 10 set inside/f1
General Comments 0
You need to be logged in to leave comments. Login now