## ## Copyright (c) 2006 Jason Dillon ## ## Licensed under the Apache License, Version 2.0 (the "License"); ## you may not use this file except in compliance with the License. ## You may obtain a copy of the License at ## ## http://www.apache.org/licenses/LICENSE-2.0 ## ## Unless required by applicable law or agreed to in writing, software ## distributed under the License is distributed on an "AS IS" BASIS, ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ## See the License for the specific language governing permissions and ## limitations under the License. ## ## ## $Id: htmlmessage.py 82 2009-08-26 09:26:36Z mindwanderer $ ## from p4spam import config, jiralinker from p4spam.message import MessageBuilder class HtmlMessageBuilder(MessageBuilder): def __init__(this, info): MessageBuilder.__init__(this, info) this.contentType = 'html' def escapeLine(this, line): ## ## HACK: Need a library to handle all this mess ## line = line.replace('&', '&') line = line.replace('>', '>') line = line.replace('<', '<') line = line.replace('\t', ' ') ## ## HACK: Trim the line here too... :-( ## l = len(line) m = config.MAX_DIFF_LINE_LENGTH if l > m: this.log.warning("Diff line longer than %s; truncating %s remaining characters" % (m, l - m)) line = line[:m] return line # parses the Description of the changelist. Separate from above, generic escapeLine because # perhaps we want to display the entire Description no matter the length and not invoke # MAX_DIFF_LINE_LENGTH to cut it off def escapeDescLine(this, line): ## ## HACK: Need a library to handle all this mess ## line = line.replace('&', '&') line = line.replace('>', '>') line = line.replace('<', '<') line = line.replace('\t', ' ') return line def generatePathAnchor(this, path): anchor = path # Condense the path... anchor = anchor.replace('/', '') anchor = anchor.replace('.', '') anchor = anchor.replace(' ', '') return anchor def writeDocument(this, buff, recipient): buff.writeln('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"') buff.writeln('"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">') buff.writeln('<html xmlns="http://www.w3.org/1999/xhtml">') this.writeHeader(buff) this.writeBody(buff, recipient) buff.writeln('</html>') def writeHeader(this, buff): buff.writeln('<head>') buff.writeln("<style type=\"text/css\"><!--\n%s\n--></style>" % config.HTML_STYLE) # Write user-header if config.USER_HEADER != None: buff.writeln(config.USER_HEADER) buff.writeln("<title>%s</title>" % this.getTitle()) buff.writeln('</head>') def writeOverview(this, buff): # Change description header buff.writeln('<table border="0" cellspacing="0" cellpadding="3" width="100%">') buff.writeln("<tr><td width=\"70\"><b>Change</b></td><td>%s</td></tr>" % this.info.getChange()) buff.writeln("<tr><td><b>Author</b></td><td>%s</td></tr>" % this.info.getAuthor()) buff.writeln("<tr><td><b>Client</b></td><td>%s</td></tr>" % this.info.getClient()) buff.writeln("<tr><td><b>Date</b></td><td>%s</td></tr>" % this.info.getDateTime()) buff.writeln('</table>') buff.writeln() # Change description/comment buff.writeln('<h3>Description</h3>') buff.writeln('<pre>') lines = this.info.getComments() for line in lines: line = this.escapeDescLine(line) line = jiralinker.render(line) buff.writeln("%s<br/>" % line) buff.writeln('</pre>') buff.writeln() # Link to change description URL buff.writeln('<h3>URL</h3>') url = config.CHANGE_LIST_URL % this.info.getChange() buff.writeln("<a href=\"%s\">%s</a>" % (url, url)) # If this change fixes a job, include the job detail fixes = this.info.getJobsFixed() if len(fixes) != 0: buff.writeln('<h3>Jobs Fixed</h3>') buff.writeln('<ul>') for fix in fixes: # Link to P4Web if configured if config.P4WEB_JOB_VIEW_URL != None: fixurl = config.P4WEB_JOB_VIEW_URL % fix.name fixlink = '<a href="%s">%s</a>' % (fixurl, fix.name) else: fixlink = fix.name buff.write('<li>%s on %s by %s %s</li>' % (fixlink, fix.date, fix.author, fix.status)) ## ## NOTE: Ignore the Job description for now. ## buff.writeln('</ul>') def writeFileActionTOC(this, buff): actions = ('edit', 'add', 'delete', 'branch', 'integrate', 'purge', 'move/delete', 'move/add', 'import') actionLables = { 'edit': 'Edited Files', 'add': 'Added Files', 'delete': 'Deleted Files', 'branch': 'Branched Files', 'integrate': 'Integrated Files', 'purge': 'Purged Files', 'move/delete': 'Moved Files (delete)', 'move/add': 'Moved Files (add)', 'import': 'Imported Files (add from remote depot)' } # Init the map actionToFilesMap = {} for a in actions: actionToFilesMap[a] = [] # Fill the map for f in this.info.getAffectedFiles(): actionToFilesMap[f.action].append(f) this.log.debug("Action to files map: %s" % (actionToFilesMap)) for action in actions: files = actionToFilesMap[action] if len(files) == 0: # Skip section if there are no files this.log.debug("Skipping '%s' section; no files" % action) continue buff.writeln("<h3>%s</h3>" % actionLables[action]) buff.writeln('<ul>') fileslimit = 0 for f in files: buff.writeln('<li>') anchor = this.generatePathAnchor(f.path) pathspec = "%s#%s" % (f.path, f.rev) link = "<a href=\"%s\">[history]</a>" % (config.FILE_HISTORY_URL % f.path) buff.writeln("<a href=\"#%s\">%s</a> %s" % (anchor, pathspec, link)) buff.writeln('</li>') fileslimit = fileslimit+1 if fileslimit >= config.MAX_FILE_LIST: buff.write('<strong class="error">') buff.write('File list truncated at %s by configuration' % config.MAX_FILE_LIST) buff.write('</strong>') break buff.writeln('</ul>') buff.writeln() def writeBody(this, buff, recipient): buff.writeln('<body>') # Write user-body-header if config.USER_BODY_HEADER != None: buff.writeln(config.USER_BODY_HEADER) buff.writeln('<div id="msg">') this.writeOverview(buff) buff.writeln() this.writeFileActionTOC(buff) buff.writeln('</div>') buff.writeln() this.writeDifferences(buff, recipient) buff.writeln() # Write user-body-footer if config.USER_BODY_FOOTER != None: buff.writeln(config.USER_BODY_FOOTER) buff.writeln('</body>') def writeDifferences(this, buff, recipient): buff.writeln('<div id="patch">') buff.writeln('<h3>Differences</h3>') fileslimit = 0 # log the recipient into p4 as the admin so we can masquerade as him/her loginresult = this.p4.raw('login', recipient) for diff in this.info.getDifferences(): # Write the anchor anchor = this.generatePathAnchor(diff.path) buff.writeln("<a id=\"%s\"></a>" % anchor) buff.writeln('<div class="modfile">') buff.writeln("<h4>%s#%s (%s)</h4>" % (diff.path, diff.rev, diff.filetype)) # this is where we'll take the recipient then decide whether or not they have # permissions to view this diff # try to print the file as the recipient syncresult = this.p4.raw('-u', recipient, 'print', diff.path) # if the 'p4 print' results are empty, it means the output went to stderr, which # is to say this user doesn't have access to that file if len(syncresult) == 0: buff.write('<strong class="error">') buff.write("You do not have appropriate Perforce permissions to view this diff!") buff.write('</strong>') buff.writeln('</div>') # Allow some filetypes to not diff if diff.filetype in config.DISABLE_DIFF_FOR_FILETYPE: buff.write('<strong class="error">') buff.write('Diff disabled for type %s by configuration' % diff.filetype) buff.write('</strong>') else: buff.writeln('<pre class="diff">') i = 0 maxlines = config.MAX_DIFF_LINES style = None lastStyle = None for line in diff.lines: line = this.escapeLine(line) if line.startswith('@@'): style = 'lines' elif line.startswith('+'): style = 'add' elif line.startswith('-'): style = 'rem' else: style = 'cx' # Only write style spans if the sytle was changed if style != lastStyle: if lastStyle != None: buff.write('</span>') buff.write('<span class="%s">' % style) buff.write(line) lastStyle = style i = i + 1 if i >= maxlines: break # Need to close up the last span buff.writeln('</span>') buff.writeln('</pre>') # If we truncated, then spit out a message totallines = len(diff.lines) if i >= maxlines and totallines != maxlines: buff.write('<strong class="error">') buff.write("Truncated at %s lines; %s more skipped" % (maxlines, totallines - maxlines)) buff.writeln('</strong>') buff.writeln('</div>') fileslimit = fileslimit + 1 if fileslimit >= config.MAX_DIFF_FILES: buff.write('<strong class="error">') buff.write('Diff section truncated at %s files by configuration' % config.MAX_DIFF_FILES) buff.write('</strong>') break buff.writeln('</div>')
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#6 | 8146 | Matthew Janulewicz | Fixing display for 'import' transaction type. | ||
#5 | 8134 | Matthew Janulewicz | Adding 'import' transaction type (branch from remote depot.) | ||
#4 | 7833 | Matthew Janulewicz | P4Spam now obeys 'p4 protects' and will not expose code diffs to subscribers that do not have the proper p4 permissions to view that diff. | ||
#3 | 7776 | Matthew Janulewicz | Now allows changelist description of any length and does not cut off each line at MAX_DIFF_LINE_LENGTH. | ||
#2 | 7732 | Matthew Janulewicz | Adding ability for P4Spam to detect and correcty categorize P4 2009.1's new 'move/add' and 'move/delete' file operations. | ||
#1 | 7731 | Matthew Janulewicz |
Adding P4Spam 1.1 code from http://p4spam.sourceforge.net/wiki/Main_Page "P4Spam is a Perforce change review daemon which spits out sexy HTML-styled notification emails." |