hgext/largefiles/lfcommands.py
changeset 15168 cfccd3bee7b3
child 15170 c1a4a3220711
equal deleted inserted replaced
15167:8df4166b6f63 15168:cfccd3bee7b3
       
     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 '''High-level command functions: lfadd() et. al, plus the cmdtable.'''
       
    10 
       
    11 import os
       
    12 import shutil
       
    13 
       
    14 from mercurial import util, match as match_, hg, node, context, error
       
    15 from mercurial.i18n import _
       
    16 
       
    17 import lfutil
       
    18 import basestore
       
    19 
       
    20 # -- Commands ----------------------------------------------------------
       
    21 
       
    22 def lfconvert(ui, src, dest, *pats, **opts):
       
    23     '''Convert a normal repository to a largefiles repository
       
    24 
       
    25     Convert source repository creating an identical repository, except that all
       
    26     files that match the patterns given, or are over the given size will be
       
    27     added as largefiles. The size used to determine whether or not to track a
       
    28     file as a largefile is the size of the first version of the file. After
       
    29     running this command you will need to make sure that largefiles is enabled
       
    30     anywhere you intend to push the new repository.'''
       
    31 
       
    32     if opts['tonormal']:
       
    33         tolfile = False
       
    34     else:
       
    35         tolfile = True
       
    36         size = opts['size']
       
    37         if not size:
       
    38             size = ui.config(lfutil.longname, 'size', default=None)
       
    39             try:
       
    40                 size = int(size)
       
    41             except ValueError:
       
    42                 raise util.Abort(_('largefiles.size must be integer, was %s\n') % \
       
    43                     size)
       
    44             except TypeError:
       
    45                 raise util.Abort(_('size must be specified'))
       
    46 
       
    47     try:
       
    48         rsrc = hg.repository(ui, src)
       
    49         if not rsrc.local():
       
    50             raise util.Abort(_('%s is not a local Mercurial repo') % src)
       
    51     except error.RepoError, err:
       
    52         ui.traceback()
       
    53         raise util.Abort(err.args[0])
       
    54     if os.path.exists(dest):
       
    55         if not os.path.isdir(dest):
       
    56             raise util.Abort(_('destination %s already exists') % dest)
       
    57         elif os.listdir(dest):
       
    58             raise util.Abort(_('destination %s is not empty') % dest)
       
    59     try:
       
    60         ui.status(_('initializing destination %s\n') % dest)
       
    61         rdst = hg.repository(ui, dest, create=True)
       
    62         if not rdst.local():
       
    63             raise util.Abort(_('%s is not a local Mercurial repo') % dest)
       
    64     except error.RepoError:
       
    65         ui.traceback()
       
    66         raise util.Abort(_('%s is not a repo') % dest)
       
    67 
       
    68     try:
       
    69         # Lock destination to prevent modification while it is converted to.
       
    70         # Don't need to lock src because we are just reading from its history
       
    71         # which can't change.
       
    72         dst_lock = rdst.lock()
       
    73 
       
    74         # Get a list of all changesets in the source.  The easy way to do this
       
    75         # is to simply walk the changelog, using changelog.nodesbewteen().
       
    76         # Take a look at mercurial/revlog.py:639 for more details.
       
    77         # Use a generator instead of a list to decrease memory usage
       
    78         ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
       
    79             rsrc.heads())[0])
       
    80         revmap = {node.nullid: node.nullid}
       
    81         if tolfile:
       
    82             lfiles = set()
       
    83             normalfiles = set()
       
    84             if not pats:
       
    85                 pats = ui.config(lfutil.longname, 'patterns', default=())
       
    86                 if pats:
       
    87                     pats = pats.split(' ')
       
    88             if pats:
       
    89                 matcher = match_.match(rsrc.root, '', list(pats))
       
    90             else:
       
    91                 matcher = None
       
    92 
       
    93             lfiletohash = {}
       
    94             for ctx in ctxs:
       
    95                 ui.progress(_('converting revisions'), ctx.rev(),
       
    96                     unit=_('revision'), total=rsrc['tip'].rev())
       
    97                 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
       
    98                     lfiles, normalfiles, matcher, size, lfiletohash)
       
    99             ui.progress(_('converting revisions'), None)
       
   100 
       
   101             if os.path.exists(rdst.wjoin(lfutil.shortname)):
       
   102                 shutil.rmtree(rdst.wjoin(lfutil.shortname))
       
   103 
       
   104             for f in lfiletohash.keys():
       
   105                 if os.path.isfile(rdst.wjoin(f)):
       
   106                     os.unlink(rdst.wjoin(f))
       
   107                 try:
       
   108                     os.removedirs(os.path.dirname(rdst.wjoin(f)))
       
   109                 except:
       
   110                     pass
       
   111 
       
   112         else:
       
   113             for ctx in ctxs:
       
   114                 ui.progress(_('converting revisions'), ctx.rev(),
       
   115                     unit=_('revision'), total=rsrc['tip'].rev())
       
   116                 _addchangeset(ui, rsrc, rdst, ctx, revmap)
       
   117 
       
   118             ui.progress(_('converting revisions'), None)
       
   119     except:
       
   120         # we failed, remove the new directory
       
   121         shutil.rmtree(rdst.root)
       
   122         raise
       
   123     finally:
       
   124         dst_lock.release()
       
   125 
       
   126 def _addchangeset(ui, rsrc, rdst, ctx, revmap):
       
   127  # Convert src parents to dst parents
       
   128     parents = []
       
   129     for p in ctx.parents():
       
   130         parents.append(revmap[p.node()])
       
   131     while len(parents) < 2:
       
   132         parents.append(node.nullid)
       
   133 
       
   134     # Generate list of changed files
       
   135     files = set(ctx.files())
       
   136     if node.nullid not in parents:
       
   137         mc = ctx.manifest()
       
   138         mp1 = ctx.parents()[0].manifest()
       
   139         mp2 = ctx.parents()[1].manifest()
       
   140         files |= (set(mp1) | set(mp2)) - set(mc)
       
   141         for f in mc:
       
   142             if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
       
   143                 files.add(f)
       
   144 
       
   145     def getfilectx(repo, memctx, f):
       
   146         if lfutil.standin(f) in files:
       
   147             # if the file isn't in the manifest then it was removed
       
   148             # or renamed, raise IOError to indicate this
       
   149             try:
       
   150                 fctx = ctx.filectx(lfutil.standin(f))
       
   151             except error.LookupError:
       
   152                 raise IOError()
       
   153             renamed = fctx.renamed()
       
   154             if renamed:
       
   155                 renamed = lfutil.splitstandin(renamed[0])
       
   156 
       
   157             hash = fctx.data().strip()
       
   158             path = lfutil.findfile(rsrc, hash)
       
   159             ### TODO: What if the file is not cached?
       
   160             data = ''
       
   161             fd = None
       
   162             try:
       
   163                 fd = open(path, 'rb')
       
   164                 data = fd.read()
       
   165             finally:
       
   166                 if fd: fd.close()
       
   167             return context.memfilectx(f, data, 'l' in fctx.flags(),
       
   168                                       'x' in fctx.flags(), renamed)
       
   169         else:
       
   170             try:
       
   171                 fctx = ctx.filectx(f)
       
   172             except error.LookupError:
       
   173                 raise IOError()
       
   174             renamed = fctx.renamed()
       
   175             if renamed:
       
   176                 renamed = renamed[0]
       
   177             data = fctx.data()
       
   178             if f == '.hgtags':
       
   179                 newdata = []
       
   180                 for line in data.splitlines():
       
   181                     id, name = line.split(' ', 1)
       
   182                     newdata.append('%s %s\n' % (node.hex(revmap[node.bin(id)]),
       
   183                         name))
       
   184                 data = ''.join(newdata)
       
   185             return context.memfilectx(f, data, 'l' in fctx.flags(),
       
   186                                       'x' in fctx.flags(), renamed)
       
   187 
       
   188     dstfiles = []
       
   189     for file in files:
       
   190         if lfutil.isstandin(file):
       
   191             dstfiles.append(lfutil.splitstandin(file))
       
   192         else:
       
   193             dstfiles.append(file)
       
   194     # Commit
       
   195     mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
       
   196                           getfilectx, ctx.user(), ctx.date(), ctx.extra())
       
   197     ret = rdst.commitctx(mctx)
       
   198     rdst.dirstate.setparents(ret)
       
   199     revmap[ctx.node()] = rdst.changelog.tip()
       
   200 
       
   201 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
       
   202         matcher, size, lfiletohash):
       
   203     # Convert src parents to dst parents
       
   204     parents = []
       
   205     for p in ctx.parents():
       
   206         parents.append(revmap[p.node()])
       
   207     while len(parents) < 2:
       
   208         parents.append(node.nullid)
       
   209 
       
   210     # Generate list of changed files
       
   211     files = set(ctx.files())
       
   212     if node.nullid not in parents:
       
   213         mc = ctx.manifest()
       
   214         mp1 = ctx.parents()[0].manifest()
       
   215         mp2 = ctx.parents()[1].manifest()
       
   216         files |= (set(mp1) | set(mp2)) - set(mc)
       
   217         for f in mc:
       
   218             if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
       
   219                 files.add(f)
       
   220 
       
   221     dstfiles = []
       
   222     for f in files:
       
   223         if f not in lfiles and f not in normalfiles:
       
   224             islfile = _islfile(f, ctx, matcher, size)
       
   225             # If this file was renamed or copied then copy
       
   226             # the lfileness of its predecessor
       
   227             if f in ctx.manifest():
       
   228                 fctx = ctx.filectx(f)
       
   229                 renamed = fctx.renamed()
       
   230                 renamedlfile = renamed and renamed[0] in lfiles
       
   231                 islfile |= renamedlfile
       
   232                 if 'l' in fctx.flags():
       
   233                     if renamedlfile:
       
   234                         raise util.Abort(
       
   235                             _('Renamed/copied largefile %s becomes symlink') % f)
       
   236                     islfile = False
       
   237             if islfile:
       
   238                 lfiles.add(f)
       
   239             else:
       
   240                 normalfiles.add(f)
       
   241 
       
   242         if f in lfiles:
       
   243             dstfiles.append(lfutil.standin(f))
       
   244             # lfile in manifest if it has not been removed/renamed
       
   245             if f in ctx.manifest():
       
   246                 if 'l' in ctx.filectx(f).flags():
       
   247                     if renamed and renamed[0] in lfiles:
       
   248                         raise util.Abort(_('largefile %s becomes symlink') % f)
       
   249 
       
   250                 # lfile was modified, update standins
       
   251                 fullpath = rdst.wjoin(f)
       
   252                 lfutil.createdir(os.path.dirname(fullpath))
       
   253                 m = util.sha1('')
       
   254                 m.update(ctx[f].data())
       
   255                 hash = m.hexdigest()
       
   256                 if f not in lfiletohash or lfiletohash[f] != hash:
       
   257                     try:
       
   258                         fd = open(fullpath, 'wb')
       
   259                         fd.write(ctx[f].data())
       
   260                     finally:
       
   261                         if fd:
       
   262                             fd.close()
       
   263                     executable = 'x' in ctx[f].flags()
       
   264                     os.chmod(fullpath, lfutil.getmode(executable))
       
   265                     lfutil.writestandin(rdst, lfutil.standin(f), hash,
       
   266                         executable)
       
   267                     lfiletohash[f] = hash
       
   268         else:
       
   269             # normal file
       
   270             dstfiles.append(f)
       
   271 
       
   272     def getfilectx(repo, memctx, f):
       
   273         if lfutil.isstandin(f):
       
   274             # if the file isn't in the manifest then it was removed
       
   275             # or renamed, raise IOError to indicate this
       
   276             srcfname = lfutil.splitstandin(f)
       
   277             try:
       
   278                 fctx = ctx.filectx(srcfname)
       
   279             except error.LookupError:
       
   280                 raise IOError()
       
   281             renamed = fctx.renamed()
       
   282             if renamed:
       
   283                 # standin is always a lfile because lfileness
       
   284                 # doesn't change after rename or copy
       
   285                 renamed = lfutil.standin(renamed[0])
       
   286 
       
   287             return context.memfilectx(f, lfiletohash[srcfname], 'l' in
       
   288                 fctx.flags(), 'x' in fctx.flags(), renamed)
       
   289         else:
       
   290             try:
       
   291                 fctx = ctx.filectx(f)
       
   292             except error.LookupError:
       
   293                 raise IOError()
       
   294             renamed = fctx.renamed()
       
   295             if renamed:
       
   296                 renamed = renamed[0]
       
   297 
       
   298             data = fctx.data()
       
   299             if f == '.hgtags':
       
   300                 newdata = []
       
   301                 for line in data.splitlines():
       
   302                     id, name = line.split(' ', 1)
       
   303                     newdata.append('%s %s\n' % (node.hex(revmap[node.bin(id)]),
       
   304                         name))
       
   305                 data = ''.join(newdata)
       
   306             return context.memfilectx(f, data, 'l' in fctx.flags(),
       
   307                                       'x' in fctx.flags(), renamed)
       
   308 
       
   309     # Commit
       
   310     mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
       
   311                           getfilectx, ctx.user(), ctx.date(), ctx.extra())
       
   312     ret = rdst.commitctx(mctx)
       
   313     rdst.dirstate.setparents(ret)
       
   314     revmap[ctx.node()] = rdst.changelog.tip()
       
   315 
       
   316 def _islfile(file, ctx, matcher, size):
       
   317     '''
       
   318     A file is a lfile if it matches a pattern or is over
       
   319     the given size.
       
   320     '''
       
   321     # Never store hgtags or hgignore as lfiles
       
   322     if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
       
   323         return False
       
   324     if matcher and matcher(file):
       
   325         return True
       
   326     try:
       
   327         return ctx.filectx(file).size() >= size * 1024 * 1024
       
   328     except error.LookupError:
       
   329         return False
       
   330 
       
   331 def uploadlfiles(ui, rsrc, rdst, files):
       
   332     '''upload largefiles to the central store'''
       
   333 
       
   334     # Don't upload locally. All largefiles are in the system wide cache
       
   335     # so the other repo can just get them from there.
       
   336     if not files or rdst.local():
       
   337         return
       
   338 
       
   339     store = basestore._openstore(rsrc, rdst, put=True)
       
   340 
       
   341     at = 0
       
   342     files = filter(lambda h: not store.exists(h), files)
       
   343     for hash in files:
       
   344         ui.progress(_('uploading largefiles'), at, unit='largefile', total=len(files))
       
   345         source = lfutil.findfile(rsrc, hash)
       
   346         if not source:
       
   347             raise util.Abort(_('Missing largefile %s needs to be uploaded') % hash)
       
   348         # XXX check for errors here
       
   349         store.put(source, hash)
       
   350         at += 1
       
   351     ui.progress('uploading largefiles', None)
       
   352 
       
   353 def verifylfiles(ui, repo, all=False, contents=False):
       
   354     '''Verify that every big file revision in the current changeset
       
   355     exists in the central store.  With --contents, also verify that
       
   356     the contents of each big file revision are correct (SHA-1 hash
       
   357     matches the revision ID).  With --all, check every changeset in
       
   358     this repository.'''
       
   359     if all:
       
   360         # Pass a list to the function rather than an iterator because we know a
       
   361         # list will work.
       
   362         revs = range(len(repo))
       
   363     else:
       
   364         revs = ['.']
       
   365 
       
   366     store = basestore._openstore(repo)
       
   367     return store.verify(revs, contents=contents)
       
   368 
       
   369 def cachelfiles(ui, repo, node):
       
   370     '''cachelfiles ensures that all largefiles needed by the specified revision
       
   371     are present in the repository's largefile cache.
       
   372 
       
   373     returns a tuple (cached, missing).  cached is the list of files downloaded
       
   374     by this operation; missing is the list of files that were needed but could
       
   375     not be found.'''
       
   376     lfiles = lfutil.listlfiles(repo, node)
       
   377     toget = []
       
   378 
       
   379     for lfile in lfiles:
       
   380         expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
       
   381         # if it exists and its hash matches, it might have been locally
       
   382         # modified before updating and the user chose 'local'.  in this case,
       
   383         # it will not be in any store, so don't look for it.
       
   384         if (not os.path.exists(repo.wjoin(lfile)) \
       
   385                 or expectedhash != lfutil.hashfile(repo.wjoin(lfile))) and \
       
   386                 not lfutil.findfile(repo, expectedhash):
       
   387             toget.append((lfile, expectedhash))
       
   388 
       
   389     if toget:
       
   390         store = basestore._openstore(repo)
       
   391         ret = store.get(toget)
       
   392         return ret
       
   393 
       
   394     return ([], [])
       
   395 
       
   396 def updatelfiles(ui, repo, filelist=None, printmessage=True):
       
   397     wlock = repo.wlock()
       
   398     try:
       
   399         lfdirstate = lfutil.openlfdirstate(ui, repo)
       
   400         lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
       
   401 
       
   402         if filelist is not None:
       
   403             lfiles = [f for f in lfiles if f in filelist]
       
   404 
       
   405         printed = False
       
   406         if printmessage and lfiles:
       
   407             ui.status(_('getting changed largefiles\n'))
       
   408             printed = True
       
   409             cachelfiles(ui, repo, '.')
       
   410 
       
   411         updated, removed = 0, 0
       
   412         for i in map(lambda f: _updatelfile(repo, lfdirstate, f), lfiles):
       
   413             # increment the appropriate counter according to _updatelfile's
       
   414             # return value
       
   415             updated += i > 0 and i or 0
       
   416             removed -= i < 0 and i or 0
       
   417             if printmessage and (removed or updated) and not printed:
       
   418                 ui.status(_('getting changed largefiles\n'))
       
   419                 printed = True
       
   420 
       
   421         lfdirstate.write()
       
   422         if printed and printmessage:
       
   423             ui.status(_('%d largefiles updated, %d removed\n') % (updated,
       
   424                 removed))
       
   425     finally:
       
   426         wlock.release()
       
   427 
       
   428 def _updatelfile(repo, lfdirstate, lfile):
       
   429     '''updates a single largefile and copies the state of its standin from
       
   430     the repository's dirstate to its state in the lfdirstate.
       
   431 
       
   432     returns 1 if the file was modified, -1 if the file was removed, 0 if the
       
   433     file was unchanged, and None if the needed largefile was missing from the
       
   434     cache.'''
       
   435     ret = 0
       
   436     abslfile = repo.wjoin(lfile)
       
   437     absstandin = repo.wjoin(lfutil.standin(lfile))
       
   438     if os.path.exists(absstandin):
       
   439         if os.path.exists(absstandin+'.orig'):
       
   440             shutil.copyfile(abslfile, abslfile+'.orig')
       
   441         expecthash = lfutil.readstandin(repo, lfile)
       
   442         if expecthash != '' and \
       
   443                 (not os.path.exists(abslfile) or \
       
   444                 expecthash != lfutil.hashfile(abslfile)):
       
   445             if not lfutil.copyfromcache(repo, expecthash, lfile):
       
   446                 return None # don't try to set the mode or update the dirstate
       
   447             ret = 1
       
   448         mode = os.stat(absstandin).st_mode
       
   449         if mode != os.stat(abslfile).st_mode:
       
   450             os.chmod(abslfile, mode)
       
   451             ret = 1
       
   452     else:
       
   453         if os.path.exists(abslfile):
       
   454             os.unlink(abslfile)
       
   455             ret = -1
       
   456     state = repo.dirstate[lfutil.standin(lfile)]
       
   457     if state == 'n':
       
   458         lfdirstate.normal(lfile)
       
   459     elif state == 'r':
       
   460         lfdirstate.remove(lfile)
       
   461     elif state == 'a':
       
   462         lfdirstate.add(lfile)
       
   463     elif state == '?':
       
   464         try:
       
   465             # Mercurial >= 1.9
       
   466             lfdirstate.drop(lfile)
       
   467         except AttributeError:
       
   468             # Mercurial <= 1.8
       
   469             lfdirstate.forget(lfile)
       
   470     return ret
       
   471 
       
   472 # -- hg commands declarations ------------------------------------------------
       
   473 
       
   474 
       
   475 cmdtable = {
       
   476     'lfconvert': (lfconvert,
       
   477                   [('s', 'size', 0, 'All files over this size (in megabytes) '
       
   478                   'will be considered largefiles. This can also be specified in '
       
   479                   'your hgrc as [largefiles].size.'),
       
   480                   ('','tonormal',False,
       
   481                       'Convert from a largefiles repo to a normal repo')],
       
   482                   _('hg lfconvert SOURCE DEST [FILE ...]')),
       
   483     }