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 Mercurial as Source Code Management tool. Offer DAV or SSH access.');
50 function getDefaultServer() {
51 return forge_get_config('default_server', 'scmhg');
56 . sprintf(_('Documentation for %1$s is available at <a href="%2$s">%2$s</a>.'),
58 'http://hgbook.red-bean.com/')
61 . _('Another short Introduction can be found at <a href="http://hginit.com/">http://hginit.com/</a>')
65 function getInstructionsForAnon($project) {
67 $b .= _('Anonymous Mercurial Access');
70 if (forge_get_config('use_dav', 'scmhg')) {
71 $protocol = forge_get_config('use_ssl', 'scmhg')? 'https' : 'http';
73 $b .= _("This project's Mercurial repository can be checked out through anonymous access with the following command:");
76 $b .= '<tt>hg clone '.$protocol.'://'.forge_get_config('anonhg_login', 'scmhg').'@' . $this->getBoxForProject($project) . '/'. 'hg' .'/'. $project->getUnixName() .'/'.'</tt><br />';
77 $b .= _('The password is ').forge_get_config('anonhg_password', 'scmhg').'<br/>';
80 $b .= '<p class="warning">'._('Please contact forge administrator, scmhg plugin is not correctly configured');
86 function getInstructionsForRW($project) {
87 $protocol = forge_get_config('use_ssl', 'scmhg')? 'https' : 'http';
88 if (session_loggedin()) {
89 $u = user_get_object(user_getid());
90 $d = $u->getUnixName();
92 if (forge_get_config('use_ssh', 'scmhg')) {
94 $b .= sprintf(_('Developer %s Access via SSH'), 'Mercurial');
97 $b .= _('Read/write access to Mercurial tree is allowed for authenticated users.');
99 $b .= _('SSH must be installed on your client machine.');
101 $b .= _('Enter your site password when prompted.');
103 // Warning : the ssh uri MUST be this form : ssh://username@scmbox//path/reponame
104 // HAVE YOU SEEN THE // starting the path ? Keep in mind the double /
105 $b .= '<p><tt>hg clone ssh://'.$d.'@' . $this->getBoxForProject($project) .'/'. forge_get_config('repos_path', 'scmhg') .'/'. $project->getUnixName().'</tt></p>';
107 if (forge_get_config('use_dav', 'scmhg')) {
109 $b .= _('Developer Mercurial Access via HTTP');
112 $b .= _('Only project developers can access the Mercurial tree via this method.');
114 $b .= _('Enter your site password when prompted.');
116 $b .= '<p><tt>hg clone '.$protocol.'://<i>'. $d .'</i>@' . $this->getBoxForProject($project) .'/hg/'. $project->getUnixName() . '</tt></p>';
119 if (forge_get_config('use_ssh', 'scmhg')) {
120 $d = '<em>developername</em>';
122 $b .= sprintf(_('Developer %s Access via SSH'), 'Mercurial');
125 $b .= sprintf(_('Only project developers can access the %s tree via this method.'), 'Mercurial');
127 $b .= _('SSH must be installed on your client machine.');
129 $b .= _('Substitute <em>developername</em> with the proper value.');
131 $b .= _('Enter your site password when prompted.');
133 // Warning : the ssh uri MUST be this form : ssh://username@scmbox//path/reponame
134 // HAVE YOU SEEN THE // starting the path ? Keep in mind the double /
135 $b .= '<p><tt>hg clone ssh://'.$d.'@' . $this->getBoxForProject($project) .'/'. forge_get_config('repos_path', 'scmhg') .'/'. $project->getUnixName().'</tt></p>';
138 $b .= _('Developer Mercurial Access via HTTP');
141 $b .= _('Only project developers can access the Mercurial tree via this method.');
143 $b .= _('Enter your site password when prompted.');
145 $b .= '<p><tt>hg clone '.$protocol.'://<i>'. _('developername') .'</i>@' . $this->getBoxForProject($project) .'/hg/'. $project->getUnixName() . '</tt></p>';
151 function getSnapshotPara($project) {
155 function getBrowserLinkBlock($project) {
157 $b = $HTML->boxMiddle(_('Hg Repository Browser'));
159 $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.');
162 $b .= util_make_link("/scm/browser.php?group_id=".$project->getID(), _('Browse Hg Repository'));
167 function getStatsBlock($project) {
171 function printShortStats($params) {
172 $project = $this->checkParams($params);
176 if ($project->usesPlugin($this->name) && forge_check_perm('scm', $project->getID(), 'read')) {
177 $result = db_query_params('SELECT sum(commits) AS commits, sum(adds) AS adds FROM stats_cvs_group WHERE group_id=$1',
178 array ($project->getID())) ;
179 $commit_num = db_result($result,0,'commits');
180 $add_num = db_result($result,0,'adds');
187 echo ' (Mercurial: '.sprintf(_('<strong>%1$s</strong> commits, <strong>%2$s</strong> adds'), number_format($commit_num, 0), number_format($add_num, 0)).")";
191 function printBrowserPage($params) {
192 $project = $this->checkParams($params);
196 if ($project->usesPlugin($this->name)) {
197 if ($this->browserDisplayable($project)) {
198 print '<iframe src="'.util_make_url('/plugins/scmhg/cgi-bin/'.$project->getUnixName().'.cgi?p='.$project->getUnixName()).'" frameborder="0" width=100% height=700></iframe>';
203 function createOrUpdateRepo($params) {
204 $project = $this->checkParams($params);
209 if (!$project->usesPlugin($this->name)) {
213 $repo = forge_get_config('repos_path', 'scmhg') . '/' . $project->getUnixName();
214 if (forge_get_config('use_ssh', 'scmhg')) {
215 $unix_group = 'scm_' . $project->getUnixName();
217 if (forge_get_config('use_dav', 'scmhg')) {
218 $unix_group = forge_get_config('apache_group');
219 $unix_user = forge_get_config('apache_user');
222 system("mkdir -p $repo");
223 /** per project configuration for http **/
224 if (forge_get_config('use_dav', 'scmhg')) {
225 //get template hgweb.cgi
226 $hgweb = forge_get_config('source_path').'/plugins/scmhg/www/cgi-bin/hgweb.cgi';
227 $project_hgweb = forge_get_config('source_path').'/www/plugins/scmhg/cgi-bin/'.$project->getUnixName().'.cgi';
228 if (!is_file($project_hgweb)) {
229 $lines = file($hgweb);
231 foreach ($lines as $line) {
232 if (preg_match("/\Aapplication = hgweb/",$line)) {
233 //link per project hgweb.cgi to the project repository
234 $repo_config .= "application = hgweb(\"".$repo."\",\"".$project->getUnixName()."\")\n";
236 $repo_config .= $line;
239 $f = fopen($project_hgweb, 'w');
240 fwrite($f, $repo_config);
242 system("chown $unix_user:$unix_group $project_hgweb");
243 system("chmod u+x $project_hgweb");
246 if (!is_dir("$repo/.hg")) {
247 system("hg init $repo");
248 $f = fopen("$repo/.hg/hgrc",'w');
250 $conf .= "baseurl = /hg";
251 $conf .= "\ndescription = ".$project->getUnixName();
252 $conf .= "\nstyle = paper";
253 $conf .= "\nallow_push = *"; //every user ( see apache configuration) is allowed to push
254 $conf .= "\nallow_read = *"; // every user is allowed to clone and pull
257 system("chgrp -R $unix_group $repo");
258 system("chmod 770 $repo" );
259 system("find $repo -type d | xargs chmod g+s" );
260 system("chmod 660 $repo/.hg/hgrc");
263 if ($project->enableAnonSCM()) {
264 system("chmod -R g+wX,o+rX-w $repo");
266 system("chmod -R g+wX,o-rwx $repo");
270 function updateRepositoryList($params) {
271 $groups = $this->getGroups();
272 if (!forge_get_config('use_dav', 'scmhg')) {
276 $unix_group = forge_get_config('apache_group');
277 $unix_user = forge_get_config('apache_user');
280 foreach ($groups as $project) {
281 if ( !$project->isActive()) {
284 if ( !$project->usesSCM()) {
288 $read = ""; /*pull,clone*/
289 $path = forge_get_config('repos_path', 'scmhg').'/'.$project->getUnixName().'/.hg';
292 $users = $project->getMembers();
293 $pname = $project->getUnixName();
294 foreach ($users as $user) {
295 if (forge_check_perm_for_user ($user,
305 $push .= $user->getUnixName();
306 $read .= $user->getUnixName();
309 $hgusers[$user->getID()] = $user;
310 }elseif (forge_check_perm_for_user ($user,
317 $read .= $user->getUnixName();
319 $hgusers[$user->getID()] = $user;
323 if ($project->enableAnonSCM()) {
327 /*make new hgrc file*/
328 if (is_file($path.'/hgrc')) {
329 $hgrc_val = parse_ini_file($path.'/hgrc', true);
330 if (isset ($hgrc_val['web'])) {
331 $hgrc_val['web']['allow_read'] = $read;
332 $hgrc_val['web']['allow_push'] = $push;
334 if (isset ($hgrc_val['notify']['test'])) {
335 /* Set the value again, because parse_ini_file() converts boolean values to "" or "1" .
336 This would break the hgrc file.*/
337 $hgrc_val['notify']['test'] = 'false';
339 if (isset ($hgrc_val['notify']['template'])) {
340 /*Set value again, because special character are not escaped*/
341 $hgrc_val['notify']['template'] = '"\ndetails: {webroot}/rev/{node|short}\nchangeset: {rev}:{node|short}\nuser: {author}\ndate: {date|date}\ndescription:\n{desc}\n"';
344 foreach ($hgrc_val as $section => $sub) {
345 $hgrc .= '['.$section."]\n";
346 foreach ($sub as $prop => $value) {
347 $hgrc .= "$prop = $value\n";
348 if ($value == end($sub)) {
355 $hgrc .= "baseurl = /hg";
356 $hgrc .= "\ndescription = ".$project->getUnixName();
357 $hgrc .= "\nstyle = paper";
358 $hgrc .= "\nallow_read = ".$read;
359 $hgrc .= "\nallow_push = ".$push;
362 $f = fopen($path.'/hgrc.new', 'w');
365 rename($path.'/hgrc.new', $path.'/hgrc');
366 system("chown $unix_user:$unix_group $path/hgrc");
367 system("chmod 660 $path/hgrc");
370 foreach ($hgusers as $user_id => $user) {
371 $password_data .= $user->getUnixName().':'.$user->getUnixPasswd()."\n";
373 $password_data .= forge_get_config('anonhg_login', 'scmhg').":".htpasswd_apr1_md5(forge_get_config('anonhg_password', 'scmhg'))."\n";
375 $fname = forge_get_config('data_path').'/hgroot-authfile';
376 $f = fopen($fname.'.new', 'w');
377 fwrite($f, $password_data);
379 chmod($fname.'.new', 0644);
380 rename($fname.'.new', $fname);
383 function generateSnapshots($params) {
384 $project = $this->checkParams($params);
389 if (! $project->usesPlugin ($this->name)) {
393 $group_name = $project->getUnixName();
394 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar'.util_get_compressed_file_extension();
396 if (!$project->enableAnonSCM()) {
397 if (is_file($tarball)) {
403 $toprepo = forge_get_config('repos_path', 'scmhg');
404 $repo = $toprepo . '/' . $project->getUnixName();
406 if (!is_dir($repo)) {
407 if (is_file($tarball)) {
413 $tmp = trim(`mktemp -d`);
417 system("tar cCf $toprepo - ".$project->getUnixName() ."|".forge_get_config('compression_method')."> $tmp/tarball") ;
418 chmod("$tmp/tarball", 0644);
419 copy("$tmp/tarball", $tarball);
420 unlink("$tmp/tarball");
421 system("rm -rf $tmp");
424 function gatherStats($params) {
425 global $last_user, $usr_adds, $usr_deletes, $usr_updates, $updates, $adds;
427 $project = $this->checkParams($params);
431 if (! $project->usesPlugin($this->name)) {
435 if ($params['mode'] == 'day') {
437 $year = $params['year'];
438 $month = $params['month'];
439 $day = $params['day'];
440 $month_string = sprintf("%04d%02d", $year, $month );
441 $start_time = gmmktime(0, 0, 0, $month, $day, $year);
442 $end_time = $start_time + 86400;
444 $usr_updates = array();
445 $usr_deletes = array();
448 $repo = forge_get_config('repos_path', 'scmhg') . '/' . $project->getUnixName();
449 if (!is_dir($repo) || !is_dir("$repo/.hg")) {
450 // echo "No repository\n";
454 // cleaning stats_cvs_* table for the current day
455 $res = db_query_params('DELETE FROM stats_cvs_group WHERE month = $1 AND day = $2 AND group_id = $3',
460 echo "Error while cleaning stats_cvs_group\n";
464 //switch into scm_repository and take a look at the log informations
465 $cdir = chdir($repo);
467 //show customised log informations
468 $pipe = popen("hg log --style fflog.tmpl -d '$start_time 0 to $end_time 0'", 'r');
470 while (!feof($pipe) && $line = fgets ($pipe)) {
471 //determine between author line and file informations
472 if (preg_match("/(\A[AMD]) .*/", $line, $matches)) {
473 if ($last_user == "") continue;
474 switch ($matches[1]) {
476 $usr_adds[$last_user]++;
480 $usr_updates[$last_user]++;
484 $usr_deletes[$last_user]++;
488 $last_user = $this->getUser($line);
493 // inserting group results in stats_cvs_groups
494 if ($updates > 0 || $adds > 0) {
495 if (!db_query_params('INSERT INTO stats_cvs_group (month, day, group_id, checkouts, commits, adds) VALUES ($1, $2, $3, $4, $5, $6)',
502 echo "Error while inserting into stats_cvs_group\n";
508 // building the user list
509 $user_list = array_unique(array_merge(array_keys($usr_adds), array_keys($usr_updates)));
510 foreach ( $user_list as $user ) {
511 $uu = $usr_updates[$user] ? $usr_updates[$user] : 0;
512 $ua = $usr_adds[$user] ? $usr_adds[$user] : 0;
513 if ($uu > 0 || $ua > 0) {
514 if (!db_query_params('INSERT INTO stats_cvs_user (month, day, group_id, user_id, commits,adds) VALUES ($1, $2, $3, $4, $5, $6)',
521 echo "Error while inserting into stats_cvs_user\n";
531 function scm_add_repo(&$params) {
532 $project = $this->checkParams($params);
536 if (! $project->usesPlugin ($this->name)) {
540 if (!isset($params['repo_name'])) {
544 if ($params['repo_name'] == $project->getUnixName()) {
545 $params['error_msg'] = sprintf(_('A repository %s already exists'), $params['repo_name']);
549 if (! util_is_valid_repository_name($params['repo_name'])) {
550 $params['error_msg'] = _('This repository name is not valid');
554 $result = db_query_params('SELECT count(*) AS count FROM plugin_scmhg_repos WHERE group_id=$1 AND repo_name = $2',
555 array($params['group_id'], $params['repo_name']));
557 $params['error_msg'] = db_error();
560 if (db_result($result, 0, 'count')) {
561 $params['error_msg'] = sprintf(_('A repository %s already exists'), $params['repo_name']);
567 if (isset($params['clone'])) {
568 $clone = $params['clone'];
569 // Verify if repository used for the clone exists?
570 $description = sprintf(_('Clone of %s repository'), $params['clone']);
572 if (isset($params['description'])) {
573 $description = $params['description'];
576 $result = db_query_params('INSERT INTO plugin_scmhg_repos (group_id, repo_name, description, clone) VALUES ($1, $2, $3, $4)',
577 array($params['group_id'], $params['repo_name'], $description, $clone));
579 $params['error_msg'] = db_error();
583 plugin_hook("scm_admin_update", $params);
587 function scm_admin_buttons(&$params) {
588 $project = $this->checkParams($params);
592 if (! $project->usesPlugin ($this->name)) {
599 '/scm/admin/?group_id='.$params['group_id'].'&form_create_repo=1',
601 array('icon' => html_image('ic/scm_repo_add.png'))
605 function scm_admin_form(&$params) {
606 $project = $this->checkParams($params);
610 if (! $project->usesPlugin ($this->name)) {
614 $project_name = $project->getUnixName();
616 $select_repo = '<select name="frontpage">' . "\n";//array($project->getPublicName());
617 $result = db_query_params('SELECT repo_name FROM plugin_scmhg_repos WHERE group_id=$1',
618 array ($params['group_id']));
620 //$params['error_msg'] = db_error();
623 $select_repo = '<select name="clone">' . "\n";
624 $select_repo .= '<option value="">'._('None').'</option>' . "\n";
625 $select_repo .= '<option value="'.$project_name.'">'.$project_name.'</option>' . "\n";
626 while($data = db_fetch_array($result)) {
627 $select_repo .= '<option value="'.$data['repo_name'].'">'.$data['repo_name'].'</option>' . "\n";
629 $select_repo .= '</select>' . "\n";
631 session_require_perm('project_admin', $params['group_id']);
633 $adminheadertitle = sprintf(_('Create SCM repository for project %1$s'), $project_name);
634 project_admin_header(array('title'=>$adminheadertitle, 'group'=>$params['group_id']));
637 <form name="form_create_repo"
638 action="<?php echo getStringFromServer('PHP_SELF'); ?>" method="post">
639 <input type="hidden" name="group_id" value="<?php echo $params['group_id'] ?>" />
640 <input type="hidden" name="create_repository" value="1" />
641 <p><strong><?php echo _('Repository name')._(': ') ?></strong><?php echo utils_requiredField(); ?><br />
642 <input type="text" required="required" size="20" name="repo_name" value="" /></p>
643 <p><strong><?php echo _('Description')._(':'); ?></strong><br />
644 <input type="text" size="60" name="description" value="" /></p>
645 <p><strong><?php echo _('Cloned from:') ?></strong><br />
646 <?php echo $select_repo ?></p>
647 <input type="submit" name="cancel" value="<?php echo _('Cancel') ?>" />
648 <input type="submit" name="submit" value="<?php echo _('Submit') ?>" />
653 project_admin_footer(array());
660 // c-file-style: "bsd"