from cvsgui.App import *
from cvsgui.Cvs import *
from cvsgui.CvsEntry import *
from cvsgui.Macro import *
import os, os.path
import re, string

"""
  CvsGui Macro "Build ChangeLog"
  $Revision: 1.2 $

  written by Oliver Giesen, Oct 2002
  contact:
    email:  ogware@gmx.net
    jabber: ogiesen@jabber.org
    icq:    18777742

  Feel free to modify or distribute in whichever way you like,
   as long as it doesn't limit my personal rights to
   modify and redistribute this code.
   Apart from that the code is supplied "as-is", i.e. without warranty of any
   kind, either expressed or implied, regarding its quality or security.
   Have fun!

  ATTENTION:
   You will need at least WinCvs 1.3.4 to execute any Python macros
   from within WinCvs! This macro has been written against WinCvs 1.3.8 .

  ======
  Usage:

  - Select one or more CVS-folders and/or files

  - Run the macro from the Macros|CVS menu

   ~the ChangeLog file should get created

  =============
  Known Issues / "Un-niceties":

  - This macro could currently not be invoked as a standalone script like
    its TCL and Perl cousins do. I'll probably look into this some day.

  Please report any problems you may encounter or suggestions you might have
  to ogware@gmx.net .
    
"""

def wrapLine( line, wrapmargin=64):
  result = []
  while len(line) > wrapmargin:
    for i in range( wrapmargin, 1, -1):
      if not line[i] in string.letters+string.digits+'@_.':
        result.append( line[:i+1])
        line = line[i+1:]
        break
  result.append( line)
  return result

