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. SSH must be installed on your client machine. Enter your site password when prompted.');
99 // Warning : the ssh uri MUST be this form : ssh://username@scmbox//path/reponame
100 // HAVE YOU SEEN THE // starting the path ? Keep in mind the double /
101 $b .= '<p><tt>hg clone ssh://'.$d.'@' . $this->getBoxForProject($project) .'/'. forge_get_config('repos_path', 'scmhg') .'/'. $project->getUnixName().'</tt></p>';
103 if (forge_get_config('use_dav', 'scmhg')) {
105 $b .= _('Developer Mercurial Access via HTTP');
108 $b .= _('Only project developers can access the Mercurial tree via this method. Enter your site password when prompted.');
110 $b .= '<p><tt>hg clone '.$protocol.'://<i>'. $d .'</i>@' . $this->getBoxForProject($project) .'/hg/'. $project->getUnixName() . '</tt></p>';
113 if (forge_get_config('use_ssh', 'scmhg')) {
114 $d = '<i>developername</i>';
116 $b .= _('Developer Mercurial Access via SSH');
119 $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.');
121 // Warning : the ssh uri MUST be this form : ssh://username@scmbox//path/reponame
122 // HAVE YOU SEEN THE // starting the path ? Keep in mind the double /
123 $b .= '<p><tt>hg clone ssh://'.$d.'@' . $this->getBoxForProject($project) .'/'. forge_get_config('repos_path', 'scmhg') .'/'. $project->getUnixName().'</tt></p>';
126 $b .= _('Developer Mercurial Access via HTTP');
129 $b .= _('Only project developers can access the Mercurial tree via this method. Enter your site password when prompted.');
131 $b .= '<p><tt>hg clone '.$protocol.'://<i>'. _('developername') .'</i>@' . $this->getBoxForProject($project) .'/hg/'. $project->getUnixName() . '</tt></p>';
137 function getSnapshotPara($project) {
141 function getBrowserLinkBlock($project) {
143 $b = $HTML->boxMiddle(_('Hg Repository Browser'));
145 $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.');
148 $b .= util_make_link("/scm/browser.php?group_id=".$project->getID(), _('Browse Hg Repository'));
153 function getStatsBlock($project) {
157 function printShortStats($params) {
158 $project = $this->checkParams($params);
162 if ($project->usesPlugin($this->name) && forge_check_perm('scm', $project->getID(), 'read')) {
163 $result = db_query_params('SELECT sum(commits) AS commits, sum(adds) AS adds FROM stats_cvs_group WHERE group_id=$1',
164 array ($project->getID())) ;
165 $commit_num = db_result($result,0,'commits');
166 $add_num = db_result($result,0,'adds');
173 echo ' (Mercurial: '.sprintf(_('<strong>%1$s</strong> commits, <strong>%2$s</strong> adds'), number_format($commit_num, 0), number_format($add_num, 0)).")";
177 function printBrowserPage($params) {
178 $project = $this->checkParams($params);
182 if ($project->usesPlugin($this->name)) {
183 if ($this->browserDisplayable($project)) {
184 print '<iframe src="'.util_make_url('/plugins/scmhg/cgi-bin/'.$project->getUnixName().'.cgi?p='.$project->getUnixName()).'" frameborder="0" width=100% height=700></iframe>';
189 function createOrUpdateRepo($params) {
190 $project = $this->checkParams($params);
194 if (!$project->usesPlugin($this->name)) {
198 $repo = forge_get_config('repos_path', 'scmhg') . '/' . $project->getUnixName();
199 if (forge_get_config('use_ssh', 'scmhg')) {
200 $unix_group = 'scm_' . $project->getUnixName();
202 if (forge_get_config('use_dav', 'scmhg')) {
203 $unix_group = forge_get_config('apache_group');
204 $unix_user = forge_get_config('apache_user');
207 system("mkdir -p $repo");
208 /** per project configuration for http **/
209 if (forge_get_config('use_dav', 'scmhg')) {
210 //get template hgweb.cgi
211 $hgweb = forge_get_config('source_path').'/plugins/scmhg/www/cgi-bin/hgweb.cgi';
212 $project_hgweb = forge_get_config('source_path').'/www/plugins/scmhg/cgi-bin/'.$project->getUnixName().'.cgi';
213 if (!is_file($project_hgweb)) {
214 $lines = file($hgweb);
216 foreach ($lines as $line) {
217 if (preg_match("/\Aapplication = hgweb/",$line)) {
218 //link per project hgweb.cgi to the project repository
219 $repo_config .= "application = hgweb(\"".$repo."\",\"".$project->getUnixName()."\")\n";
221 $repo_config .= $line;
224 $f = fopen($project_hgweb, 'w');
225 fwrite($f, $repo_config);
227 system("chown $unix_user:$unix_group $project_hgweb");
228 system("chmod u+x $project_hgweb");
231 if (!is_dir("$repo/.hg")) {
232 system("hg init $repo");
233 $f = fopen("$repo/.hg/hgrc",'w');
235 $conf .= "baseurl = /hg";
236 $conf .= "\ndescription = ".$project->getUnixName();
237 $conf .= "\nstyle = paper";
238 $conf .= "\nallow_push = *"; //every user ( see apache configuration) is allowed to push
239 $conf .= "\nallow_read = *"; // every user is allowed to clone and pull
242 system("chgrp -R $unix_group $repo");
243 system("chmod 770 $repo" );
244 system("find $repo -type d | xargs chmod g+s" );
245 system("chmod 660 $repo/.hg/hgrc");
248 if ($project->enableAnonSCM()) {
249 system("chmod -R g+wX,o+rX-w $repo");
251 system("chmod -R g+wX,o-rwx $repo");
255 function updateRepositoryList($params) {
256 $groups = $this->getGroups();
257 if (!forge_get_config('use_dav', 'scmhg')) {
261 $unix_group = forge_get_config('apache_group');
262 $unix_user = forge_get_config('apache_user');
265 foreach ($groups as $project) {
266 if ( !$project->isActive()) {
269 if ( !$project->usesSCM()) {
273 $read = ""; /*pull,clone*/
274 $path = forge_get_config('repos_path', 'scmhg').'/'.$project->getUnixName().'/.hg';
277 $users = $project->getMembers();
278 $pname = $project->getUnixName();
279 foreach ($users as $user) {
280 if (forge_check_perm_for_user ($user,
290 $push .= $user->getUnixName();
291 $read .= $user->getUnixName();
294 $hgusers[$user->getID()] = $user;
295 }elseif (forge_check_perm_for_user ($user,
302 $read .= $user->getUnixName();
304 $hgusers[$user->getID()] = $user;
308 if ($project->enableAnonSCM()) {
312 /*make new hgrc file*/
313 if (is_file($path.'/hgrc')) {
314 $hgrc_val = parse_ini_file($path.'/hgrc', true);
315 if (isset ($hgrc_val['web'])) {
316 $hgrc_val['web']['allow_read'] = $read;
317 $hgrc_val['web']['allow_push'] = $push;
319 if (isset ($hgrc_val['notify']['test'])) {
320 /* Set the value again, because parse_ini_file() converts boolean values to "" or "1" .
321 This would break the hgrc file.*/
322 $hgrc_val['notify']['test'] = 'false';
324 if (isset ($hgrc_val['notify']['template'])) {
325 /*Set value again, because special character are not escaped*/
326 $hgrc_val['notify']['template'] = '"\ndetails: {webroot}/rev/{node|short}\nchangeset: {rev}:{node|short}\nuser: {author}\ndate: {date|date}\ndescription:\n{desc}\n"';
329 foreach ($hgrc_val as $section => $sub) {
330 $hgrc .= '['.$section."]\n";
331 foreach ($sub as $prop => $value) {
332 $hgrc .= "$prop = $value\n";
333 if ($value == end($sub)) {
340 $hgrc .= "baseurl = /hg";
341 $hgrc .= "\ndescription = ".$project->getUnixName();
342 $hgrc .= "\nstyle = paper";
343 $hgrc .= "\nallow_read = ".$read;
344 $hgrc .= "\nallow_push = ".$push;
347 $f = fopen($path.'/hgrc.new', 'w');
350 rename($path.'/hgrc.new', $path.'/hgrc');
351 system("chown $unix_user:$unix_group $path/hgrc");
352 system("chmod 660 $path/hgrc");
355 foreach ($hgusers as $user_id => $user) {
356 $password_data .= $user->getUnixName().':'.$user->getUnixPasswd()."\n";
358 $password_data .= forge_get_config('anonhg_login', 'scmhg').":".htpasswd_apr1_md5(forge_get_config('anonhg_password', 'scmhg'))."\n";
360 $fname = forge_get_config('data_path').'/hgroot-authfile';
361 $f = fopen($fname.'.new', 'w');
362 fwrite($f, $password_data);
364 chmod($fname.'.new', 0644);
365 rename($fname.'.new', $fname);
368 function generateSnapshots($params) {
369 $project = $this->checkParams($params);
373 if (! $project->usesPlugin ($this->name)) {
377 $group_name = $project->getUnixName();
378 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar'.util_get_compressed_file_extension();
380 if (!$project->enableAnonSCM()) {
381 if (is_file($tarball)) {
387 $toprepo = forge_get_config('repos_path', 'scmhg');
388 $repo = $toprepo . '/' . $project->getUnixName();
390 if (!is_dir($repo)) {
391 if (is_file($tarball)) {
397 $tmp = trim(`mktemp -d`);
401 system("tar cCf $toprepo - ".$project->getUnixName() ."|".forge_get_config('compression_method')."> $tmp/tarball") ;
402 chmod("$tmp/tarball", 0644);
403 copy("$tmp/tarball", $tarball);
404 unlink("$tmp/tarball");
405 system("rm -rf $tmp");
408 function gatherStats($params) {
409 global $last_user, $usr_adds, $usr_deletes, $usr_updates, $updates, $adds;
411 $project = $this->checkParams($params);
415 if (! $project->usesPlugin($this->name)) {
419 if ($params['mode'] == 'day') {
421 $year = $params['year'];
422 $month = $params['month'];
423 $day = $params['day'];
424 $month_string = sprintf("%04d%02d", $year, $month );
425 $start_time = gmmktime(0, 0, 0, $month, $day, $year);
426 $end_time = $start_time + 86400;
428 $usr_updates = array();
429 $usr_deletes = array();
432 $repo = forge_get_config('repos_path', 'scmhg') . '/' . $project->getUnixName();
433 if (!is_dir($repo) || !is_dir("$repo/.hg")) {
434 // echo "No repository\n";
438 // cleaning stats_cvs_* table for the current day
439 $res = db_query_params('DELETE FROM stats_cvs_group WHERE month = $1 AND day = $2 AND group_id = $3',
444 echo "Error while cleaning stats_cvs_group\n";
448 //switch into scm_repository and take a look at the log informations
449 $cdir = chdir($repo);
451 //show customised log informations
452 $pipe = popen("hg log --style fflog.tmpl -d '$start_time 0 to $end_time 0'", 'r');
454 while (!feof($pipe) && $line = fgets ($pipe)) {
455 //determine between author line and file informations
456 if (preg_match("/(\A[AMD]) .*/", $line, $matches)) {
457 if ($last_user == "") continue;
458 switch ($matches[1]) {
460 $usr_adds[$last_user]++;
464 $usr_updates[$last_user]++;
468 $usr_deletes[$last_user]++;
472 $last_user = $this->getUser($line);
477 // inserting group results in stats_cvs_groups
478 if ($updates > 0 || $adds > 0) {
479 if (!db_query_params('INSERT INTO stats_cvs_group (month, day, group_id, checkouts, commits, adds) VALUES ($1, $2, $3, $4, $5, $6)',
486 echo "Error while inserting into stats_cvs_group\n";
492 // building the user list
493 $user_list = array_unique(array_merge(array_keys($usr_adds), array_keys($usr_updates)));
494 foreach ( $user_list as $user ) {
495 $uu = $usr_updates[$user] ? $usr_updates[$user] : 0;
496 $ua = $usr_adds[$user] ? $usr_adds[$user] : 0;
497 if ($uu > 0 || $ua > 0) {
498 if (!db_query_params('INSERT INTO stats_cvs_user (month, day, group_id, user_id, commits,adds) VALUES ($1, $2, $3, $4, $5, $6)',
505 echo "Error while inserting into stats_cvs_user\n";
515 function scm_add_repo(&$params) {
516 $project = $this->checkParams($params);
520 if (! $project->usesPlugin ($this->name)) {
524 if (!isset($params['repo_name'])) {
528 if ($params['repo_name'] == $project->getUnixName()) {
529 $params['error_msg'] = sprintf(_('A repository %s already exists'), $params['repo_name']);
533 if (! util_is_valid_repository_name($params['repo_name'])) {
534 $params['error_msg'] = _('This repository name is not valid');
538 $result = db_query_params('SELECT count(*) AS count FROM plugin_scmhg_repos WHERE group_id=$1 AND repo_name = $2',
539 array ($params['group_id'], $params['repo_name']));
541 $params['error_msg'] = db_error();
544 if (db_result($result, 0, 'count')) {
545 $params['error_msg'] = sprintf(_('A repository %s already exists'), $params['repo_name']);
551 if (isset($params['clone'])) {
552 $clone = $params['clone'];
553 // Verify if repository used for the clone exists?
554 $description = sprintf(_('Clone of %s repository'), $params['clone']);
556 if (isset($params['description'])) {
557 $description = $params['description'];
560 $result = db_query_params ('INSERT INTO plugin_scmhg_repos (group_id, repo_name, description, clone) VALUES ($1, $2, $3, $4)',
561 array ($params['group_id'], $params['repo_name'], $description, $clone));
563 $params['error_msg'] = db_error();
567 plugin_hook ("scm_admin_update", $params);
571 function scm_admin_buttons(&$params) {
572 $project = $this->checkParams($params);
576 if (! $project->usesPlugin ($this->name)) {
583 '/scm/admin/?group_id='.$params['group_id'].'&form_create_repo=1',
585 array('icon' => html_image('ic/scm_repo_add.png'))
589 function scm_admin_form(&$params) {
590 $project = $this->checkParams($params);
594 if (! $project->usesPlugin ($this->name)) {
598 $project_name = $project->getUnixName();
600 $select_repo = '<select name="frontpage">' . "\n";//array($project->getPublicName());
601 $result = db_query_params('SELECT repo_name FROM plugin_scmhg_repos WHERE group_id=$1',
602 array ($params['group_id']));
604 //$params['error_msg'] = db_error();
607 $select_repo = '<select name="clone">' . "\n";
608 $select_repo .= '<option value="">'._('None').'</option>' . "\n";
609 $select_repo .= '<option value="'.$project_name.'">'.$project_name.'</option>' . "\n";
610 while($data = db_fetch_array($result)) {
611 $select_repo .= '<option value="'.$data['repo_name'].'">'.$data['repo_name'].'</option>' . "\n";
613 $select_repo .= '</select>' . "\n";
615 session_require_perm('project_admin', $params['group_id']);
617 $adminheadertitle = sprintf(_('Create SCM repository for project %1$s'), $project_name);
618 project_admin_header(array('title'=>$adminheadertitle, 'group'=>$params['group_id']));
621 <form name="form_create_repo"
622 action="<?php echo getStringFromServer('PHP_SELF'); ?>" method="post">
623 <input type="hidden" name="group_id" value="<?php echo $params['group_id'] ?>" />
624 <input type="hidden" name="create_repository" value="1" />
625 <p><strong><?php echo _('Repository name')._(': ') ?></strong><?php echo utils_requiredField(); ?><br />
626 <input type="text" required="required" size="20" name="repo_name" value="" /></p>
627 <p><strong><?php echo _('Description')._(':'); ?></strong><br />
628 <input type="text" size="60" name="description" value="" /></p>
629 <p><strong><?php echo _('Cloned from:') ?></strong><br />
630 <?php echo $select_repo ?></p>
631 <input type="submit" name="cancel" value="<?php echo _('Cancel') ?>" />
632 <input type="submit" name="submit" value="<?php echo _('Submit') ?>" />
637 project_admin_footer(array());
645 // c-file-style: "bsd"