##// END OF EJS Templates
lfs: remove pycompat.iteritems()...
Gregory Szorc -
r49772:1672c5af default
parent child Browse files
Show More
@@ -1,446 +1,446 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 The extension reads its configuration from a versioned ``.hglfs``
57 57 configuration file found in the root of the working directory. The
58 58 ``.hglfs`` file uses the same syntax as all other Mercurial
59 59 configuration files. It uses a single section, ``[track]``.
60 60
61 61 The ``[track]`` section specifies which files are stored as LFS (or
62 62 not). Each line is keyed by a file pattern, with a predicate value.
63 63 The first file pattern match is used, so put more specific patterns
64 64 first. The available predicates are ``all()``, ``none()``, and
65 65 ``size()``. See "hg help filesets.size" for the latter.
66 66
67 67 Example versioned ``.hglfs`` file::
68 68
69 69 [track]
70 70 # No Makefile or python file, anywhere, will be LFS
71 71 **Makefile = none()
72 72 **.py = none()
73 73
74 74 **.zip = all()
75 75 **.exe = size(">1MB")
76 76
77 77 # Catchall for everything not matched above
78 78 ** = size(">10MB")
79 79
80 80 Configs::
81 81
82 82 [lfs]
83 83 # Remote endpoint. Multiple protocols are supported:
84 84 # - http(s)://user:pass@example.com/path
85 85 # git-lfs endpoint
86 86 # - file:///tmp/path
87 87 # local filesystem, usually for testing
88 88 # if unset, lfs will assume the remote repository also handles blob storage
89 89 # for http(s) URLs. Otherwise, lfs will prompt to set this when it must
90 90 # 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
124 124 import sys
125 125
126 126 from mercurial.i18n import _
127 127 from mercurial.node import bin
128 128
129 129 from mercurial import (
130 130 bundlecaches,
131 131 config,
132 132 context,
133 133 error,
134 134 extensions,
135 135 exthelper,
136 136 filelog,
137 137 filesetlang,
138 138 localrepo,
139 139 logcmdutil,
140 140 minifileset,
141 141 pycompat,
142 142 revlog,
143 143 scmutil,
144 144 templateutil,
145 145 util,
146 146 )
147 147
148 148 from mercurial.interfaces import repository
149 149
150 150 from . import (
151 151 blobstore,
152 152 wireprotolfsserver,
153 153 wrapper,
154 154 )
155 155
156 156 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
157 157 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
158 158 # be specifying the version(s) of Mercurial they are tested with, or
159 159 # leave the attribute unspecified.
160 160 testedwith = b'ships-with-hg-core'
161 161
162 162 eh = exthelper.exthelper()
163 163 eh.merge(wrapper.eh)
164 164 eh.merge(wireprotolfsserver.eh)
165 165
166 166 cmdtable = eh.cmdtable
167 167 configtable = eh.configtable
168 168 extsetup = eh.finalextsetup
169 169 uisetup = eh.finaluisetup
170 170 filesetpredicate = eh.filesetpredicate
171 171 reposetup = eh.finalreposetup
172 172 templatekeyword = eh.templatekeyword
173 173
174 174 eh.configitem(
175 175 b'experimental',
176 176 b'lfs.serve',
177 177 default=True,
178 178 )
179 179 eh.configitem(
180 180 b'experimental',
181 181 b'lfs.user-agent',
182 182 default=None,
183 183 )
184 184 eh.configitem(
185 185 b'experimental',
186 186 b'lfs.disableusercache',
187 187 default=False,
188 188 )
189 189 eh.configitem(
190 190 b'experimental',
191 191 b'lfs.worker-enable',
192 192 default=True,
193 193 )
194 194
195 195 eh.configitem(
196 196 b'lfs',
197 197 b'url',
198 198 default=None,
199 199 )
200 200 eh.configitem(
201 201 b'lfs',
202 202 b'usercache',
203 203 default=None,
204 204 )
205 205 # Deprecated
206 206 eh.configitem(
207 207 b'lfs',
208 208 b'threshold',
209 209 default=None,
210 210 )
211 211 eh.configitem(
212 212 b'lfs',
213 213 b'track',
214 214 default=b'none()',
215 215 )
216 216 eh.configitem(
217 217 b'lfs',
218 218 b'retry',
219 219 default=5,
220 220 )
221 221
222 222 lfsprocessor = (
223 223 wrapper.readfromstore,
224 224 wrapper.writetostore,
225 225 wrapper.bypasscheckhash,
226 226 )
227 227
228 228
229 229 def featuresetup(ui, supported):
230 230 # don't die on seeing a repo with the lfs requirement
231 231 supported |= {b'lfs'}
232 232
233 233
234 234 @eh.uisetup
235 235 def _uisetup(ui):
236 236 localrepo.featuresetupfuncs.add(featuresetup)
237 237
238 238
239 239 @eh.reposetup
240 240 def _reposetup(ui, repo):
241 241 # Nothing to do with a remote repo
242 242 if not repo.local():
243 243 return
244 244
245 245 repo.svfs.lfslocalblobstore = blobstore.local(repo)
246 246 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
247 247
248 248 class lfsrepo(repo.__class__):
249 249 @localrepo.unfilteredmethod
250 250 def commitctx(self, ctx, error=False, origctx=None):
251 251 repo.svfs.options[b'lfstrack'] = _trackedmatcher(self)
252 252 return super(lfsrepo, self).commitctx(ctx, error, origctx=origctx)
253 253
254 254 repo.__class__ = lfsrepo
255 255
256 256 if b'lfs' not in repo.requirements:
257 257
258 258 def checkrequireslfs(ui, repo, **kwargs):
259 259 with repo.lock():
260 260 if b'lfs' in repo.requirements:
261 261 return 0
262 262
263 263 last = kwargs.get('node_last')
264 264 if last:
265 265 s = repo.set(b'%n:%n', bin(kwargs['node']), bin(last))
266 266 else:
267 267 s = repo.set(b'%n', bin(kwargs['node']))
268 268 match = repo._storenarrowmatch
269 269 for ctx in s:
270 270 # TODO: is there a way to just walk the files in the commit?
271 271 if any(
272 272 ctx[f].islfs()
273 273 for f in ctx.files()
274 274 if f in ctx and match(f)
275 275 ):
276 276 repo.requirements.add(b'lfs')
277 277 repo.features.add(repository.REPO_FEATURE_LFS)
278 278 scmutil.writereporequirements(repo)
279 279 repo.prepushoutgoinghooks.add(b'lfs', wrapper.prepush)
280 280 break
281 281
282 282 ui.setconfig(b'hooks', b'commit.lfs', checkrequireslfs, b'lfs')
283 283 ui.setconfig(
284 284 b'hooks', b'pretxnchangegroup.lfs', checkrequireslfs, b'lfs'
285 285 )
286 286 else:
287 287 repo.prepushoutgoinghooks.add(b'lfs', wrapper.prepush)
288 288
289 289
290 290 def _trackedmatcher(repo):
291 291 """Return a function (path, size) -> bool indicating whether or not to
292 292 track a given file with lfs."""
293 293 if not repo.wvfs.exists(b'.hglfs'):
294 294 # No '.hglfs' in wdir. Fallback to config for now.
295 295 trackspec = repo.ui.config(b'lfs', b'track')
296 296
297 297 # deprecated config: lfs.threshold
298 298 threshold = repo.ui.configbytes(b'lfs', b'threshold')
299 299 if threshold:
300 300 filesetlang.parse(trackspec) # make sure syntax errors are confined
301 301 trackspec = b"(%s) | size('>%d')" % (trackspec, threshold)
302 302
303 303 return minifileset.compile(trackspec)
304 304
305 305 data = repo.wvfs.tryread(b'.hglfs')
306 306 if not data:
307 307 return lambda p, s: False
308 308
309 309 # Parse errors here will abort with a message that points to the .hglfs file
310 310 # and line number.
311 311 cfg = config.config()
312 312 cfg.parse(b'.hglfs', data)
313 313
314 314 try:
315 315 rules = [
316 316 (minifileset.compile(pattern), minifileset.compile(rule))
317 317 for pattern, rule in cfg.items(b'track')
318 318 ]
319 319 except error.ParseError as e:
320 320 # The original exception gives no indicator that the error is in the
321 321 # .hglfs file, so add that.
322 322
323 323 # TODO: See if the line number of the file can be made available.
324 324 raise error.Abort(_(b'parse error in .hglfs: %s') % e)
325 325
326 326 def _match(path, size):
327 327 for pat, rule in rules:
328 328 if pat(path, size):
329 329 return rule(path, size)
330 330
331 331 return False
332 332
333 333 return _match
334 334
335 335
336 336 # Called by remotefilelog
337 337 def wrapfilelog(filelog):
338 338 wrapfunction = extensions.wrapfunction
339 339
340 340 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
341 341 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
342 342 wrapfunction(filelog, 'size', wrapper.filelogsize)
343 343
344 344
345 345 @eh.wrapfunction(localrepo, b'resolverevlogstorevfsoptions')
346 346 def _resolverevlogstorevfsoptions(orig, ui, requirements, features):
347 347 opts = orig(ui, requirements, features)
348 348 for name, module in extensions.extensions(ui):
349 349 if module is sys.modules[__name__]:
350 350 if revlog.REVIDX_EXTSTORED in opts[b'flagprocessors']:
351 351 msg = (
352 352 _(b"cannot register multiple processors on flag '%#x'.")
353 353 % revlog.REVIDX_EXTSTORED
354 354 )
355 355 raise error.Abort(msg)
356 356
357 357 opts[b'flagprocessors'][revlog.REVIDX_EXTSTORED] = lfsprocessor
358 358 break
359 359
360 360 return opts
361 361
362 362
363 363 @eh.extsetup
364 364 def _extsetup(ui):
365 365 wrapfilelog(filelog.filelog)
366 366
367 367 context.basefilectx.islfs = wrapper.filectxislfs
368 368
369 369 scmutil.fileprefetchhooks.add(b'lfs', wrapper._prefetchfiles)
370 370
371 371 # Make bundle choose changegroup3 instead of changegroup2. This affects
372 372 # "hg bundle" command. Note: it does not cover all bundle formats like
373 373 # "packed1". Using "packed1" with lfs will likely cause trouble.
374 374 bundlecaches._bundlespeccontentopts[b"v2"][b"cg.version"] = b"03"
375 375
376 376
377 377 @eh.filesetpredicate(b'lfs()')
378 378 def lfsfileset(mctx, x):
379 379 """File that uses LFS storage."""
380 380 # i18n: "lfs" is a keyword
381 381 filesetlang.getargs(x, 0, 0, _(b"lfs takes no arguments"))
382 382 ctx = mctx.ctx
383 383
384 384 def lfsfilep(f):
385 385 return wrapper.pointerfromctx(ctx, f, removed=True) is not None
386 386
387 387 return mctx.predicate(lfsfilep, predrepr=b'<lfs>')
388 388
389 389
390 390 @eh.templatekeyword(b'lfs_files', requires={b'ctx'})
391 391 def lfsfiles(context, mapping):
392 392 """List of strings. All files modified, added, or removed by this
393 393 changeset."""
394 394 ctx = context.resource(mapping, b'ctx')
395 395
396 396 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
397 397 files = sorted(pointers.keys())
398 398
399 399 def pointer(v):
400 400 # In the file spec, version is first and the other keys are sorted.
401 401 sortkeyfunc = lambda x: (x[0] != b'version', x)
402 items = sorted(pycompat.iteritems(pointers[v]), key=sortkeyfunc)
402 items = sorted(pointers[v].items(), key=sortkeyfunc)
403 403 return util.sortdict(items)
404 404
405 405 makemap = lambda v: {
406 406 b'file': v,
407 407 b'lfsoid': pointers[v].oid() if pointers[v] else None,
408 408 b'lfspointer': templateutil.hybriddict(pointer(v)),
409 409 }
410 410
411 411 # TODO: make the separator ', '?
412 412 f = templateutil._showcompatlist(context, mapping, b'lfs_file', files)
413 413 return templateutil.hybrid(f, files, makemap, pycompat.identity)
414 414
415 415
416 416 @eh.command(
417 417 b'debuglfsupload',
418 418 [(b'r', b'rev', [], _(b'upload large files introduced by REV'))],
419 419 )
420 420 def debuglfsupload(ui, repo, **opts):
421 421 """upload lfs blobs added by the working copy parent or given revisions"""
422 422 revs = opts.get('rev', [])
423 423 pointers = wrapper.extractpointers(repo, logcmdutil.revrange(repo, revs))
424 424 wrapper.uploadblobs(repo, pointers)
425 425
426 426
427 427 @eh.wrapcommand(
428 428 b'verify',
429 429 opts=[(b'', b'no-lfs', None, _(b'skip missing lfs blob content'))],
430 430 )
431 431 def verify(orig, ui, repo, **opts):
432 432 skipflags = repo.ui.configint(b'verify', b'skipflags')
433 433 no_lfs = opts.pop('no_lfs')
434 434
435 435 if skipflags:
436 436 # --lfs overrides the config bit, if set.
437 437 if no_lfs is False:
438 438 skipflags &= ~repository.REVISION_FLAG_EXTSTORED
439 439 else:
440 440 skipflags = 0
441 441
442 442 if no_lfs is True:
443 443 skipflags |= repository.REVISION_FLAG_EXTSTORED
444 444
445 445 with ui.configoverride({(b'verify', b'skipflags'): skipflags}):
446 446 return orig(ui, repo, **opts)
@@ -1,88 +1,88 b''
1 1 # pointer.py - Git-LFS pointer serialization
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
9 9 import re
10 10
11 11 from mercurial.i18n import _
12 12
13 13 from mercurial import (
14 14 error,
15 15 pycompat,
16 16 )
17 17 from mercurial.utils import stringutil
18 18
19 19
20 20 class InvalidPointer(error.StorageError):
21 21 pass
22 22
23 23
24 24 class gitlfspointer(dict):
25 25 VERSION = b'https://git-lfs.github.com/spec/v1'
26 26
27 27 def __init__(self, *args, **kwargs):
28 28 self[b'version'] = self.VERSION
29 29 super(gitlfspointer, self).__init__(*args)
30 30 self.update(pycompat.byteskwargs(kwargs))
31 31
32 32 @classmethod
33 33 def deserialize(cls, text):
34 34 try:
35 35 return cls(l.split(b' ', 1) for l in text.splitlines()).validate()
36 36 except ValueError: # l.split returns 1 item instead of 2
37 37 raise InvalidPointer(
38 38 _(b'cannot parse git-lfs text: %s') % stringutil.pprint(text)
39 39 )
40 40
41 41 def serialize(self):
42 42 sortkeyfunc = lambda x: (x[0] != b'version', x)
43 items = sorted(pycompat.iteritems(self.validate()), key=sortkeyfunc)
43 items = sorted(self.validate().items(), key=sortkeyfunc)
44 44 return b''.join(b'%s %s\n' % (k, v) for k, v in items)
45 45
46 46 def oid(self):
47 47 return self[b'oid'].split(b':')[-1]
48 48
49 49 def size(self):
50 50 return int(self[b'size'])
51 51
52 52 # regular expressions used by _validate
53 53 # see https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
54 54 _keyre = re.compile(br'\A[a-z0-9.-]+\Z')
55 55 _valuere = re.compile(br'\A[^\n]*\Z')
56 56 _requiredre = {
57 57 b'size': re.compile(br'\A[0-9]+\Z'),
58 58 b'oid': re.compile(br'\Asha256:[0-9a-f]{64}\Z'),
59 59 b'version': re.compile(br'\A%s\Z' % stringutil.reescape(VERSION)),
60 60 }
61 61
62 62 def validate(self):
63 63 """raise InvalidPointer on error. return self if there is no error"""
64 64 requiredcount = 0
65 65 for k, v in self.items():
66 66 if k in self._requiredre:
67 67 if not self._requiredre[k].match(v):
68 68 raise InvalidPointer(
69 69 _(b'unexpected lfs pointer value: %s=%s')
70 70 % (k, stringutil.pprint(v))
71 71 )
72 72 requiredcount += 1
73 73 elif not self._keyre.match(k):
74 74 raise InvalidPointer(_(b'unexpected lfs pointer key: %s') % k)
75 75 if not self._valuere.match(v):
76 76 raise InvalidPointer(
77 77 _(b'unexpected lfs pointer value: %s=%s')
78 78 % (k, stringutil.pprint(v))
79 79 )
80 80 if len(self._requiredre) != requiredcount:
81 81 miss = sorted(set(self._requiredre.keys()).difference(self.keys()))
82 82 raise InvalidPointer(
83 83 _(b'missing lfs pointer keys: %s') % b', '.join(miss)
84 84 )
85 85 return self
86 86
87 87
88 88 deserialize = gitlfspointer.deserialize
General Comments 0
You need to be logged in to leave comments. Login now