class BuildChangeLog( Macro):
  def __init__( self):
    Macro.__init__( self, 'Build ChangeLog', MACRO_SELECTION, 0, 'CVS')
    
  def OnCmdUI( self, cmdui):
    self.sel = App.GetSelection()
    enabled = len( self.sel) > 0
    if enabled:
      for entry in self.sel:
        if entry.IsUnknown():
          enabled = 0
          break
    cmdui.Enable( enabled)
  
  def prepareTargets( self):
    targets = []
    if len( self.sel) > 1:
      for entry in self.sel:
        targets.append( entry.GetFullName())
      basedir = os.path.commonprefix( targets)

      if not os.path.exists( basedir):
        raise Exception, 'Error determining common base dir for selected entries!'
      
      for i in range(len(targets)):
        targets[i] = targets[i][len(basedir):]
    else:
      entry = self.sel[0]
      if entry.IsFile():
        basedir = entry.GetPath()
        targets.append( entry.GetName())
      else:
        basedir = entry.GetFullName()
    #  print 'Determined base dir :', self.basedir
    #  print 'Determined targets : ', string.join( self.targets)
    return basedir, targets
    
  def prepareLogFile( self, basedir):
    filename = os.path.join( basedir, 'ChangeLog')
    lastDate = None
    logfile = []
    print
    if os.path.exists( filename):
      print 'Analyzing existing ChangeLog...'
      clFile = open( filename, 'r')
      try:
        oldlog = clFile.readlines()
        lastDate = oldlog[0][:10]
        #print lastDate
        rx = re.compile( '^[0-9]{4}[\-\/][0-9]{2}[\-\/][0-9]{2}')
        assert rx.match(lastDate), 'Existing ChangeLog has unknown format and could not be rewritten!'
        i = 0
        for line in oldlog[1:]:
          m = rx.match(line)
          if m and m.group()<>lastDate:
            break
          else:
            i+=1
        logfile += oldlog[i:]
        #print string.join( logfile[:25], '')
      finally:
        clFile.close()
    return filename, logfile, lastDate
        
  def getLog( self, basedir, lastDate, targets):
    print 'Downloading the log...'
    os.chdir( basedir)
    cvs = Cvs(1, 0)
    args = ['-Q', '-z9', 'log']
    if lastDate:
      args += ['-d%s<'%lastDate]
    args += targets
    #print 'cvs', string.join( args), '\t(in %s)'%os.getcwd()
    code, out, err = cvs.Run( *args)
    if code == 0:
      return code, out
    else:
      return code, err
    
  def parseRevision( self, text, filename, branchnames, entries):
    if text == '': return
    lines = text.splitlines()
    revno = re.match( '^revision ([0-9\.]+)', lines[0]).group(1)
    m = re.match( '^date: ([^ ]+).*author: ([^;]+)', lines[1])
    assert m
    date = string.replace( m.group(1), '/', '-')
    author = m.group(2)
    #skip "branches" line, if any:
    txtidx = 2
    if lines[txtidx][:9] == 'branches:':
      txtidx+=1
    revtext = string.join( lines[txtidx:], '\n')
    if revtext == 'Initial revision' or revtext == 'no message':
      return
    
    del lines, txtidx
    if not entries.has_key(date):
      entries[date]={}
    if not entries[date].has_key(author):
      entries[date][author] = {}
    if not entries[date][author].has_key(revtext):
      entries[date][author][revtext] = []

    rootrev = re.search('(.*)\.[0-9]+$', revno).group(1)
    if branchnames.has_key(rootrev):
      branch = '[%s] '%branchnames[rootrev]
    else:
      branch = ''
    entries[date][author][revtext].append( '%s %s%s'%(filename, branch, revno))
    
  def parseFile( self, text, entries):
    #don't even scan files without selected revisions:
    m = re.search( 'selected revisions: ([0-9]+)', text, re.MULTILINE)
    if m and m.group(1) == '0':
      return
    #parse header:
    filename = None
    branchnames= {}
    section = 0
    idx = 0
    lines = text.splitlines()
    for line in lines:
      if section == 0:
        m = re.match( '^Working file: ([^,]+)', line)
        if m:
          filename = m.group(1)
          #don't log changes to ourselves:
          if filename == self.clName:
            return
          section+=1
      elif section == 1:
        if line[:15] == 'symbolic names:':
          section+=1
      elif section == 2:
        if line[0] == '\t':
          m = re.search( '^\t([^:]+): ([0-9\.]+)\.0(\.[0-9]+)', line)
          if m:
            branchnames[m.group(2)+m.group(3)] = m.group(1)
        else:
          section+=1
      else:
        if line == '-'*28:
          break
      idx+=1
    else:
      return

    del section
    #iterate revisions:
    revisions = string.split( string.join( lines[idx:], '\n'), '-'*28+'\n')
    del lines, idx
    for revision in revisions:
      self.parseRevision( revision, filename, branchnames, entries)
    
  def parseLog( self, logdump):
    print 'Parsing log output...'
    logentries = {}
    files = string.split( logdump, '='*77)
    for file in files:
      self.parseFile( file, logentries)
    return logentries
    
  def writeLogMsg( self, msg, file):
    indent = 2
    dowrap = (len(msg)>64) and( string.count( msg, '\n')== 0)
    for line in msg.splitlines():
      if dowrap:
        for wrappedline in wrapLine( line):
          file.write( '\t'*indent+wrappedline+'\n')
      else:
        file.write( '\t'*indent+line+'\n')
        
  def writeLog( self, entries, filename, oldlog):
    print 'Sorting entries...'
    dates = entries.keys()
    dates.sort()
    dates.reverse()
    print 'Rewriting ChangeLog...'
    clFile = open( filename, 'w')
    try:
      for date in dates:
        authors = entries[date]
        for author, logs in authors.items():
          clFile.write( '%s\t%s\n'%(date, author))
          for log, files in logs.items():
            files.sort()
            for file in files:
              clFile.write( '\t* %s:\n'%file)
            self.writeLogMsg( log, clFile)
            pos = clFile.tell()
            clFile.write('\n')
      clFile.seek(pos)
      clFile.writelines( oldlog)
    finally:
      clFile.close()
      
  def Run( self):
    basedir, targets = self.prepareTargets()
    filename, oldlog, lastDate = self.prepareLogFile( basedir)
    self.clName = filename[len(basedir)+1:]
    code, logdump = self.getLog( basedir, lastDate, targets)
    del targets
    if code > 0:
      print logdump
    else:
      entries = self.parseLog( logdump)
      self.writeLog( entries, filename, oldlog)
    print 'Done.'
             
BuildChangeLog()
