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>
8 * This file is part of FusionForge.
10 * FusionForge is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published
12 * by the Free Software Foundation; either version 2 of the License,
13 * or (at your option) any later version.
15 * FusionForge is distributed in the hope that it will be useful, but
16 * WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 * General Public License for more details.
20 * You should have received a copy of the GNU General Public License
21 * along with FusionForge; if not, write to the Free Software
22 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
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 = '/svn';
43 $this->hooks[] = 'scm_browser_page';
44 $this->hooks[] = 'scm_update_repolist' ;
45 $this->hooks[] = 'scm_generate_snapshots' ;
46 $this->hooks[] = 'scm_gather_stats' ;
48 $this->provides['svn'] = true;
53 function getDefaultServer() {
54 return forge_get_config('default_server', 'scmsvn') ;
57 function printShortStats ($params) {
58 $project = $this->checkParams ($params) ;
63 if ($project->usesPlugin($this->name)) {
64 $result = db_query_params('SELECT sum(commits) AS commits, sum(adds) AS adds FROM stats_cvs_group WHERE group_id=$1',
65 array ($project->getID())) ;
66 $commit_num = db_result($result,0,'commits');
67 $add_num = db_result($result,0,'adds');
74 echo ' (SVN: '.sprintf(_('<strong>%1$s</strong> commits, <strong>%2$s</strong> adds'), number_format($commit_num, 0), number_format($add_num, 0)).")";
78 function getBlurb () {
79 return '<p>' . _('Documentation for Subversion (sometimes referred to as "SVN") is available <a href="http://svnbook.red-bean.com/">here</a>.') . '</p>';
82 function topModule ($project) {
83 // Check toplevel module presence
84 $repo = 'file://' . forge_get_config('repos_path', $this->name).'/'.$project->getUnixName().'/';
87 if (!(exec("svn ls '$repo'", $res) && in_array($module.'/', $res)))
95 function getInstructionsForAnon ($project) {
96 $b = '<h2>' . _('Anonymous Subversion Access') . '</h2>';
98 $b .= _("This project's SVN repository can be checked out through anonymous access with the following command(s).");
102 $module = $this->topModule($project);
103 if (forge_get_config('use_ssh', 'scmsvn')) {
104 $b .= '<tt>svn checkout svn://'.$project->getSCMBox().$this->svn_root.'/'.$project->getUnixName().$module.'</tt><br />';
106 if (forge_get_config('use_dav', 'scmsvn')) {
107 $b .= '<tt>svn checkout --username '.forge_get_config('anonsvn_login', 'scmsvn').' http'.((forge_get_config('use_ssl', 'scmsvn')) ? 's' : '').'://' . $project->getSCMBox(). $this->svn_root .'/'. $project->getUnixName() .$module.'</tt><br/><br/>';
108 $b .= _('The password is ').forge_get_config('anonsvn_password', 'scmsvn').'<br/>';
114 function getInstructionsForRW ($project) {
117 $module = $this->topModule($project);
119 if (session_loggedin()) {
120 $u =& user_get_object(user_getid()) ;
121 $d = $u->getUnixName() ;
122 if (forge_get_config('use_ssh', 'scmsvn')) {
124 $b .= _('Developer Subversion Access via SSH');
127 $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.');
129 $b .= '<p><tt>svn checkout svn+ssh://'.$d.'@' . $project->getSCMBox() . $this->svn_root .'/'. $project->getUnixName().$module.'</tt></p>' ;
131 if (forge_get_config('use_dav', 'scmsvn')) {
133 $b .= _('Developer Subversion Access via DAV');
136 $b .= _('Only project developers can access the SVN tree via this method. Enter your site password when prompted.');
138 $b .= '<p><tt>svn checkout --username '.$d.' http'.((forge_get_config('use_ssl', 'scmsvn')) ? 's' : '').'://'. $project->getSCMBox() . $this->svn_root .'/'.$project->getUnixName().$module.'</tt></p>' ;
141 if (forge_get_config('use_ssh', 'scmsvn')) {
143 $b .= _('Developer Subversion Access via SSH');
146 $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.');
148 $b .= '<p><tt>svn checkout svn+ssh://<i>'._('developername').'</i>@' . $project->getSCMBox() . $this->svn_root .'/'. $project->getUnixName().$module.'</tt></p>' ;
150 if (forge_get_config('use_dav', 'scmsvn')) {
152 $b .= _('Developer Subversion Access via DAV');
155 $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.');
157 $b .= '<p><tt>svn checkout --username <i>'._('developername').'</i> http'.((forge_get_config('use_ssl', 'scmsvn')) ? 's' : '').'://'. $project->getSCMBox() . $this->svn_root .'/'.$project->getUnixName().$module.'</tt></p>' ;
163 function getSnapshotPara ($project) {
167 function getBrowserLinkBlock ($project) {
169 $b = $HTML->boxMiddle(_('Subversion Repository Browser'));
171 $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.');
174 $b .= util_make_link ("/scm/browser.php?group_id=".$project->getID(),
175 _('Browse Subversion Repository')
181 function getStatsBlock ($project) {
185 $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',
186 array ($project->getID()));
188 if (db_numrows($result) > 0) {
189 $b .= $HTML->boxMiddle(_('Repository Statistics'));
191 $tableHeaders = array(
196 $b .= $HTML->listTableTop($tableHeaders);
199 $total = array('adds' => 0, 'commits' => 0);
201 while($data = db_fetch_array($result)) {
202 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
203 $b .= '<td width="50%">' ;
204 $b .= util_make_link_u ($data['user_name'], $data['user_id'], $data['realname']) ;
205 $b .= '</td><td width="25%" align="right">'.$data['adds']. '</td>'.
206 '<td width="25%" align="right">'.$data['commits'].'</td></tr>';
207 $total['adds'] += $data['adds'];
208 $total['commits'] += $data['commits'];
211 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
212 $b .= '<td width="50%"><strong>'._('Total').':</strong></td>'.
213 '<td width="25%" align="right"><strong>'.$total['adds']. '</strong></td>'.
214 '<td width="25%" align="right"><strong>'.$total['commits'].'</strong></td>';
216 $b .= $HTML->listTableBottom();
222 function printBrowserPage ($params) {
223 $project = $this->checkParams ($params) ;
228 if ($project->usesPlugin ($this->name)) {
229 if ($this->browserDisplayable ($project)) {
230 print '<iframe src="'.util_make_url ("/scm/viewvc.php/?root=".$project->getUnixName()).'" frameborder="no" width=100% height=700></iframe>' ;
235 function createOrUpdateRepo ($params) {
236 $project = $this->checkParams ($params) ;
241 if (! $project->usesPlugin ($this->name)) {
245 $repo = forge_get_config('repos_path', 'scmsvn') . '/' . $project->getUnixName() ;
247 if (!is_dir ($repo) || !is_file ("$repo/format")) {
248 system ("svnadmin create $repo") ;
249 system ("svn mkdir -m'Init' file:///$repo/trunk file:///$repo/tags file:///$repo/branches") ;
252 if (forge_get_config('use_ssh', 'scmsvn')) {
253 $unix_group = 'scm_' . $project->getUnixName() ;
254 system ("find $repo -type d | xargs chmod g+s") ;
255 system ("chgrp -R $unix_group $repo") ;
256 if ($project->enableAnonSCM()) {
257 system ("chmod -R g+wX,o+rX-w $repo") ;
259 system ("chmod -R g+wX,o-rwx $repo") ;
262 $unix_user = forge_get_config('apache_user');
263 $unix_group = forge_get_config('apache_group');
264 system ("chown -R $unix_user:$unix_group $repo") ;
265 system ("chmod -R g-rwx,o-rwx $repo") ;
269 function updateRepositoryList ($params) {
270 $groups = $this->getGroups () ;
272 // Update WebDAV stuff
273 if (!forge_get_config('use_dav', 'scmsvn')) {
278 $password_data = '' ;
280 $svnusers = array () ;
281 foreach ($groups as $project) {
282 if ( !$project->isActive()) {
285 $access_data .= '[' . $project->getUnixName () . ":/]\n" ;
287 $users = $project->getMembers () ;
288 foreach ($users as $user) {
289 if (forge_check_perm_for_user ($user,
293 $access_data .= $user->getUnixName() . "= rw\n" ;
294 $svnusers[$user->getID()] = $user ;
295 } elseif (forge_check_perm_for_user ($user,
299 $access_data .= $user->getUnixName() . "= r\n" ;
300 $svnusers[$user->getID()] = $user ;
304 if ( $project->enableAnonSCM() ) {
305 $access_data .= ".forge_get_config('anonsvn_login', 'scmsvn').= r\n" ;
306 $access_data .= "* = r\n" ;
309 $access_data .= "\n" ;
312 foreach ($svnusers as $user_id => $user) {
313 $password_data .= $user->getUnixName().':'.$user->getUnixPasswd()."\n" ;
315 $password_data .= forge_get_config('anonsvn_login', 'scmsvn').":".htpasswd_apr1_md5(forge_get_config('anonsvn_pass', 'scmsvn'))."\n";
317 $fname = forge_get_config('data_path').'/svnroot-authfile' ;
318 $f = fopen ($fname.'.new', 'w') ;
319 fwrite ($f, $password_data) ;
321 chmod ($fname.'.new', 0644) ;
322 rename ($fname.'.new', $fname) ;
324 $fname = forge_get_config('data_path').'/svnroot-access' ;
325 $f = fopen ($fname.'.new', 'w') ;
326 fwrite ($f, $access_data) ;
328 chmod ($fname.'.new', 0644) ;
329 rename ($fname.'.new', $fname) ;
332 function gatherStats ($params) {
333 global $last_user, $last_time, $last_tag, $time_ok, $start_time, $end_time,
334 $adds, $deletes, $updates, $commits, $date_key,
335 $usr_adds, $usr_deletes, $usr_updates;
339 $project = $this->checkParams ($params) ;
344 if (! $project->usesPlugin ($this->name)) {
348 if ($params['mode'] == 'day') {
351 $year = $params ['year'] ;
352 $month = $params ['month'] ;
353 $day = $params ['day'] ;
354 $month_string = sprintf( "%04d%02d", $year, $month );
355 $start_time = gmmktime( 0, 0, 0, $month, $day, $year);
356 $end_time = $start_time + 86400;
360 $usr_adds = array () ;
361 $usr_updates = array () ;
363 $repo = forge_get_config('repos_path', 'scmsvn') . '/' . $project->getUnixName() ;
364 if (!is_dir ($repo) || !is_file ("$repo/format")) {
365 echo "No repository\n" ;
370 $d1 = date ('Y-m-d', $start_time - 150000) ;
371 $d2 = date ('Y-m-d', $end_time + 150000) ;
373 $pipe = popen ("svn log file://$repo --xml -v -q -r '".'{'.$d2.'}:{'.$d1.'}'."' 2> /dev/null", 'r' ) ;
375 // cleaning stats_cvs_* table for the current day
376 $res = db_query_params ('DELETE FROM stats_cvs_group WHERE month=$1 AND day=$2 AND group_id=$3',
377 array ($month_string,
379 $project->getID())) ;
381 echo "Error while cleaning stats_cvs_group\n" ;
386 $res = db_query_params ('DELETE FROM stats_cvs_user WHERE month=$1 AND day=$2 AND group_id=$3',
387 array ($month_string,
389 $project->getID())) ;
391 echo "Error while cleaning stats_cvs_user\n" ;
396 $xml_parser = xml_parser_create();
397 xml_set_element_handler($xml_parser, "SVNPluginStartElement", "SVNPluginEndElement");
398 xml_set_character_data_handler($xml_parser, "SVNPluginCharData");
400 // Analyzing history stream
401 while (!feof($pipe) &&
402 $data = fgets ($pipe, 4096)) {
404 if (!xml_parse ($xml_parser, $data, feof ($pipe))) {
405 debug("Unable to parse XML with error " .
406 xml_error_string(xml_get_error_code($xml_parser)) .
408 xml_get_current_line_number($xml_parser));
415 xml_parser_free ($xml_parser);
417 // inserting group results in stats_cvs_groups
418 if ($updates > 0 || $adds > 0) {
419 if (!db_query_params ('INSERT INTO stats_cvs_group (month,day,group_id,checkouts,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
420 array ($month_string,
426 echo "Error while inserting into stats_cvs_group\n" ;
432 // building the user list
433 $user_list = array_unique( array_merge( array_keys( $usr_adds ), array_keys( $usr_updates ) ) );
435 foreach ( $user_list as $user ) {
436 // trying to get user id from user name
437 $u = &user_get_object_by_name ($user) ;
439 $user_id = $u->getID();
444 $uu = $usr_updates[$user] ? $usr_updates[$user] : 0 ;
445 $ua = $usr_adds[$user] ? $usr_adds[$user] : 0 ;
446 if ($uu > 0 || $ua > 0) {
447 if (!db_query_params ('INSERT INTO stats_cvs_user (month,day,group_id,user_id,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
448 array ($month_string,
454 echo "Error while inserting into stats_cvs_user\n" ;
464 function generateSnapshots ($params) {
466 $project = $this->checkParams ($params) ;
471 $group_name = $project->getUnixName() ;
473 $snapshot = forge_get_config('scm_snapshots_path').'/'.$group_name.'-scm-latest.tar.gz';
474 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar.gz';
476 if (! $project->usesPlugin ($this->name)) {
480 if (! $project->enableAnonSCM()) {
481 if (is_file($snapshot)) {
484 if (is_file($tarball)) {
490 $toprepo = forge_get_config('repos_path', 'scmsvn') ;
491 $repo = $toprepo . '/' . $project->getUnixName() ;
493 if (!is_dir ($repo) || !is_file ("$repo/format")) {
494 if (is_file($snapshot)) {
497 if (is_file($tarball)) {
503 $tmp = trim (`mktemp -d`) ;
507 $today = date ('Y-m-d') ;
508 $dir = $project->getUnixName ()."-$today" ;
509 system ("mkdir -p $tmp") ;
511 system ("svn ls file://$repo/trunk > /dev/null", $code) ;
513 system ("cd $tmp ; svn checkout file://$repo/trunk $dir > /dev/null 2>&1") ;
514 system ("tar czCf $tmp $tmp/snapshot.tar.gz $dir") ;
515 chmod ("$tmp/snapshot.tar.gz", 0644) ;
516 copy ("$tmp/snapshot.tar.gz", $snapshot) ;
517 unlink ("$tmp/snapshot.tar.gz") ;
518 system ("rm -rf $tmp/$dir") ;
520 if (is_file($snapshot)) {
525 system ("tar czCf $toprepo $tmp/tarball.tar.gz " . $project->getUnixName()) ;
526 chmod ("$tmp/tarball.tar.gz", 0644) ;
527 copy ("$tmp/tarball.tar.gz", $tarball) ;
528 unlink ("$tmp/tarball.tar.gz") ;
529 system ("rm -rf $tmp") ;
533 // End of class, helper functions now
535 function SVNPluginCharData ($parser, $chars) {
536 global $last_tag, $last_user, $last_time, $start_time, $end_time,
537 $time_ok, $user_list;
540 $last_user = ereg_replace ('[^a-z0-9_-]', '',
541 strtolower (trim ($chars))) ;
544 $chars = preg_replace('/T(\d\d:\d\d:\d\d)\.\d+Z?$/', ' ${1}', $chars);
545 $last_time = strtotime($chars);
546 if ($start_time <= $last_time && $last_time < $end_time) {
555 function SVNPluginStartElement($parser, $name, $attrs) {
556 global $last_user, $last_time, $last_tag, $time_ok,
557 $adds, $updates, $usr_adds, $usr_updates;
561 // Make sure we clean up before doing a new log entry
567 if ($attrs['ACTION'] == "M") {
570 $usr_updates[$last_user]++;
572 } elseif ($attrs['ACTION'] == "A") {
575 $usr_adds[$last_user]++;
583 function SVNPluginEndElement ($parser, $name) {
584 global $time_ok, $last_tag, $commits;
585 if ($name == "LOGENTRY" && $time_ok) {
593 // c-file-style: "bsd"