2 /** FusionForge Subversion plugin
4 * Copyright 2003-2010, Roland Mas, Franck Villaume
5 * Copyright 2004, GForge, LLC
6 * Copyright 2010, Alain Peyrat <aljeux@free.fr>
7 * Copyright 2012, Franck Villaume - TrivialDev
9 * This file is part of FusionForge.
11 * FusionForge is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published
13 * by the Free Software Foundation; either version 2 of the License,
14 * or (at your option) any later version.
16 * FusionForge is distributed in the hope that it will be useful, but
17 * WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 * General Public License for more details.
21 * You should have received a copy of the GNU General Public License along
22 * with this program; if not, write to the Free Software Foundation, Inc.,
23 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 forge_define_config_item('default_server', 'scmsvn', forge_get_config ('web_host'));
27 forge_define_config_item('repos_path', 'scmsvn', forge_get_config('chroot').'/scmrepos/svn');
28 forge_define_config_item('use_ssh', 'scmsvn', false);
29 forge_set_config_item_bool('use_ssh', 'scmsvn');
30 forge_define_config_item('use_dav', 'scmsvn', true);
31 forge_set_config_item_bool('use_dav', 'scmsvn');
32 forge_define_config_item('use_ssl', 'scmsvn', true);
33 forge_set_config_item_bool('use_ssl', 'scmsvn');
34 forge_define_config_item('anonsvn_login','scmsvn', 'anonsvn');
35 forge_define_config_item('anonsvn_password','scmsvn', 'anonsvn');
37 class SVNPlugin extends SCMPlugin {
38 function SVNPlugin() {
40 $this->name = 'scmsvn';
41 $this->text = 'Subversion';
42 $this->svn_root_fs = '/scmrepos/svn';
43 if (!file_exists($this->svn_root_fs.'/.')) {
44 $this->svn_root_fs = forge_get_config('repos_path',
47 $this->svn_root_dav = '/svn';
48 $this->_addHook('scm_browser_page');
49 $this->_addHook('scm_update_repolist');
50 $this->_addHook('scm_generate_snapshots');
51 $this->_addHook('scm_gather_stats');
52 $this->_addHook('activity');
54 $this->provides['svn'] = true;
59 function getDefaultServer() {
60 return forge_get_config('default_server', 'scmsvn') ;
63 function printShortStats($params) {
64 $project = $this->checkParams($params);
69 if ($project->usesPlugin($this->name)) {
70 $result = db_query_params('SELECT sum(commits) AS commits, sum(adds) AS adds FROM stats_cvs_group WHERE group_id=$1',
71 array ($project->getID())) ;
72 $commit_num = db_result($result,0,'commits');
73 $add_num = db_result($result,0,'adds');
80 echo ' (Subversion: '.sprintf(_('<strong>%1$s</strong> commits, <strong>%2$s</strong> adds'), number_format($commit_num, 0), number_format($add_num, 0)).")";
85 return '<p>' . _('Documentation for Subversion (sometimes referred to as "SVN") is available <a href="http://svnbook.red-bean.com/">here</a>.') . '</p>';
88 function topModule($project) {
89 // Check toplevel module presence
90 $repo = 'file://' . forge_get_config('repos_path', $this->name).'/'.$project->getUnixName().'/';
93 if (!(exec("svn ls '$repo'", $res) && in_array($module.'/', $res)))
101 function getInstructionsForAnon($project) {
102 $b = '<h2>' . _('Anonymous Subversion Access') . '</h2>';
104 $b .= _("This project's SVN repository can be checked out through anonymous access with the following command(s).");
108 $module = $this->topModule($project);
109 if (forge_get_config('use_ssh', 'scmsvn')) {
110 $b .= '<tt>svn checkout svn://'.$this->getBoxForProject($project).$this->svn_root_fs.'/'.$project->getUnixName().$module.'</tt><br />';
112 if (forge_get_config('use_dav', 'scmsvn')) {
113 $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 />';
114 $b .= _('The password is ').forge_get_config('anonsvn_password', 'scmsvn').'<br />';
120 function getInstructionsForRW($project) {
123 $module = $this->topModule($project);
125 if (session_loggedin()) {
126 $u =& user_get_object(user_getid());
127 $d = $u->getUnixName() ;
128 if (forge_get_config('use_ssh', 'scmsvn')) {
130 $b .= _('Developer Subversion Access via SSH');
133 $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.');
135 $b .= '<p><tt>svn checkout svn+ssh://'.$d.'@' . $this->getBoxForProject($project) . $this->svn_root_fs .'/'. $project->getUnixName().$module.'</tt></p>' ;
137 if (forge_get_config('use_dav', 'scmsvn')) {
139 $b .= _('Developer Subversion Access via DAV');
142 $b .= _('Only project developers can access the SVN tree via this method. Enter your site password when prompted.');
144 $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>' ;
147 if (forge_get_config('use_ssh', 'scmsvn')) {
149 $b .= _('Developer Subversion Access via SSH');
152 $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.');
154 $b .= '<p><tt>svn checkout svn+ssh://<i>'._('developername').'</i>@' . $this->getBoxForProject($project) . $this->svn_root_fs .'/'. $project->getUnixName().$module.'</tt></p>' ;
156 if (forge_get_config('use_dav', 'scmsvn')) {
158 $b .= _('Developer Subversion Access via DAV');
161 $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.');
163 $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>' ;
169 function getSnapshotPara($project) {
173 function getBrowserLinkBlock($project) {
175 $b = $HTML->boxMiddle(_('Subversion Repository Browser'));
177 $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.');
180 $b .= util_make_link ("/scm/browser.php?group_id=".$project->getID(),
181 _('Browse Subversion Repository')
187 function getStatsBlock($project) {
191 $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',
192 array ($project->getID()));
194 if (db_numrows($result) > 0) {
195 $b .= $HTML->boxMiddle(_('Repository Statistics'));
197 $tableHeaders = array(
202 $b .= $HTML->listTableTop($tableHeaders);
205 $total = array('adds' => 0, 'commits' => 0);
207 while($data = db_fetch_array($result)) {
208 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
209 $b .= '<td width="50%">' ;
210 $b .= util_make_link_u ($data['user_name'], $data['user_id'], $data['realname']) ;
211 $b .= '</td><td width="25%" align="right">'.$data['adds']. '</td>'.
212 '<td width="25%" align="right">'.$data['commits'].'</td></tr>';
213 $total['adds'] += $data['adds'];
214 $total['commits'] += $data['commits'];
217 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
218 $b .= '<td width="50%"><strong>'._('Total').':</strong></td>'.
219 '<td width="25%" align="right"><strong>'.$total['adds']. '</strong></td>'.
220 '<td width="25%" align="right"><strong>'.$total['commits'].'</strong></td>';
222 $b .= $HTML->listTableBottom();
228 function printBrowserPage($params) {
229 $project = $this->checkParams($params);
234 if ($project->usesPlugin($this->name)) {
235 if ($this->browserDisplayable($project)) {
236 session_redirect("/scm/viewvc.php/?root=".$project->getUnixName());
241 function createOrUpdateRepo($params) {
242 $project = $this->checkParams($params);
247 if (! $project->usesPlugin($this->name)) {
251 $repo = forge_get_config('repos_path', 'scmsvn') . '/' . $project->getUnixName();
253 if (!is_dir ($repo) || !is_file ("$repo/format")) {
254 system("svnadmin create $repo") ;
255 system("svn mkdir -m 'Init' file:///$repo/trunk file:///$repo/tags file:///$repo/branches >/dev/null");
258 $this->installOrUpdateCmds($project, $project->getUnixName(), $repo);
260 if (forge_get_config('use_ssh', 'scmsvn')) {
261 $unix_group = 'scm_' . $project->getUnixName();
262 system("find $repo -type d | xargs -I{} chmod g+s {}");
263 if (forge_get_config('use_dav', 'scmsvn')) {
264 $unix_user = forge_get_config('apache_user');
265 system("chown -R $unix_user:$unix_group $repo");
267 system("chgrp -R $unix_group $repo");
269 if ($project->enableAnonSCM()) {
270 system("chmod -R g+wX,o+rX-w $repo");
272 system("chmod -R g+wX,o-rwx $repo");
275 $unix_user = forge_get_config('apache_user');
276 $unix_group = forge_get_config('apache_group');
277 system("chown -R $unix_user:$unix_group $repo");
278 system("chmod -R g-rwx,o-rwx $repo");
282 function updateRepositoryList($params) {
283 $groups = $this->getGroups();
285 // Update WebDAV stuff
286 if (!forge_get_config('use_dav', 'scmsvn')) {
292 $engine = RBACEngine::getInstance() ;
295 foreach ($groups as $project) {
296 if ( !$project->isActive()) {
299 if ( !$project->usesSCM()) {
302 $access_data .= '[' . $project->getUnixName() . ":/]\n";
304 $users = $engine->getUsersByAllowedAction('scm',$project->getID(),'read');
305 foreach ($users as $user) {
306 $svnusers[$user->getID()] = $user;
307 if (forge_check_perm_for_user($user,
311 $access_data .= $user->getUnixName() . "= rw\n";
313 $access_data .= $user->getUnixName() . "= r\n";
317 if ($project->enableAnonSCM()) {
318 $access_data .= forge_get_config('anonsvn_login', 'scmsvn')." = r\n";
319 $access_data .= "* = r\n";
322 $access_data .= "\n";
325 foreach ($svnusers as $user_id => $user) {
326 $password_data .= $user->getUnixName().':'.$user->getUnixPasswd()."\n";
328 $password_data .= forge_get_config('anonsvn_login', 'scmsvn').":".htpasswd_apr1_md5(forge_get_config('anonsvn_password', 'scmsvn'))."\n";
330 $fname = forge_get_config('data_path').'/svnroot-authfile';
331 $f = fopen($fname.'.new', 'w');
332 fwrite($f, $password_data);
334 chmod($fname.'.new', 0644);
335 rename($fname.'.new', $fname);
337 $fname = forge_get_config('data_path').'/svnroot-access';
338 $f = fopen($fname.'.new', 'w');
339 fwrite($f, $access_data);
341 chmod($fname.'.new', 0644);
342 rename($fname.'.new', $fname);
345 function gatherStats($params) {
346 global $last_user, $last_time, $last_tag, $time_ok, $start_time, $end_time,
347 $adds, $deletes, $updates, $commits, $date_key,
348 $usr_adds, $usr_deletes, $usr_updates;
352 $project = $this->checkParams($params);
357 if (! $project->usesPlugin($this->name)) {
361 if ($params['mode'] == 'day') {
364 $year = $params['year'];
365 $month = $params['month'];
366 $day = $params['day'];
367 $month_string = sprintf("%04d%02d", $year, $month);
368 $start_time = gmmktime(0, 0, 0, $month, $day, $year);
369 $end_time = $start_time + 86400;
374 $usr_updates = array();
376 $repo = forge_get_config('repos_path', 'scmsvn') . '/' . $project->getUnixName();
377 if (!is_dir ($repo) || !is_file ("$repo/format")) {
378 echo "No repository\n";
383 $d1 = date('Y-m-d', $start_time - 150000);
384 $d2 = date('Y-m-d', $end_time + 150000);
386 $pipe = popen ("svn log file://$repo --xml -v -q -r '".'{'.$d2.'}:{'.$d1.'}'."' 2> /dev/null", 'r' ) ;
388 // cleaning stats_cvs_* table for the current day
389 $res = db_query_params('DELETE FROM stats_cvs_group WHERE month=$1 AND day=$2 AND group_id=$3',
394 echo "Error while cleaning stats_cvs_group\n" ;
399 $res = db_query_params ('DELETE FROM stats_cvs_user WHERE month=$1 AND day=$2 AND group_id=$3',
400 array ($month_string,
402 $project->getID())) ;
404 echo "Error while cleaning stats_cvs_user\n" ;
409 $xml_parser = xml_parser_create();
410 xml_set_element_handler($xml_parser, "SVNPluginStartElement", "SVNPluginEndElement");
411 xml_set_character_data_handler($xml_parser, "SVNPluginCharData");
413 // Analyzing history stream
414 while (!feof($pipe) &&
415 $data = fgets ($pipe, 4096)) {
416 if (!xml_parse ($xml_parser, $data, feof ($pipe))) {
417 debug("Unable to parse XML with error " .
418 xml_error_string(xml_get_error_code($xml_parser)) .
420 xml_get_current_line_number($xml_parser));
427 xml_parser_free($xml_parser);
429 // inserting group results in stats_cvs_groups
430 if ($updates > 0 || $adds > 0) {
431 if (!db_query_params('INSERT INTO stats_cvs_group (month,day,group_id,checkouts,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
432 array ($month_string,
438 echo "Error while inserting into stats_cvs_group\n" ;
444 // building the user list
445 $user_list = array_unique( array_merge( array_keys( $usr_adds ), array_keys( $usr_updates ) ) );
447 foreach ( $user_list as $user ) {
448 // trying to get user id from user name
449 $u = &user_get_object_by_name ($user) ;
451 $user_id = $u->getID();
456 $uu = isset($usr_updates[$user]) ? $usr_updates[$user] : 0 ;
457 $ua = isset($usr_adds[$user]) ? $usr_adds[$user] : 0 ;
458 if ($uu > 0 || $ua > 0) {
459 if (!db_query_params ('INSERT INTO stats_cvs_user (month,day,group_id,user_id,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
460 array ($month_string,
466 echo "Error while inserting into stats_cvs_user\n" ;
476 function generateSnapshots($params) {
478 $project = $this->checkParams($params);
483 $group_name = $project->getUnixName();
485 $snapshot = forge_get_config('scm_snapshots_path').'/'.$group_name.'-scm-latest.tar'.util_get_compressed_file_extension();
486 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar'.util_get_compressed_file_extension();
488 if (! $project->usesPlugin($this->name)) {
492 if (! $project->enableAnonSCM()) {
493 if (is_file($snapshot)) {
496 if (is_file($tarball)) {
502 $toprepo = forge_get_config('repos_path', 'scmsvn');
503 $repo = $toprepo . '/' . $project->getUnixName();
505 if (!is_dir ($repo) || !is_file ("$repo/format")) {
506 if (is_file($snapshot)) {
509 if (is_file($tarball)) {
515 $tmp = trim (`mktemp -d`) ;
519 $today = date ('Y-m-d') ;
520 $dir = $project->getUnixName ()."-$today" ;
521 system ("mkdir -p $tmp") ;
523 system ("svn ls file://$repo/trunk > /dev/null", $code) ;
525 system ("cd $tmp ; svn export file://$repo/trunk $dir > /dev/null 2>&1") ;
526 system ("tar cCf $tmp - $dir |".forge_get_config('compression_method')."> $tmp/snapshot") ;
527 chmod ("$tmp/snapshot", 0644) ;
528 copy ("$tmp/snapshot", $snapshot) ;
529 unlink ("$tmp/snapshot") ;
530 system ("rm -rf $tmp/$dir") ;
532 if (is_file($snapshot)) {
537 system ("tar cCf $toprepo - ".$project->getUnixName() ."|".forge_get_config('compression_method')."> $tmp/tarball") ;
538 chmod ("$tmp/tarball", 0644) ;
539 copy ("$tmp/tarball", $tarball) ;
540 unlink ("$tmp/tarball") ;
541 system ("rm -rf $tmp") ;
544 function activity($params) {
545 global $last_user, $last_time, $last_tag, $time_ok, $start_time, $end_time,
546 $adds, $deletes, $updates, $commits, $date_key,
547 $usr_adds, $usr_deletes, $usr_updates,
548 $messages, $last_message, $times, $revisions;
549 $group_id = $params['group'];
550 $project = group_get_object($group_id);
551 if (! $project->usesPlugin($this->name)) {
555 if (in_array('scm', $params['show'])) {
556 $start_time = $params['begin'];
557 $end_time = $params['end'];
558 $d1 = date('Y-m-d', $start_time - 80000);
559 $d2 = date('Y-m-d', $end_time + 80000);
561 $repo = forge_get_config('repos_path', 'scmsvn') . '/' . $project->getUnixName();
562 $pipe = popen("svn log file://$repo --xml -v -r '".'{'.$d2.'}:{'.$d1.'}'."' 2> /dev/null", 'r' );
563 $xml_parser = xml_parser_create();
564 xml_set_element_handler($xml_parser, "SVNPluginStartElement", "SVNPluginEndElement");
565 xml_set_character_data_handler($xml_parser, "SVNPluginCharData");
566 while (!feof($pipe) && $data = fgets($pipe, 4096)) {
567 if (!xml_parse($xml_parser, $data, feof ($pipe))) {
568 debug("Unable to parse XML with error " .
569 xml_error_string(xml_get_error_code($xml_parser)) .
571 xml_get_current_line_number($xml_parser));
576 xml_parser_free($xml_parser);
577 if ($adds > 0 || $updates > 0) {
579 foreach ($messages as $message) {
581 $result['section'] = 'scm';
582 $result['group_id'] = $group_id;
583 $result['ref_id'] = 'viewvc.php/?root='.$project->getUnixName();
584 $result['description'] = $message.' (r'.$revisions[$i].')';
585 $result['realname'] = '';
586 $result['activity_date'] = $times[$i];
587 $result['subref_id'] = '&view=rev&revision='.$revisions[$i];
588 $params['results'][] = $result;
593 $params['ids'][] = 'scm';
594 $params['texts'][] = _('SCM SVN Commits');
598 function installOrUpdateCmds($project, $unix_group_name, $repos) {
602 $params['unix_group_name'] = $unix_group_name;
603 $group = group_get_object_by_name($unix_group_name);
604 $params['group_id'] = $group->getID();
605 $params['repos'] = $repos;
606 $params['hooks'] = &$hooks;
607 plugin_hook_by_reference('cmd_for_post_commit_hook', $params);
609 foreach ($params['hooks'] as $plugin => $cmd ) {
610 if (getenv('sys_localinc')) {
611 $cmd = 'sys_localinc='.getenv(sys_localinc).' '.$cmd;
613 $contents = @file_get_contents($repos."/hooks/post-commit");
614 if ($project->usesPlugin($plugin)) {
615 if (strstr($contents, "#begin added by $plugin") === false ) {
616 $this->installCmdInHook($repos, $plugin, $cmd);
618 $this->updateCmdInHook($repos, $plugin, $cmd);
620 } elseif (!$project->usesPlugin($plugin) &&
621 (strstr($contents, "#begin added by $plugin") !== false )) {
622 $this->removeCmdInHook($repos, $plugin);
627 function installCmdInHook($repos, $name, $text) {
629 if (file_exists($repos.'/hooks/post-commit')) {
630 $FOut = fopen($repos.'/hooks/post-commit', "a+");
633 $FOut = fopen($repos.'/hooks/post-commit', "w");
634 $Line = '#!/bin/sh'."\n"; // add this line to first line or else the script fails
638 $Line .= "\n#begin added by $name\n$text\n#end added by $name\n";
640 fwrite($FOut, $Line);
642 system("chmod 700 $repos/hooks/post-commit");
647 function updateCmdInHook($repos, $plugin, $text) {
649 $contents = @file_get_contents($repos."/hooks/post-commit");
651 $new = preg_replace("/(#begin added by $plugin\n)(.*)(\n#end added by $plugin)/s", '$1{COMMAND}$3', $contents);
652 $new = str_replace('{COMMAND}', $text, $new);
654 if ($contents !== $new) {
655 $fout = fopen($repos.'/hooks/post-commit', "w");
661 function removeCmdInHook($repos, $plugin) {
663 $contents = @file_get_contents($repos."/hooks/post-commit");
664 $new = preg_replace("/#begin added by $plugin\n.*?\n#end added by $plugin/s", '', $contents);
666 if ($contents !== $new) {
667 if (preg_match("/^#\!\/bin\/sh(\n+)$/s", $new)) {
668 unlink($repos.'/hooks/post-commit');
670 $fout = fopen($repos.'/hooks/post-commit', "w");
678 // End of class, helper functions now
680 function SVNPluginCharData($parser, $chars) {
681 global $last_tag, $last_user, $last_time, $start_time, $end_time,
682 $time_ok, $user_list, $last_message, $messages, $times;
685 $last_user = preg_replace('/[^a-z0-9_-]/', '', strtolower(trim($chars)));
689 $chars = preg_replace('/T(\d\d:\d\d:\d\d)\.\d+Z?$/', ' ${1}', $chars);
690 $last_time = strtotime($chars);
691 if ($start_time <= $last_time && $last_time < $end_time) {
696 $times[] = $last_time;
700 $messages[] = $chars;
706 function SVNPluginStartElement($parser, $name, $attrs) {
707 global $last_user, $last_time, $last_tag, $time_ok,
708 $adds, $updates, $usr_adds, $usr_updates, $last_message, $messages, $times, $revisions;
712 // Make sure we clean up before doing a new log entry
715 $revisions[] = $attrs['REVISION'];
720 if ($attrs['ACTION'] == "M") {
723 $usr_updates[$last_user] = isset($usr_updates[$last_user]) ? ($usr_updates[$last_user]+1) : 1 ;
725 } elseif ($attrs['ACTION'] == "A") {
728 $usr_adds[$last_user] = isset($usr_adds[$last_user]) ? ($usr_adds[$last_user]+1) : 1 ;
737 function SVNPluginEndElement($parser, $name) {
738 global $time_ok, $last_tag, $commits;
739 if ($name == "LOGENTRY" && $time_ok) {
747 // c-file-style: "bsd"