[Zope3-checkins] CVS: Products3/z3checkins - message_plain.pt:1.1 configure.zcml:1.2 interfaces.py:1.2 message.pt:1.2 message.py:1.3
Marius Gedminas
mgedmin@codeworks.lt
Thu, 3 Apr 2003 07:08:55 -0500
Update of /cvs-repository/Products3/z3checkins
In directory cvs.zope.org:/tmp/cvs-serv24870/z3checkins
Modified Files:
configure.zcml interfaces.py message.pt message.py
Added Files:
message_plain.pt
Log Message:
- Plain text view for checkin messages (called 'index.txt')
- Next/Previous/First/Last navigation via <link> elements
- Tabs and trailing whitespace are highlighted in diffs
- Message parsing uses less regexes (old way triggered stack overflows)
=== Added File Products3/z3checkins/message_plain.pt ===
From: <span tal:replace="structure context/author" />
Date: <span tal:replace="structure context/date/@@rfc822" />
Subject: <span tal:replace="structure string:[Zope3-checkins] CVS: ${context/subject}" />
Message-Id: <span tal:replace="structure context/message_id" />
<div tal:replace="structure context/body" />
=== Products3/z3checkins/configure.zcml 1.1.1.1 => 1.2 ===
--- Products3/z3checkins/configure.zcml:1.1.1.1 Thu Apr 3 03:19:58 2003
+++ Products3/z3checkins/configure.zcml Thu Apr 3 07:08:24 2003
@@ -15,6 +15,11 @@
<!-- Utilities -->
+ <adapter for="zope.app.interfaces.container.IReadContainer"
+ factory=".message.CheckinMessageAdapter"
+ permission="zope.View"
+ provides=".interfaces.ICheckinMessageArchive" />
+
<utility factory=".message.CheckinMessageParser"
permission="zope.View"
provides=".interfaces.ICheckinMessageParser" />
@@ -67,6 +72,13 @@
for=".interfaces.ICheckinMessage"
name="index.html"
template="message.pt"
+ class=".message.CheckinMessageView"
+ permission="zope.View" />
+
+ <browser:page
+ for=".interfaces.ICheckinMessage"
+ name="index.txt"
+ template="message_plain.pt"
class=".message.CheckinMessageView"
permission="zope.View" />
=== Products3/z3checkins/interfaces.py 1.1.1.1 => 1.2 ===
--- Products3/z3checkins/interfaces.py:1.1.1.1 Thu Apr 3 03:19:59 2003
+++ Products3/z3checkins/interfaces.py Thu Apr 3 07:08:24 2003
@@ -36,3 +36,26 @@
checkin message.
"""
+class ICheckinMessageArchive(Interface):
+ """A chronologically ordered sequence of checkin messages.
+
+ Implements the Python sequence procotol.
+ """
+
+ def __len__():
+ """Returns the number of messages in the archive."""
+
+ def __getitem__(index):
+ """Returns a given message."""
+
+ def __getslice__(start, stop):
+ """Returns a range of messages."""
+
+ def __iter__():
+ """Returns an iterator."""
+
+ def index(message):
+ """Returns the index of a given message.
+
+ Raises ValueError if message is not in the archive."""
+
=== Products3/z3checkins/message.pt 1.1.1.1 => 1.2 ===
--- Products3/z3checkins/message.pt:1.1.1.1 Thu Apr 3 03:19:58 2003
+++ Products3/z3checkins/message.pt Thu Apr 3 07:08:24 2003
@@ -1,6 +1,14 @@
<html>
-<head>
+<head tal:define="next view/next; prev view/previous; first view/first; last view/last">
<title tal:content="string: ${context/author_name} - ${context/subject}" />
+ <link rel="next" tal:condition="next"
+ tal:attributes="href next/@@absolute_url" />
+ <link rel="previous" tal:condition="prev"
+ tal:attributes="href prev/@@absolute_url" />
+ <link rel="first" tal:condition="first"
+ tal:attributes="href first/@@absolute_url" />
+ <link rel="last" tal:condition="last"
+ tal:attributes="href last/@@absolute_url" />
<style type="text/css">
body { color: black; background: white; }
.header { font-weight: bold; }
@@ -16,10 +24,11 @@
.old { background: #e3e0ff; color: red; }
.new { background: #e0ffe6; color: green; }
.signature { color: gray; }
+ .trail { color: gray; }
+ .tab { color: gray; }
</style>
</head>
<body>
-<h1 tal:content="string: ${context/author_name} - ${context/subject}" />
<pre class="headers">
<span class="header">From:</span> <span class="value" tal:content="context/author" />
<span class="header">Date:</span> <span class="value" tal:content="context/date/@@rfc822" />
=== Products3/z3checkins/message.py 1.2 => 1.3 ===
--- Products3/z3checkins/message.py:1.2 Thu Apr 3 04:17:46 2003
+++ Products3/z3checkins/message.py Thu Apr 3 07:08:24 2003
@@ -11,12 +11,16 @@
from StringIO import StringIO
from datetime import datetime, tzinfo, timedelta
from persistence import Persistent
-from zope.component import getUtility
+from zope.component import getUtility, getAdapter, queryAdapter
from zope.app.browser.form.widget import FileWidget
from zope.app.form.widget import CustomWidget
from zope.proxy.context import ContextWrapper
+from zope.app.interfaces.container import IReadContainer
+from zope.proxy.introspection import removeAllProxies
+from zope.proxy.context import getWrapperContainer
from interfaces import ICheckinMessage, ICheckinMessageParser, FormatError
+from interfaces import ICheckinMessageArchive
__metaclass__ = type
@@ -165,6 +169,39 @@
return "".join(log_message).strip()
+class CheckinMessageAdapter:
+ """Adapts a container to a checkin message archive."""
+
+ __implements__ = ICheckinMessageArchive
+ __used_for__ = IReadContainer
+
+ def __init__(self, context):
+ self.context = context
+ items = []
+ for key, item in self.context.items():
+ if ICheckinMessage.isImplementedBy(item):
+ items.append((item.date, key, item))
+ items.sort()
+ self.messages = [ContextWrapper(item, self.context, name=key)
+ for date, key, item in items]
+
+ def __len__(self):
+ return len(self.messages)
+
+ def __getitem__(self, index):
+ return self.messages[index]
+
+ def __getslice__(self, start, stop):
+ return self.messages[start:stop]
+
+ def __iter__(self):
+ return iter(self.messages)
+
+ def index(self, message):
+ return self.messages.index(message)
+
+
+
#
# Browser views
#
@@ -190,14 +227,12 @@
"""Returns a list of the last 'how_many' checkin messages in
self.context, newest first.
"""
- items = []
- for key, item in self.context.items():
- if ICheckinMessage.isImplementedBy(item):
- items.append((item.date, key, item))
- items.sort()
+ if not hasattr(self, '_archive'):
+ self._archive = getAdapter(self.context, ICheckinMessageArchive)
+ items = self._archive[-how_many:]
+ items = removeAllProxies(items) # needed for reverse
items.reverse()
- return [ContextWrapper(item, self.context, name=key)
- for date, key, item in items[:how_many]]
+ return items
class CheckinMessageView:
@@ -205,6 +240,48 @@
Provides a syntax-highlighted body."""
+ def _calc_index(self):
+ if not hasattr(self, '_archive'):
+ container = getWrapperContainer(self.context)
+ self._archive = container and queryAdapter(container,
+ ICheckinMessageArchive)
+ if not self._archive:
+ self._index = None
+ elif not hasattr(self, '_index'):
+ self._index = self._archive.index(self.context)
+
+ def next(self):
+ """Returns the next message in archive."""
+ self._calc_index()
+ if self._index is not None and self._index < len(self._archive) - 1:
+ return self._archive[self._index + 1]
+ else:
+ return None
+
+ def previous(self):
+ """Returns the previous message in archive."""
+ self._calc_index()
+ if self._index is not None and self._index > 0:
+ return self._archive[self._index - 1]
+ else:
+ return None
+
+ def first(self):
+ """Returns the first message in archive."""
+ self._calc_index()
+ if self._archive:
+ return self._archive[0]
+ else:
+ return None
+
+ def last(self):
+ """Returns the last message in archive."""
+ self._calc_index()
+ if self._archive:
+ return self._archive[-1]
+ else:
+ return None
+
def body(self):
"""Colorizes checkin message body."""
@@ -213,24 +290,48 @@
.replace('<', '<') \
.replace('>', '>') \
.replace('"', '"')
- m = re.match('(?s)(.*?\nLog [mM]essage:\n)(.*?\n)\n*(Status:\n\n.*?)?'
- '(===.*?)?'
- '(\n_______________________________________________.*)?\Z',
- text)
- if not m:
+ log_idx = text.find('\nLog message:\n')
+ if log_idx == -1:
+ log_idx = text.find('\nLog Message:\n')
+ if log_idx == -1:
return '<pre>%s</pre>' % text
- intro, log, import_status, diff, sig = m.groups()
+ log_idx += len('\nLog message:\n')
+
+ sig_idx = text.rfind(
+ '\n_______________________________________________')
+ if sig_idx == -1:
+ sig_idx = len(text)
+
+ diff_idx = text.find('\n===')
+ if diff_idx == -1:
+ diff_idx = sig_idx
+
+ status_idx = text.find('\nStatus:\n')
+ if status_idx == -1:
+ status_idx = diff_idx
+
+ assert log_idx <= status_idx <= diff_idx <= sig_idx
+
+ intro = text[:log_idx]
+ log = text[log_idx:status_idx].strip()
+ import_status = text[status_idx:diff_idx]
+ diff = text[diff_idx:sig_idx]
+ sig = text[sig_idx:]
log = '<p>%s</p>' % '</p><p>'.join(log.splitlines())
if import_status is None:
import_status = ''
- # Unified diff
if diff is None:
diff = ''
+
+ diff = "\n".join(map(self.mark_whitespace, diff.splitlines()))
+
def colorize(style):
return r'<div class="%s">\1</div>' % style
+
+ # Unified diff
diff = re.sub(r'(?m)^(===.*)$', colorize("file"), diff)
diff = re.sub(r'(?m)^(---.*)$', colorize("oldfile"), diff)
diff = re.sub(r'(?m)^(\+\+\+.*)$', colorize("newfile"), diff)
@@ -241,10 +342,62 @@
# Postprocess for Mozilla
diff = re.sub('</div>\n', '\n</div>', diff)
- if sig is None:
- sig = ''
- else:
+ if sig:
sig = '<div class="signature">%s</div>' % sig
return '<pre>%s</pre><div class="log">%s</div><pre>%s%s%s</pre>' \
% (intro, log, import_status, diff, sig)
+
+ def mark_whitespace(self, line, tab=('>', '-'), trail='.'):
+ """Mark whitespace in diff lines.
+
+ Suggested values for tab: ('>', '-'), ('»', ' '),
+ ('»', '‐')
+
+ Suggested values for trail: '.', '␣'
+ """
+ if line == ' ' or (not line.endswith(' ') and '\t' not in line):
+ return line
+ m = re.search('\s+\Z', line)
+ if m:
+ n = m.start()
+ if n == 0:
+ n = 1 # don't highlight the first space in a diff
+ line = '%s<span class="trail">%s</span>' % (line[:n],
+ line[n:].replace(' ', trail))
+ if '\t' in line:
+ NORMAL, TAG, ENTITY = 0, 1, 2
+ idx = col = 0
+ mode = NORMAL
+ tabs = []
+ for c in line[1:]: # ignore first space in a diff
+ idx += 1
+ if mode == TAG:
+ if c == '>':
+ mode = NORMAL
+ elif mode == ENTITY:
+ if c == ';':
+ col += 1
+ mode = NORMAL
+ else:
+ if c == '<':
+ mode = TAG
+ elif c == '&':
+ mode = ENTITY
+ elif c == '\t':
+ width = 8 - (col % 8)
+ tabs.append((idx, width))
+ col += width
+ else:
+ col += 1
+ if tabs:
+ parts = []
+ last = 0
+ for idx, width in tabs:
+ parts.append(line[last:idx])
+ parts.append('<span class="tab">%s%s</span>'
+ % (tab[0], tab[1] * (width - 1)))
+ last = idx + 1
+ parts.append(line[last:])
+ line = "".join(parts)
+ return line