You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
239 lines
7.9 KiB
Python
239 lines
7.9 KiB
Python
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
|
|
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
|
#
|
|
# This file is part of logilab-common.
|
|
#
|
|
# logilab-common is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Lesser General Public License as published by the Free
|
|
# Software Foundation, either version 2.1 of the License, or (at your option) any
|
|
# later version.
|
|
#
|
|
# logilab-common is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License along
|
|
# with logilab-common. If not, see <http://www.gnu.org/licenses/>.
|
|
"""Manipulation of upstream change log files.
|
|
|
|
The upstream change log files format handled is simpler than the one
|
|
often used such as those generated by the default Emacs changelog mode.
|
|
|
|
Sample ChangeLog format::
|
|
|
|
Change log for project Yoo
|
|
==========================
|
|
|
|
--
|
|
* add a new functionality
|
|
|
|
2002-02-01 -- 0.1.1
|
|
* fix bug #435454
|
|
* fix bug #434356
|
|
|
|
2002-01-01 -- 0.1
|
|
* initial release
|
|
|
|
|
|
There is 3 entries in this change log, one for each released version and one
|
|
for the next version (i.e. the current entry).
|
|
Each entry contains a set of messages corresponding to changes done in this
|
|
release.
|
|
All the non empty lines before the first entry are considered as the change
|
|
log title.
|
|
"""
|
|
|
|
__docformat__ = "restructuredtext en"
|
|
|
|
import sys
|
|
from stat import S_IWRITE
|
|
|
|
from six import string_types
|
|
|
|
BULLET = '*'
|
|
SUBBULLET = '-'
|
|
INDENT = ' ' * 4
|
|
|
|
class NoEntry(Exception):
|
|
"""raised when we are unable to find an entry"""
|
|
|
|
class EntryNotFound(Exception):
|
|
"""raised when we are unable to find a given entry"""
|
|
|
|
class Version(tuple):
|
|
"""simple class to handle soft version number has a tuple while
|
|
correctly printing it as X.Y.Z
|
|
"""
|
|
def __new__(cls, versionstr):
|
|
if isinstance(versionstr, string_types):
|
|
versionstr = versionstr.strip(' :') # XXX (syt) duh?
|
|
parsed = cls.parse(versionstr)
|
|
else:
|
|
parsed = versionstr
|
|
return tuple.__new__(cls, parsed)
|
|
|
|
@classmethod
|
|
def parse(cls, versionstr):
|
|
versionstr = versionstr.strip(' :')
|
|
try:
|
|
return [int(i) for i in versionstr.split('.')]
|
|
except ValueError as ex:
|
|
raise ValueError("invalid literal for version '%s' (%s)"%(versionstr, ex))
|
|
|
|
def __str__(self):
|
|
return '.'.join([str(i) for i in self])
|
|
|
|
# upstream change log #########################################################
|
|
|
|
class ChangeLogEntry(object):
|
|
"""a change log entry, i.e. a set of messages associated to a version and
|
|
its release date
|
|
"""
|
|
version_class = Version
|
|
|
|
def __init__(self, date=None, version=None, **kwargs):
|
|
self.__dict__.update(kwargs)
|
|
if version:
|
|
self.version = self.version_class(version)
|
|
else:
|
|
self.version = None
|
|
self.date = date
|
|
self.messages = []
|
|
|
|
def add_message(self, msg):
|
|
"""add a new message"""
|
|
self.messages.append(([msg], []))
|
|
|
|
def complete_latest_message(self, msg_suite):
|
|
"""complete the latest added message
|
|
"""
|
|
if not self.messages:
|
|
raise ValueError('unable to complete last message as there is no previous message)')
|
|
if self.messages[-1][1]: # sub messages
|
|
self.messages[-1][1][-1].append(msg_suite)
|
|
else: # message
|
|
self.messages[-1][0].append(msg_suite)
|
|
|
|
def add_sub_message(self, sub_msg, key=None):
|
|
if not self.messages:
|
|
raise ValueError('unable to complete last message as there is no previous message)')
|
|
if key is None:
|
|
self.messages[-1][1].append([sub_msg])
|
|
else:
|
|
raise NotImplementedError("sub message to specific key are not implemented yet")
|
|
|
|
def write(self, stream=sys.stdout):
|
|
"""write the entry to file """
|
|
stream.write('%s -- %s\n' % (self.date or '', self.version or ''))
|
|
for msg, sub_msgs in self.messages:
|
|
stream.write('%s%s %s\n' % (INDENT, BULLET, msg[0]))
|
|
stream.write(''.join(msg[1:]))
|
|
if sub_msgs:
|
|
stream.write('\n')
|
|
for sub_msg in sub_msgs:
|
|
stream.write('%s%s %s\n' % (INDENT * 2, SUBBULLET, sub_msg[0]))
|
|
stream.write(''.join(sub_msg[1:]))
|
|
stream.write('\n')
|
|
|
|
stream.write('\n\n')
|
|
|
|
class ChangeLog(object):
|
|
"""object representation of a whole ChangeLog file"""
|
|
|
|
entry_class = ChangeLogEntry
|
|
|
|
def __init__(self, changelog_file, title=''):
|
|
self.file = changelog_file
|
|
self.title = title
|
|
self.additional_content = ''
|
|
self.entries = []
|
|
self.load()
|
|
|
|
def __repr__(self):
|
|
return '<ChangeLog %s at %s (%s entries)>' % (self.file, id(self),
|
|
len(self.entries))
|
|
|
|
def add_entry(self, entry):
|
|
"""add a new entry to the change log"""
|
|
self.entries.append(entry)
|
|
|
|
def get_entry(self, version='', create=None):
|
|
""" return a given changelog entry
|
|
if version is omitted, return the current entry
|
|
"""
|
|
if not self.entries:
|
|
if version or not create:
|
|
raise NoEntry()
|
|
self.entries.append(self.entry_class())
|
|
if not version:
|
|
if self.entries[0].version and create is not None:
|
|
self.entries.insert(0, self.entry_class())
|
|
return self.entries[0]
|
|
version = self.version_class(version)
|
|
for entry in self.entries:
|
|
if entry.version == version:
|
|
return entry
|
|
raise EntryNotFound()
|
|
|
|
def add(self, msg, create=None):
|
|
"""add a new message to the latest opened entry"""
|
|
entry = self.get_entry(create=create)
|
|
entry.add_message(msg)
|
|
|
|
def load(self):
|
|
""" read a logilab's ChangeLog from file """
|
|
try:
|
|
stream = open(self.file)
|
|
except IOError:
|
|
return
|
|
last = None
|
|
expect_sub = False
|
|
for line in stream.readlines():
|
|
sline = line.strip()
|
|
words = sline.split()
|
|
# if new entry
|
|
if len(words) == 1 and words[0] == '--':
|
|
expect_sub = False
|
|
last = self.entry_class()
|
|
self.add_entry(last)
|
|
# if old entry
|
|
elif len(words) == 3 and words[1] == '--':
|
|
expect_sub = False
|
|
last = self.entry_class(words[0], words[2])
|
|
self.add_entry(last)
|
|
# if title
|
|
elif sline and last is None:
|
|
self.title = '%s%s' % (self.title, line)
|
|
# if new entry
|
|
elif sline and sline[0] == BULLET:
|
|
expect_sub = False
|
|
last.add_message(sline[1:].strip())
|
|
# if new sub_entry
|
|
elif expect_sub and sline and sline[0] == SUBBULLET:
|
|
last.add_sub_message(sline[1:].strip())
|
|
# if new line for current entry
|
|
elif sline and last.messages:
|
|
last.complete_latest_message(line)
|
|
else:
|
|
expect_sub = True
|
|
self.additional_content += line
|
|
stream.close()
|
|
|
|
def format_title(self):
|
|
return '%s\n\n' % self.title.strip()
|
|
|
|
def save(self):
|
|
"""write back change log"""
|
|
# filetutils isn't importable in appengine, so import locally
|
|
from logilab.common.fileutils import ensure_fs_mode
|
|
ensure_fs_mode(self.file, S_IWRITE)
|
|
self.write(open(self.file, 'w'))
|
|
|
|
def write(self, stream=sys.stdout):
|
|
"""write changelog to stream"""
|
|
stream.write(self.format_title())
|
|
for entry in self.entries:
|
|
entry.write(stream)
|
|
|