# Copyright 2009-2010 Gregory P. Ward # Copyright 2009-2010 Intelerad Medical Systems Incorporated # Copyright 2010-2011 Fog Creek Software # Copyright 2010-2011 Unity Technologies # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. '''setup for largefiles repositories: reposetup''' import copy import types import os import re from mercurial import context, error, manifest, match as match_, \ node, util from mercurial.i18n import _ import lfcommands import proto import lfutil def reposetup(ui, repo): # wire repositories should be given new wireproto functions but not the # other largefiles modifications if not repo.local(): return proto.wirereposetup(ui, repo) for name in ('status', 'commitctx', 'commit', 'push'): method = getattr(repo, name) #if not (isinstance(method, types.MethodType) and # method.im_func is repo.__class__.commitctx.im_func): if isinstance(method, types.FunctionType) and method.func_name == \ 'wrap': ui.warn(_('largefiles: repo method %r appears to have already been' ' wrapped by another extension: ' 'largefiles may behave incorrectly\n') % name) class lfiles_repo(repo.__class__): lfstatus = False def status_nolfiles(self, *args, **kwargs): return super(lfiles_repo, self).status(*args, **kwargs) # When lfstatus is set, return a context that gives the names of lfiles # instead of their corresponding standins and identifies the lfiles as # always binary, regardless of their actual contents. def __getitem__(self, changeid): ctx = super(lfiles_repo, self).__getitem__(changeid) if self.lfstatus: class lfiles_manifestdict(manifest.manifestdict): def __contains__(self, filename): if super(lfiles_manifestdict, self).__contains__(filename): return True return super(lfiles_manifestdict, self).__contains__(lfutil.shortname+'/' + filename) class lfiles_ctx(ctx.__class__): def files(self): filenames = super(lfiles_ctx, self).files() return [re.sub('^\\'+lfutil.shortname+'/', '', filename) for filename in filenames] def manifest(self): man1 = super(lfiles_ctx, self).manifest() man1.__class__ = lfiles_manifestdict return man1 def filectx(self, path, fileid=None, filelog=None): try: result = super(lfiles_ctx, self).filectx(path, fileid, filelog) except error.LookupError: # Adding a null character will cause Mercurial to # identify this as a binary file. result = super(lfiles_ctx, self).filectx( lfutil.shortname + '/' + path, fileid, filelog) olddata = result.data result.data = lambda: olddata() + '\0' return result ctx.__class__ = lfiles_ctx return ctx # Figure out the status of big files and insert them into the # appropriate list in the result. Also removes standin files from # the listing. This function reverts to the original status if # self.lfstatus is False def status(self, node1='.', node2=None, match=None, ignored=False, clean=False, unknown=False, listsubrepos=False): listignored, listclean, listunknown = ignored, clean, unknown if not self.lfstatus: try: return super(lfiles_repo, self).status(node1, node2, match, listignored, listclean, listunknown, listsubrepos) except TypeError: return super(lfiles_repo, self).status(node1, node2, match, listignored, listclean, listunknown) else: # some calls in this function rely on the old version of status self.lfstatus = False if isinstance(node1, context.changectx): ctx1 = node1 else: ctx1 = repo[node1] if isinstance(node2, context.changectx): ctx2 = node2 else: ctx2 = repo[node2] working = ctx2.rev() is None parentworking = working and ctx1 == self['.'] def inctx(file, ctx): try: if ctx.rev() is None: return file in ctx.manifest() ctx[file] return True except KeyError: return False # create a copy of match that matches standins instead of # lfiles if matcher not set then it is the always matcher so # overwrite that if match is None: match = match_.always(self.root, self.getcwd()) def tostandin(file): if inctx(lfutil.standin(file), ctx2): return lfutil.standin(file) return file m = copy.copy(match) m._files = [tostandin(f) for f in m._files] # get ignored clean and unknown but remove them later if they # were not asked for try: result = super(lfiles_repo, self).status(node1, node2, m, True, True, True, listsubrepos) except TypeError: result = super(lfiles_repo, self).status(node1, node2, m, True, True, True) if working: # Hold the wlock while we read lfiles and update the # lfdirstate wlock = repo.wlock() try: # Any non lfiles that were explicitly listed must be # taken out or lfdirstate.status will report an error. # The status of these files was already computed using # super's status. lfdirstate = lfutil.openlfdirstate(ui, self) match._files = [f for f in match._files if f in lfdirstate] s = lfdirstate.status(match, [], listignored, listclean, listunknown) (unsure, modified, added, removed, missing, unknown, ignored, clean) = s if parentworking: for lfile in unsure: if ctx1[lfutil.standin(lfile)].data().strip() \ != lfutil.hashfile(self.wjoin(lfile)): modified.append(lfile) else: clean.append(lfile) lfdirstate.normal(lfile) lfdirstate.write() else: tocheck = unsure + modified + added + clean modified, added, clean = [], [], [] for lfile in tocheck: standin = lfutil.standin(lfile) if inctx(standin, ctx1): if ctx1[standin].data().strip() != \ lfutil.hashfile(self.wjoin(lfile)): modified.append(lfile) else: clean.append(lfile) else: added.append(lfile) finally: wlock.release() for standin in ctx1.manifest(): if not lfutil.isstandin(standin): continue lfile = lfutil.splitstandin(standin) if not match(lfile): continue if lfile not in lfdirstate: removed.append(lfile) # Handle unknown and ignored differently lfiles = (modified, added, removed, missing, [], [], clean) result = list(result) # Unknown files result[4] = [f for f in unknown if repo.dirstate[f] == '?'\ and not lfutil.isstandin(f)] # Ignored files must be ignored by both the dirstate and # lfdirstate result[5] = set(ignored).intersection(set(result[5])) # combine normal files and lfiles normals = [[fn for fn in filelist if not \ lfutil.isstandin(fn)] for filelist in result] result = [sorted(list1 + list2) for (list1, list2) in \ zip(normals, lfiles)] else: def toname(f): if lfutil.isstandin(f): return lfutil.splitstandin(f) return f result = [[toname(f) for f in items] for items in result] if not listunknown: result[4] = [] if not listignored: result[5] = [] if not listclean: result[6] = [] self.lfstatus = True return result # This call happens after a commit has occurred. Copy all of the lfiles # into the cache def commitctx(self, *args, **kwargs): node = super(lfiles_repo, self).commitctx(*args, **kwargs) ctx = self[node] for filename in ctx.files(): if lfutil.isstandin(filename) and filename in ctx.manifest(): realfile = lfutil.splitstandin(filename) lfutil.copytocache(self, ctx.node(), realfile) return node # This call happens before a commit has occurred. The lfile standins # have not had their contents updated (to reflect the hash of their # lfile). Do that here. def commit(self, text="", user=None, date=None, match=None, force=False, editor=False, extra={}): orig = super(lfiles_repo, self).commit wlock = repo.wlock() try: if getattr(repo, "_isrebasing", False): # We have to take the time to pull down the new lfiles now. # Otherwise if we are rebasing, any lfiles that were # modified in the changesets we are rebasing on top of get # overwritten either by the rebase or in the first commit # after the rebase. lfcommands.updatelfiles(repo.ui, repo) # Case 1: user calls commit with no specific files or # include/exclude patterns: refresh and commit everything. if (match is None) or (not match.anypats() and not \ match.files()): lfiles = lfutil.listlfiles(self) lfdirstate = lfutil.openlfdirstate(ui, self) # this only loops through lfiles that exist (not # removed/renamed) for lfile in lfiles: if os.path.exists(self.wjoin(lfutil.standin(lfile))): # this handles the case where a rebase is being # performed and the working copy is not updated # yet. if os.path.exists(self.wjoin(lfile)): lfutil.updatestandin(self, lfutil.standin(lfile)) lfdirstate.normal(lfile) for lfile in lfdirstate: if not os.path.exists( repo.wjoin(lfutil.standin(lfile))): try: # Mercurial >= 1.9 lfdirstate.drop(lfile) except AttributeError: # Mercurial <= 1.8 lfdirstate.forget(lfile) lfdirstate.write() return orig(text=text, user=user, date=date, match=match, force=force, editor=editor, extra=extra) for file in match.files(): if lfutil.isstandin(file): raise util.Abort( "Don't commit largefile standin. Commit largefile.") # Case 2: user calls commit with specified patterns: refresh # any matching big files. smatcher = lfutil.composestandinmatcher(self, match) standins = lfutil.dirstate_walk(self.dirstate, smatcher) # No matching big files: get out of the way and pass control to # the usual commit() method. if not standins: return orig(text=text, user=user, date=date, match=match, force=force, editor=editor, extra=extra) # Refresh all matching big files. It's possible that the # commit will end up failing, in which case the big files will # stay refreshed. No harm done: the user modified them and # asked to commit them, so sooner or later we're going to # refresh the standins. Might as well leave them refreshed. lfdirstate = lfutil.openlfdirstate(ui, self) for standin in standins: lfile = lfutil.splitstandin(standin) if lfdirstate[lfile] <> 'r': lfutil.updatestandin(self, standin) lfdirstate.normal(lfile) else: try: # Mercurial >= 1.9 lfdirstate.drop(lfile) except AttributeError: # Mercurial <= 1.8 lfdirstate.forget(lfile) lfdirstate.write() # Cook up a new matcher that only matches regular files or # standins corresponding to the big files requested by the # user. Have to modify _files to prevent commit() from # complaining "not tracked" for big files. lfiles = lfutil.listlfiles(repo) match = copy.copy(match) orig_matchfn = match.matchfn # Check both the list of lfiles and the list of standins # because if a lfile was removed, it won't be in the list of # lfiles at this point match._files += sorted(standins) actualfiles = [] for f in match._files: fstandin = lfutil.standin(f) # Ignore known lfiles and standins if f in lfiles or fstandin in standins: continue # Append directory separator to avoid collisions if not fstandin.endswith(os.sep): fstandin += os.sep # Prevalidate matching standin directories if lfutil.any_(st for st in match._files if \ st.startswith(fstandin)): continue actualfiles.append(f) match._files = actualfiles def matchfn(f): if orig_matchfn(f): return f not in lfiles else: return f in standins match.matchfn = matchfn return orig(text=text, user=user, date=date, match=match, force=force, editor=editor, extra=extra) finally: wlock.release() def push(self, remote, force=False, revs=None, newbranch=False): o = lfutil.findoutgoing(repo, remote, force) if o: toupload = set() o = repo.changelog.nodesbetween(o, revs)[0] for n in o: parents = [p for p in repo.changelog.parents(n) if p != \ node.nullid] ctx = repo[n] files = set(ctx.files()) if len(parents) == 2: mc = ctx.manifest() mp1 = ctx.parents()[0].manifest() mp2 = ctx.parents()[1].manifest() for f in mp1: if f not in mc: files.add(f) for f in mp2: if f not in mc: files.add(f) for f in mc: if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None): files.add(f) toupload = toupload.union(set([ctx[f].data().strip() for f\ in files if lfutil.isstandin(f) and f in ctx])) lfcommands.uploadlfiles(ui, self, remote, toupload) # Mercurial >= 1.6 takes the newbranch argument, try that first. try: return super(lfiles_repo, self).push(remote, force, revs, newbranch) except TypeError: return super(lfiles_repo, self).push(remote, force, revs) repo.__class__ = lfiles_repo def checkrequireslfiles(ui, repo, **kwargs): if 'largefiles' not in repo.requirements and lfutil.any_( lfutil.shortname+'/' in f[0] for f in repo.store.datafiles()): # work around bug in mercurial 1.9 whereby requirements is a list # on newly-cloned repos repo.requirements = set(repo.requirements) repo.requirements |= set(['largefiles']) repo._writerequirements() checkrequireslfiles(ui, repo) ui.setconfig('hooks', 'changegroup.lfiles', checkrequireslfiles) ui.setconfig('hooks', 'commit.lfiles', checkrequireslfiles)