|
1 # Copyright 2009-2010 Gregory P. Ward |
|
2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated |
|
3 # Copyright 2010-2011 Fog Creek Software |
|
4 # Copyright 2010-2011 Unity Technologies |
|
5 # |
|
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. |
|
8 |
|
9 '''setup for largefiles repositories: reposetup''' |
|
10 import copy |
|
11 import types |
|
12 import os |
|
13 import re |
|
14 |
|
15 from mercurial import context, error, manifest, match as match_, \ |
|
16 node, util |
|
17 from mercurial.i18n import _ |
|
18 |
|
19 import lfcommands |
|
20 import proto |
|
21 import lfutil |
|
22 |
|
23 def reposetup(ui, repo): |
|
24 # wire repositories should be given new wireproto functions but not the |
|
25 # other largefiles modifications |
|
26 if not repo.local(): |
|
27 return proto.wirereposetup(ui, repo) |
|
28 |
|
29 for name in ('status', 'commitctx', 'commit', 'push'): |
|
30 method = getattr(repo, name) |
|
31 #if not (isinstance(method, types.MethodType) and |
|
32 # method.im_func is repo.__class__.commitctx.im_func): |
|
33 if isinstance(method, types.FunctionType) and method.func_name == \ |
|
34 'wrap': |
|
35 ui.warn(_('largefiles: repo method %r appears to have already been' |
|
36 ' wrapped by another extension: ' |
|
37 'largefiles may behave incorrectly\n') |
|
38 % name) |
|
39 |
|
40 class lfiles_repo(repo.__class__): |
|
41 lfstatus = False |
|
42 def status_nolfiles(self, *args, **kwargs): |
|
43 return super(lfiles_repo, self).status(*args, **kwargs) |
|
44 |
|
45 # When lfstatus is set, return a context that gives the names of lfiles |
|
46 # instead of their corresponding standins and identifies the lfiles as |
|
47 # always binary, regardless of their actual contents. |
|
48 def __getitem__(self, changeid): |
|
49 ctx = super(lfiles_repo, self).__getitem__(changeid) |
|
50 if self.lfstatus: |
|
51 class lfiles_manifestdict(manifest.manifestdict): |
|
52 def __contains__(self, filename): |
|
53 if super(lfiles_manifestdict, |
|
54 self).__contains__(filename): |
|
55 return True |
|
56 return super(lfiles_manifestdict, |
|
57 self).__contains__(lfutil.shortname+'/' + filename) |
|
58 class lfiles_ctx(ctx.__class__): |
|
59 def files(self): |
|
60 filenames = super(lfiles_ctx, self).files() |
|
61 return [re.sub('^\\'+lfutil.shortname+'/', '', filename) for filename |
|
62 in filenames] |
|
63 def manifest(self): |
|
64 man1 = super(lfiles_ctx, self).manifest() |
|
65 man1.__class__ = lfiles_manifestdict |
|
66 return man1 |
|
67 def filectx(self, path, fileid=None, filelog=None): |
|
68 try: |
|
69 result = super(lfiles_ctx, self).filectx(path, |
|
70 fileid, filelog) |
|
71 except error.LookupError: |
|
72 # Adding a null character will cause Mercurial to |
|
73 # identify this as a binary file. |
|
74 result = super(lfiles_ctx, self).filectx( |
|
75 lfutil.shortname + '/' + path, fileid, |
|
76 filelog) |
|
77 olddata = result.data |
|
78 result.data = lambda: olddata() + '\0' |
|
79 return result |
|
80 ctx.__class__ = lfiles_ctx |
|
81 return ctx |
|
82 |
|
83 # Figure out the status of big files and insert them into the |
|
84 # appropriate list in the result. Also removes standin files from |
|
85 # the listing. This function reverts to the original status if |
|
86 # self.lfstatus is False |
|
87 def status(self, node1='.', node2=None, match=None, ignored=False, |
|
88 clean=False, unknown=False, listsubrepos=False): |
|
89 listignored, listclean, listunknown = ignored, clean, unknown |
|
90 if not self.lfstatus: |
|
91 try: |
|
92 return super(lfiles_repo, self).status(node1, node2, match, |
|
93 listignored, listclean, listunknown, listsubrepos) |
|
94 except TypeError: |
|
95 return super(lfiles_repo, self).status(node1, node2, match, |
|
96 listignored, listclean, listunknown) |
|
97 else: |
|
98 # some calls in this function rely on the old version of status |
|
99 self.lfstatus = False |
|
100 if isinstance(node1, context.changectx): |
|
101 ctx1 = node1 |
|
102 else: |
|
103 ctx1 = repo[node1] |
|
104 if isinstance(node2, context.changectx): |
|
105 ctx2 = node2 |
|
106 else: |
|
107 ctx2 = repo[node2] |
|
108 working = ctx2.rev() is None |
|
109 parentworking = working and ctx1 == self['.'] |
|
110 |
|
111 def inctx(file, ctx): |
|
112 try: |
|
113 if ctx.rev() is None: |
|
114 return file in ctx.manifest() |
|
115 ctx[file] |
|
116 return True |
|
117 except: |
|
118 return False |
|
119 |
|
120 # create a copy of match that matches standins instead of |
|
121 # lfiles if matcher not set then it is the always matcher so |
|
122 # overwrite that |
|
123 if match is None: |
|
124 match = match_.always(self.root, self.getcwd()) |
|
125 |
|
126 def tostandin(file): |
|
127 if inctx(lfutil.standin(file), ctx2): |
|
128 return lfutil.standin(file) |
|
129 return file |
|
130 |
|
131 m = copy.copy(match) |
|
132 m._files = [tostandin(f) for f in m._files] |
|
133 |
|
134 # get ignored clean and unknown but remove them later if they |
|
135 # were not asked for |
|
136 try: |
|
137 result = super(lfiles_repo, self).status(node1, node2, m, |
|
138 True, True, True, listsubrepos) |
|
139 except TypeError: |
|
140 result = super(lfiles_repo, self).status(node1, node2, m, |
|
141 True, True, True) |
|
142 if working: |
|
143 # Hold the wlock while we read lfiles and update the |
|
144 # lfdirstate |
|
145 wlock = repo.wlock() |
|
146 try: |
|
147 # Any non lfiles that were explicitly listed must be |
|
148 # taken out or lfdirstate.status will report an error. |
|
149 # The status of these files was already computed using |
|
150 # super's status. |
|
151 lfdirstate = lfutil.openlfdirstate(ui, self) |
|
152 match._files = [f for f in match._files if f in |
|
153 lfdirstate] |
|
154 s = lfdirstate.status(match, [], listignored, |
|
155 listclean, listunknown) |
|
156 (unsure, modified, added, removed, missing, unknown, |
|
157 ignored, clean) = s |
|
158 if parentworking: |
|
159 for lfile in unsure: |
|
160 if ctx1[lfutil.standin(lfile)].data().strip() \ |
|
161 != lfutil.hashfile(self.wjoin(lfile)): |
|
162 modified.append(lfile) |
|
163 else: |
|
164 clean.append(lfile) |
|
165 lfdirstate.normal(lfile) |
|
166 lfdirstate.write() |
|
167 else: |
|
168 tocheck = unsure + modified + added + clean |
|
169 modified, added, clean = [], [], [] |
|
170 |
|
171 for lfile in tocheck: |
|
172 standin = lfutil.standin(lfile) |
|
173 if inctx(standin, ctx1): |
|
174 if ctx1[standin].data().strip() != \ |
|
175 lfutil.hashfile(self.wjoin(lfile)): |
|
176 modified.append(lfile) |
|
177 else: |
|
178 clean.append(lfile) |
|
179 else: |
|
180 added.append(lfile) |
|
181 finally: |
|
182 wlock.release() |
|
183 |
|
184 for standin in ctx1.manifest(): |
|
185 if not lfutil.isstandin(standin): |
|
186 continue |
|
187 lfile = lfutil.splitstandin(standin) |
|
188 if not match(lfile): |
|
189 continue |
|
190 if lfile not in lfdirstate: |
|
191 removed.append(lfile) |
|
192 # Handle unknown and ignored differently |
|
193 lfiles = (modified, added, removed, missing, [], [], clean) |
|
194 result = list(result) |
|
195 # Unknown files |
|
196 result[4] = [f for f in unknown if repo.dirstate[f] == '?'\ |
|
197 and not lfutil.isstandin(f)] |
|
198 # Ignored files must be ignored by both the dirstate and |
|
199 # lfdirstate |
|
200 result[5] = set(ignored).intersection(set(result[5])) |
|
201 # combine normal files and lfiles |
|
202 normals = [[fn for fn in filelist if not \ |
|
203 lfutil.isstandin(fn)] for filelist in result] |
|
204 result = [sorted(list1 + list2) for (list1, list2) in \ |
|
205 zip(normals, lfiles)] |
|
206 else: |
|
207 def toname(f): |
|
208 if lfutil.isstandin(f): |
|
209 return lfutil.splitstandin(f) |
|
210 return f |
|
211 result = [[toname(f) for f in items] for items in result] |
|
212 |
|
213 if not listunknown: |
|
214 result[4] = [] |
|
215 if not listignored: |
|
216 result[5] = [] |
|
217 if not listclean: |
|
218 result[6] = [] |
|
219 self.lfstatus = True |
|
220 return result |
|
221 |
|
222 # This call happens after a commit has occurred. Copy all of the lfiles |
|
223 # into the cache |
|
224 def commitctx(self, *args, **kwargs): |
|
225 node = super(lfiles_repo, self).commitctx(*args, **kwargs) |
|
226 ctx = self[node] |
|
227 for filename in ctx.files(): |
|
228 if lfutil.isstandin(filename) and filename in ctx.manifest(): |
|
229 realfile = lfutil.splitstandin(filename) |
|
230 lfutil.copytocache(self, ctx.node(), realfile) |
|
231 |
|
232 return node |
|
233 |
|
234 # This call happens before a commit has occurred. The lfile standins |
|
235 # have not had their contents updated (to reflect the hash of their |
|
236 # lfile). Do that here. |
|
237 def commit(self, text="", user=None, date=None, match=None, |
|
238 force=False, editor=False, extra={}): |
|
239 orig = super(lfiles_repo, self).commit |
|
240 |
|
241 wlock = repo.wlock() |
|
242 try: |
|
243 if getattr(repo, "_isrebasing", False): |
|
244 # We have to take the time to pull down the new lfiles now. |
|
245 # Otherwise if we are rebasing, any lfiles that were |
|
246 # modified in the changesets we are rebasing on top of get |
|
247 # overwritten either by the rebase or in the first commit |
|
248 # after the rebase. |
|
249 lfcommands.updatelfiles(repo.ui, repo) |
|
250 # Case 1: user calls commit with no specific files or |
|
251 # include/exclude patterns: refresh and commit everything. |
|
252 if (match is None) or (not match.anypats() and not \ |
|
253 match.files()): |
|
254 lfiles = lfutil.listlfiles(self) |
|
255 lfdirstate = lfutil.openlfdirstate(ui, self) |
|
256 # this only loops through lfiles that exist (not |
|
257 # removed/renamed) |
|
258 for lfile in lfiles: |
|
259 if os.path.exists(self.wjoin(lfutil.standin(lfile))): |
|
260 # this handles the case where a rebase is being |
|
261 # performed and the working copy is not updated |
|
262 # yet. |
|
263 if os.path.exists(self.wjoin(lfile)): |
|
264 lfutil.updatestandin(self, |
|
265 lfutil.standin(lfile)) |
|
266 lfdirstate.normal(lfile) |
|
267 for lfile in lfdirstate: |
|
268 if not os.path.exists( |
|
269 repo.wjoin(lfutil.standin(lfile))): |
|
270 try: |
|
271 # Mercurial >= 1.9 |
|
272 lfdirstate.drop(lfile) |
|
273 except AttributeError: |
|
274 # Mercurial <= 1.8 |
|
275 lfdirstate.forget(lfile) |
|
276 lfdirstate.write() |
|
277 |
|
278 return orig(text=text, user=user, date=date, match=match, |
|
279 force=force, editor=editor, extra=extra) |
|
280 |
|
281 for file in match.files(): |
|
282 if lfutil.isstandin(file): |
|
283 raise util.Abort( |
|
284 "Don't commit largefile standin. Commit largefile.") |
|
285 |
|
286 # Case 2: user calls commit with specified patterns: refresh |
|
287 # any matching big files. |
|
288 smatcher = lfutil.composestandinmatcher(self, match) |
|
289 standins = lfutil.dirstate_walk(self.dirstate, smatcher) |
|
290 |
|
291 # No matching big files: get out of the way and pass control to |
|
292 # the usual commit() method. |
|
293 if not standins: |
|
294 return orig(text=text, user=user, date=date, match=match, |
|
295 force=force, editor=editor, extra=extra) |
|
296 |
|
297 # Refresh all matching big files. It's possible that the |
|
298 # commit will end up failing, in which case the big files will |
|
299 # stay refreshed. No harm done: the user modified them and |
|
300 # asked to commit them, so sooner or later we're going to |
|
301 # refresh the standins. Might as well leave them refreshed. |
|
302 lfdirstate = lfutil.openlfdirstate(ui, self) |
|
303 for standin in standins: |
|
304 lfile = lfutil.splitstandin(standin) |
|
305 if lfdirstate[lfile] <> 'r': |
|
306 lfutil.updatestandin(self, standin) |
|
307 lfdirstate.normal(lfile) |
|
308 else: |
|
309 try: |
|
310 # Mercurial >= 1.9 |
|
311 lfdirstate.drop(lfile) |
|
312 except AttributeError: |
|
313 # Mercurial <= 1.8 |
|
314 lfdirstate.forget(lfile) |
|
315 lfdirstate.write() |
|
316 |
|
317 # Cook up a new matcher that only matches regular files or |
|
318 # standins corresponding to the big files requested by the |
|
319 # user. Have to modify _files to prevent commit() from |
|
320 # complaining "not tracked" for big files. |
|
321 lfiles = lfutil.listlfiles(repo) |
|
322 match = copy.copy(match) |
|
323 orig_matchfn = match.matchfn |
|
324 |
|
325 # Check both the list of lfiles and the list of standins |
|
326 # because if a lfile was removed, it won't be in the list of |
|
327 # lfiles at this point |
|
328 match._files += sorted(standins) |
|
329 |
|
330 actualfiles = [] |
|
331 for f in match._files: |
|
332 fstandin = lfutil.standin(f) |
|
333 |
|
334 # Ignore known lfiles and standins |
|
335 if f in lfiles or fstandin in standins: |
|
336 continue |
|
337 |
|
338 # Append directory separator to avoid collisions |
|
339 if not fstandin.endswith(os.sep): |
|
340 fstandin += os.sep |
|
341 |
|
342 # Prevalidate matching standin directories |
|
343 if lfutil.any_(st for st in match._files if \ |
|
344 st.startswith(fstandin)): |
|
345 continue |
|
346 actualfiles.append(f) |
|
347 match._files = actualfiles |
|
348 |
|
349 def matchfn(f): |
|
350 if orig_matchfn(f): |
|
351 return f not in lfiles |
|
352 else: |
|
353 return f in standins |
|
354 |
|
355 match.matchfn = matchfn |
|
356 return orig(text=text, user=user, date=date, match=match, |
|
357 force=force, editor=editor, extra=extra) |
|
358 finally: |
|
359 wlock.release() |
|
360 |
|
361 def push(self, remote, force=False, revs=None, newbranch=False): |
|
362 o = lfutil.findoutgoing(repo, remote, force) |
|
363 if o: |
|
364 toupload = set() |
|
365 o = repo.changelog.nodesbetween(o, revs)[0] |
|
366 for n in o: |
|
367 parents = [p for p in repo.changelog.parents(n) if p != \ |
|
368 node.nullid] |
|
369 ctx = repo[n] |
|
370 files = set(ctx.files()) |
|
371 if len(parents) == 2: |
|
372 mc = ctx.manifest() |
|
373 mp1 = ctx.parents()[0].manifest() |
|
374 mp2 = ctx.parents()[1].manifest() |
|
375 for f in mp1: |
|
376 if f not in mc: |
|
377 files.add(f) |
|
378 for f in mp2: |
|
379 if f not in mc: |
|
380 files.add(f) |
|
381 for f in mc: |
|
382 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, |
|
383 None): |
|
384 files.add(f) |
|
385 |
|
386 toupload = toupload.union(set([ctx[f].data().strip() for f\ |
|
387 in files if lfutil.isstandin(f) and f in ctx])) |
|
388 lfcommands.uploadlfiles(ui, self, remote, toupload) |
|
389 # Mercurial >= 1.6 takes the newbranch argument, try that first. |
|
390 try: |
|
391 return super(lfiles_repo, self).push(remote, force, revs, |
|
392 newbranch) |
|
393 except TypeError: |
|
394 return super(lfiles_repo, self).push(remote, force, revs) |
|
395 |
|
396 repo.__class__ = lfiles_repo |
|
397 |
|
398 def checkrequireslfiles(ui, repo, **kwargs): |
|
399 if 'largefiles' not in repo.requirements and lfutil.any_( |
|
400 lfutil.shortname+'/' in f[0] for f in repo.store.datafiles()): |
|
401 # work around bug in mercurial 1.9 whereby requirements is a list |
|
402 # on newly-cloned repos |
|
403 repo.requirements = set(repo.requirements) |
|
404 |
|
405 repo.requirements |= set(['largefiles']) |
|
406 repo._writerequirements() |
|
407 |
|
408 checkrequireslfiles(ui, repo) |
|
409 |
|
410 ui.setconfig('hooks', 'changegroup.lfiles', checkrequireslfiles) |
|
411 ui.setconfig('hooks', 'commit.lfiles', checkrequireslfiles) |