3 * FusionForge Subversion plugin
5 * Copyright 2003-2010, Roland Mas, Franck Villaume
6 * Copyright 2004, GForge, LLC
7 * Copyright 2010, Alain Peyrat <aljeux@free.fr>
8 * Copyright 2012-2013, Franck Villaume - TrivialDev
9 * Copyright 2013, French Ministry of National Education
11 * This file is part of FusionForge.
13 * FusionForge is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published
15 * by the Free Software Foundation; either version 2 of the License,
16 * or (at your option) any later version.
18 * FusionForge is distributed in the hope that it will be useful, but
19 * WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 * General Public License for more details.
23 * You should have received a copy of the GNU General Public License along
24 * with this program; if not, write to the Free Software Foundation, Inc.,
25 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
28 forge_define_config_item('default_server', 'scmsvn', forge_get_config ('web_host'));
29 forge_define_config_item('repos_path', 'scmsvn', forge_get_config('chroot').'/scmrepos/svn');
30 forge_define_config_item('use_ssh', 'scmsvn', false);
31 forge_set_config_item_bool('use_ssh', 'scmsvn');
32 forge_define_config_item('use_dav', 'scmsvn', true);
33 forge_set_config_item_bool('use_dav', 'scmsvn');
34 forge_define_config_item('use_ssl', 'scmsvn', true);
35 forge_set_config_item_bool('use_ssl', 'scmsvn');
36 forge_define_config_item('anonsvn_login','scmsvn', 'anonsvn');
37 forge_define_config_item('anonsvn_password','scmsvn', 'anonsvn');
39 class SVNPlugin extends SCMPlugin {
40 function SVNPlugin() {
42 $this->name = 'scmsvn';
43 $this->text = 'Subversion';
44 $this->svn_root_fs = '/scmrepos/svn';
45 if (!file_exists($this->svn_root_fs.'/.')) {
46 $this->svn_root_fs = forge_get_config('repos_path',
49 $this->svn_root_dav = '/svn';
50 $this->_addHook('scm_browser_page');
51 $this->_addHook('scm_update_repolist');
52 $this->_addHook('scm_generate_snapshots');
53 $this->_addHook('scm_gather_stats');
54 $this->_addHook('activity');
56 $this->provides['svn'] = true;
61 function getDefaultServer() {
62 return forge_get_config('default_server', 'scmsvn') ;
65 function printShortStats($params) {
66 $project = $this->checkParams($params);
71 if ($project->usesPlugin($this->name) && forge_check_perm('scm', $project->getID(), 'read')) {
72 $result = db_query_params('SELECT sum(commits) AS commits, sum(adds) AS adds FROM stats_cvs_group WHERE group_id=$1',
73 array ($project->getID())) ;
74 $commit_num = db_result($result,0,'commits');
75 $add_num = db_result($result,0,'adds');
82 echo ' (Subversion: '.sprintf(_('<strong>%1$s</strong> commits, <strong>%2$s</strong> adds'), number_format($commit_num, 0), number_format($add_num, 0)).")";
87 return '<p>' . _('Documentation for Subversion (sometimes referred to as "SVN") is available <a href="http://svnbook.red-bean.com/">here</a>.') . '</p>';
90 function topModule($project) {
91 // Check toplevel module presence
92 $repo = 'file://' . forge_get_config('repos_path', $this->name).'/'.$project->getUnixName().'/';
95 if (!(exec("svn ls '$repo'", $res) && in_array($module.'/', $res)))
103 function getInstructionsForAnon($project) {
104 $b = '<h2>' . _('Anonymous Subversion Access') . '</h2>';
106 $b .= _("This project's SVN repository can be checked out through anonymous access with the following command(s).");
110 $module = $this->topModule($project);
111 if (forge_get_config('use_ssh', 'scmsvn')) {
112 $b .= '<tt>svn checkout svn://'.$this->getBoxForProject($project).$this->svn_root_fs.'/'.$project->getUnixName().$module.'</tt><br />';
114 if (forge_get_config('use_dav', 'scmsvn')) {
115 $b .= '<tt>svn checkout --username '.forge_get_config('anonsvn_login', 'scmsvn').' http'.((forge_get_config('use_ssl', 'scmsvn')) ? 's' : '').'://' . $this->getBoxForProject($project). $this->svn_root_dav .'/'. $project->getUnixName() .$module.'</tt><br />';
116 $b .= _('The password is ').forge_get_config('anonsvn_password', 'scmsvn').'<br />';
122 function getInstructionsForRW($project) {
125 $module = $this->topModule($project);
127 if (session_loggedin()) {
128 $u =& user_get_object(user_getid());
129 $d = $u->getUnixName() ;
130 if (forge_get_config('use_ssh', 'scmsvn')) {
132 $b .= _('Developer Subversion Access via SSH');
135 $b .= _('Only project developers can access the SVN tree via this method. SSH must be installed on your client machine. Enter your site password when prompted.');
137 $b .= '<p><tt>svn checkout svn+ssh://'.$d.'@' . $this->getBoxForProject($project) . $this->svn_root_fs .'/'. $project->getUnixName().$module.'</tt></p>' ;
139 if (forge_get_config('use_dav', 'scmsvn')) {
141 $b .= _('Developer Subversion Access via DAV');
144 $b .= _('Only project developers can access the SVN tree via this method. Enter your site password when prompted.');
146 $b .= '<p><tt>svn checkout --username '.$d.' http'.((forge_get_config('use_ssl', 'scmsvn')) ? 's' : '').'://'. $this->getBoxForProject($project) . $this->svn_root_dav .'/'.$project->getUnixName().$module.'</tt></p>' ;
149 if (forge_get_config('use_ssh', 'scmsvn')) {
151 $b .= _('Developer Subversion Access via SSH');
154 $b .= _('Only project developers can access the SVN tree via this method. SSH must be installed on your client machine. Substitute <i>developername</i> with the proper values. Enter your site password when prompted.');
156 $b .= '<p><tt>svn checkout svn+ssh://<i>'._('developername').'</i>@' . $this->getBoxForProject($project) . $this->svn_root_fs .'/'. $project->getUnixName().$module.'</tt></p>' ;
158 if (forge_get_config('use_dav', 'scmsvn')) {
160 $b .= _('Developer Subversion Access via DAV');
163 $b .= _('Only project developers can access the SVN tree via this method. Substitute <i>developername</i> with the proper values. Enter your site password when prompted.');
165 $b .= '<p><tt>svn checkout --username <i>'._('developername').'</i> http'.((forge_get_config('use_ssl', 'scmsvn')) ? 's' : '').'://'. $this->getBoxForProject($project) . $this->svn_root_dav .'/'.$project->getUnixName().$module.'</tt></p>' ;
171 function getSnapshotPara($project) {
175 function getBrowserLinkBlock($project) {
177 $b = $HTML->boxMiddle(_('Subversion Repository Browser'));
179 $b .= _('Browsing the Subversion tree gives you a view into the current status of this project\'s code. You may also view the complete histories of any file in the repository.');
182 $b .= util_make_link ("/scm/browser.php?group_id=".$project->getID(),
183 _('Browse Subversion Repository')
189 function getStatsBlock($project) {
193 $result = db_query_params('SELECT u.realname, u.user_name, u.user_id, sum(commits) as commits, sum(adds) as adds, sum(adds+commits) as combined FROM stats_cvs_user s, users u WHERE group_id=$1 AND s.user_id=u.user_id AND (commits>0 OR adds >0) GROUP BY u.user_id, realname, user_name, u.user_id ORDER BY combined DESC, realname',
194 array ($project->getID()));
196 if (db_numrows($result) > 0) {
197 $b .= $HTML->boxMiddle(_('Repository Statistics'));
199 $tableHeaders = array(
204 $b .= $HTML->listTableTop($tableHeaders);
207 $total = array('adds' => 0, 'commits' => 0);
209 while($data = db_fetch_array($result)) {
210 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
211 $b .= '<td width="50%">' ;
212 $b .= util_make_link_u ($data['user_name'], $data['user_id'], $data['realname']) ;
213 $b .= '</td><td width="25%" align="right">'.$data['adds']. '</td>'.
214 '<td width="25%" align="right">'.$data['commits'].'</td></tr>';
215 $total['adds'] += $data['adds'];
216 $total['commits'] += $data['commits'];
219 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
220 $b .= '<td width="50%"><strong>'._('Total').':</strong></td>'.
221 '<td width="25%" align="right"><strong>'.$total['adds']. '</strong></td>'.
222 '<td width="25%" align="right"><strong>'.$total['commits'].'</strong></td>';
224 $b .= $HTML->listTableBottom();
230 function printBrowserPage($params) {
231 $project = $this->checkParams($params);
236 if ($project->usesPlugin($this->name)) {
237 if ($this->browserDisplayable($project)) {
238 session_redirect("/scm/viewvc.php/?root=".$project->getUnixName());
243 function createOrUpdateRepo($params) {
244 $project = $this->checkParams($params);
249 if (! $project->usesPlugin($this->name)) {
253 $repo = forge_get_config('repos_path', 'scmsvn') . '/' . $project->getUnixName();
255 if (!is_dir ($repo) || !is_file ("$repo/format")) {
256 if (!mkdir($repo, 0700, true)) {
260 system ("svnadmin create $repo", $ret);
264 if (forge_get_config('use_ssh', 'scmsvn')) {
265 $unix_group = 'scm_' . $project->getUnixName() ;
266 system ("find $repo -type d | xargs -I{} chmod g+s {}") ;
267 if ($project->enableAnonSCM()) {
268 system ("chmod -R g+wX,o+rX-w $repo") ;
270 system ("chmod -R g+wX,o-rwx $repo") ;
272 system ("chgrp -R $unix_group $repo") ;
274 $unix_user = forge_get_config('apache_user');
275 $unix_group = forge_get_config('apache_group');
276 system ("chmod -R g-rwx,o-rwx $repo") ;
277 system ("chown -R $unix_user:$unix_group $repo") ;
279 system ("svn mkdir -m'Init' file:///$repo/trunk file:///$repo/tags file:///$repo/branches >/dev/null") ;
282 $this->installOrUpdateCmds($project, $project->getUnixName(), $repo);
284 if (forge_get_config('use_ssh', 'scmsvn')) {
285 $unix_group = 'scm_' . $project->getUnixName();
286 system("find $repo -type d | xargs -I{} chmod g+s {}");
287 if (forge_get_config('use_dav', 'scmsvn')) {
288 $unix_user = forge_get_config('apache_user');
289 system("chown $unix_user:$unix_group $repo");
291 system("chgrp $unix_group $repo");
293 if ($project->enableAnonSCM()) {
294 system("chmod g+wX,o+rX-w $repo") ;
296 system("chmod g+wX,o-rwx $repo") ;
299 $unix_user = forge_get_config('apache_user');
300 $unix_group = forge_get_config('apache_group');
301 system("chown $unix_user:$unix_group $repo") ;
302 system("chmod g-rwx,o-rwx $repo") ;
306 function updateRepositoryList($params) {
307 $groups = $this->getGroups();
309 // Update WebDAV stuff
310 if (!forge_get_config('use_dav', 'scmsvn')) {
316 $engine = RBACEngine::getInstance() ;
319 foreach ($groups as $project) {
320 if ( !$project->isActive()) {
323 if ( !$project->usesSCM()) {
326 $access_data .= '[' . $project->getUnixName() . ":/]\n";
328 $users = $engine->getUsersByAllowedAction('scm',$project->getID(),'read');
329 foreach ($users as $user) {
330 $svnusers[$user->getID()] = $user;
331 if (forge_check_perm_for_user($user,
335 $access_data .= $user->getUnixName() . "= rw\n";
337 $access_data .= $user->getUnixName() . "= r\n";
341 if ($project->enableAnonSCM()) {
342 $access_data .= forge_get_config('anonsvn_login', 'scmsvn')." = r\n";
343 $access_data .= "* = r\n";
346 $access_data .= "\n";
349 foreach ($svnusers as $user_id => $user) {
350 $password_data .= $user->getUnixName().':'.$user->getUnixPasswd()."\n";
352 $password_data .= forge_get_config('anonsvn_login', 'scmsvn').":".htpasswd_apr1_md5(forge_get_config('anonsvn_password', 'scmsvn'))."\n";
354 $fname = forge_get_config('data_path').'/svnroot-authfile';
355 $f = fopen($fname.'.new', 'w');
356 fwrite($f, $password_data);
358 chmod($fname.'.new', 0644);
359 rename($fname.'.new', $fname);
361 $fname = forge_get_config('data_path').'/svnroot-access';
362 $f = fopen($fname.'.new', 'w');
363 fwrite($f, $access_data);
365 chmod($fname.'.new', 0644);
366 rename($fname.'.new', $fname);
369 function gatherStats($params) {
370 global $last_user, $last_time, $last_tag, $time_ok, $start_time, $end_time,
371 $adds, $deletes, $updates, $commits, $date_key,
372 $usr_adds, $usr_deletes, $usr_updates;
376 $project = $this->checkParams($params);
381 if (! $project->usesPlugin($this->name)) {
385 if ($params['mode'] == 'day') {
388 $year = $params['year'];
389 $month = $params['month'];
390 $day = $params['day'];
391 $month_string = sprintf("%04d%02d", $year, $month);
392 $start_time = gmmktime(0, 0, 0, $month, $day, $year);
393 $end_time = $start_time + 86400;
398 $usr_updates = array();
400 $repo = forge_get_config('repos_path', 'scmsvn') . '/' . $project->getUnixName();
401 if (!is_dir ($repo) || !is_file ("$repo/format")) {
402 echo "No repository\n";
407 $d1 = date('Y-m-d', $start_time - 150000);
408 $d2 = date('Y-m-d', $end_time + 150000);
410 $pipe = popen ("svn log file://$repo --xml -v -q -r '".'{'.$d2.'}:{'.$d1.'}'."' 2> /dev/null", 'r' ) ;
412 // cleaning stats_cvs_* table for the current day
413 $res = db_query_params('DELETE FROM stats_cvs_group WHERE month=$1 AND day=$2 AND group_id=$3',
418 echo "Error while cleaning stats_cvs_group\n" ;
423 $res = db_query_params ('DELETE FROM stats_cvs_user WHERE month=$1 AND day=$2 AND group_id=$3',
424 array ($month_string,
426 $project->getID())) ;
428 echo "Error while cleaning stats_cvs_user\n" ;
433 $xml_parser = xml_parser_create();
434 xml_set_element_handler($xml_parser, "SVNPluginStartElement", "SVNPluginEndElement");
435 xml_set_character_data_handler($xml_parser, "SVNPluginCharData");
437 // Analyzing history stream
438 while (!feof($pipe) &&
439 $data = fgets ($pipe, 4096)) {
440 if (!xml_parse ($xml_parser, $data, feof ($pipe))) {
441 debug("Unable to parse XML with error " .
442 xml_error_string(xml_get_error_code($xml_parser)) .
444 xml_get_current_line_number($xml_parser));
451 xml_parser_free($xml_parser);
453 // inserting group results in stats_cvs_groups
454 if ($updates > 0 || $adds > 0) {
455 if (!db_query_params('INSERT INTO stats_cvs_group (month,day,group_id,checkouts,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
456 array ($month_string,
462 echo "Error while inserting into stats_cvs_group\n" ;
468 // building the user list
469 $user_list = array_unique( array_merge( array_keys( $usr_adds ), array_keys( $usr_updates ) ) );
471 foreach ( $user_list as $user ) {
472 // trying to get user id from user name
473 $u = &user_get_object_by_name ($user) ;
475 $user_id = $u->getID();
480 $uu = isset($usr_updates[$user]) ? $usr_updates[$user] : 0 ;
481 $ua = isset($usr_adds[$user]) ? $usr_adds[$user] : 0 ;
482 if ($uu > 0 || $ua > 0) {
483 if (!db_query_params ('INSERT INTO stats_cvs_user (month,day,group_id,user_id,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
484 array ($month_string,
490 echo "Error while inserting into stats_cvs_user\n" ;
500 function generateSnapshots($params) {
502 $project = $this->checkParams($params);
507 $group_name = $project->getUnixName();
509 $snapshot = forge_get_config('scm_snapshots_path').'/'.$group_name.'-scm-latest.tar'.util_get_compressed_file_extension();
510 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar'.util_get_compressed_file_extension();
512 if (! $project->usesPlugin($this->name)) {
516 if (! $project->enableAnonSCM()) {
517 if (is_file($snapshot)) {
520 if (is_file($tarball)) {
526 $toprepo = forge_get_config('repos_path', 'scmsvn');
527 $repo = $toprepo . '/' . $project->getUnixName();
529 if (!is_dir ($repo) || !is_file ("$repo/format")) {
530 if (is_file($snapshot)) {
533 if (is_file($tarball)) {
539 $tmp = trim (`mktemp -d`) ;
543 $today = date ('Y-m-d') ;
544 $dir = $project->getUnixName ()."-$today" ;
545 system ("mkdir -p $tmp") ;
547 system ("svn ls file://$repo/trunk > /dev/null", $code) ;
549 system ("cd $tmp ; svn export file://$repo/trunk $dir > /dev/null 2>&1") ;
550 system ("tar cCf $tmp - $dir |".forge_get_config('compression_method')."> $tmp/snapshot") ;
551 chmod ("$tmp/snapshot", 0644) ;
552 copy ("$tmp/snapshot", $snapshot) ;
553 unlink ("$tmp/snapshot") ;
554 system ("rm -rf $tmp/$dir") ;
556 if (is_file($snapshot)) {
561 system ("tar cCf $toprepo - ".$project->getUnixName() ."|".forge_get_config('compression_method')."> $tmp/tarball") ;
562 chmod ("$tmp/tarball", 0644) ;
563 copy ("$tmp/tarball", $tarball) ;
564 unlink ("$tmp/tarball") ;
565 system ("rm -rf $tmp") ;
568 function activity($params) {
569 global $last_user, $last_time, $last_tag, $time_ok, $start_time, $end_time,
570 $adds, $deletes, $updates, $commits, $date_key,
571 $usr_adds, $usr_deletes, $usr_updates, $old_commit,
572 $messages, $last_message, $times, $revisions, $users;
573 $group_id = $params['group'];
574 $project = group_get_object($group_id);
575 if (! $project->usesPlugin($this->name)) {
579 if (in_array('scmsvn', $params['show']) || (count($params['show']) < 1)) {
582 $start_time = $params['begin'];
583 $end_time = $params['end'];
584 $d1 = date('Y-m-d', $start_time - 80000);
585 $d2 = date('Y-m-d', $end_time + 80000);
587 $repo = forge_get_config('repos_path', 'scmsvn') . '/' . $project->getUnixName();
588 $pipe = popen("svn log file://$repo --xml -v -r '".'{'.$d2.'}:{'.$d1.'}'."' 2> /dev/null", 'r' );
589 $xml_parser = xml_parser_create();
590 xml_set_element_handler($xml_parser, "SVNPluginStartElement", "SVNPluginEndElement");
591 xml_set_character_data_handler($xml_parser, "SVNPluginCharData");
592 while (!feof($pipe) && $data = fgets($pipe, 4096)) {
593 if (!xml_parse($xml_parser, $data, feof ($pipe))) {
594 debug("Unable to parse XML with error " .
595 xml_error_string(xml_get_error_code($xml_parser)) .
597 xml_get_current_line_number($xml_parser));
602 xml_parser_free($xml_parser);
603 if ($adds > 0 || $updates > 0) {
605 foreach ($messages as $message) {
607 $result['section'] = 'scm';
608 $result['group_id'] = $group_id;
609 $result['ref_id'] = 'viewvc.php/?root='.$project->getUnixName();
610 $result['description'] = $message.' (r'.$revisions[$i].')';
611 $result['user_name'] = $users[$i];
612 $userObject = user_get_object_by_name($users[$i]);
613 if (is_a($userObject, 'GFUser')) {
614 $result['realname'] = $userObject->getFirstName().' '.$userObject->getLastName();
615 $result['user_id'] = $userObject->getId();
617 $result['realname'] = '';
618 $result['user_id'] = '';
620 $result['activity_date'] = $times[$i];
621 $result['subref_id'] = '&view=rev&revision='.$revisions[$i];
622 $params['results'][] = $result;
627 $params['ids'][] = $this->name;
628 $params['texts'][] = _('Subversion Commits');
632 function installOrUpdateCmds($project, $unix_group_name, $repos) {
636 $params['unix_group_name'] = $unix_group_name;
637 $group = group_get_object_by_name($unix_group_name);
638 $params['group_id'] = $group->getID();
639 $params['repos'] = $repos;
640 $params['hooks'] = &$hooks;
641 plugin_hook_by_reference('cmd_for_post_commit_hook', $params);
643 foreach ($params['hooks'] as $plugin => $cmd ) {
644 if (getenv('sys_localinc')) {
645 $cmd = 'sys_localinc='.getenv(sys_localinc).' '.$cmd;
647 $contents = @file_get_contents($repos."/hooks/post-commit");
648 if ($project->usesPlugin($plugin)) {
649 if (strstr($contents, "#begin added by $plugin") === false ) {
650 $this->installCmdInHook($repos, $plugin, $cmd);
652 $this->updateCmdInHook($repos, $plugin, $cmd);
654 } elseif (!$project->usesPlugin($plugin) &&
655 (strstr($contents, "#begin added by $plugin") !== false )) {
656 $this->removeCmdInHook($repos, $plugin);
661 function installCmdInHook($repos, $name, $text) {
663 if (file_exists($repos.'/hooks/post-commit')) {
664 $FOut = fopen($repos.'/hooks/post-commit', "a+");
667 $FOut = fopen($repos.'/hooks/post-commit', "w");
668 $Line = '#!/bin/sh'."\n"; // add this line to first line or else the script fails
672 $Line .= "\n#begin added by $name\n$text\n#end added by $name\n";
674 fwrite($FOut, $Line);
676 system("chmod 700 $repos/hooks/post-commit");
681 function updateCmdInHook($repos, $plugin, $text) {
683 $contents = @file_get_contents($repos."/hooks/post-commit");
685 $new = preg_replace("/(#begin added by $plugin\n)(.*)(\n#end added by $plugin)/s", '$1{COMMAND}$3', $contents);
686 $new = str_replace('{COMMAND}', $text, $new);
688 if ($contents !== $new) {
689 $fout = fopen($repos.'/hooks/post-commit', "w");
695 function removeCmdInHook($repos, $plugin) {
697 $contents = @file_get_contents($repos."/hooks/post-commit");
698 $new = preg_replace("/#begin added by $plugin\n.*?\n#end added by $plugin/s", '', $contents);
700 if ($contents !== $new) {
701 if (preg_match("/^#\!\/bin\/sh(\n+)$/s", $new)) {
702 unlink($repos.'/hooks/post-commit');
704 $fout = fopen($repos.'/hooks/post-commit', "w");
712 // End of class, helper functions now
714 function SVNPluginCharData($parser, $chars) {
715 global $last_tag, $last_user, $last_time, $start_time, $end_time, $old_commit, $commits,
716 $time_ok, $user_list, $last_message, $messages, $times, $users;
719 $last_user = preg_replace('/[^a-z0-9_-]/', '', strtolower(trim($chars)));
720 $users[] = $last_user;
724 $chars = preg_replace('/T(\d\d:\d\d:\d\d)\.\d+Z?$/', ' ${1}', $chars);
725 $last_time = strtotime($chars);
726 if ($start_time <= $last_time && $last_time < $end_time) {
731 $times[] = $last_time;
735 /* If commit id is the same, then concatenate the string with the previous
736 + * (happen when the message contain accents).
738 if ($old_commit == $commits) {
739 $messages[count($messages)-1] .= $chars;
741 $messages[] = $chars;
743 $old_commit = $commits;
749 function SVNPluginStartElement($parser, $name, $attrs) {
750 global $last_user, $last_time, $last_tag, $time_ok,
751 $adds, $updates, $usr_adds, $usr_updates, $last_message, $messages, $times, $revisions;
755 // Make sure we clean up before doing a new log entry
758 $revisions[] = $attrs['REVISION'];
763 if ($attrs['ACTION'] == "M") {
766 $usr_updates[$last_user] = isset($usr_updates[$last_user]) ? ($usr_updates[$last_user]+1) : 1 ;
768 } elseif ($attrs['ACTION'] == "A") {
771 $usr_adds[$last_user] = isset($usr_adds[$last_user]) ? ($usr_adds[$last_user]+1) : 1 ;
780 function SVNPluginEndElement($parser, $name) {
781 global $time_ok, $last_tag, $commits;
782 if ($name == "LOGENTRY" && $time_ok) {
790 // c-file-style: "bsd"