3 * FusionForge Mercurial (Hg) plugin
5 * Copyright 2009, Roland Mas
6 * Copyright 2012, Denise Patzker
7 * Copyright 2012-2013, 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', 'scmhg', forge_get_config('web_host'));
27 forge_define_config_item('repos_path', 'scmhg', forge_get_config('chroot').'/scmrepos/hg');
29 class HgPlugin extends SCMPlugin {
32 $this->name = 'scmhg';
33 $this->text = 'Mercurial';
34 $this->_addHook('scm_browser_page');
35 $this->_addHook('scm_update_repolist');
36 $this->_addHook('scm_generate_snapshots');
37 $this->_addHook('scm_gather_stats');
42 * getPluginDescription - display the description of this plugin in pluginman admin page
44 * @return string the description
46 function getPluginDescription() {
47 return _('Use Mercuial as Source Code Management tool. Offer DAV or SSH access.');
50 function getDefaultServer() {
51 return forge_get_config('default_server', 'scmhg');
55 return '<p>' . _('Documentation for Mercurial is available at <a href="http://hgbook.red-bean.com/">http://hgbook.red-bean.com/</a> . ')._(' Another short Introduction can be found at <a href="http://hginit.com/">http://hginit.com/</a>').'</p>';
58 function getInstructionsForAnon($project) {
60 $b .= _('Anonymous Mercurial Access');
63 if (forge_get_config('use_dav', 'scmhg')) {
64 $protocol = forge_get_config('use_ssl', 'scmhg')? 'https' : 'http';
66 $b .= 'This project\'s Mercurial repository can be checked out through anonymous access with the following command.';
69 $b .= '<tt>hg clone '.$protocol.'://'.forge_get_config('anonhg_login', 'scmhg').'@' . $this->getBoxForProject($project) . '/'. 'hg' .'/'. $project->getUnixName() .'/'.'</tt><br />';
70 $b .= _('The password is ').forge_get_config('anonhg_password', 'scmhg').'<br/>';
73 $b .= '<p class="warning">'._('Please contact forge administrator, scmhg plugin is not correctly configured');
79 function getInstructionsForRW($project) {
80 $protocol = forge_get_config('use_ssl', 'scmhg')? 'https' : 'http';
81 if (session_loggedin()) {
82 $u = user_get_object(user_getid());
83 $d = $u->getUnixName();
85 if (forge_get_config('use_ssh', 'scmhg')) {
87 $b .= _('Developer Mercurial Access via SSH');
90 $b .= _('Only project developers can access the Mercurial tree via this method. SSH must be installed on your client machine. Enter your site password when prompted.');
92 // Warning : the ssh uri MUST be this form : ssh://username@scmbox//path/reponame
93 // HAVE YOU SEEN THE // starting the path ? Keep in mind the double /
94 $b .= '<p><tt>hg clone ssh://'.$d.'@' . $this->getBoxForProject($project) .'/'. forge_get_config('repos_path', 'scmhg') .'/'. $project->getUnixName().'</tt></p>';
96 if (forge_get_config('use_dav', 'scmhg')) {
98 $b .= _('Developer Mercurial Access via HTTP');
101 $b .= _('Only project developers can access the Mercurial tree via this method. Enter your site password when prompted.');
103 $b .= '<p><tt>hg clone '.$protocol.'://<i>'. $d .'</i>@' . $this->getBoxForProject($project) .'/hg/'. $project->getUnixName() . '</tt></p>';
106 if (forge_get_config('use_ssh', 'scmhg')) {
107 $d = '<i>developername</i>';
109 $b .= _('Developer Mercurial Access via SSH');
112 $b .= _('Only project developers can access the Mercurial tree via this method. SSH must be installed on your client machine. Substitute <i>developername</i> with the proper value. Enter your site password when prompted.');
114 // Warning : the ssh uri MUST be this form : ssh://username@scmbox//path/reponame
115 // HAVE YOU SEEN THE // starting the path ? Keep in mind the double /
116 $b .= '<p><tt>hg clone ssh://'.$d.'@' . $this->getBoxForProject($project) .'/'. forge_get_config('repos_path', 'scmhg') .'/'. $project->getUnixName().'</tt></p>';
119 $b .= _('Developer Mercurial Access via HTTP');
122 $b .= _('Only project developers can access the Mercurial tree via this method. Enter your site password when prompted.');
124 $b .= '<p><tt>hg clone '.$protocol.'://<i>'. _('developername') .'</i>@' . $this->getBoxForProject($project) .'/hg/'. $project->getUnixName() . '</tt></p>';
130 function getSnapshotPara($project) {
134 function getBrowserLinkBlock($project) {
136 $b = $HTML->boxMiddle(_('Hg Repository Browser'));
138 $b .= _('Browsing the Mercurial 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.');
141 $b .= util_make_link("/scm/browser.php?group_id=".$project->getID(), _('Browse Hg Repository'));
146 function getStatsBlock($project) {
150 function printShortStats($params) {
151 $project = $this->checkParams($params);
155 if ($project->usesPlugin($this->name) && forge_check_perm('scm', $project->getID(), 'read')) {
156 $result = db_query_params('SELECT sum(commits) AS commits, sum(adds) AS adds FROM stats_cvs_group WHERE group_id=$1',
157 array ($project->getID())) ;
158 $commit_num = db_result($result,0,'commits');
159 $add_num = db_result($result,0,'adds');
166 echo ' (Mercurial: '.sprintf(_('<strong>%1$s</strong> commits, <strong>%2$s</strong> adds'), number_format($commit_num, 0), number_format($add_num, 0)).")";
170 function printBrowserPage($params) {
171 $project = $this->checkParams($params);
175 if ($project->usesPlugin($this->name)) {
176 if ($this->browserDisplayable($project)) {
177 print '<iframe src="'.util_make_url('/plugins/scmhg/cgi-bin/'.$project->getUnixName().'.cgi?p='.$project->getUnixName()).'" frameborder="0" width=100% height=700></iframe>';
182 function createOrUpdateRepo($params) {
183 $project = $this->checkParams($params);
187 if (!$project->usesPlugin($this->name)) {
191 $repo = forge_get_config('repos_path', 'scmhg') . '/' . $project->getUnixName();
192 if (forge_get_config('use_ssh', 'scmhg')) {
193 $unix_group = 'scm_' . $project->getUnixName();
195 if (forge_get_config('use_dav', 'scmhg')) {
196 $unix_group = forge_get_config('apache_group');
197 $unix_user = forge_get_config('apache_user');
200 system("mkdir -p $repo");
201 /** per project configuration for http **/
202 if (forge_get_config('use_dav', 'scmhg')) {
203 //get template hgweb.cgi
204 $hgweb = forge_get_config('source_path').'/plugins/scmhg/www/cgi-bin/hgweb.cgi';
205 $project_hgweb = forge_get_config('source_path').'/www/plugins/scmhg/cgi-bin/'.$project->getUnixName().'.cgi';
206 if (!is_file($project_hgweb)) {
207 $lines = file($hgweb);
209 foreach ($lines as $line) {
210 if (preg_match("/\Aapplication = hgweb/",$line)) {
211 //link per project hgweb.cgi to the project repository
212 $repo_config .= "application = hgweb(\"".$repo."\",\"".$project->getUnixName()."\")\n";
214 $repo_config .= $line;
217 $f = fopen($project_hgweb, 'w');
218 fwrite($f, $repo_config);
220 system("chown $unix_user:$unix_group $project_hgweb");
221 system("chmod u+x $project_hgweb");
224 if (!is_dir("$repo/.hg")) {
225 system("hg init $repo");
226 $f = fopen("$repo/.hg/hgrc",'w');
228 $conf .= "baseurl = /hg";
229 $conf .= "\ndescription = ".$project->getUnixName();
230 $conf .= "\nstyle = paper";
231 $conf .= "\nallow_push = *"; //every user ( see apache configuration) is allowed to push
232 $conf .= "\nallow_read = *"; // every user is allowed to clone and pull
235 system("chgrp -R $unix_group $repo");
236 system("chmod 770 $repo" );
237 system("find $repo -type d | xargs chmod g+s" );
238 system("chmod 660 $repo/.hg/hgrc");
241 if ($project->enableAnonSCM()) {
242 system("chmod -R g+wX,o+rX-w $repo");
244 system("chmod -R g+wX,o-rwx $repo");
248 function updateRepositoryList($params) {
249 $groups = $this->getGroups();
250 if (!forge_get_config('use_dav', 'scmhg')) {
254 $unix_group = forge_get_config('apache_group');
255 $unix_user = forge_get_config('apache_user');
258 foreach ($groups as $project) {
259 if ( !$project->isActive()) {
262 if ( !$project->usesSCM()) {
266 $read = ""; /*pull,clone*/
267 $path = forge_get_config('repos_path', 'scmhg').'/'.$project->getUnixName().'/.hg';
270 $users = $project->getMembers();
271 $pname = $project->getUnixName();
272 foreach ($users as $user) {
273 if (forge_check_perm_for_user ($user,
283 $push .= $user->getUnixName();
284 $read .= $user->getUnixName();
287 $hgusers[$user->getID()] = $user;
288 }elseif (forge_check_perm_for_user ($user,
295 $read .= $user->getUnixName();
297 $hgusers[$user->getID()] = $user;
301 if ($project->enableAnonSCM()) {
305 /*make new hgrc file*/
306 if (is_file($path.'/hgrc')) {
307 $hgrc_val = parse_ini_file($path.'/hgrc', true);
308 if (isset ($hgrc_val['web'])) {
309 $hgrc_val['web']['allow_read'] = $read;
310 $hgrc_val['web']['allow_push'] = $push;
312 if (isset ($hgrc_val['notify']['test'])) {
313 /* Set the value again, because parse_ini_file() converts boolean values to "" or "1" .
314 This would break the hgrc file.*/
315 $hgrc_val['notify']['test'] = 'false';
317 if (isset ($hgrc_val['notify']['template'])) {
318 /*Set value again, because special character are not escaped*/
319 $hgrc_val['notify']['template'] = '"\ndetails: {webroot}/rev/{node|short}\nchangeset: {rev}:{node|short}\nuser: {author}\ndate: {date|date}\ndescription:\n{desc}\n"';
322 foreach ($hgrc_val as $section => $sub) {
323 $hgrc .= '['.$section."]\n";
324 foreach ($sub as $prop => $value) {
325 $hgrc .= "$prop = $value\n";
326 if ($value == end($sub)) {
333 $hgrc .= "baseurl = /hg";
334 $hgrc .= "\ndescription = ".$project->getUnixName();
335 $hgrc .= "\nstyle = paper";
336 $hgrc .= "\nallow_read = ".$read;
337 $hgrc .= "\nallow_push = ".$push;
340 $f = fopen($path.'/hgrc.new', 'w');
343 rename($path.'/hgrc.new', $path.'/hgrc');
344 system("chown $unix_user:$unix_group $path/hgrc");
345 system("chmod 660 $path/hgrc");
348 foreach ($hgusers as $user_id => $user) {
349 $password_data .= $user->getUnixName().':'.$user->getUnixPasswd()."\n";
351 $password_data .= forge_get_config('anonhg_login', 'scmhg').":".htpasswd_apr1_md5(forge_get_config('anonhg_password', 'scmhg'))."\n";
353 $fname = forge_get_config('data_path').'/hgroot-authfile';
354 $f = fopen($fname.'.new', 'w');
355 fwrite($f, $password_data);
357 chmod($fname.'.new', 0644);
358 rename($fname.'.new', $fname);
361 function generateSnapshots($params) {
362 $project = $this->checkParams($params);
366 if (! $project->usesPlugin ($this->name)) {
370 $group_name = $project->getUnixName();
371 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar'.util_get_compressed_file_extension();
373 if (!$project->enableAnonSCM()) {
374 if (is_file($tarball)) {
380 $toprepo = forge_get_config('repos_path', 'scmhg');
381 $repo = $toprepo . '/' . $project->getUnixName();
383 if (!is_dir($repo)) {
384 if (is_file($tarball)) {
390 $tmp = trim(`mktemp -d`);
394 system("tar cCf $toprepo - ".$project->getUnixName() ."|".forge_get_config('compression_method')."> $tmp/tarball") ;
395 chmod("$tmp/tarball", 0644);
396 copy("$tmp/tarball", $tarball);
397 unlink("$tmp/tarball");
398 system("rm -rf $tmp");
401 function gatherStats($params) {
402 global $last_user, $usr_adds, $usr_deletes, $usr_updates, $updates, $adds;
404 $project = $this->checkParams($params);
408 if (! $project->usesPlugin($this->name)) {
412 if ($params['mode'] == 'day') {
414 $year = $params['year'];
415 $month = $params['month'];
416 $day = $params['day'];
417 $month_string = sprintf("%04d%02d", $year, $month );
418 $start_time = gmmktime(0, 0, 0, $month, $day, $year);
419 $end_time = $start_time + 86400;
421 $usr_updates = array();
422 $usr_deletes = array();
425 $repo = forge_get_config('repos_path', 'scmhg') . '/' . $project->getUnixName();
426 if (!is_dir($repo) || !is_dir("$repo/.hg")) {
427 // echo "No repository\n";
431 // cleaning stats_cvs_* table for the current day
432 $res = db_query_params('DELETE FROM stats_cvs_group WHERE month = $1 AND day = $2 AND group_id = $3',
437 echo "Error while cleaning stats_cvs_group\n";
441 //switch into scm_repository and take a look at the log informations
442 $cdir = chdir($repo);
444 //show customised log informations
445 $pipe = popen("hg log --style fflog.tmpl -d '$start_time 0 to $end_time 0'", 'r');
447 while (!feof($pipe) && $line = fgets ($pipe)) {
448 //determine between author line and file informations
449 if (preg_match("/(\A[AMD]) .*/", $line, $matches)) {
450 if ($last_user == "") continue;
451 switch ($matches[1]) {
453 $usr_adds[$last_user]++;
457 $usr_updates[$last_user]++;
461 $usr_deletes[$last_user]++;
465 $last_user = $this->getUser($line);
470 // inserting group results in stats_cvs_groups
471 if ($updates > 0 || $adds > 0) {
472 if (!db_query_params('INSERT INTO stats_cvs_group (month, day, group_id, checkouts, commits, adds) VALUES ($1, $2, $3, $4, $5, $6)',
479 echo "Error while inserting into stats_cvs_group\n";
485 // building the user list
486 $user_list = array_unique(array_merge(array_keys($usr_adds), array_keys($usr_updates)));
487 foreach ( $user_list as $user ) {
488 $uu = $usr_updates[$user] ? $usr_updates[$user] : 0;
489 $ua = $usr_adds[$user] ? $usr_adds[$user] : 0;
490 if ($uu > 0 || $ua > 0) {
491 if (!db_query_params('INSERT INTO stats_cvs_user (month, day, group_id, user_id, commits,adds) VALUES ($1, $2, $3, $4, $5, $6)',
498 echo "Error while inserting into stats_cvs_user\n";
508 function scm_add_repo(&$params) {
509 $project = $this->checkParams($params);
513 if (! $project->usesPlugin ($this->name)) {
517 if (!isset($params['repo_name'])) {
521 if ($params['repo_name'] == $project->getUnixName()) {
522 $params['error_msg'] = sprintf(_('A repository %s already exists'), $params['repo_name']);
526 if (! util_is_valid_repository_name($params['repo_name'])) {
527 $params['error_msg'] = _('This repository name is not valid');
531 $result = db_query_params('SELECT count(*) AS count FROM plugin_scmhg_repos WHERE group_id=$1 AND repo_name = $2',
532 array ($params['group_id'], $params['repo_name']));
534 $params['error_msg'] = db_error();
537 if (db_result($result, 0, 'count')) {
538 $params['error_msg'] = sprintf(_('A repository %s already exists'), $params['repo_name']);
544 if (isset($params['clone'])) {
545 $clone = $params['clone'];
546 // Verify if repository used for the clone exists?
547 $description = sprintf(_('Clone of %s repository'), $params['clone']);
549 if (isset($params['description'])) {
550 $description = $params['description'];
553 $result = db_query_params ('INSERT INTO plugin_scmhg_repos (group_id, repo_name, description, clone) VALUES ($1, $2, $3, $4)',
554 array ($params['group_id'], $params['repo_name'], $description, $clone));
556 $params['error_msg'] = db_error();
560 plugin_hook ("scm_admin_update", $params);
564 function scm_admin_buttons(&$params) {
565 $project = $this->checkParams($params);
569 if (! $project->usesPlugin ($this->name)) {
576 '/scm/admin/?group_id='.$params['group_id'].'&form_create_repo=1',
578 array('icon' => html_image('ic/scm_repo_add.png'))
582 function scm_admin_form(&$params) {
583 $project = $this->checkParams($params);
587 if (! $project->usesPlugin ($this->name)) {
591 $project_name = $project->getUnixName();
593 $select_repo = '<select name="frontpage">' . "\n";//array($project->getPublicName());
594 $result = db_query_params('SELECT repo_name FROM plugin_scmhg_repos WHERE group_id=$1',
595 array ($params['group_id']));
597 //$params['error_msg'] = db_error();
600 $select_repo = '<select name="clone">' . "\n";
601 $select_repo .= '<option value="">'._('None').'</option>' . "\n";
602 $select_repo .= '<option value="'.$project_name.'">'.$project_name.'</option>' . "\n";
603 while($data = db_fetch_array($result)) {
604 $select_repo .= '<option value="'.$data['repo_name'].'">'.$data['repo_name'].'</option>' . "\n";
606 $select_repo .= '</select>' . "\n";
608 session_require_perm('project_admin', $params['group_id']);
610 $adminheadertitle = sprintf(_('Create SCM repository for project %1$s'), $project_name);
611 project_admin_header(array('title'=>$adminheadertitle, 'group'=>$params['group_id']));
614 <form name="form_create_repo"
615 action="<?php echo getStringFromServer('PHP_SELF'); ?>" method="post">
616 <input type="hidden" name="group_id" value="<?php echo $params['group_id'] ?>" />
617 <input type="hidden" name="create_repository" value="1" />
618 <p><strong><?php echo _('Repository name:') ?></strong><?php echo utils_requiredField(); ?><br />
619 <input type="text" required="required" size="20" name="repo_name" value="" /></p>
620 <p><strong><?php echo _('Description')._(':'); ?></strong><br />
621 <input type="text" size="60" name="description" value="" /></p>
622 <p><strong><?php echo _('Cloned from:') ?></strong><br />
623 <?php echo $select_repo ?></p>
624 <input type="submit" name="cancel" value="<?php echo _('Cancel') ?>" />
625 <input type="submit" name="submit" value="<?php echo _('Submit') ?>" />
630 project_admin_footer(array());
638 // c-file-style: "bsd"