##// END OF EJS Templates
largefiles: introduce "_lfcommithooks" to abstract pre-committing procedures...
FUJIWARA Katsunori -
r23186:6de61d0b default
parent child Browse files
Show More
@@ -1,357 +1,362 b''
1 # Copyright 2009-2010 Gregory P. Ward
1 # Copyright 2009-2010 Gregory P. Ward
2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 # Copyright 2010-2011 Fog Creek Software
3 # Copyright 2010-2011 Fog Creek Software
4 # Copyright 2010-2011 Unity Technologies
4 # Copyright 2010-2011 Unity Technologies
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 '''setup for largefiles repositories: reposetup'''
9 '''setup for largefiles repositories: reposetup'''
10 import copy
10 import copy
11 import os
11 import os
12
12
13 from mercurial import error, manifest, match as match_, util
13 from mercurial import error, manifest, match as match_, util
14 from mercurial.i18n import _
14 from mercurial.i18n import _
15 from mercurial import localrepo, scmutil
15 from mercurial import localrepo, scmutil
16
16
17 import lfcommands
17 import lfcommands
18 import lfutil
18 import lfutil
19
19
20 def reposetup(ui, repo):
20 def reposetup(ui, repo):
21 # wire repositories should be given new wireproto functions
21 # wire repositories should be given new wireproto functions
22 # by "proto.wirereposetup()" via "hg.wirepeersetupfuncs"
22 # by "proto.wirereposetup()" via "hg.wirepeersetupfuncs"
23 if not repo.local():
23 if not repo.local():
24 return
24 return
25
25
26 class lfilesrepo(repo.__class__):
26 class lfilesrepo(repo.__class__):
27 lfstatus = False
27 lfstatus = False
28 def status_nolfiles(self, *args, **kwargs):
28 def status_nolfiles(self, *args, **kwargs):
29 return super(lfilesrepo, self).status(*args, **kwargs)
29 return super(lfilesrepo, self).status(*args, **kwargs)
30
30
31 # When lfstatus is set, return a context that gives the names
31 # When lfstatus is set, return a context that gives the names
32 # of largefiles instead of their corresponding standins and
32 # of largefiles instead of their corresponding standins and
33 # identifies the largefiles as always binary, regardless of
33 # identifies the largefiles as always binary, regardless of
34 # their actual contents.
34 # their actual contents.
35 def __getitem__(self, changeid):
35 def __getitem__(self, changeid):
36 ctx = super(lfilesrepo, self).__getitem__(changeid)
36 ctx = super(lfilesrepo, self).__getitem__(changeid)
37 if self.lfstatus:
37 if self.lfstatus:
38 class lfilesmanifestdict(manifest.manifestdict):
38 class lfilesmanifestdict(manifest.manifestdict):
39 def __contains__(self, filename):
39 def __contains__(self, filename):
40 orig = super(lfilesmanifestdict, self).__contains__
40 orig = super(lfilesmanifestdict, self).__contains__
41 return orig(filename) or orig(lfutil.standin(filename))
41 return orig(filename) or orig(lfutil.standin(filename))
42 class lfilesctx(ctx.__class__):
42 class lfilesctx(ctx.__class__):
43 def files(self):
43 def files(self):
44 filenames = super(lfilesctx, self).files()
44 filenames = super(lfilesctx, self).files()
45 return [lfutil.splitstandin(f) or f for f in filenames]
45 return [lfutil.splitstandin(f) or f for f in filenames]
46 def manifest(self):
46 def manifest(self):
47 man1 = super(lfilesctx, self).manifest()
47 man1 = super(lfilesctx, self).manifest()
48 man1.__class__ = lfilesmanifestdict
48 man1.__class__ = lfilesmanifestdict
49 return man1
49 return man1
50 def filectx(self, path, fileid=None, filelog=None):
50 def filectx(self, path, fileid=None, filelog=None):
51 orig = super(lfilesctx, self).filectx
51 orig = super(lfilesctx, self).filectx
52 try:
52 try:
53 if filelog is not None:
53 if filelog is not None:
54 result = orig(path, fileid, filelog)
54 result = orig(path, fileid, filelog)
55 else:
55 else:
56 result = orig(path, fileid)
56 result = orig(path, fileid)
57 except error.LookupError:
57 except error.LookupError:
58 # Adding a null character will cause Mercurial to
58 # Adding a null character will cause Mercurial to
59 # identify this as a binary file.
59 # identify this as a binary file.
60 if filelog is not None:
60 if filelog is not None:
61 result = orig(lfutil.standin(path), fileid,
61 result = orig(lfutil.standin(path), fileid,
62 filelog)
62 filelog)
63 else:
63 else:
64 result = orig(lfutil.standin(path), fileid)
64 result = orig(lfutil.standin(path), fileid)
65 olddata = result.data
65 olddata = result.data
66 result.data = lambda: olddata() + '\0'
66 result.data = lambda: olddata() + '\0'
67 return result
67 return result
68 ctx.__class__ = lfilesctx
68 ctx.__class__ = lfilesctx
69 return ctx
69 return ctx
70
70
71 # Figure out the status of big files and insert them into the
71 # Figure out the status of big files and insert them into the
72 # appropriate list in the result. Also removes standin files
72 # appropriate list in the result. Also removes standin files
73 # from the listing. Revert to the original status if
73 # from the listing. Revert to the original status if
74 # self.lfstatus is False.
74 # self.lfstatus is False.
75 # XXX large file status is buggy when used on repo proxy.
75 # XXX large file status is buggy when used on repo proxy.
76 # XXX this needs to be investigated.
76 # XXX this needs to be investigated.
77 @localrepo.unfilteredmethod
77 @localrepo.unfilteredmethod
78 def status(self, node1='.', node2=None, match=None, ignored=False,
78 def status(self, node1='.', node2=None, match=None, ignored=False,
79 clean=False, unknown=False, listsubrepos=False):
79 clean=False, unknown=False, listsubrepos=False):
80 listignored, listclean, listunknown = ignored, clean, unknown
80 listignored, listclean, listunknown = ignored, clean, unknown
81 orig = super(lfilesrepo, self).status
81 orig = super(lfilesrepo, self).status
82 if not self.lfstatus:
82 if not self.lfstatus:
83 return orig(node1, node2, match, listignored, listclean,
83 return orig(node1, node2, match, listignored, listclean,
84 listunknown, listsubrepos)
84 listunknown, listsubrepos)
85
85
86 # some calls in this function rely on the old version of status
86 # some calls in this function rely on the old version of status
87 self.lfstatus = False
87 self.lfstatus = False
88 ctx1 = self[node1]
88 ctx1 = self[node1]
89 ctx2 = self[node2]
89 ctx2 = self[node2]
90 working = ctx2.rev() is None
90 working = ctx2.rev() is None
91 parentworking = working and ctx1 == self['.']
91 parentworking = working and ctx1 == self['.']
92
92
93 if match is None:
93 if match is None:
94 match = match_.always(self.root, self.getcwd())
94 match = match_.always(self.root, self.getcwd())
95
95
96 wlock = None
96 wlock = None
97 try:
97 try:
98 try:
98 try:
99 # updating the dirstate is optional
99 # updating the dirstate is optional
100 # so we don't wait on the lock
100 # so we don't wait on the lock
101 wlock = self.wlock(False)
101 wlock = self.wlock(False)
102 except error.LockError:
102 except error.LockError:
103 pass
103 pass
104
104
105 # First check if paths or patterns were specified on the
105 # First check if paths or patterns were specified on the
106 # command line. If there were, and they don't match any
106 # command line. If there were, and they don't match any
107 # largefiles, we should just bail here and let super
107 # largefiles, we should just bail here and let super
108 # handle it -- thus gaining a big performance boost.
108 # handle it -- thus gaining a big performance boost.
109 lfdirstate = lfutil.openlfdirstate(ui, self)
109 lfdirstate = lfutil.openlfdirstate(ui, self)
110 if not match.always():
110 if not match.always():
111 for f in lfdirstate:
111 for f in lfdirstate:
112 if match(f):
112 if match(f):
113 break
113 break
114 else:
114 else:
115 return orig(node1, node2, match, listignored, listclean,
115 return orig(node1, node2, match, listignored, listclean,
116 listunknown, listsubrepos)
116 listunknown, listsubrepos)
117
117
118 # Create a copy of match that matches standins instead
118 # Create a copy of match that matches standins instead
119 # of largefiles.
119 # of largefiles.
120 def tostandins(files):
120 def tostandins(files):
121 if not working:
121 if not working:
122 return files
122 return files
123 newfiles = []
123 newfiles = []
124 dirstate = self.dirstate
124 dirstate = self.dirstate
125 for f in files:
125 for f in files:
126 sf = lfutil.standin(f)
126 sf = lfutil.standin(f)
127 if sf in dirstate:
127 if sf in dirstate:
128 newfiles.append(sf)
128 newfiles.append(sf)
129 elif sf in dirstate.dirs():
129 elif sf in dirstate.dirs():
130 # Directory entries could be regular or
130 # Directory entries could be regular or
131 # standin, check both
131 # standin, check both
132 newfiles.extend((f, sf))
132 newfiles.extend((f, sf))
133 else:
133 else:
134 newfiles.append(f)
134 newfiles.append(f)
135 return newfiles
135 return newfiles
136
136
137 m = copy.copy(match)
137 m = copy.copy(match)
138 m._files = tostandins(m._files)
138 m._files = tostandins(m._files)
139
139
140 result = orig(node1, node2, m, ignored, clean, unknown,
140 result = orig(node1, node2, m, ignored, clean, unknown,
141 listsubrepos)
141 listsubrepos)
142 if working:
142 if working:
143
143
144 def sfindirstate(f):
144 def sfindirstate(f):
145 sf = lfutil.standin(f)
145 sf = lfutil.standin(f)
146 dirstate = self.dirstate
146 dirstate = self.dirstate
147 return sf in dirstate or sf in dirstate.dirs()
147 return sf in dirstate or sf in dirstate.dirs()
148
148
149 match._files = [f for f in match._files
149 match._files = [f for f in match._files
150 if sfindirstate(f)]
150 if sfindirstate(f)]
151 # Don't waste time getting the ignored and unknown
151 # Don't waste time getting the ignored and unknown
152 # files from lfdirstate
152 # files from lfdirstate
153 unsure, s = lfdirstate.status(match, [], False, listclean,
153 unsure, s = lfdirstate.status(match, [], False, listclean,
154 False)
154 False)
155 (modified, added, removed, clean) = (s.modified, s.added,
155 (modified, added, removed, clean) = (s.modified, s.added,
156 s.removed, s.clean)
156 s.removed, s.clean)
157 if parentworking:
157 if parentworking:
158 for lfile in unsure:
158 for lfile in unsure:
159 standin = lfutil.standin(lfile)
159 standin = lfutil.standin(lfile)
160 if standin not in ctx1:
160 if standin not in ctx1:
161 # from second parent
161 # from second parent
162 modified.append(lfile)
162 modified.append(lfile)
163 elif ctx1[standin].data().strip() \
163 elif ctx1[standin].data().strip() \
164 != lfutil.hashfile(self.wjoin(lfile)):
164 != lfutil.hashfile(self.wjoin(lfile)):
165 modified.append(lfile)
165 modified.append(lfile)
166 else:
166 else:
167 if listclean:
167 if listclean:
168 clean.append(lfile)
168 clean.append(lfile)
169 lfdirstate.normal(lfile)
169 lfdirstate.normal(lfile)
170 else:
170 else:
171 tocheck = unsure + modified + added + clean
171 tocheck = unsure + modified + added + clean
172 modified, added, clean = [], [], []
172 modified, added, clean = [], [], []
173
173
174 for lfile in tocheck:
174 for lfile in tocheck:
175 standin = lfutil.standin(lfile)
175 standin = lfutil.standin(lfile)
176 if standin in ctx1:
176 if standin in ctx1:
177 abslfile = self.wjoin(lfile)
177 abslfile = self.wjoin(lfile)
178 if ((ctx1[standin].data().strip() !=
178 if ((ctx1[standin].data().strip() !=
179 lfutil.hashfile(abslfile)) or
179 lfutil.hashfile(abslfile)) or
180 (('x' in ctx1.flags(standin)) !=
180 (('x' in ctx1.flags(standin)) !=
181 bool(lfutil.getexecutable(abslfile)))):
181 bool(lfutil.getexecutable(abslfile)))):
182 modified.append(lfile)
182 modified.append(lfile)
183 elif listclean:
183 elif listclean:
184 clean.append(lfile)
184 clean.append(lfile)
185 else:
185 else:
186 added.append(lfile)
186 added.append(lfile)
187
187
188 # at this point, 'removed' contains largefiles
188 # at this point, 'removed' contains largefiles
189 # marked as 'R' in the working context.
189 # marked as 'R' in the working context.
190 # then, largefiles not managed also in the target
190 # then, largefiles not managed also in the target
191 # context should be excluded from 'removed'.
191 # context should be excluded from 'removed'.
192 removed = [lfile for lfile in removed
192 removed = [lfile for lfile in removed
193 if lfutil.standin(lfile) in ctx1]
193 if lfutil.standin(lfile) in ctx1]
194
194
195 # Standins no longer found in lfdirstate has been
195 # Standins no longer found in lfdirstate has been
196 # removed
196 # removed
197 for standin in ctx1.walk(lfutil.getstandinmatcher(self)):
197 for standin in ctx1.walk(lfutil.getstandinmatcher(self)):
198 lfile = lfutil.splitstandin(standin)
198 lfile = lfutil.splitstandin(standin)
199 if not match(lfile):
199 if not match(lfile):
200 continue
200 continue
201 if lfile not in lfdirstate:
201 if lfile not in lfdirstate:
202 removed.append(lfile)
202 removed.append(lfile)
203
203
204 # Filter result lists
204 # Filter result lists
205 result = list(result)
205 result = list(result)
206
206
207 # Largefiles are not really removed when they're
207 # Largefiles are not really removed when they're
208 # still in the normal dirstate. Likewise, normal
208 # still in the normal dirstate. Likewise, normal
209 # files are not really removed if they are still in
209 # files are not really removed if they are still in
210 # lfdirstate. This happens in merges where files
210 # lfdirstate. This happens in merges where files
211 # change type.
211 # change type.
212 removed = [f for f in removed
212 removed = [f for f in removed
213 if f not in self.dirstate]
213 if f not in self.dirstate]
214 result[2] = [f for f in result[2]
214 result[2] = [f for f in result[2]
215 if f not in lfdirstate]
215 if f not in lfdirstate]
216
216
217 lfiles = set(lfdirstate._map)
217 lfiles = set(lfdirstate._map)
218 # Unknown files
218 # Unknown files
219 result[4] = set(result[4]).difference(lfiles)
219 result[4] = set(result[4]).difference(lfiles)
220 # Ignored files
220 # Ignored files
221 result[5] = set(result[5]).difference(lfiles)
221 result[5] = set(result[5]).difference(lfiles)
222 # combine normal files and largefiles
222 # combine normal files and largefiles
223 normals = [[fn for fn in filelist
223 normals = [[fn for fn in filelist
224 if not lfutil.isstandin(fn)]
224 if not lfutil.isstandin(fn)]
225 for filelist in result]
225 for filelist in result]
226 lfstatus = (modified, added, removed, s.deleted, [], [],
226 lfstatus = (modified, added, removed, s.deleted, [], [],
227 clean)
227 clean)
228 result = [sorted(list1 + list2)
228 result = [sorted(list1 + list2)
229 for (list1, list2) in zip(normals, lfstatus)]
229 for (list1, list2) in zip(normals, lfstatus)]
230 else: # not against working directory
230 else: # not against working directory
231 result = [[lfutil.splitstandin(f) or f for f in items]
231 result = [[lfutil.splitstandin(f) or f for f in items]
232 for items in result]
232 for items in result]
233
233
234 if wlock:
234 if wlock:
235 lfdirstate.write()
235 lfdirstate.write()
236
236
237 finally:
237 finally:
238 if wlock:
238 if wlock:
239 wlock.release()
239 wlock.release()
240
240
241 self.lfstatus = True
241 self.lfstatus = True
242 return scmutil.status(*result)
242 return scmutil.status(*result)
243
243
244 # As part of committing, copy all of the largefiles into the
244 # As part of committing, copy all of the largefiles into the
245 # cache.
245 # cache.
246 def commitctx(self, ctx, *args, **kwargs):
246 def commitctx(self, ctx, *args, **kwargs):
247 node = super(lfilesrepo, self).commitctx(ctx, *args, **kwargs)
247 node = super(lfilesrepo, self).commitctx(ctx, *args, **kwargs)
248 lfutil.copyalltostore(self, node)
248 lfutil.copyalltostore(self, node)
249 class lfilesctx(ctx.__class__):
249 class lfilesctx(ctx.__class__):
250 def markcommitted(self, node):
250 def markcommitted(self, node):
251 orig = super(lfilesctx, self).markcommitted
251 orig = super(lfilesctx, self).markcommitted
252 return lfutil.markcommitted(orig, self, node)
252 return lfutil.markcommitted(orig, self, node)
253 ctx.__class__ = lfilesctx
253 ctx.__class__ = lfilesctx
254 return node
254 return node
255
255
256 # Before commit, largefile standins have not had their
256 # Before commit, largefile standins have not had their
257 # contents updated to reflect the hash of their largefile.
257 # contents updated to reflect the hash of their largefile.
258 # Do that here.
258 # Do that here.
259 def commit(self, text="", user=None, date=None, match=None,
259 def commit(self, text="", user=None, date=None, match=None,
260 force=False, editor=False, extra={}):
260 force=False, editor=False, extra={}):
261 orig = super(lfilesrepo, self).commit
261 orig = super(lfilesrepo, self).commit
262
262
263 wlock = self.wlock()
263 wlock = self.wlock()
264 try:
264 try:
265 match = lfutil.updatestandinsbymatch(self, match)
265 lfcommithook = self._lfcommithooks[-1]
266 match = lfcommithook(self, match)
266 result = orig(text=text, user=user, date=date, match=match,
267 result = orig(text=text, user=user, date=date, match=match,
267 force=force, editor=editor, extra=extra)
268 force=force, editor=editor, extra=extra)
268 return result
269 return result
269 finally:
270 finally:
270 wlock.release()
271 wlock.release()
271
272
272 def push(self, remote, force=False, revs=None, newbranch=False):
273 def push(self, remote, force=False, revs=None, newbranch=False):
273 if remote.local():
274 if remote.local():
274 missing = set(self.requirements) - remote.local().supported
275 missing = set(self.requirements) - remote.local().supported
275 if missing:
276 if missing:
276 msg = _("required features are not"
277 msg = _("required features are not"
277 " supported in the destination:"
278 " supported in the destination:"
278 " %s") % (', '.join(sorted(missing)))
279 " %s") % (', '.join(sorted(missing)))
279 raise util.Abort(msg)
280 raise util.Abort(msg)
280 return super(lfilesrepo, self).push(remote, force=force, revs=revs,
281 return super(lfilesrepo, self).push(remote, force=force, revs=revs,
281 newbranch=newbranch)
282 newbranch=newbranch)
282
283
283 # TODO: _subdirlfs should be moved into "lfutil.py", because
284 # TODO: _subdirlfs should be moved into "lfutil.py", because
284 # it is referred only from "lfutil.updatestandinsbymatch"
285 # it is referred only from "lfutil.updatestandinsbymatch"
285 def _subdirlfs(self, files, lfiles):
286 def _subdirlfs(self, files, lfiles):
286 '''
287 '''
287 Adjust matched file list
288 Adjust matched file list
288 If we pass a directory to commit whose only commitable files
289 If we pass a directory to commit whose only commitable files
289 are largefiles, the core commit code aborts before finding
290 are largefiles, the core commit code aborts before finding
290 the largefiles.
291 the largefiles.
291 So we do the following:
292 So we do the following:
292 For directories that only have largefiles as matches,
293 For directories that only have largefiles as matches,
293 we explicitly add the largefiles to the match list and remove
294 we explicitly add the largefiles to the match list and remove
294 the directory.
295 the directory.
295 In other cases, we leave the match list unmodified.
296 In other cases, we leave the match list unmodified.
296 '''
297 '''
297 actualfiles = []
298 actualfiles = []
298 dirs = []
299 dirs = []
299 regulars = []
300 regulars = []
300
301
301 for f in files:
302 for f in files:
302 if lfutil.isstandin(f + '/'):
303 if lfutil.isstandin(f + '/'):
303 raise util.Abort(
304 raise util.Abort(
304 _('file "%s" is a largefile standin') % f,
305 _('file "%s" is a largefile standin') % f,
305 hint=('commit the largefile itself instead'))
306 hint=('commit the largefile itself instead'))
306 # Scan directories
307 # Scan directories
307 if os.path.isdir(self.wjoin(f)):
308 if os.path.isdir(self.wjoin(f)):
308 dirs.append(f)
309 dirs.append(f)
309 else:
310 else:
310 regulars.append(f)
311 regulars.append(f)
311
312
312 for f in dirs:
313 for f in dirs:
313 matcheddir = False
314 matcheddir = False
314 d = self.dirstate.normalize(f) + '/'
315 d = self.dirstate.normalize(f) + '/'
315 # Check for matched normal files
316 # Check for matched normal files
316 for mf in regulars:
317 for mf in regulars:
317 if self.dirstate.normalize(mf).startswith(d):
318 if self.dirstate.normalize(mf).startswith(d):
318 actualfiles.append(f)
319 actualfiles.append(f)
319 matcheddir = True
320 matcheddir = True
320 break
321 break
321 if not matcheddir:
322 if not matcheddir:
322 # If no normal match, manually append
323 # If no normal match, manually append
323 # any matching largefiles
324 # any matching largefiles
324 for lf in lfiles:
325 for lf in lfiles:
325 if self.dirstate.normalize(lf).startswith(d):
326 if self.dirstate.normalize(lf).startswith(d):
326 actualfiles.append(lf)
327 actualfiles.append(lf)
327 if not matcheddir:
328 if not matcheddir:
328 actualfiles.append(lfutil.standin(f))
329 actualfiles.append(lfutil.standin(f))
329 matcheddir = True
330 matcheddir = True
330 # Nothing in dir, so readd it
331 # Nothing in dir, so readd it
331 # and let commit reject it
332 # and let commit reject it
332 if not matcheddir:
333 if not matcheddir:
333 actualfiles.append(f)
334 actualfiles.append(f)
334
335
335 # Always add normal files
336 # Always add normal files
336 actualfiles += regulars
337 actualfiles += regulars
337 return actualfiles
338 return actualfiles
338
339
339 repo.__class__ = lfilesrepo
340 repo.__class__ = lfilesrepo
340
341
342 # stack of hooks being executed before committing.
343 # only last element ("_lfcommithooks[-1]") is used for each committing.
344 repo._lfcommithooks = [lfutil.updatestandinsbymatch]
345
341 def prepushoutgoinghook(local, remote, outgoing):
346 def prepushoutgoinghook(local, remote, outgoing):
342 if outgoing.missing:
347 if outgoing.missing:
343 toupload = set()
348 toupload = set()
344 addfunc = lambda fn, lfhash: toupload.add(lfhash)
349 addfunc = lambda fn, lfhash: toupload.add(lfhash)
345 lfutil.getlfilestoupload(local, outgoing.missing, addfunc)
350 lfutil.getlfilestoupload(local, outgoing.missing, addfunc)
346 lfcommands.uploadlfiles(ui, local, remote, toupload)
351 lfcommands.uploadlfiles(ui, local, remote, toupload)
347 repo.prepushoutgoinghooks.add("largefiles", prepushoutgoinghook)
352 repo.prepushoutgoinghooks.add("largefiles", prepushoutgoinghook)
348
353
349 def checkrequireslfiles(ui, repo, **kwargs):
354 def checkrequireslfiles(ui, repo, **kwargs):
350 if 'largefiles' not in repo.requirements and util.any(
355 if 'largefiles' not in repo.requirements and util.any(
351 lfutil.shortname+'/' in f[0] for f in repo.store.datafiles()):
356 lfutil.shortname+'/' in f[0] for f in repo.store.datafiles()):
352 repo.requirements.add('largefiles')
357 repo.requirements.add('largefiles')
353 repo._writerequirements()
358 repo._writerequirements()
354
359
355 ui.setconfig('hooks', 'changegroup.lfiles', checkrequireslfiles,
360 ui.setconfig('hooks', 'changegroup.lfiles', checkrequireslfiles,
356 'largefiles')
361 'largefiles')
357 ui.setconfig('hooks', 'commit.lfiles', checkrequireslfiles, 'largefiles')
362 ui.setconfig('hooks', 'commit.lfiles', checkrequireslfiles, 'largefiles')
General Comments 0
You need to be logged in to leave comments. Login now