##// 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 # lfs - hash-preserving large file support using Git-LFS protocol
1 # lfs - hash-preserving large file support using Git-LFS protocol
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """lfs - large file support (EXPERIMENTAL)
8 """lfs - large file support (EXPERIMENTAL)
9
9
10 This extension allows large files to be tracked outside of the normal
10 This extension allows large files to be tracked outside of the normal
11 repository storage and stored on a centralized server, similar to the
11 repository storage and stored on a centralized server, similar to the
12 ``largefiles`` extension. The ``git-lfs`` protocol is used when
12 ``largefiles`` extension. The ``git-lfs`` protocol is used when
13 communicating with the server, so existing git infrastructure can be
13 communicating with the server, so existing git infrastructure can be
14 harnessed. Even though the files are stored outside of the repository,
14 harnessed. Even though the files are stored outside of the repository,
15 they are still integrity checked in the same manner as normal files.
15 they are still integrity checked in the same manner as normal files.
16
16
17 The files stored outside of the repository are downloaded on demand,
17 The files stored outside of the repository are downloaded on demand,
18 which reduces the time to clone, and possibly the local disk usage.
18 which reduces the time to clone, and possibly the local disk usage.
19 This changes fundamental workflows in a DVCS, so careful thought
19 This changes fundamental workflows in a DVCS, so careful thought
20 should be given before deploying it. :hg:`convert` can be used to
20 should be given before deploying it. :hg:`convert` can be used to
21 convert LFS repositories to normal repositories that no longer
21 convert LFS repositories to normal repositories that no longer
22 require this extension, and do so without changing the commit hashes.
22 require this extension, and do so without changing the commit hashes.
23 This allows the extension to be disabled if the centralized workflow
23 This allows the extension to be disabled if the centralized workflow
24 becomes burdensome. However, the pre and post convert clones will
24 becomes burdensome. However, the pre and post convert clones will
25 not be able to communicate with each other unless the extension is
25 not be able to communicate with each other unless the extension is
26 enabled on both.
26 enabled on both.
27
27
28 To start a new repository, or to add LFS files to an existing one, just
28 To start a new repository, or to add LFS files to an existing one, just
29 create an ``.hglfs`` file as described below in the root directory of
29 create an ``.hglfs`` file as described below in the root directory of
30 the repository. Typically, this file should be put under version
30 the repository. Typically, this file should be put under version
31 control, so that the settings will propagate to other repositories with
31 control, so that the settings will propagate to other repositories with
32 push and pull. During any commit, Mercurial will consult this file to
32 push and pull. During any commit, Mercurial will consult this file to
33 determine if an added or modified file should be stored externally. The
33 determine if an added or modified file should be stored externally. The
34 type of storage depends on the characteristics of the file at each
34 type of storage depends on the characteristics of the file at each
35 commit. A file that is near a size threshold may switch back and forth
35 commit. A file that is near a size threshold may switch back and forth
36 between LFS and normal storage, as needed.
36 between LFS and normal storage, as needed.
37
37
38 Alternately, both normal repositories and largefile controlled
38 Alternately, both normal repositories and largefile controlled
39 repositories can be converted to LFS by using :hg:`convert` and the
39 repositories can be converted to LFS by using :hg:`convert` and the
40 ``lfs.track`` config option described below. The ``.hglfs`` file
40 ``lfs.track`` config option described below. The ``.hglfs`` file
41 should then be created and added, to control subsequent LFS selection.
41 should then be created and added, to control subsequent LFS selection.
42 The hashes are also unchanged in this case. The LFS and non-LFS
42 The hashes are also unchanged in this case. The LFS and non-LFS
43 repositories can be distinguished because the LFS repository will
43 repositories can be distinguished because the LFS repository will
44 abort any command if this extension is disabled.
44 abort any command if this extension is disabled.
45
45
46 Committed LFS files are held locally, until the repository is pushed.
46 Committed LFS files are held locally, until the repository is pushed.
47 Prior to pushing the normal repository data, the LFS files that are
47 Prior to pushing the normal repository data, the LFS files that are
48 tracked by the outgoing commits are automatically uploaded to the
48 tracked by the outgoing commits are automatically uploaded to the
49 configured central server. No LFS files are transferred on
49 configured central server. No LFS files are transferred on
50 :hg:`pull` or :hg:`clone`. Instead, the files are downloaded on
50 :hg:`pull` or :hg:`clone`. Instead, the files are downloaded on
51 demand as they need to be read, if a cached copy cannot be found
51 demand as they need to be read, if a cached copy cannot be found
52 locally. Both committing and downloading an LFS file will link the
52 locally. Both committing and downloading an LFS file will link the
53 file to a usercache, to speed up future access. See the `usercache`
53 file to a usercache, to speed up future access. See the `usercache`
54 config setting described below.
54 config setting described below.
55
55
56 The extension reads its configuration from a versioned ``.hglfs``
56 The extension reads its configuration from a versioned ``.hglfs``
57 configuration file found in the root of the working directory. The
57 configuration file found in the root of the working directory. The
58 ``.hglfs`` file uses the same syntax as all other Mercurial
58 ``.hglfs`` file uses the same syntax as all other Mercurial
59 configuration files. It uses a single section, ``[track]``.
59 configuration files. It uses a single section, ``[track]``.
60
60
61 The ``[track]`` section specifies which files are stored as LFS (or
61 The ``[track]`` section specifies which files are stored as LFS (or
62 not). Each line is keyed by a file pattern, with a predicate value.
62 not). Each line is keyed by a file pattern, with a predicate value.
63 The first file pattern match is used, so put more specific patterns
63 The first file pattern match is used, so put more specific patterns
64 first. The available predicates are ``all()``, ``none()``, and
64 first. The available predicates are ``all()``, ``none()``, and
65 ``size()``. See "hg help filesets.size" for the latter.
65 ``size()``. See "hg help filesets.size" for the latter.
66
66
67 Example versioned ``.hglfs`` file::
67 Example versioned ``.hglfs`` file::
68
68
69 [track]
69 [track]
70 # No Makefile or python file, anywhere, will be LFS
70 # No Makefile or python file, anywhere, will be LFS
71 **Makefile = none()
71 **Makefile = none()
72 **.py = none()
72 **.py = none()
73
73
74 **.zip = all()
74 **.zip = all()
75 **.exe = size(">1MB")
75 **.exe = size(">1MB")
76
76
77 # Catchall for everything not matched above
77 # Catchall for everything not matched above
78 ** = size(">10MB")
78 ** = size(">10MB")
79
79
80 Configs::
80 Configs::
81
81
82 [lfs]
82 [lfs]
83 # Remote endpoint. Multiple protocols are supported:
83 # Remote endpoint. Multiple protocols are supported:
84 # - http(s)://user:pass@example.com/path
84 # - http(s)://user:pass@example.com/path
85 # git-lfs endpoint
85 # git-lfs endpoint
86 # - file:///tmp/path
86 # - file:///tmp/path
87 # local filesystem, usually for testing
87 # local filesystem, usually for testing
88 # if unset, lfs will assume the remote repository also handles blob storage
88 # if unset, lfs will assume the remote repository also handles blob storage
89 # for http(s) URLs. Otherwise, lfs will prompt to set this when it must
89 # for http(s) URLs. Otherwise, lfs will prompt to set this when it must
90 # use this value.
90 # use this value.
91 # (default: unset)
91 # (default: unset)
92 url = https://example.com/repo.git/info/lfs
92 url = https://example.com/repo.git/info/lfs
93
93
94 # Which files to track in LFS. Path tests are "**.extname" for file
94 # Which files to track in LFS. Path tests are "**.extname" for file
95 # extensions, and "path:under/some/directory" for path prefix. Both
95 # extensions, and "path:under/some/directory" for path prefix. Both
96 # are relative to the repository root.
96 # are relative to the repository root.
97 # File size can be tested with the "size()" fileset, and tests can be
97 # File size can be tested with the "size()" fileset, and tests can be
98 # joined with fileset operators. (See "hg help filesets.operators".)
98 # joined with fileset operators. (See "hg help filesets.operators".)
99 #
99 #
100 # Some examples:
100 # Some examples:
101 # - all() # everything
101 # - all() # everything
102 # - none() # nothing
102 # - none() # nothing
103 # - size(">20MB") # larger than 20MB
103 # - size(">20MB") # larger than 20MB
104 # - !**.txt # anything not a *.txt file
104 # - !**.txt # anything not a *.txt file
105 # - **.zip | **.tar.gz | **.7z # some types of compressed files
105 # - **.zip | **.tar.gz | **.7z # some types of compressed files
106 # - path:bin # files under "bin" in the project root
106 # - path:bin # files under "bin" in the project root
107 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
107 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
108 # | (path:bin & !path:/bin/README) | size(">1GB")
108 # | (path:bin & !path:/bin/README) | size(">1GB")
109 # (default: none())
109 # (default: none())
110 #
110 #
111 # This is ignored if there is a tracked '.hglfs' file, and this setting
111 # This is ignored if there is a tracked '.hglfs' file, and this setting
112 # will eventually be deprecated and removed.
112 # will eventually be deprecated and removed.
113 track = size(">10M")
113 track = size(">10M")
114
114
115 # how many times to retry before giving up on transferring an object
115 # how many times to retry before giving up on transferring an object
116 retry = 5
116 retry = 5
117
117
118 # the local directory to store lfs files for sharing across local clones.
118 # the local directory to store lfs files for sharing across local clones.
119 # If not set, the cache is located in an OS specific cache location.
119 # If not set, the cache is located in an OS specific cache location.
120 usercache = /path/to/global/cache
120 usercache = /path/to/global/cache
121 """
121 """
122
122
123
123
124 import sys
124 import sys
125
125
126 from mercurial.i18n import _
126 from mercurial.i18n import _
127 from mercurial.node import bin
127 from mercurial.node import bin
128
128
129 from mercurial import (
129 from mercurial import (
130 bundlecaches,
130 bundlecaches,
131 config,
131 config,
132 context,
132 context,
133 error,
133 error,
134 extensions,
134 extensions,
135 exthelper,
135 exthelper,
136 filelog,
136 filelog,
137 filesetlang,
137 filesetlang,
138 localrepo,
138 localrepo,
139 logcmdutil,
139 logcmdutil,
140 minifileset,
140 minifileset,
141 pycompat,
141 pycompat,
142 revlog,
142 revlog,
143 scmutil,
143 scmutil,
144 templateutil,
144 templateutil,
145 util,
145 util,
146 )
146 )
147
147
148 from mercurial.interfaces import repository
148 from mercurial.interfaces import repository
149
149
150 from . import (
150 from . import (
151 blobstore,
151 blobstore,
152 wireprotolfsserver,
152 wireprotolfsserver,
153 wrapper,
153 wrapper,
154 )
154 )
155
155
156 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
156 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
157 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
157 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
158 # be specifying the version(s) of Mercurial they are tested with, or
158 # be specifying the version(s) of Mercurial they are tested with, or
159 # leave the attribute unspecified.
159 # leave the attribute unspecified.
160 testedwith = b'ships-with-hg-core'
160 testedwith = b'ships-with-hg-core'
161
161
162 eh = exthelper.exthelper()
162 eh = exthelper.exthelper()
163 eh.merge(wrapper.eh)
163 eh.merge(wrapper.eh)
164 eh.merge(wireprotolfsserver.eh)
164 eh.merge(wireprotolfsserver.eh)
165
165
166 cmdtable = eh.cmdtable
166 cmdtable = eh.cmdtable
167 configtable = eh.configtable
167 configtable = eh.configtable
168 extsetup = eh.finalextsetup
168 extsetup = eh.finalextsetup
169 uisetup = eh.finaluisetup
169 uisetup = eh.finaluisetup
170 filesetpredicate = eh.filesetpredicate
170 filesetpredicate = eh.filesetpredicate
171 reposetup = eh.finalreposetup
171 reposetup = eh.finalreposetup
172 templatekeyword = eh.templatekeyword
172 templatekeyword = eh.templatekeyword
173
173
174 eh.configitem(
174 eh.configitem(
175 b'experimental',
175 b'experimental',
176 b'lfs.serve',
176 b'lfs.serve',
177 default=True,
177 default=True,
178 )
178 )
179 eh.configitem(
179 eh.configitem(
180 b'experimental',
180 b'experimental',
181 b'lfs.user-agent',
181 b'lfs.user-agent',
182 default=None,
182 default=None,
183 )
183 )
184 eh.configitem(
184 eh.configitem(
185 b'experimental',
185 b'experimental',
186 b'lfs.disableusercache',
186 b'lfs.disableusercache',
187 default=False,
187 default=False,
188 )
188 )
189 eh.configitem(
189 eh.configitem(
190 b'experimental',
190 b'experimental',
191 b'lfs.worker-enable',
191 b'lfs.worker-enable',
192 default=True,
192 default=True,
193 )
193 )
194
194
195 eh.configitem(
195 eh.configitem(
196 b'lfs',
196 b'lfs',
197 b'url',
197 b'url',
198 default=None,
198 default=None,
199 )
199 )
200 eh.configitem(
200 eh.configitem(
201 b'lfs',
201 b'lfs',
202 b'usercache',
202 b'usercache',
203 default=None,
203 default=None,
204 )
204 )
205 # Deprecated
205 # Deprecated
206 eh.configitem(
206 eh.configitem(
207 b'lfs',
207 b'lfs',
208 b'threshold',
208 b'threshold',
209 default=None,
209 default=None,
210 )
210 )
211 eh.configitem(
211 eh.configitem(
212 b'lfs',
212 b'lfs',
213 b'track',
213 b'track',
214 default=b'none()',
214 default=b'none()',
215 )
215 )
216 eh.configitem(
216 eh.configitem(
217 b'lfs',
217 b'lfs',
218 b'retry',
218 b'retry',
219 default=5,
219 default=5,
220 )
220 )
221
221
222 lfsprocessor = (
222 lfsprocessor = (
223 wrapper.readfromstore,
223 wrapper.readfromstore,
224 wrapper.writetostore,
224 wrapper.writetostore,
225 wrapper.bypasscheckhash,
225 wrapper.bypasscheckhash,
226 )
226 )
227
227
228
228
229 def featuresetup(ui, supported):
229 def featuresetup(ui, supported):
230 # don't die on seeing a repo with the lfs requirement
230 # don't die on seeing a repo with the lfs requirement
231 supported |= {b'lfs'}
231 supported |= {b'lfs'}
232
232
233
233
234 @eh.uisetup
234 @eh.uisetup
235 def _uisetup(ui):
235 def _uisetup(ui):
236 localrepo.featuresetupfuncs.add(featuresetup)
236 localrepo.featuresetupfuncs.add(featuresetup)
237
237
238
238
239 @eh.reposetup
239 @eh.reposetup
240 def _reposetup(ui, repo):
240 def _reposetup(ui, repo):
241 # Nothing to do with a remote repo
241 # Nothing to do with a remote repo
242 if not repo.local():
242 if not repo.local():
243 return
243 return
244
244
245 repo.svfs.lfslocalblobstore = blobstore.local(repo)
245 repo.svfs.lfslocalblobstore = blobstore.local(repo)
246 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
246 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
247
247
248 class lfsrepo(repo.__class__):
248 class lfsrepo(repo.__class__):
249 @localrepo.unfilteredmethod
249 @localrepo.unfilteredmethod
250 def commitctx(self, ctx, error=False, origctx=None):
250 def commitctx(self, ctx, error=False, origctx=None):
251 repo.svfs.options[b'lfstrack'] = _trackedmatcher(self)
251 repo.svfs.options[b'lfstrack'] = _trackedmatcher(self)
252 return super(lfsrepo, self).commitctx(ctx, error, origctx=origctx)
252 return super(lfsrepo, self).commitctx(ctx, error, origctx=origctx)
253
253
254 repo.__class__ = lfsrepo
254 repo.__class__ = lfsrepo
255
255
256 if b'lfs' not in repo.requirements:
256 if b'lfs' not in repo.requirements:
257
257
258 def checkrequireslfs(ui, repo, **kwargs):
258 def checkrequireslfs(ui, repo, **kwargs):
259 with repo.lock():
259 with repo.lock():
260 if b'lfs' in repo.requirements:
260 if b'lfs' in repo.requirements:
261 return 0
261 return 0
262
262
263 last = kwargs.get('node_last')
263 last = kwargs.get('node_last')
264 if last:
264 if last:
265 s = repo.set(b'%n:%n', bin(kwargs['node']), bin(last))
265 s = repo.set(b'%n:%n', bin(kwargs['node']), bin(last))
266 else:
266 else:
267 s = repo.set(b'%n', bin(kwargs['node']))
267 s = repo.set(b'%n', bin(kwargs['node']))
268 match = repo._storenarrowmatch
268 match = repo._storenarrowmatch
269 for ctx in s:
269 for ctx in s:
270 # TODO: is there a way to just walk the files in the commit?
270 # TODO: is there a way to just walk the files in the commit?
271 if any(
271 if any(
272 ctx[f].islfs()
272 ctx[f].islfs()
273 for f in ctx.files()
273 for f in ctx.files()
274 if f in ctx and match(f)
274 if f in ctx and match(f)
275 ):
275 ):
276 repo.requirements.add(b'lfs')
276 repo.requirements.add(b'lfs')
277 repo.features.add(repository.REPO_FEATURE_LFS)
277 repo.features.add(repository.REPO_FEATURE_LFS)
278 scmutil.writereporequirements(repo)
278 scmutil.writereporequirements(repo)
279 repo.prepushoutgoinghooks.add(b'lfs', wrapper.prepush)
279 repo.prepushoutgoinghooks.add(b'lfs', wrapper.prepush)
280 break
280 break
281
281
282 ui.setconfig(b'hooks', b'commit.lfs', checkrequireslfs, b'lfs')
282 ui.setconfig(b'hooks', b'commit.lfs', checkrequireslfs, b'lfs')
283 ui.setconfig(
283 ui.setconfig(
284 b'hooks', b'pretxnchangegroup.lfs', checkrequireslfs, b'lfs'
284 b'hooks', b'pretxnchangegroup.lfs', checkrequireslfs, b'lfs'
285 )
285 )
286 else:
286 else:
287 repo.prepushoutgoinghooks.add(b'lfs', wrapper.prepush)
287 repo.prepushoutgoinghooks.add(b'lfs', wrapper.prepush)
288
288
289
289
290 def _trackedmatcher(repo):
290 def _trackedmatcher(repo):
291 """Return a function (path, size) -> bool indicating whether or not to
291 """Return a function (path, size) -> bool indicating whether or not to
292 track a given file with lfs."""
292 track a given file with lfs."""
293 if not repo.wvfs.exists(b'.hglfs'):
293 if not repo.wvfs.exists(b'.hglfs'):
294 # No '.hglfs' in wdir. Fallback to config for now.
294 # No '.hglfs' in wdir. Fallback to config for now.
295 trackspec = repo.ui.config(b'lfs', b'track')
295 trackspec = repo.ui.config(b'lfs', b'track')
296
296
297 # deprecated config: lfs.threshold
297 # deprecated config: lfs.threshold
298 threshold = repo.ui.configbytes(b'lfs', b'threshold')
298 threshold = repo.ui.configbytes(b'lfs', b'threshold')
299 if threshold:
299 if threshold:
300 filesetlang.parse(trackspec) # make sure syntax errors are confined
300 filesetlang.parse(trackspec) # make sure syntax errors are confined
301 trackspec = b"(%s) | size('>%d')" % (trackspec, threshold)
301 trackspec = b"(%s) | size('>%d')" % (trackspec, threshold)
302
302
303 return minifileset.compile(trackspec)
303 return minifileset.compile(trackspec)
304
304
305 data = repo.wvfs.tryread(b'.hglfs')
305 data = repo.wvfs.tryread(b'.hglfs')
306 if not data:
306 if not data:
307 return lambda p, s: False
307 return lambda p, s: False
308
308
309 # Parse errors here will abort with a message that points to the .hglfs file
309 # Parse errors here will abort with a message that points to the .hglfs file
310 # and line number.
310 # and line number.
311 cfg = config.config()
311 cfg = config.config()
312 cfg.parse(b'.hglfs', data)
312 cfg.parse(b'.hglfs', data)
313
313
314 try:
314 try:
315 rules = [
315 rules = [
316 (minifileset.compile(pattern), minifileset.compile(rule))
316 (minifileset.compile(pattern), minifileset.compile(rule))
317 for pattern, rule in cfg.items(b'track')
317 for pattern, rule in cfg.items(b'track')
318 ]
318 ]
319 except error.ParseError as e:
319 except error.ParseError as e:
320 # The original exception gives no indicator that the error is in the
320 # The original exception gives no indicator that the error is in the
321 # .hglfs file, so add that.
321 # .hglfs file, so add that.
322
322
323 # TODO: See if the line number of the file can be made available.
323 # TODO: See if the line number of the file can be made available.
324 raise error.Abort(_(b'parse error in .hglfs: %s') % e)
324 raise error.Abort(_(b'parse error in .hglfs: %s') % e)
325
325
326 def _match(path, size):
326 def _match(path, size):
327 for pat, rule in rules:
327 for pat, rule in rules:
328 if pat(path, size):
328 if pat(path, size):
329 return rule(path, size)
329 return rule(path, size)
330
330
331 return False
331 return False
332
332
333 return _match
333 return _match
334
334
335
335
336 # Called by remotefilelog
336 # Called by remotefilelog
337 def wrapfilelog(filelog):
337 def wrapfilelog(filelog):
338 wrapfunction = extensions.wrapfunction
338 wrapfunction = extensions.wrapfunction
339
339
340 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
340 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
341 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
341 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
342 wrapfunction(filelog, 'size', wrapper.filelogsize)
342 wrapfunction(filelog, 'size', wrapper.filelogsize)
343
343
344
344
345 @eh.wrapfunction(localrepo, b'resolverevlogstorevfsoptions')
345 @eh.wrapfunction(localrepo, b'resolverevlogstorevfsoptions')
346 def _resolverevlogstorevfsoptions(orig, ui, requirements, features):
346 def _resolverevlogstorevfsoptions(orig, ui, requirements, features):
347 opts = orig(ui, requirements, features)
347 opts = orig(ui, requirements, features)
348 for name, module in extensions.extensions(ui):
348 for name, module in extensions.extensions(ui):
349 if module is sys.modules[__name__]:
349 if module is sys.modules[__name__]:
350 if revlog.REVIDX_EXTSTORED in opts[b'flagprocessors']:
350 if revlog.REVIDX_EXTSTORED in opts[b'flagprocessors']:
351 msg = (
351 msg = (
352 _(b"cannot register multiple processors on flag '%#x'.")
352 _(b"cannot register multiple processors on flag '%#x'.")
353 % revlog.REVIDX_EXTSTORED
353 % revlog.REVIDX_EXTSTORED
354 )
354 )
355 raise error.Abort(msg)
355 raise error.Abort(msg)
356
356
357 opts[b'flagprocessors'][revlog.REVIDX_EXTSTORED] = lfsprocessor
357 opts[b'flagprocessors'][revlog.REVIDX_EXTSTORED] = lfsprocessor
358 break
358 break
359
359
360 return opts
360 return opts
361
361
362
362
363 @eh.extsetup
363 @eh.extsetup
364 def _extsetup(ui):
364 def _extsetup(ui):
365 wrapfilelog(filelog.filelog)
365 wrapfilelog(filelog.filelog)
366
366
367 context.basefilectx.islfs = wrapper.filectxislfs
367 context.basefilectx.islfs = wrapper.filectxislfs
368
368
369 scmutil.fileprefetchhooks.add(b'lfs', wrapper._prefetchfiles)
369 scmutil.fileprefetchhooks.add(b'lfs', wrapper._prefetchfiles)
370
370
371 # Make bundle choose changegroup3 instead of changegroup2. This affects
371 # Make bundle choose changegroup3 instead of changegroup2. This affects
372 # "hg bundle" command. Note: it does not cover all bundle formats like
372 # "hg bundle" command. Note: it does not cover all bundle formats like
373 # "packed1". Using "packed1" with lfs will likely cause trouble.
373 # "packed1". Using "packed1" with lfs will likely cause trouble.
374 bundlecaches._bundlespeccontentopts[b"v2"][b"cg.version"] = b"03"
374 bundlecaches._bundlespeccontentopts[b"v2"][b"cg.version"] = b"03"
375
375
376
376
377 @eh.filesetpredicate(b'lfs()')
377 @eh.filesetpredicate(b'lfs()')
378 def lfsfileset(mctx, x):
378 def lfsfileset(mctx, x):
379 """File that uses LFS storage."""
379 """File that uses LFS storage."""
380 # i18n: "lfs" is a keyword
380 # i18n: "lfs" is a keyword
381 filesetlang.getargs(x, 0, 0, _(b"lfs takes no arguments"))
381 filesetlang.getargs(x, 0, 0, _(b"lfs takes no arguments"))
382 ctx = mctx.ctx
382 ctx = mctx.ctx
383
383
384 def lfsfilep(f):
384 def lfsfilep(f):
385 return wrapper.pointerfromctx(ctx, f, removed=True) is not None
385 return wrapper.pointerfromctx(ctx, f, removed=True) is not None
386
386
387 return mctx.predicate(lfsfilep, predrepr=b'<lfs>')
387 return mctx.predicate(lfsfilep, predrepr=b'<lfs>')
388
388
389
389
390 @eh.templatekeyword(b'lfs_files', requires={b'ctx'})
390 @eh.templatekeyword(b'lfs_files', requires={b'ctx'})
391 def lfsfiles(context, mapping):
391 def lfsfiles(context, mapping):
392 """List of strings. All files modified, added, or removed by this
392 """List of strings. All files modified, added, or removed by this
393 changeset."""
393 changeset."""
394 ctx = context.resource(mapping, b'ctx')
394 ctx = context.resource(mapping, b'ctx')
395
395
396 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
396 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
397 files = sorted(pointers.keys())
397 files = sorted(pointers.keys())
398
398
399 def pointer(v):
399 def pointer(v):
400 # In the file spec, version is first and the other keys are sorted.
400 # In the file spec, version is first and the other keys are sorted.
401 sortkeyfunc = lambda x: (x[0] != b'version', x)
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 return util.sortdict(items)
403 return util.sortdict(items)
404
404
405 makemap = lambda v: {
405 makemap = lambda v: {
406 b'file': v,
406 b'file': v,
407 b'lfsoid': pointers[v].oid() if pointers[v] else None,
407 b'lfsoid': pointers[v].oid() if pointers[v] else None,
408 b'lfspointer': templateutil.hybriddict(pointer(v)),
408 b'lfspointer': templateutil.hybriddict(pointer(v)),
409 }
409 }
410
410
411 # TODO: make the separator ', '?
411 # TODO: make the separator ', '?
412 f = templateutil._showcompatlist(context, mapping, b'lfs_file', files)
412 f = templateutil._showcompatlist(context, mapping, b'lfs_file', files)
413 return templateutil.hybrid(f, files, makemap, pycompat.identity)
413 return templateutil.hybrid(f, files, makemap, pycompat.identity)
414
414
415
415
416 @eh.command(
416 @eh.command(
417 b'debuglfsupload',
417 b'debuglfsupload',
418 [(b'r', b'rev', [], _(b'upload large files introduced by REV'))],
418 [(b'r', b'rev', [], _(b'upload large files introduced by REV'))],
419 )
419 )
420 def debuglfsupload(ui, repo, **opts):
420 def debuglfsupload(ui, repo, **opts):
421 """upload lfs blobs added by the working copy parent or given revisions"""
421 """upload lfs blobs added by the working copy parent or given revisions"""
422 revs = opts.get('rev', [])
422 revs = opts.get('rev', [])
423 pointers = wrapper.extractpointers(repo, logcmdutil.revrange(repo, revs))
423 pointers = wrapper.extractpointers(repo, logcmdutil.revrange(repo, revs))
424 wrapper.uploadblobs(repo, pointers)
424 wrapper.uploadblobs(repo, pointers)
425
425
426
426
427 @eh.wrapcommand(
427 @eh.wrapcommand(
428 b'verify',
428 b'verify',
429 opts=[(b'', b'no-lfs', None, _(b'skip missing lfs blob content'))],
429 opts=[(b'', b'no-lfs', None, _(b'skip missing lfs blob content'))],
430 )
430 )
431 def verify(orig, ui, repo, **opts):
431 def verify(orig, ui, repo, **opts):
432 skipflags = repo.ui.configint(b'verify', b'skipflags')
432 skipflags = repo.ui.configint(b'verify', b'skipflags')
433 no_lfs = opts.pop('no_lfs')
433 no_lfs = opts.pop('no_lfs')
434
434
435 if skipflags:
435 if skipflags:
436 # --lfs overrides the config bit, if set.
436 # --lfs overrides the config bit, if set.
437 if no_lfs is False:
437 if no_lfs is False:
438 skipflags &= ~repository.REVISION_FLAG_EXTSTORED
438 skipflags &= ~repository.REVISION_FLAG_EXTSTORED
439 else:
439 else:
440 skipflags = 0
440 skipflags = 0
441
441
442 if no_lfs is True:
442 if no_lfs is True:
443 skipflags |= repository.REVISION_FLAG_EXTSTORED
443 skipflags |= repository.REVISION_FLAG_EXTSTORED
444
444
445 with ui.configoverride({(b'verify', b'skipflags'): skipflags}):
445 with ui.configoverride({(b'verify', b'skipflags'): skipflags}):
446 return orig(ui, repo, **opts)
446 return orig(ui, repo, **opts)
@@ -1,88 +1,88 b''
1 # pointer.py - Git-LFS pointer serialization
1 # pointer.py - Git-LFS pointer serialization
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8
8
9 import re
9 import re
10
10
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 from mercurial import (
13 from mercurial import (
14 error,
14 error,
15 pycompat,
15 pycompat,
16 )
16 )
17 from mercurial.utils import stringutil
17 from mercurial.utils import stringutil
18
18
19
19
20 class InvalidPointer(error.StorageError):
20 class InvalidPointer(error.StorageError):
21 pass
21 pass
22
22
23
23
24 class gitlfspointer(dict):
24 class gitlfspointer(dict):
25 VERSION = b'https://git-lfs.github.com/spec/v1'
25 VERSION = b'https://git-lfs.github.com/spec/v1'
26
26
27 def __init__(self, *args, **kwargs):
27 def __init__(self, *args, **kwargs):
28 self[b'version'] = self.VERSION
28 self[b'version'] = self.VERSION
29 super(gitlfspointer, self).__init__(*args)
29 super(gitlfspointer, self).__init__(*args)
30 self.update(pycompat.byteskwargs(kwargs))
30 self.update(pycompat.byteskwargs(kwargs))
31
31
32 @classmethod
32 @classmethod
33 def deserialize(cls, text):
33 def deserialize(cls, text):
34 try:
34 try:
35 return cls(l.split(b' ', 1) for l in text.splitlines()).validate()
35 return cls(l.split(b' ', 1) for l in text.splitlines()).validate()
36 except ValueError: # l.split returns 1 item instead of 2
36 except ValueError: # l.split returns 1 item instead of 2
37 raise InvalidPointer(
37 raise InvalidPointer(
38 _(b'cannot parse git-lfs text: %s') % stringutil.pprint(text)
38 _(b'cannot parse git-lfs text: %s') % stringutil.pprint(text)
39 )
39 )
40
40
41 def serialize(self):
41 def serialize(self):
42 sortkeyfunc = lambda x: (x[0] != b'version', x)
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 return b''.join(b'%s %s\n' % (k, v) for k, v in items)
44 return b''.join(b'%s %s\n' % (k, v) for k, v in items)
45
45
46 def oid(self):
46 def oid(self):
47 return self[b'oid'].split(b':')[-1]
47 return self[b'oid'].split(b':')[-1]
48
48
49 def size(self):
49 def size(self):
50 return int(self[b'size'])
50 return int(self[b'size'])
51
51
52 # regular expressions used by _validate
52 # regular expressions used by _validate
53 # see https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
53 # see https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
54 _keyre = re.compile(br'\A[a-z0-9.-]+\Z')
54 _keyre = re.compile(br'\A[a-z0-9.-]+\Z')
55 _valuere = re.compile(br'\A[^\n]*\Z')
55 _valuere = re.compile(br'\A[^\n]*\Z')
56 _requiredre = {
56 _requiredre = {
57 b'size': re.compile(br'\A[0-9]+\Z'),
57 b'size': re.compile(br'\A[0-9]+\Z'),
58 b'oid': re.compile(br'\Asha256:[0-9a-f]{64}\Z'),
58 b'oid': re.compile(br'\Asha256:[0-9a-f]{64}\Z'),
59 b'version': re.compile(br'\A%s\Z' % stringutil.reescape(VERSION)),
59 b'version': re.compile(br'\A%s\Z' % stringutil.reescape(VERSION)),
60 }
60 }
61
61
62 def validate(self):
62 def validate(self):
63 """raise InvalidPointer on error. return self if there is no error"""
63 """raise InvalidPointer on error. return self if there is no error"""
64 requiredcount = 0
64 requiredcount = 0
65 for k, v in self.items():
65 for k, v in self.items():
66 if k in self._requiredre:
66 if k in self._requiredre:
67 if not self._requiredre[k].match(v):
67 if not self._requiredre[k].match(v):
68 raise InvalidPointer(
68 raise InvalidPointer(
69 _(b'unexpected lfs pointer value: %s=%s')
69 _(b'unexpected lfs pointer value: %s=%s')
70 % (k, stringutil.pprint(v))
70 % (k, stringutil.pprint(v))
71 )
71 )
72 requiredcount += 1
72 requiredcount += 1
73 elif not self._keyre.match(k):
73 elif not self._keyre.match(k):
74 raise InvalidPointer(_(b'unexpected lfs pointer key: %s') % k)
74 raise InvalidPointer(_(b'unexpected lfs pointer key: %s') % k)
75 if not self._valuere.match(v):
75 if not self._valuere.match(v):
76 raise InvalidPointer(
76 raise InvalidPointer(
77 _(b'unexpected lfs pointer value: %s=%s')
77 _(b'unexpected lfs pointer value: %s=%s')
78 % (k, stringutil.pprint(v))
78 % (k, stringutil.pprint(v))
79 )
79 )
80 if len(self._requiredre) != requiredcount:
80 if len(self._requiredre) != requiredcount:
81 miss = sorted(set(self._requiredre.keys()).difference(self.keys()))
81 miss = sorted(set(self._requiredre.keys()).difference(self.keys()))
82 raise InvalidPointer(
82 raise InvalidPointer(
83 _(b'missing lfs pointer keys: %s') % b', '.join(miss)
83 _(b'missing lfs pointer keys: %s') % b', '.join(miss)
84 )
84 )
85 return self
85 return self
86
86
87
87
88 deserialize = gitlfspointer.deserialize
88 deserialize = gitlfspointer.deserialize
General Comments 0
You need to be logged in to leave comments. Login now