# changelog.py - changelog class for mercurial
#
# Copyright 2005-2007 Matt Mackall <[email protected]>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

from node import bin, hex, nullid
from i18n import _
import util, error, revlog, encoding

def _string_escape(text):
   """
   >>> d = {'nl': chr(10), 'bs': chr(92), 'cr': chr(13), 'nul': chr(0)}
   >>> s = "ab%(nl)scd%(bs)s%(bs)sn%(nul)sab%(cr)scd%(bs)s%(nl)s" % d
   >>> s
   'ab\\ncd\\\\\\\\n\\x00ab\\rcd\\\\\\n'
   >>> res = _string_escape(s)
   >>> s == res.decode('string_escape')
   True
   """
   # subset of the string_escape codec
   text = text.replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r')
   return text.replace('\0', '\\0')

def decodeextra(text):
   extra = {}
   for l in text.split('\0'):
       if l:
           k, v = l.decode('string_escape').split(':', 1)
           extra[k] = v
   return extra

def encodeextra(d):
   # keys must be sorted to produce a deterministic changelog entry
   items = [_string_escape('%s:%s' % (k, d[k])) for k in sorted(d)]
   return "\0".join(items)

class appender(object):
   '''the changelog index must be updated last on disk, so we use this class
   to delay writes to it'''
   def __init__(self, fp, buf):
       self.data = buf
       self.fp = fp
       self.offset = fp.tell()
       self.size = util.fstat(fp).st_size

   def end(self):
       return self.size + len("".join(self.data))
   def tell(self):
       return self.offset
   def flush(self):
       pass
   def close(self):
       self.fp.close()

   def seek(self, offset, whence=0):
       '''virtual file offset spans real file and data'''
       if whence == 0:
           self.offset = offset
       elif whence == 1:
           self.offset += offset
       elif whence == 2:
           self.offset = self.end() + offset
       if self.offset < self.size:
           self.fp.seek(self.offset)

   def read(self, count=-1):
       '''only trick here is reads that span real file and data'''
       ret = ""
       if self.offset < self.size:
           s = self.fp.read(count)
           ret = s
           self.offset += len(s)
           if count > 0:
               count -= len(s)
       if count != 0:
           doff = self.offset - self.size
           self.data.insert(0, "".join(self.data))
           del self.data[1:]
           s = self.data[0][doff:doff+count]
           self.offset += len(s)
           ret += s
       return ret

   def write(self, s):
       self.data.append(str(s))
       self.offset += len(s)

def delayopener(opener, target, divert, buf):
   def o(name, mode='r'):
       if name != target:
           return opener(name, mode)
       if divert:
           return opener(name + ".a", mode.replace('a', 'w'))
       # otherwise, divert to memory
       return appender(opener(name, mode), buf)
   return o

class changelog(revlog.revlog):
   def __init__(self, opener):
       revlog.revlog.__init__(self, opener, "00changelog.i")
       self._realopener = opener
       self._delayed = False
       self._divert = False

   def delayupdate(self):
       "delay visibility of index updates to other readers"
       self._delayed = True
       self._divert = (len(self) == 0)
       self._delaybuf = []
       self.opener = delayopener(self._realopener, self.indexfile,
                                 self._divert, self._delaybuf)

   def finalize(self, tr):
       "finalize index updates"
       self._delayed = False
       self.opener = self._realopener
       # move redirected index data back into place
       if self._divert:
           n = self.opener(self.indexfile + ".a").name
           util.rename(n, n[:-2])
       elif self._delaybuf:
           fp = self.opener(self.indexfile, 'a')
           fp.write("".join(self._delaybuf))
           fp.close()
           self._delaybuf = []
       # split when we're done
       self.checkinlinesize(tr)

   def readpending(self, file):
       r = revlog.revlog(self.opener, file)
       self.index = r.index
       self.nodemap = r.nodemap
       self._chunkcache = r._chunkcache

   def writepending(self):
       "create a file containing the unfinalized state for pretxnchangegroup"
       if self._delaybuf:
           # make a temporary copy of the index
           fp1 = self._realopener(self.indexfile)
           fp2 = self._realopener(self.indexfile + ".a", "w")
           fp2.write(fp1.read())
           # add pending data
           fp2.write("".join(self._delaybuf))
           fp2.close()
           # switch modes so finalize can simply rename
           self._delaybuf = []
           self._divert = True

       if self._divert:
           return True

       return False

   def checkinlinesize(self, tr, fp=None):
       if not self._delayed:
           revlog.revlog.checkinlinesize(self, tr, fp)

   def read(self, node):
       """
       format used:
       nodeid\n        : manifest node in ascii
       user\n          : user, no \n or \r allowed
       time tz extra\n : date (time is int or float, timezone is int)
                       : extra is metadatas, encoded and separated by '\0'
                       : older versions ignore it
       files\n\n       : files modified by the cset, no \n or \r allowed
       (.*)            : comment (free text, ideally utf-8)

       changelog v0 doesn't use extra
       """
       text = self.revision(node)
       if not text:
           return (nullid, "", (0, 0), [], "", {'branch': 'default'})
       last = text.index("\n\n")
       desc = encoding.tolocal(text[last + 2:])
       l = text[:last].split('\n')
       manifest = bin(l[0])
       user = encoding.tolocal(l[1])

       extra_data = l[2].split(' ', 2)
       if len(extra_data) != 3:
           time = float(extra_data.pop(0))
           try:
               # various tools did silly things with the time zone field.
               timezone = int(extra_data[0])
           except:
               timezone = 0
           extra = {}
       else:
           time, timezone, extra = extra_data
           time, timezone = float(time), int(timezone)
           extra = decodeextra(extra)
       if not extra.get('branch'):
           extra['branch'] = 'default'
       files = l[3:]
       return (manifest, user, (time, timezone), files, desc, extra)

   def add(self, manifest, files, desc, transaction, p1, p2,
                 user, date=None, extra={}):
       user = user.strip()
       # An empty username or a username with a "\n" will make the
       # revision text contain two "\n\n" sequences -> corrupt
       # repository since read cannot unpack the revision.
       if not user:
           raise error.RevlogError(_("empty username"))
       if "\n" in user:
           raise error.RevlogError(_("username %s contains a newline")
                                   % repr(user))

       # strip trailing whitespace and leading and trailing empty lines
       desc = '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n')

       user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)

       if date:
           parseddate = "%d %d" % util.parsedate(date)
       else:
           parseddate = "%d %d" % util.makedate()
       if extra and extra.get("branch") in ("default", ""):
           del extra["branch"]
       if extra:
           extra = encodeextra(extra)
           parseddate = "%s %s" % (parseddate, extra)
       l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc]
       text = "\n".join(l)
       return self.addrevision(text, transaction, len(self), p1, p2)