Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 104
- Log:
Initial import of CVSHistory Python script, used by RCVSWeb to generate
approximate revision summaries.
- Author:
- adh
- Date:
- Mon Oct 23 18:01:24 +0100 2006
- Size:
- 29023 Bytes
- Properties:
- Property svn:executable is set
1 | #!/usr/bin/env python |
2 | |
3 | # CVSHistory -- A cvsweb/viewcvs-integrating CVS history |
4 | # browsing script/web frontend/thingie. |
5 | |
6 | # Jamie Turner <jamwt@jamwt.com> |
7 | |
8 | ##### USER EDITABLE SECTION ##### |
9 | CONFIGFILE = "/path/to/cvshistory.conf" |
10 | # END OF USER EDITABLE SECTION! |
11 | |
12 | |
13 | ##### LICENSE ##### |
14 | |
15 | # Modified BSD |
16 | # ------------ |
17 | # |
18 | # All of the documentation and software included in this software |
19 | # is copyrighted by Jamie Turner <jamwt@jamwt.com> |
20 | # |
21 | # Copyright 2004 Jamie Turner. All rights reserved. |
22 | # |
23 | # Redistribution and use in source and binary forms, with or without |
24 | # modification, are permitted provided that the following conditions |
25 | # are met: |
26 | # 1. Redistributions of source code must retain the above copyright |
27 | # notice, this list of conditions and the following disclaimer. |
28 | # 2. Redistributions in binary form must reproduce the above copyright |
29 | # notice, this list of conditions and the following disclaimer in the |
30 | # documentation and/or other materials provided with the distribution. |
31 | # 3. The name of the author may not be used to endorse or promote products |
32 | # derived from this software without specific prior written permission. |
33 | # |
34 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
35 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
36 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
37 | # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
38 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED |
39 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
40 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
41 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
42 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, |
43 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
44 | |
45 | # ViewCVS Look & feel modeled after the ViewCVS project |
46 | # viewcvs.sourceforge.net |
47 | |
48 | # CVSweb Look & feel modeled after the FreeBSD CVSweb project |
49 | # www.freebsd.org/projects/cvsweb.html |
50 | |
51 | ##### CODE ##### |
52 | |
53 | # Integration mode constants. |
54 | INT_NONE = 0 # don't edit |
55 | INT_VIEWCVS = 1 # don't edit |
56 | INT_CVSWEB = 2 # don't edit |
57 | |
58 | # Predefined time formats. |
59 | USTIME = "%m-%d-%Y %H:%M" |
60 | WORLDTIME = "%d-%m-%Y %H:%M" |
61 | |
62 | # Performance mode constants. |
63 | MODE_FAST = 1 |
64 | MODE_SLOW = 2 |
65 | |
66 | # Load configuration. |
67 | |
68 | execfile(CONFIGFILE) |
69 | |
70 | # let's handle the options above |
71 | # first, formatting |
72 | |
73 | # we default to ViewCVS layout, with no integration. it's prettier! |
74 | if INTEGRATION == INT_NONE or INTEGRATION == INT_VIEWCVS: |
75 | _SORTEDCOL = "88ff88" |
76 | _REGCOL = "cccccc" |
77 | _ROWS = ["ffffff","ccccee"] |
78 | _TABLETOP = '<table width="100%%" border=0 cellspacing=1 cellpadding=2>' |
79 | |
80 | else: |
81 | _SORTEDCOL = "ffcc66" |
82 | _REGCOL = "ffffcc" |
83 | _ROWS = ["ffffff","ffffff"] |
84 | _TABLETOP = '<table style="border-width: 0; background-color: #cccccc" width="100%" cellspacing="1" cellpadding="2">' |
85 | |
86 | _SCRIPT = SCRIPTPATH |
87 | |
88 | _LOGO = " " |
89 | _ICON = '<img src="/icons/dir.gif" alt="[DIR]" border="0" width="20" height="22">' |
90 | |
91 | if INTEGRATION == INT_VIEWCVS: |
92 | |
93 | _SCRIPT = SCRIPTPATH |
94 | |
95 | _LOGO = '<img src="/icons/apache_pb.gif" alt="(logo)" border=0 width=259 height=32>' |
96 | _ICON = '<img src="/icons/small/dir.gif" alt="(dir)" border=0 width=16 height=16>' |
97 | |
98 | elif INTEGRATION == INT_NONE: |
99 | _LOGO = '' |
100 | _ICON = '' |
101 | |
102 | |
103 | |
104 | |
105 | |
106 | # dependencies |
107 | import time |
108 | import os |
109 | import urllib |
110 | import urlparse |
111 | import cgi |
112 | import re |
113 | import xml.sax.saxutils |
114 | |
115 | |
116 | _VERSION = "2.2" |
117 | # basic fields: |
118 | # Operation Type(Operation Date)|username|<ignore>|directory|revision|fn |
119 | |
120 | _OPTYPES = { |
121 | "O" : "Checkout" , |
122 | "F" : "Release" , |
123 | "T" : "RTag", |
124 | "W" : "Delete on update", |
125 | "U" : "Update", |
126 | "P" : "Update by patch", |
127 | "G" : "Merge on update", |
128 | "C" : "Conflict on update", |
129 | "M" : "Commit", |
130 | "A" : "Addition", |
131 | "R" : "Removal", |
132 | "E" : "Export", |
133 | } |
134 | |
135 | def op2text(op): |
136 | return _OPTYPES.get(op, "Unknown (%s)" % op) |
137 | |
138 | _SELF_URL = os.environ['SCRIPT_NAME'] |
139 | |
140 | class ReverseReadline: |
141 | def __init__(self,fd,BUFSIZ=262144): |
142 | self.fd = fd |
143 | self.ind = 0 |
144 | self.BUFSIZ = BUFSIZ |
145 | self.fd.seek(0,2) |
146 | self.siz = self.fd.tell() |
147 | self.buff = "" |
148 | self.ind = 0 |
149 | self.scount = -1 |
150 | self.eof = 0 |
151 | self.first = 1 |
152 | |
153 | def readline(self): |
154 | # find the start index |
155 | nind = self.buff.rfind("\n",0,self.ind-1) |
156 | if nind != -1 and self.ind != 0: |
157 | ret = self.buff[nind+1:self.ind+1] |
158 | self.ind = nind |
159 | return ret |
160 | # oops.. outta newlines |
161 | |
162 | # keep the existing data |
163 | old = self.buff[:self.ind+1] |
164 | |
165 | # can't seek backward anymore? |
166 | if self.eof: |
167 | self.buff = "" |
168 | return old |
169 | |
170 | # seek to the right location |
171 | sksiz = self.BUFSIZ * self.scount |
172 | if sksiz * -1 > self.siz: |
173 | sksiz = self.siz * -1 |
174 | self.eof = 1 |
175 | rd = self.siz - ( ( (self.scount * -1) - 1) * self.BUFSIZ) |
176 | else: |
177 | rd = self.BUFSIZ |
178 | |
179 | self.fd.seek(sksiz,2) |
180 | |
181 | self.buff = self.fd.read(rd) |
182 | |
183 | if self.first: |
184 | self.ind = rd - 1 |
185 | self.first = 0 |
186 | else: |
187 | self.ind = rd |
188 | |
189 | self.scount -= 1 |
190 | |
191 | rl = self.readline() |
192 | if rl[-1] == "\n": |
193 | return old + rl |
194 | else: |
195 | return rl + old |
196 | |
197 | # fast mode, for big servers. No sorting! |
198 | def get_history_fast(conds,opts): |
199 | reader = ReverseReadline(open(HISTORY[opts["cvsroot"]])) |
200 | |
201 | # datelimit |
202 | lasttime = time.time() |
203 | if LIMITDAYS: |
204 | ltm = lasttime |
205 | ltm -= (86400 * LIMITDAYS) |
206 | else: |
207 | ltm = 0 |
208 | |
209 | offset = 0 |
210 | if opts.has_key("offset"): |
211 | offset = int(opts["offset"]) |
212 | |
213 | data = [] |
214 | skip = 0 |
215 | |
216 | line = reader.readline() |
217 | |
218 | while len(data) < PERPAGE and line and lasttime > ltm: |
219 | cur = line.strip().split("|",5) # maxsplit @ 5 |
220 | |
221 | # we play a little game here. we don't care about the data |
222 | # at index two, so we'll put the time there |
223 | |
224 | lasttime = get_time(cur[0][1:]) |
225 | cur[2] = lasttime |
226 | cur[0] = cur[0][0] # the op code |
227 | |
228 | failed = 0 |
229 | for cond in conds: |
230 | if not cond.test(cur): |
231 | failed = 1 |
232 | break |
233 | |
234 | if not failed and skip < offset: |
235 | skip += 1 |
236 | elif not failed and lasttime > ltm: |
237 | data.append(cur) |
238 | |
239 | line = reader.readline() |
240 | |
241 | return data |
242 | |
243 | |
244 | # this is a bit uneasy for especially large logs, but.. |
245 | # we'll worry about that later. |
246 | def get_history(opts): |
247 | |
248 | # datelimit |
249 | lasttime = time.time() |
250 | if LIMITDAYS: |
251 | ltm = lasttime |
252 | ltm -= (86400 * LIMITDAYS) |
253 | else: |
254 | ltm = 0 |
255 | |
256 | fd = open(HISTORY[opts["cvsroot"]],"r") |
257 | |
258 | data = [] |
259 | |
260 | line = fd.readline() |
261 | while line: |
262 | cur = line.strip().split("|",5) |
263 | |
264 | # we play a little game here. we don't care about the data |
265 | # at index two, so we'll put the time there |
266 | |
267 | lasttime = get_time(cur[0][1:]) |
268 | cur[2] = lasttime |
269 | cur[0] = cur[0][0] # the op code |
270 | |
271 | if lasttime > ltm: |
272 | data.append(cur) |
273 | |
274 | line = fd.readline() |
275 | |
276 | fd.close() |
277 | |
278 | # data is: |
279 | # <string>op:<string>user:<int>time:dir:revision:file/module |
280 | # * number of entries |
281 | data.reverse() |
282 | |
283 | return data |
284 | |
285 | _OTYPE_CHECKBOX = 1 |
286 | _OTYPE_TEXT = 2 |
287 | _OTYPE_SELECT = 3 |
288 | |
289 | def opt_pre(opts,type,opt,opt2=None): |
290 | if type == _OTYPE_CHECKBOX: |
291 | if opts.has_key(opt) and opts[opt] == "on": |
292 | return "CHECKED" |
293 | elif type == _OTYPE_TEXT: |
294 | if opts.has_key(opt) and opts[opt].strip() != "": |
295 | return "value=\"%s\"" % opts[opt] |
296 | elif type == _OTYPE_SELECT: |
297 | if opts.has_key(opt) and opts[opt] == opt2: |
298 | return "SELECTED" |
299 | return "" |
300 | |
301 | def get_time(hextime): |
302 | tm = 0 |
303 | ht_len = len(hextime) |
304 | |
305 | for x in xrange(0,ht_len): |
306 | d = ord(hextime[x]) - 48 |
307 | if d > 9: |
308 | d -= 39 |
309 | tm += d * (16 ** (ht_len - x - 1 ) ) |
310 | |
311 | return tm |
312 | |
313 | _COLUMNS = ["Date","User","Operation","Directory","File","Revision"] |
314 | |
315 | |
316 | def get_script_absolute_url (): |
317 | |
318 | port = "" |
319 | if os.environ.get('HTTPS') or os.environ['SERVER_PROTOCOL'][:5] == 'HTTPS': |
320 | url = 'https://' |
321 | if os.environ['SERVER_PORT'] != '443': |
322 | port = os.environ['SERVER_PORT'] |
323 | else: |
324 | url = 'http://' |
325 | if os.environ['SERVER_PORT'] != '80': |
326 | port = os.environ['SERVER_PORT'] |
327 | |
328 | url += os.environ['SERVER_NAME'] |
329 | |
330 | if port: url += ':' + port |
331 | |
332 | url += _SELF_URL |
333 | |
334 | return url |
335 | |
336 | def pretty_print_rss(data,options): |
337 | |
338 | cvsHistoryURL = get_script_absolute_url () |
339 | |
340 | print 'Content-Type: text/xml; charset=iso-8859-1' |
341 | |
342 | print \ |
343 | '''<?xml version="1.0" encoding="ISO-8859-1"?> |
344 | <rss version="2.0"> |
345 | <channel> |
346 | <title>CVSHistory</title> |
347 | <link>%s</link> |
348 | <description>CVS Changelog History</description> |
349 | <docs>http://blogs.law.harvard.edu/tech/rss</docs> |
350 | <generator>CVSHistory</generator> |
351 | <webMaster>%s</webMaster> |
352 | <language>en</language> |
353 | ''' % (cvsHistoryURL, SITE_ADMIN) |
354 | |
355 | for row in data: |
356 | fileName = row[5] |
357 | dirName = row[3] |
358 | author = row[1] |
359 | revision = row[4] |
360 | op = op2text(row[0]) |
361 | |
362 | if fileName == dirName: |
363 | link = '%s/%s/' % (_SCRIPT, dirName) |
364 | else: |
365 | link = '%s/%s/%s' % (_SCRIPT, dirName, fileName) |
366 | |
367 | link = urlparse.urljoin (cvsHistoryURL, link) |
368 | |
369 | title = fileName |
370 | |
371 | description = '''%s: %s %s (%s)''' % (author, op, revision, dirName) |
372 | |
373 | milliseconds = row[2] |
374 | |
375 | date = time.strftime ('%a, %d %b %Y %H:%M:%S %Z', time.localtime (milliseconds)) |
376 | |
377 | print \ |
378 | ''' <item> |
379 | <title>%s</title> |
380 | <description>%s</description> |
381 | <link>%s</link> |
382 | <category>%s</category> |
383 | <pubDate>%s</pubDate> |
384 | <guid>%s</guid>''' % ( |
385 | xml.sax.saxutils.escape (title), |
386 | xml.sax.saxutils.escape (description), |
387 | xml.sax.saxutils.escape (link), |
388 | op, |
389 | date, |
390 | author + '/' + op + '/' + fileName + '/' + str (milliseconds) |
391 | ) |
392 | if AUTHOR_EMAIL_DOMAIN: |
393 | print " <author>%s@%s (%s)</author>" % (author, AUTHOR_EMAIL_DOMAIN, author) |
394 | print ' </item>' |
395 | |
396 | print \ |
397 | ''' </channel> |
398 | </rss> |
399 | ''' |
400 | |
401 | def pretty_print(data,options): |
402 | if options.has_key("rss"): |
403 | pretty_print_rss(data,options) |
404 | return |
405 | |
406 | # set up paging |
407 | offset = 0 |
408 | if options.has_key("offset"): |
409 | try: |
410 | offset = int(options["offset"]) |
411 | except: |
412 | offset = 0 |
413 | |
414 | pstr = "" |
415 | nstr = "" |
416 | |
417 | if PERFORMANCE == MODE_SLOW: |
418 | sortby = "Date" |
419 | if options.has_key("sortby"): |
420 | sortby = options["sortby"] |
421 | |
422 | |
423 | |
424 | nend = PERPAGE |
425 | |
426 | if len(data) - offset - PERPAGE < PERPAGE: |
427 | nend = len(data) - offset - PERPAGE |
428 | |
429 | if offset - PERPAGE >= 0: |
430 | pstr = '<td><font size="2"><a href="%s?%s">( Previous %s )</a></font></td>' % ( |
431 | _SELF_URL,getstring(options,["offset","%d" % (offset - PERPAGE)]),PERPAGE) |
432 | if offset + PERPAGE < len(data): |
433 | nstr = '<td><font size="2"><a href="%s?%s">( Next %s )</a></font></td>' % ( |
434 | _SELF_URL,getstring(options,["offset","%d" % (offset + PERPAGE)]),nend) |
435 | |
436 | cend = offset + PERPAGE |
437 | if cend > len(data): |
438 | cend = len(data) |
439 | ofstr = " of <b>%s</b>" % len(data) |
440 | srange = offset |
441 | erange = cend |
442 | elif PERFORMANCE == MODE_FAST: |
443 | if offset >= PERPAGE: |
444 | pstr = '<td><font size="2"><a href="%s?%s">( Newer Entries )</a></font></td>' % ( |
445 | _SELF_URL,getstring(options,["offset","%d" % (offset - PERPAGE)])) |
446 | |
447 | nstr = '<td><font size="2"><a href="%s?%s">( Older Entries )</a></font></td>' % ( |
448 | _SELF_URL,getstring(options,["offset","%d" % (offset + PERPAGE)])) |
449 | cend = offset + PERPAGE |
450 | if cend - offset > len(data): |
451 | cend = len(data) + offset |
452 | |
453 | ofstr = "" |
454 | srange = 0 |
455 | erange = len(data) |
456 | |
457 | rssURL = get_script_absolute_url () |
458 | |
459 | rssURL += '?' |
460 | |
461 | form = cgi.FieldStorage() |
462 | for paramName in form.keys(): |
463 | paramValue = form[paramName].value |
464 | if type(paramValue) == type([]): |
465 | paramValue = paramValue[0] |
466 | rssURL += paramName + '=' + urllib.quote (paramValue) + '&' |
467 | |
468 | rssURL += 'rss=1' |
469 | |
470 | print \ |
471 | '''Content-Type: text/html\r |
472 | \r |
473 | <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
474 | <html> |
475 | <head><title>CVSHistory</title> |
476 | <!-- CVSHistory: Jamie Turner, jamwt@jamwt.com --> |
477 | <link rel="alternate" type="application/rss+xml" title="RSS" href="%s"> |
478 | </head> |
479 | <body bgcolor="#ffffff" text="#000000"> |
480 | <table width="100%%" border=0 cellspacing=0 cellpadding=0> |
481 | <tr> |
482 | <td rowspan=2><h1>CVSHistory</h1></td> |
483 | <td align=right>%s</td> |
484 | </tr> |
485 | |
486 | </table> |
487 | |
488 | <hr noshade size="1"> |
489 | <table> |
490 | <tr><td> |
491 | <form action="%s" method="get"> |
492 | <table> |
493 | <tr> |
494 | <td valign="top" width="100">User:</td> |
495 | <td valign="top"><input type="text" name="usearch" %s><br> |
496 | <font size="2">Regular expression? <input type="checkbox" name="usearchre" %s></font></td> |
497 | <td width="50"> </td> |
498 | <td valign="top">Revision: |
499 | <br><select name="revsel1"> |
500 | <option value="na" %s>(No Restriction)</option> |
501 | <option value="eq" %s>Equal to</option> |
502 | <option value="gt" %s>Greater than</option> |
503 | <option value="lt" %s>Less than</option> |
504 | </select> |
505 | <input type="text" name="revval1" size="6" %s> |
506 | <select name="revsel2"> |
507 | <option value="na" %s>(No Restriction)</option> |
508 | <option value="eq" %s>Equal to</option> |
509 | <option value="gt" %s>Greater than</option> |
510 | <option value="lt" %s>Less than</option> |
511 | </select> |
512 | <input type="text" name="revval2" size="6" %s> |
513 | </td> |
514 | |
515 | </tr> |
516 | <tr> |
517 | <td valign="top" width="100">File:</td> |
518 | <td><input type="text" name="fsearch" %s><br> |
519 | <font size="2">Regular expression? <input type="checkbox" name="fsearchre" %s></font></td> |
520 | <td width="50"> </td> |
521 | <td valign="top">Date: |
522 | |
523 | <font size="2"><i>format: %s or %s</i></font> |
524 | <br><select name="datesel1"> |
525 | <option value="na" %s>(No Restriction)</option> |
526 | <option value="eq" %s>On/At</option> |
527 | <option value="gt" %s>On/After</option> |
528 | <option value="lt" %s>Before</option> |
529 | </select> |
530 | <input type="text" name="dateval1" size="30" %s> |
531 | </td> |
532 | |
533 | </tr> |
534 | <tr> |
535 | <td valign="top" width="100">Directory:</td> |
536 | <td><input type="text" name="dsearch" %s><br> |
537 | <font size="2">Regular expression? <input type="checkbox" name="dsearchre" %s></font><br> |
538 | <font size="2">Include subdirectories? <input type="checkbox" name="dsearchsub" %s></font></td> |
539 | <td width="50"> </td> |
540 | <td valign="top"> |
541 | <select name="datesel2"> |
542 | <option value="na" %s>(No Restriction)</option> |
543 | <option value="eq" %s>On/At</option> |
544 | <option value="gt" %s>On/After</option> |
545 | <option value="lt" %s>Before</option> |
546 | </select> |
547 | <input type="text" name="dateval2" size="30" %s> |
548 | </td> |
549 | </tr> |
550 | <tr> |
551 | <td valign="top">Operations:</td> |
552 | <td colspan="3"> |
553 | <select name="selop"> |
554 | <option value="na" %s>(No Restriction)</option> |
555 | <option value="in" %s>Include</option> |
556 | <option value="out" %s>Exclude</option> |
557 | </select> |
558 | Addition <input type="checkbox" name="opA" %s> |
559 | Checkout <input type="checkbox" name="opO" %s> |
560 | Commit <input type="checkbox" name="opM" %s> |
561 | Conflict on update <input type="checkbox" name="opC" %s> |
562 | <br> |
563 | Delete on update <input type="checkbox" name="opW" %s> |
564 | Merge on update <input type="checkbox" name="opG" %s> |
565 | Release <input type="checkbox" name="opF" %s> |
566 | Removal <input type="checkbox" name="opR" %s> |
567 | Rtag <input type="checkbox" name="opT" %s> |
568 | Update <input type="checkbox" name="opU" %s> |
569 | Update by patch <input type="checkbox" name="opP" %s> |
570 | </td> |
571 | <tr><td colspan="4" align="center"><input type="submit" value="Limit History"> |
572 | <a href="%s">(Reset/View All)</a></td></tr> |
573 | </table> |
574 | <input type="hidden" name="limit" value="1"> |
575 | %s |
576 | </form> |
577 | |
578 | </td></tr> |
579 | </table> |
580 | |
581 | <hr noshade> |
582 | <table align="center" cellpadding="10"><tr><td>Showing records <b>%s-%s</b>%s</td> |
583 | %s%s</tr></table> |
584 | |
585 | %s |
586 | <tr>''' % (cgi.escape(rssURL), _LOGO, _SELF_URL, |
587 | opt_pre(options,_OTYPE_TEXT,"usearch"), |
588 | opt_pre(options,_OTYPE_CHECKBOX,"usearchre"), |
589 | opt_pre(options,_OTYPE_SELECT,"revsel1","na"), |
590 | opt_pre(options,_OTYPE_SELECT,"revsel1","eq"), |
591 | opt_pre(options,_OTYPE_SELECT,"revsel1","gt"), |
592 | opt_pre(options,_OTYPE_SELECT,"revsel1","lt"), |
593 | opt_pre(options,_OTYPE_TEXT,"revval1"), |
594 | opt_pre(options,_OTYPE_SELECT,"revsel2","na"), |
595 | opt_pre(options,_OTYPE_SELECT,"revsel2","eq"), |
596 | opt_pre(options,_OTYPE_SELECT,"revsel2","gt"), |
597 | opt_pre(options,_OTYPE_SELECT,"revsel2","lt"), |
598 | opt_pre(options,_OTYPE_TEXT,"revval2"), |
599 | opt_pre(options,_OTYPE_TEXT,"fsearch"), |
600 | opt_pre(options,_OTYPE_CHECKBOX,"fsearchre"), |
601 | |
602 | #date break... |
603 | time.strftime(TIMEFORMAT,time.localtime(time.time())), |
604 | time.strftime(TIMEFORMAT[:-6],time.localtime(time.time())) , |
605 | |
606 | opt_pre(options,_OTYPE_SELECT,"datesel1","na"), |
607 | opt_pre(options,_OTYPE_SELECT,"datesel1","eq"), |
608 | opt_pre(options,_OTYPE_SELECT,"datesel1","gt"), |
609 | opt_pre(options,_OTYPE_SELECT,"datesel1","lt"), |
610 | opt_pre(options,_OTYPE_TEXT,"dateval1"), |
611 | opt_pre(options,_OTYPE_TEXT,"dsearch"), |
612 | opt_pre(options,_OTYPE_CHECKBOX,"dsearchre"), |
613 | opt_pre(options,_OTYPE_CHECKBOX,"dsearchsub"), |
614 | opt_pre(options,_OTYPE_SELECT,"datesel2","na"), |
615 | opt_pre(options,_OTYPE_SELECT,"datesel2","eq"), |
616 | opt_pre(options,_OTYPE_SELECT,"datesel2","gt"), |
617 | opt_pre(options,_OTYPE_SELECT,"datesel2","lt"), |
618 | opt_pre(options,_OTYPE_TEXT,"dateval2"), |
619 | opt_pre(options,_OTYPE_SELECT,"selop","na"), |
620 | opt_pre(options,_OTYPE_SELECT,"selop","in"), |
621 | opt_pre(options,_OTYPE_SELECT,"selop","out"), |
622 | opt_pre(options,_OTYPE_CHECKBOX,"opA"), |
623 | opt_pre(options,_OTYPE_CHECKBOX,"opO"), |
624 | opt_pre(options,_OTYPE_CHECKBOX,"opM"), |
625 | opt_pre(options,_OTYPE_CHECKBOX,"opC"), |
626 | opt_pre(options,_OTYPE_CHECKBOX,"opW"), |
627 | opt_pre(options,_OTYPE_CHECKBOX,"opG"), |
628 | opt_pre(options,_OTYPE_CHECKBOX,"opF"), |
629 | opt_pre(options,_OTYPE_CHECKBOX,"opR"), |
630 | opt_pre(options,_OTYPE_CHECKBOX,"opT"), |
631 | opt_pre(options,_OTYPE_CHECKBOX,"opU"), |
632 | opt_pre(options,_OTYPE_CHECKBOX,"opP"), |
633 | |
634 | # so we submit to the same place! |
635 | not options["cvsroot"] and _SELF_URL or |
636 | (_SELF_URL + ("?cvsroot=%s" % options["cvsroot"])), |
637 | |
638 | # so we submit to the same place! |
639 | not options["cvsroot"] and "" or ( |
640 | '<input type="hidden" name="cvsroot" value="%s" />' % options["cvsroot"]), |
641 | |
642 | # paging |
643 | offset + 1, cend, ofstr, pstr,nstr, |
644 | _TABLETOP, |
645 | ) |
646 | options["offset"] = "%d" % offset |
647 | |
648 | for x in xrange(0,len(_COLUMNS)): |
649 | if ( (PERFORMANCE == MODE_SLOW and _COLUMNS[x] == sortby) or |
650 | (PERFORMANCE == MODE_FAST and _COLUMNS[x] == "Date") ): |
651 | print '''<th align=left bgcolor="#%s" |
652 | >%s</th> ''' % (_SORTEDCOL, _COLUMNS[x]) |
653 | elif PERFORMANCE == MODE_FAST: |
654 | print '''<th align=left bgcolor="#%s" |
655 | >%s</th> ''' % (_REGCOL, _COLUMNS[x]) |
656 | else: |
657 | print '''<th align=left bgcolor="#%s" |
658 | ><a href="%s?%s">%s</a></th> ''' % (_REGCOL, _SELF_URL, |
659 | getstring(options,["sortby",_COLUMNS[x]]),_COLUMNS[x]) |
660 | |
661 | |
662 | print "</tr>\n" |
663 | |
664 | |
665 | for x in xrange(srange,erange): |
666 | fn = data[x][5] |
667 | dn = data[x][3] |
668 | if fn == dn: |
669 | fn = "<i>N/A</I>" |
670 | elif INTEGRATION: |
671 | fn = '<a href="%s/%s/%s">%s</a>' % ( |
672 | _SCRIPT, dn ,fn,fn ) |
673 | if INTEGRATION: |
674 | dn = '<a href="%s/%s/">%s%s</a>' % ( |
675 | _SCRIPT,dn,_ICON,dn ) |
676 | |
677 | |
678 | |
679 | rev = data[x][4] |
680 | if rev == '': |
681 | rev = "<i>N/A</I>" |
682 | |
683 | print \ |
684 | '''<tr bgcolor="#%s"> |
685 | |
686 | <td>%s</td> |
687 | <td>%s</td> |
688 | <td>%s</td> |
689 | <td>%s</td> |
690 | <td>%s</td> |
691 | <td>%s</td> |
692 | |
693 | </tr> |
694 | ''' % (_ROWS[x % 2], # background color |
695 | time.strftime(TIMEFORMAT,time.localtime(data[x][2])), #Date |
696 | data[x][1], # user |
697 | op2text(data[x][0]), # operation |
698 | dn, # directory |
699 | fn, # file/module |
700 | rev, # revision |
701 | ) |
702 | |
703 | print \ |
704 | '''</table><hr noshade> |
705 | <table width="100%%" border=0 cellpadding=0 cellspacing=0><tr> |
706 | <td align=left valign="top"> |
707 | <a href="%s">RSS</a>''' % cgi.escape(rssURL) |
708 | |
709 | if PUBLIC_SERVER: |
710 | print ' <a href="http://purl.org/net/syndication/subscribe/?rss=%s">Subscribe</a>' % urllib.quote (rssURL) |
711 | |
712 | print \ |
713 | '''</td> |
714 | <td align=right> |
715 | Powered by<br><a href="http://www.jamwt.com/CVSHistory/">CVSHistory %s</a> |
716 | </td></tr></table> |
717 | </body></html>''' % _VERSION |
718 | |
719 | |
720 | |
721 | def getstring(options,add=None,ignore=[]): |
722 | |
723 | if add: |
724 | options[add[0]] = add[1] |
725 | |
726 | gs = [] |
727 | for k in options.keys(): |
728 | if not k in ignore: |
729 | gs.append("%s=%s" % (k,urllib.quote_plus(options[k]))) |
730 | |
731 | |
732 | return cgi.escape("&".join(gs), 1) |
733 | |
734 | |
735 | def get_options(): |
736 | form = cgi.FieldStorage() |
737 | opts = {} |
738 | |
739 | for o in form.keys(): |
740 | d = form[o].value |
741 | if type(d) == type([]): |
742 | d = d[0] |
743 | opts[o] = d |
744 | if not opts.has_key("cvsroot") or not HISTORY.has_key(opts["cvsroot"]): |
745 | opts["cvsroot"] = "" |
746 | |
747 | return opts |
748 | |
749 | def sm_user(a,b): |
750 | if a[1].upper() < b[1].upper(): |
751 | return -1 |
752 | if a[1].upper() == b[1].upper(): |
753 | return 0 |
754 | return 1 |
755 | |
756 | def sm_op(a,b): |
757 | oa = op2text(a[0]) |
758 | ob = op2text(b[0]) |
759 | if oa < ob: |
760 | return -1 |
761 | if oa == ob: |
762 | return 0 |
763 | return 1 |
764 | |
765 | def sm_dir(a,b): |
766 | if a[3].upper() < b[3].upper(): |
767 | return -1 |
768 | if a[3].upper() == b[3].upper(): |
769 | return 0 |
770 | return 1 |
771 | |
772 | def sm_fn(a,b): |
773 | if a[3] == a[5]: |
774 | return 1 |
775 | if a[5].upper() < b[5].upper(): |
776 | return -1 |
777 | if a[5].upper() == b[5].upper(): |
778 | return 0 |
779 | return 1 |
780 | |
781 | def str_to_float(a): |
782 | if a == '': |
783 | return 0.0 |
784 | return float(a) |
785 | |
786 | def cmp_rev(a,b): |
787 | al = map(str_to_float, a.split('.')) |
788 | bl = map(str_to_float, b.split('.')) |
789 | if al > bl: |
790 | return 1 |
791 | if al < bl: |
792 | return -1 |
793 | return 0 |
794 | |
795 | def sm_rev(a,b): |
796 | return cmp_rev(a[4],b[4]) |
797 | |
798 | _SORTMETHODS = { |
799 | "User" : sm_user, |
800 | "Operation" : sm_op, |
801 | "Directory" : sm_dir, |
802 | "File" : sm_fn, |
803 | "Revision" : sm_rev, |
804 | } |
805 | |
806 | #conds = [ |
807 | |
808 | _CTYPE_MATCH = 1 |
809 | _CTYPE_REMATCH = 2 |
810 | _CTYPE_DATE = 3 |
811 | _CTYPE_REV = 4 |
812 | _CTYPE_OPS = 5 |
813 | |
814 | _ARG2_OUT = 0 |
815 | _ARG2_IN = 1 |
816 | |
817 | _ARG2_EQ = 2 |
818 | _ARG2_GT = 3 |
819 | _ARG2_LT = 4 |
820 | |
821 | _ARG2_SEQ = 5 |
822 | _ARG2_NUM_GT = 6 |
823 | _ARG2_NUM_LT = 7 |
824 | |
825 | class Condition: |
826 | def __init__(self,type,dfield,arg,arg2=None): |
827 | self.dfield = dfield |
828 | self.error = None |
829 | |
830 | if type == _CTYPE_MATCH: |
831 | self.arg = arg.strip() |
832 | self.test = self.MATCH_test |
833 | elif type == _CTYPE_REMATCH: |
834 | try: |
835 | self.arg = re.compile(arg) |
836 | self.test = self.REMATCH_test |
837 | except: |
838 | self.error = "invalid regular expression" |
839 | |
840 | elif type == _CTYPE_DATE: |
841 | try: |
842 | arg = arg.strip() |
843 | arg = re.sub(" +"," ",arg) |
844 | if arg.count(" "): |
845 | self.arg = time.mktime(time.strptime(arg,TIMEFORMAT)) |
846 | longdate = 1 |
847 | else: |
848 | self.arg = time.mktime(time.strptime(arg,TIMEFORMAT[:-6])) |
849 | longdate = 0 |
850 | self.test = self.COMP_test |
851 | except: |
852 | self.error = "invalid date" |
853 | if arg2 == "eq": |
854 | if longdate: |
855 | self.arg2 = _ARG2_EQ |
856 | else: |
857 | self.arg2 = _ARG2_SEQ |
858 | elif arg2 == "gt": |
859 | self.arg2 = _ARG2_GT |
860 | elif arg2 == "lt": |
861 | self.arg2 = _ARG2_LT |
862 | else: |
863 | self.error = "unexpected comparison specification for date" |
864 | |
865 | elif type == _CTYPE_REV: |
866 | self.arg = arg |
867 | self.test = self.COMP_test |
868 | if arg2 == "eq": |
869 | self.arg2 = _ARG2_EQ |
870 | elif arg2 == "gt": |
871 | self.arg2 = _ARG2_NUM_GT |
872 | elif arg2 == "lt": |
873 | self.arg2 = _ARG2_NUM_LT |
874 | else: |
875 | self.error = "unexpected comparison specification for revisions" |
876 | elif type == _CTYPE_OPS: |
877 | self.arg = arg |
878 | self.test = self.OPS_test |
879 | if arg2 == "in": |
880 | self.arg2 = _ARG2_IN |
881 | elif arg2 == "out": |
882 | self.arg2 = _ARG2_OUT |
883 | else: |
884 | self.error = "unexpected argument for operation exclusion/inclusion" |
885 | |
886 | |
887 | |
888 | |
889 | def MATCH_test(self,dataitem): |
890 | if dataitem[self.dfield] == self.arg: |
891 | return 1 |
892 | return 0 |
893 | |
894 | def REMATCH_test(self,dataitem): |
895 | if self.arg.search(dataitem[self.dfield]): |
896 | return 1 |
897 | return 0 |
898 | |
899 | def COMP_test(self,dataitem): |
900 | if self.arg2 == _ARG2_SEQ: |
901 | # we have a date... |
902 | dt = time.localtime(dataitem[self.dfield]) |
903 | mt = dt[:3] + (0,0,0) + dt[6:] |
904 | if time.mktime(mt) == self.arg: |
905 | return 1 |
906 | return 0 |
907 | elif self.arg2 == _ARG2_EQ: |
908 | if dataitem[self.dfield] == self.arg: |
909 | return 1 |
910 | return 0 |
911 | elif self.arg2 == _ARG2_GT: |
912 | if dataitem[self.dfield] > self.arg: |
913 | return 1 |
914 | return 0 |
915 | elif self.arg2 == _ARG2_LT: |
916 | if dataitem[self.dfield] < self.arg: |
917 | return 1 |
918 | return 0 |
919 | elif self.arg2 == _ARG2_NUM_GT: |
920 | if cmp_rev(dataitem[self.dfield], self.arg) == 1: |
921 | return 1 |
922 | return 0 |
923 | elif self.arg2 == _ARG2_NUM_LT: |
924 | if cmp_rev(dataitem[self.dfield], self.arg) == -1: |
925 | return 1 |
926 | return 0 |
927 | return 0 |
928 | |
929 | |
930 | def OPS_test(self,dataitem): |
931 | if dataitem[self.dfield] in self.arg: |
932 | return 1 * self.arg2 # will 0 out if OUT |
933 | return 1 - self.arg2 # inverse |
934 | |
935 | |
936 | |
937 | |
938 | |
939 | def get_conds(opts): |
940 | conds = [] |
941 | if not HISTORY.get(opts["cvsroot"]): |
942 | return "Error: unknown cvsroot: '%s'" % opts["cvsroot"] |
943 | for opt in opts.keys(): |
944 | # username |
945 | madecond = 0 |
946 | if opt == "usearch" and opts[opt].strip() != "": |
947 | madecond = 1 |
948 | if opts.has_key("usearchre") and opts["usearchre"] == "on": |
949 | conds.append(Condition(_CTYPE_REMATCH,1,opts["usearch"]) ) |
950 | else: |
951 | conds.append(Condition(_CTYPE_MATCH,1,opts["usearch"]) ) |
952 | if opt == "fsearch" and opts[opt].strip() != "": |
953 | madecond = 1 |
954 | if opts.has_key("fsearchre") and opts["fsearchre"] == "on": |
955 | conds.append(Condition(_CTYPE_REMATCH,5,opts["fsearch"]) ) |
956 | else: |
957 | conds.append(Condition(_CTYPE_MATCH,5,opts["fsearch"]) ) |
958 | elif opt == "dsearch" and opts[opt].strip() != "": |
959 | madecond = 1 |
960 | if opts.has_key("dsearchre") and opts["dsearchre"] == "on": |
961 | conds.append(Condition(_CTYPE_REMATCH,3,opts["dsearch"]) ) |
962 | else: |
963 | conds.append(Condition(_CTYPE_MATCH,3,opts["dsearch"]) ) |
964 | elif opt == "dsearch" and opts[opt].strip() != "": |
965 | madecond = 1 |
966 | if opts.has_key("dsearchre") and opts["dsearchre"] == "on": |
967 | conds.append(Condition(_CTYPE_REMATCH,3,opts["dsearch"]) ) |
968 | else: |
969 | conds.append(Condition(_CTYPE_MATCH,3,opts["dsearch"]) ) |
970 | elif opt == "revsel1" and opts[opt].strip() != "na": |
971 | madecond = 1 |
972 | if not opts.has_key("revval1") or opts["revval1"].strip() == "": |
973 | return "Error processing revision: please include revision value" |
974 | conds.append(Condition(_CTYPE_REV,4,opts["revval1"],opts["revsel1"]) ) |
975 | elif opt == "revsel2" and opts[opt].strip() != "na": |
976 | madecond = 1 |
977 | if not opts.has_key("revval2") or opts["revval2"].strip() == "": |
978 | return "Error processing revision: please include revision value" |
979 | conds.append(Condition(_CTYPE_REV,4,opts["revval2"],opts["revsel2"]) ) |
980 | elif opt == "datesel1" and opts[opt].strip() != "na": |
981 | madecond = 1 |
982 | if not opts.has_key("dateval1") or opts["dateval1"].strip() == "": |
983 | return "Error processing date: please include date value" |
984 | conds.append(Condition(_CTYPE_DATE,2,opts["dateval1"],opts["datesel1"]) ) |
985 | elif opt == "datesel2" and opts[opt].strip() != "na": |
986 | madecond = 1 |
987 | if not opts.has_key("dateval2") or opts["dateval2"].strip() == "": |
988 | return "Error processing date: please include date value" |
989 | conds.append(Condition(_CTYPE_DATE,2,opts["dateval2"],opts["datesel2"]) ) |
990 | |
991 | elif opt == "selop" and opts["selop"] != "na": |
992 | madecond = 1 |
993 | ops_arg = [] |
994 | for inkey in opts.keys(): |
995 | if inkey[:2] == "op" and opts[inkey] == "on": |
996 | ops_arg.append(inkey[2]) |
997 | conds.append(Condition(_CTYPE_OPS,0,"".join(ops_arg),opts["selop"]) ) |
998 | |
999 | |
1000 | if madecond and conds[-1].error: |
1001 | return "Error processing form input: " + conds[-1].error |
1002 | |
1003 | return conds |
1004 | |
1005 | def limit(data,conds): |
1006 | x = 0 |
1007 | while x < len(data): |
1008 | for c in conds: |
1009 | if not c.test(data[x]): |
1010 | del data[x] |
1011 | x -= 1 |
1012 | break |
1013 | x += 1 |
1014 | |
1015 | return data |
1016 | |
1017 | def correct_opts(opts): |
1018 | # revision |
1019 | if opts.has_key("revsel1") and opts["revsel1"] == "na": |
1020 | opts["revval1"] = "" |
1021 | if opts.has_key("revsel2") and opts["revsel2"] == "na": |
1022 | opts["revval2"] = "" |
1023 | if opts.has_key("datesel1") and opts["datesel1"] == "na": |
1024 | opts["dateval1"] = "" |
1025 | if opts.has_key("datesel2") and opts["datesel2"] == "na": |
1026 | opts["dateval2"] = "" |
1027 | if opts.has_key("selop") and opts["selop"] == "na": |
1028 | for ok in opts.keys(): |
1029 | if ok[:2] == "op": |
1030 | opts[ok] = "off" |
1031 | |
1032 | return opts |
1033 | |
1034 | def error(mess): |
1035 | print \ |
1036 | '''Content-Type: text/html\r |
1037 | \r |
1038 | <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
1039 | <html><head><title>CVSHistory -- Error</title> |
1040 | </head><body bgcolor="#ffffff"> |
1041 | <h2>Error</h2> |
1042 | CVSHistory encountered an error on form input:<br> |
1043 | <i>%s</i></body></html>''' % cgi.escape(mess) |
1044 | |
1045 | |
1046 | def go_slow(): |
1047 | opts = get_options() |
1048 | data = get_history(opts) |
1049 | |
1050 | # fix directory recursion |
1051 | if opts.has_key("dsearchsub") and opts["dsearchsub"] == "on": |
1052 | if not opts.has_key("dsearchre") or opts["dsearchre"] != "on": |
1053 | opts["dsearch"] = re.escape(opts["dsearch"]) |
1054 | opts["dsearchre"] = "on" |
1055 | opts["dsearch"] = "^" + opts["dsearch"] + ".*" |
1056 | opts["dsearchsub"] = "off" |
1057 | |
1058 | printed = 0 |
1059 | |
1060 | conds = None |
1061 | if opts.has_key("limit") and opts["limit"] == "1": |
1062 | conds = get_conds(opts) |
1063 | if type(conds) == type(""): |
1064 | error(conds) |
1065 | printed = 1 |
1066 | else: |
1067 | opts = correct_opts(opts) |
1068 | data = limit(data,conds) |
1069 | |
1070 | # sorting |
1071 | if not printed: |
1072 | if opts.has_key("sortby") and opts["sortby"] != "Date": |
1073 | data.sort(_SORTMETHODS[opts["sortby"]]) |
1074 | |
1075 | pretty_print(data,opts) |
1076 | |
1077 | #fast mode |
1078 | def go_fast(): |
1079 | |
1080 | opts = get_options() |
1081 | |
1082 | # fix directory recursion |
1083 | if opts.has_key("dsearchsub") and opts["dsearchsub"] == "on": |
1084 | if not opts.has_key("dsearchre") or opts["dsearchre"] != "on": |
1085 | opts["dsearch"] = re.escape(opts["dsearch"]) |
1086 | opts["dsearchre"] = "on" |
1087 | opts["dsearch"] = "^" + opts["dsearch"] + ".*" |
1088 | opts["dsearchsub"] = "off" |
1089 | printed = 0 |
1090 | |
1091 | conds = [] |
1092 | if opts.has_key("limit") and opts["limit"] == "1": |
1093 | conds = get_conds(opts) |
1094 | if type(conds) == type(""): |
1095 | error(conds) |
1096 | printed = 1 |
1097 | else: |
1098 | opts = correct_opts(opts) |
1099 | |
1100 | if not printed: |
1101 | data = get_history_fast(conds,opts) |
1102 | pretty_print(data,opts) |
1103 | |
1104 | if __name__ == "__main__": |
1105 | if PERFORMANCE == MODE_FAST: |
1106 | go_fast() |
1107 | if PERFORMANCE == MODE_SLOW: |
1108 | go_slow() |