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)).")";
88 . sprintf(_('Documentation for %1$s is available at <a href="%2$s">%2$s</a>.'),
90 'href="http://svnbook.red-bean.com/')
94 function topModule($project) {
95 // Check toplevel module presence
96 $repo = 'file://' . forge_get_config('repos_path', $this->name).'/'.$project->getUnixName().'/';
99 if (!(exec("svn ls '$repo'", $res) && in_array($module.'/', $res)))
107 function getInstructionsForAnon($project) {
108 $b = '<h2>' . _('Anonymous Subversion Access') . '</h2>';
110 $b .= _("This project's SVN repository can be checked out through anonymous access with the following command(s).");
114 $module = $this->topModule($project);
115 if (forge_get_config('use_ssh', 'scmsvn')) {
116 $b .= '<tt>svn checkout svn://'.$this->getBoxForProject($project).$this->svn_root_fs.'/'.$project->getUnixName().$module.'</tt><br />';
118 if (forge_get_config('use_dav', 'scmsvn')) {
119 $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 />';
120 $b .= _('The password is ').forge_get_config('anonsvn_password', 'scmsvn').'<br />';
126 function getInstructionsForRW($project) {
129 $module = $this->topModule($project);
131 if (session_loggedin()) {
132 $u =& user_get_object(user_getid());
133 $d = $u->getUnixName() ;
134 if (forge_get_config('use_ssh', 'scmsvn')) {
136 $b .= sprintf(_('Developer %s Access via SSH'), 'Subversion');
139 $b .= sprintf(_('Only project developers can access the %s tree via this method.'), 'Subversion');
141 $b .= _('SSH must be installed on your client machine.');
143 $b .= _('Enter your site password when prompted.');
145 $b .= '<p><tt>svn checkout svn+ssh://'.$d.'@' . $this->getBoxForProject($project) . $this->svn_root_fs .'/'. $project->getUnixName().$module.'</tt></p>' ;
147 if (forge_get_config('use_dav', 'scmsvn')) {
149 $b .= _('Developer Subversion Access via DAV');
152 $b .= sprintf(_('Only project developers can access the %s tree via this method.'), 'Subversion');
154 $b .= _('Enter your site password when prompted.');
156 $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>' ;
159 if (forge_get_config('use_ssh', 'scmsvn')) {
161 $b .= sprintf(_('Developer %s Access via SSH'), 'Subversion');
164 $b .= sprintf(_('Only project developers can access the %s tree via this method.'), 'Subversion');
166 $b .= _('SSH must be installed on your client machine.');
168 $b .= _('Substitute <i>developername</i> with the proper values.');
170 $b .= _('Enter your site password when prompted.');
172 $b .= '<p><tt>svn checkout svn+ssh://<i>'._('developername').'</i>@' . $this->getBoxForProject($project) . $this->svn_root_fs .'/'. $project->getUnixName().$module.'</tt></p>' ;
174 if (forge_get_config('use_dav', 'scmsvn')) {
176 $b .= _('Developer Subversion Access via DAV');
179 $b .= sprintf(_('Only project developers can access the %s tree via this method.'), 'Subversion');
181 $b .= _('Substitute <i>developername</i> with the proper values.');
183 $b .= _('Enter your site password when prompted.');
185 $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>' ;
191 function getSnapshotPara($project) {
195 function getBrowserLinkBlock($project) {
197 $b = $HTML->boxMiddle(sprintf(_('%s Repository Browser'), 'Subversion'));
199 $b .= sprintf(_("Browsing the %s tree gives you a view into the current status of this project's code."), 'Subversion');
201 $b .= _('You may also view the complete histories of any file in the repository.');
204 $b .= util_make_link ("/scm/browser.php?group_id=".$project->getID(),
205 sprintf(_('Browse %s Repository'), 'Subversion')
211 function getStatsBlock($project) {
215 $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',
216 array ($project->getID()));
218 if (db_numrows($result) > 0) {
219 $b .= $HTML->boxMiddle(_('Repository Statistics'));
221 $tableHeaders = array(
226 $b .= $HTML->listTableTop($tableHeaders);
229 $total = array('adds' => 0, 'commits' => 0);
231 while($data = db_fetch_array($result)) {
232 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
233 $b .= '<td width="50%">' ;
234 $b .= util_make_link_u ($data['user_name'], $data['user_id'], $data['realname']) ;
235 $b .= '</td><td width="25%" align="right">'.$data['adds']. '</td>'.
236 '<td width="25%" align="right">'.$data['commits'].'</td></tr>';
237 $total['adds'] += $data['adds'];
238 $total['commits'] += $data['commits'];
241 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
242 $b .= '<td width="50%"><strong>'._('Total').':</strong></td>'.
243 '<td width="25%" align="right"><strong>'.$total['adds']. '</strong></td>'.
244 '<td width="25%" align="right"><strong>'.$total['commits'].'</strong></td>';
246 $b .= $HTML->listTableBottom();
252 function printBrowserPage($params) {
253 $project = $this->checkParams($params);
258 if ($project->usesPlugin($this->name)) {
259 if ($this->browserDisplayable($project)) {
260 session_redirect("/scm/viewvc.php/?root=".$project->getUnixName());
265 function createOrUpdateRepo($params) {
266 $project = $this->checkParams($params);
271 if (! $project->usesPlugin($this->name)) {
275 $repo = forge_get_config('repos_path', 'scmsvn') . '/' . $project->getUnixName();
277 if (!is_dir ($repo) || !is_file ("$repo/format")) {
278 if (!mkdir($repo, 0700, true)) {
282 system ("svnadmin create $repo", $ret);
286 system ("svn mkdir -m'Init' file:///$repo/trunk file:///$repo/tags file:///$repo/branches >/dev/null") ;
287 if (forge_get_config('use_ssh', 'scmsvn')) {
288 $unix_group = 'scm_' . $project->getUnixName() ;
289 system ("find $repo -type d | xargs -I{} chmod g+s {}") ;
290 if ($project->enableAnonSCM()) {
291 system ("chmod -R g+wX,o+rX-w $repo") ;
293 system ("chmod -R g+wX,o-rwx $repo") ;
295 system ("chgrp -R $unix_group $repo") ;
297 $unix_user = forge_get_config('apache_user');
298 $unix_group = forge_get_config('apache_group');
299 system ("chmod -R g-rwx,o-rwx $repo") ;
300 system ("chown -R $unix_user:$unix_group $repo") ;
304 if (forge_get_config('use_ssh', 'scmsvn')) {
305 $unix_group = 'scm_' . $project->getUnixName();
306 system("find $repo -type d | xargs -I{} chmod g+s {}");
307 if (forge_get_config('use_dav', 'scmsvn')) {
308 $unix_user = forge_get_config('apache_user');
309 system("chown $unix_user:$unix_group $repo");
311 system("chgrp $unix_group $repo");
313 if ($project->enableAnonSCM()) {
314 system("chmod g+wX,o+rX-w $repo") ;
316 system("chmod g+wX,o-rwx $repo") ;
319 $unix_user = forge_get_config('apache_user');
320 $unix_group = forge_get_config('apache_group');
321 system("chown $unix_user:$unix_group $repo") ;
322 system("chmod g-rwx,o-rwx $repo") ;
326 function updateRepositoryList($params) {
327 $groups = $this->getGroups();
329 // Update WebDAV stuff
330 if (!forge_get_config('use_dav', 'scmsvn')) {
336 $engine = RBACEngine::getInstance() ;
339 foreach ($groups as $project) {
340 if ( !$project->isActive()) {
343 if ( !$project->usesSCM()) {
346 $access_data .= '[' . $project->getUnixName() . ":/]\n";
348 $users = $engine->getUsersByAllowedAction('scm',$project->getID(),'read');
349 foreach ($users as $user) {
350 $svnusers[$user->getID()] = $user;
351 if (forge_check_perm_for_user($user,
355 $access_data .= $user->getUnixName() . "= rw\n";
357 $access_data .= $user->getUnixName() . "= r\n";
361 if ($project->enableAnonSCM()) {
362 $access_data .= forge_get_config('anonsvn_login', 'scmsvn')." = r\n";
363 $access_data .= "* = r\n";
366 $access_data .= "\n";
369 foreach ($svnusers as $user_id => $user) {
370 $password_data .= $user->getUnixName().':'.$user->getUnixPasswd()."\n";
372 $password_data .= forge_get_config('anonsvn_login', 'scmsvn').":".htpasswd_apr1_md5(forge_get_config('anonsvn_password', 'scmsvn'))."\n";
374 $fname = forge_get_config('data_path').'/svnroot-authfile';
375 $f = fopen($fname.'.new', 'w');
376 fwrite($f, $password_data);
378 chmod($fname.'.new', 0644);
379 rename($fname.'.new', $fname);
381 $fname = forge_get_config('data_path').'/svnroot-access';
382 $f = fopen($fname.'.new', 'w');
383 fwrite($f, $access_data);
385 chmod($fname.'.new', 0644);
386 rename($fname.'.new', $fname);
389 function gatherStats($params) {
390 global $last_user, $last_time, $last_tag, $time_ok, $start_time, $end_time,
391 $adds, $deletes, $updates, $commits, $date_key,
392 $usr_adds, $usr_deletes, $usr_updates;
396 $project = $this->checkParams($params);
401 if (! $project->usesPlugin($this->name)) {
405 if ($params['mode'] == 'day') {
408 $year = $params['year'];
409 $month = $params['month'];
410 $day = $params['day'];
411 $month_string = sprintf("%04d%02d", $year, $month);
412 $start_time = gmmktime(0, 0, 0, $month, $day, $year);
413 $end_time = $start_time + 86400;
418 $usr_updates = array();
420 $repo = forge_get_config('repos_path', 'scmsvn') . '/' . $project->getUnixName();
421 if (!is_dir ($repo) || !is_file ("$repo/format")) {
422 echo "No repository $repo\n";
427 $d1 = date('Y-m-d', $start_time - 150000);
428 $d2 = date('Y-m-d', $end_time + 150000);
430 $pipe = popen ("svn log file://$repo --xml -v -q -r '".'{'.$d2.'}:{'.$d1.'}'."' 2> /dev/null", 'r' ) ;
432 // cleaning stats_cvs_* table for the current day
433 $res = db_query_params('DELETE FROM stats_cvs_group WHERE month=$1 AND day=$2 AND group_id=$3',
438 echo "Error while cleaning stats_cvs_group\n" ;
443 $res = db_query_params ('DELETE FROM stats_cvs_user WHERE month=$1 AND day=$2 AND group_id=$3',
444 array ($month_string,
446 $project->getID())) ;
448 echo "Error while cleaning stats_cvs_user\n" ;
453 $xml_parser = xml_parser_create();
454 xml_set_element_handler($xml_parser, "SVNPluginStartElement", "SVNPluginEndElement");
455 xml_set_character_data_handler($xml_parser, "SVNPluginCharData");
457 // Analyzing history stream
458 while (!feof($pipe) &&
459 $data = fgets ($pipe, 4096)) {
460 if (!xml_parse ($xml_parser, $data, feof ($pipe))) {
461 debug("Unable to parse XML with error " .
462 xml_error_string(xml_get_error_code($xml_parser)) .
464 xml_get_current_line_number($xml_parser));
471 xml_parser_free($xml_parser);
473 // inserting group results in stats_cvs_groups
474 if ($updates > 0 || $adds > 0) {
475 if (!db_query_params('INSERT INTO stats_cvs_group (month,day,group_id,checkouts,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
476 array ($month_string,
482 echo "Error while inserting into stats_cvs_group\n" ;
488 // building the user list
489 $user_list = array_unique( array_merge( array_keys( $usr_adds ), array_keys( $usr_updates ) ) );
491 foreach ( $user_list as $user ) {
492 // trying to get user id from user name
493 $u = &user_get_object_by_name ($user) ;
495 $user_id = $u->getID();
500 $uu = isset($usr_updates[$user]) ? $usr_updates[$user] : 0 ;
501 $ua = isset($usr_adds[$user]) ? $usr_adds[$user] : 0 ;
502 if ($uu > 0 || $ua > 0) {
503 if (!db_query_params ('INSERT INTO stats_cvs_user (month,day,group_id,user_id,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
504 array ($month_string,
510 echo "Error while inserting into stats_cvs_user\n" ;
520 function generateSnapshots($params) {
522 $project = $this->checkParams($params);
527 $group_name = $project->getUnixName();
529 $snapshot = forge_get_config('scm_snapshots_path').'/'.$group_name.'-scm-latest.tar'.util_get_compressed_file_extension();
530 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar'.util_get_compressed_file_extension();
532 if (! $project->usesPlugin($this->name)) {
536 if (! $project->enableAnonSCM()) {
537 if (is_file($snapshot)) {
540 if (is_file($tarball)) {
546 $toprepo = forge_get_config('repos_path', 'scmsvn');
547 $repo = $toprepo . '/' . $project->getUnixName();
549 if (!is_dir ($repo) || !is_file ("$repo/format")) {
550 if (is_file($snapshot)) {
553 if (is_file($tarball)) {
559 $tmp = trim (`mktemp -d`) ;
563 $today = date ('Y-m-d') ;
564 $dir = $project->getUnixName ()."-$today" ;
565 system ("mkdir -p $tmp") ;
567 system ("svn ls file://$repo/trunk > /dev/null", $code) ;
569 system ("cd $tmp ; svn export file://$repo/trunk $dir > /dev/null 2>&1") ;
570 system ("tar cCf $tmp - $dir |".forge_get_config('compression_method')."> $tmp/snapshot") ;
571 chmod ("$tmp/snapshot", 0644) ;
572 copy ("$tmp/snapshot", $snapshot) ;
573 unlink ("$tmp/snapshot") ;
574 system ("rm -rf $tmp/$dir") ;
576 if (is_file($snapshot)) {
581 system ("tar cCf $toprepo - ".$project->getUnixName() ."|".forge_get_config('compression_method')."> $tmp/tarball") ;
582 chmod ("$tmp/tarball", 0644) ;
583 copy ("$tmp/tarball", $tarball) ;
584 unlink ("$tmp/tarball") ;
585 system ("rm -rf $tmp") ;
588 function activity($params) {
589 global $last_user, $last_time, $last_tag, $time_ok, $start_time, $end_time,
590 $adds, $deletes, $updates, $commits, $date_key,
591 $usr_adds, $usr_deletes, $usr_updates, $old_commit,
592 $messages, $last_message, $times, $revisions, $users;
593 $group_id = $params['group'];
594 $project = group_get_object($group_id);
595 if (! $project->usesPlugin($this->name)) {
599 if (in_array('scmsvn', $params['show']) || (count($params['show']) < 1)) {
602 $start_time = $params['begin'];
603 $end_time = $params['end'];
604 $d1 = date('Y-m-d', $start_time - 80000);
605 $d2 = date('Y-m-d', $end_time + 80000);
607 $repo = forge_get_config('repos_path', 'scmsvn') . '/' . $project->getUnixName();
608 $pipe = popen("svn log file://$repo --xml -v -r '".'{'.$d2.'}:{'.$d1.'}'."' 2> /dev/null", 'r' );
609 $xml_parser = xml_parser_create();
610 xml_set_element_handler($xml_parser, "SVNPluginStartElement", "SVNPluginEndElement");
611 xml_set_character_data_handler($xml_parser, "SVNPluginCharData");
612 while (!feof($pipe) && $data = fgets($pipe, 4096)) {
613 if (!xml_parse($xml_parser, $data, feof ($pipe))) {
614 debug("Unable to parse XML with error " .
615 xml_error_string(xml_get_error_code($xml_parser)) .
617 xml_get_current_line_number($xml_parser));
622 xml_parser_free($xml_parser);
623 if ($adds > 0 || $updates > 0) {
625 foreach ($messages as $message) {
627 $result['section'] = 'scm';
628 $result['group_id'] = $group_id;
629 $result['ref_id'] = 'viewvc.php/?root='.$project->getUnixName();
630 $result['description'] = $message.' (r'.$revisions[$i].')';
631 $result['user_name'] = $users[$i];
632 $userObject = user_get_object_by_name($users[$i]);
633 if (is_a($userObject, 'GFUser')) {
634 $result['realname'] = $userObject->getFirstName().' '.$userObject->getLastName();
635 $result['user_id'] = $userObject->getId();
637 $result['realname'] = '';
638 $result['user_id'] = '';
640 $result['activity_date'] = $times[$i];
641 $result['subref_id'] = '&view=rev&revision='.$revisions[$i];
642 $params['results'][] = $result;
647 $params['ids'][] = $this->name;
648 $params['texts'][] = _('Subversion Commits');
653 // End of class, helper functions now
655 function SVNPluginCharData($parser, $chars) {
656 global $last_tag, $last_user, $last_time, $start_time, $end_time, $old_commit, $commits,
657 $time_ok, $user_list, $last_message, $messages, $times, $users;
660 $last_user = preg_replace('/[^a-z0-9_-]/', '', strtolower(trim($chars)));
661 $users[] = $last_user;
665 $chars = preg_replace('/T(\d\d:\d\d:\d\d)\.\d+Z?$/', ' ${1}', $chars);
666 $last_time = strtotime($chars);
667 if ($start_time <= $last_time && $last_time < $end_time) {
672 $times[] = $last_time;
676 /* If commit id is the same, then concatenate the string with the previous
677 + * (happen when the message contain accents).
679 if ($old_commit == $commits) {
680 $messages[count($messages)-1] .= $chars;
682 $messages[] = $chars;
684 $old_commit = $commits;
690 function SVNPluginStartElement($parser, $name, $attrs) {
691 global $last_user, $last_time, $last_tag, $time_ok,
692 $adds, $updates, $usr_adds, $usr_updates, $last_message, $messages, $times, $revisions;
696 // Make sure we clean up before doing a new log entry
699 $revisions[] = $attrs['REVISION'];
704 if ($attrs['ACTION'] == "M") {
707 $usr_updates[$last_user] = isset($usr_updates[$last_user]) ? ($usr_updates[$last_user]+1) : 1 ;
709 } elseif ($attrs['ACTION'] == "A") {
712 $usr_adds[$last_user] = isset($usr_adds[$last_user]) ? ($usr_adds[$last_user]+1) : 1 ;
721 function SVNPluginEndElement($parser, $name) {
722 global $time_ok, $last_tag, $commits;
723 if ($name == "LOGENTRY" && $time_ok) {
731 // c-file-style: "bsd"