3 * FusionForge Darcs plugin
5 * Copyright 2009, Roland Mas
6 * Copyright 2013-2014,2016-2017, Franck Villaume - TrivialDev
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 along
21 * with this program; if not, write to the Free Software Foundation, Inc.,
22 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 require_once $gfcommon.'include/plugins_utils.php';
27 forge_define_config_item('default_server', 'scmdarcs', forge_get_config('scm_host'));
28 forge_define_config_item('repos_path', 'scmdarcs', forge_get_config('chroot').'/scmrepos/darcs');
30 class DarcsPlugin extends SCMPlugin {
31 function __construct() {
33 parent::__construct();
34 $this->name = 'scmdarcs';
35 $this->text = _('Darcs');
37 _("This plugin contains the Darcs subsystem of FusionForge. It allows each
38 FusionForge project to have its own Darcs repository, and gives some control
39 over it to the project's administrator.");
40 $this->hooks[] = 'scm_generate_snapshots';
41 $this->hooks[] = 'scm_update_repolist';
42 $this->hooks[] = 'scm_browser_page';
43 $this->hooks[] = 'scm_gather_stats';
48 function getDefaultServer() {
49 return forge_get_config('default_server', 'scmdarcs');
52 function getRootRepositories($project) {
53 return (forge_get_config('repos_path', 'scmdarcs').'/'.$project->getUnixName());
56 function getRepositories($project, $autoinclude = true) {
58 $toprepo = $this->getRootRepositories($project);
59 if (is_dir($toprepo)) {
60 foreach (scandir($toprepo) as $repo_name) {
61 $repo = $toprepo . '/' . $repo_name;
62 if (is_dir($repo) && is_dir($repo . '/_darcs')) {
70 function printShortStats($params) {
71 $project = $this->checkParams($params);
76 if (forge_check_perm('scm', $project->getID(), 'read')) {
77 $result = db_query_params('SELECT sum(commits) AS commits, sum(adds) AS adds FROM stats_cvs_group WHERE group_id=$1',
78 array($project->getID()));
79 $commit_num = db_result($result, 0, 'commits');
80 $add_num = db_result($result, 0, 'adds');
87 $params['result'] .= ' (Darcs: '.sprintf(_('<strong>%1$s</strong> updates, <strong>%2$s</strong> adds'), number_format($commit_num, 0), number_format($add_num, 0)).")";
92 return html_e('p', array(),
93 sprintf(_('Documentation for %1$s is available at <a href="%2$s">%2$s</a>.'),
95 'http://darcs.net/'));
98 function getInstructionForDarcs($project, $rw) {
100 $repo_names = $this->getRepositories($project);
101 if (count($repo_names) > 0) {
102 $default_repo = "REPO";
103 if (count($repo_names) == 1) {
104 $default_repo = $repo_names[0];
107 $url = $this->getBoxForProject($project).':'.$this->getRootRepositories($project).'/'.$default_repo;
109 $protocol = forge_get_config('use_ssl')? 'https' : 'http';
110 $url = $protocol.'://'.$this->getBoxForProject($project).'/anonscm/darcs/'.$project->getUnixName().'/'.$default_repo;
112 $b = '<p><kbd>darcs get '.$url.'</kbd></p>';
113 if (count($repo_names) > 1) {
114 $b .= '<p>'._('where REPO can be: ').implode(_(', '), $repo_names).'</p>';
116 } else if (is_dir($this->getRootRepositories($project))) {
117 $b = $HTML->information(_('No repositories defined.'));
119 $b = $HTML->information(_('Repository not yet created, wait an hour.'));
124 function getInstructionsForAnon($project) {
125 $b = html_e('h2', array(), _('Anonymous Access'));
126 $b .= html_e('p', array(), _("This project's Darcs repository can be checked out through anonymous access with the following command."));
127 $b .= $this->getInstructionForDarcs($project, false);
131 function getInstructionsForRW($project) {
133 $b .= sprintf(_('Developer %s Access via SSH'), 'Darcs');
136 $b .= sprintf(_('Only project developers can access the %s tree via this method.'), 'Darcs');
138 $b .= _('SSH must be installed on your client machine.');
140 $b .= _('Substitute <em>developername</em> with the proper value.');
142 $b .= _('Enter your site password when prompted.');
144 $b .= $this->getInstructionForDarcs($project, true);
148 function getSnapshotPara($project) {
150 $filename = $project->getUnixName().'-scm-latest.tar'.util_get_compressed_file_extension();
151 if (file_exists(forge_get_config('scm_snapshots_path').'/'.$filename)) {
152 $b .= html_e('p', array(), '['.util_make_link("/snapshots.php?group_id=".$project->getID(), _('Download the nightly snapshot')).']');
157 function getBrowserLinkBlock($project) {
159 $b = html_e('h2', array(), _('Darcs Repository Browser'));
160 $b .= html_e('p', array(), _("Browsing the Darcs tree gives you a view into the current status"
161 . " of this project's code. You may also view the complete"
162 . " history of any file in the repository."));
163 $repo_names = $this->getRepositories($project);
164 if (count($repo_names) > 0) {
165 foreach ($repo_names as $repo_name) {
166 $b .= html_e('p', array(), '['.util_make_link('/scm/browser.php?group_id='.$project->getID()."&repo_name=".$repo_name.'&scm_plugin='.$this->name,
167 _('Browse Darcs repository').' '.$repo_name).']');
170 $b .= $HTML->information(_('No repositories to browse'));
175 function getStatsBlock($project) {
179 $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, reponame 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, reponame ORDER BY reponame, combined DESC, realname',
180 array($project->getID()));
182 if (db_numrows($result) > 0) {
183 $tableHeaders = array(
188 $b .= $HTML->listTableTop($tableHeaders, array(), '', 'repo-history-'.$this->name);
191 $total = array('adds' => 0, 'commits' => 0);
194 while ($data = db_fetch_array($result)) {
195 if ($prevrepo != $data['reponame']) {
196 if ($prevrepo != '') {
198 $cells[] = array(html_e('strong', array(), _('Total')._(':')), 'class' => 'halfwidth');
199 $cells[] = array($total['adds'], 'class' => 'onequarterwidth align-right');
200 $cells[] = array($total['updates'], 'class' => 'onequarterwidth align-right');
201 $b .= $HTML->multiTableRow(array(), $cells);
203 $prevrepo = $data['reponame'];
204 $total = array('adds' => 0, 'updates' => 0);
206 $cells[] = array(html_e('strong', array(), $data['reponame'].' '._('Statistics')), 'colspan' => 3);
207 $b .= $HTML->multiTableRow(array(), $cells);
210 $cells[] = array(util_display_user($data['user_name'], $data['user_id'], $data['realname']), 'class' => 'halfwidth');
211 $cells[] = array($data['adds'], 'class' => 'onequarterwidth align-right');
212 $cells[] = array($data['commits'], 'class' => 'onequarterwidth align-right');
213 $b .= $HTML->multiTableRow(array(), $cells);
214 $total['adds'] += $data['adds'];
215 $total['commits'] += $data['commits'];
219 $cells[] = array(html_e('strong', array(), _('Total')._(':')), 'class' => 'halfwidth');
220 $cells[] = array($total['adds'], 'class' => 'onequarterwidth align-right');
221 $cells[] = array($total['commits'], 'class' => 'onequarterwidth align-right');
222 $b .= $HTML->multiTableRow(array(), $cells);
223 $b .= $HTML->listTableBottom();
225 $b .= $HTML->warning_msg(_('No history yet.'));
231 function printBrowserPage($params) {
232 if ($params['scm_plugin'] != $this->name) {
235 $project = $this->checkParams($params);
240 if ($this->browserDisplayable($project)) {
241 htmlIframe('/plugins/scmdarcs/cgi-bin/darcsweb.cgi?r='.$project->getUnixName().'/'.$params['repo_name'],array('id'=>'scmdarcs_iframe'));
245 function createOrUpdateRepo($params) {
246 $project = $this->checkParams($params);
251 $toprepo = $this->getRootRepositories($project);
252 $unix_group = 'scm_'.$project->getUnixName();
254 system("chmod g+ws $toprepo");
256 $result = db_query_params(
257 "SELECT repo_name, clone_repo_name FROM plugin_scmdarcs_create_repos WHERE group_id=$1",
258 array($project->getID()));
260 echo "Error while retrieving darcs repository to create\n";
262 while ($res = db_fetch_array($result)) {
263 $repo = $toprepo . '/' . $res['repo_name'];
265 if ($res['clone_repo_name'] != '') {
266 $clone_repo = $toprepo . '/' . $res['clone_repo_name'];
268 if (!is_dir($repo."/_darcs")) {
269 system("mkdir -p '$repo'");
270 system("cd $repo ; darcs init >/dev/null");
272 system("darcs fetch '$clone_repo'");
274 system("find $repo -type d | xargs chmod g+s");
276 $result1 = db_query_params(
277 "DELETE FROM plugin_scmdarcs_create_repos WHERE group_id=$1 AND repo_name=$2",
278 array($project->getID(), $res['repo_name']));
280 echo "Cannot remove scheduling of darcs repository creation ".$res['repo_name']."\n";
285 foreach ($this->getRepositories($project) as $repo_name) {
286 $repo = $toprepo.'/'.$repo_name;
288 system("chgrp -R $unix_group $repo");
289 if ($project->enableAnonSCM()) {
290 system("chmod -R g+wX,o+rX-w $repo");
292 system("chmod -R g+wX,o-rwx $repo");
297 function darcswebRepository($project, $repo_name, $repo_url, $repo_dir) {
298 $classname = preg_replace('/\W/', '_', 'repo_' . $repo_name);
299 return ("class $classname:\n"
300 ."\trepodir = '$repo_dir'\n"
301 ."\treponame = '$repo_name'\n"
302 ."\t".'repodesc = """Repository ' . $repo_name . ' of '.$project->getPublicName().'"""'."\n"
303 ."\trepourl = '" . util_make_url('/anonscm/darcs/' . $repo_url) . "'\n"
304 ."\trepoprojurl = '" . util_make_url('/projects/' . $repo_url) . "'\n"
305 ."\trepoencoding = 'utf8'\n"
309 function updateRepositoryList($params) {
310 $groups = $this->getGroups();
312 foreach ($groups as $project) {
313 if ($this->browserDisplayable($project)) {
318 $config_dir = forge_get_config('config_path').'/plugins/scmdarcs';
319 if (!is_dir($config_dir)) {
320 mkdir($config_dir, 0755, true);
322 $fname = $config_dir.'/config.py';
324 $f = fopen($fname.'.new', 'w');
326 fwrite($f, "class base:\n"
327 ."\tdarcslogo = '".util_make_url('/plugins/scmdarcs/darcsweb/darcs.png')."'\n"
328 ."\tdarcsfav = '".util_make_url('/plugins/scmdarcs/darcsweb/minidarcs.png')."'\n"
329 ."\tcssfile = '".util_make_url('/plugins/scmdarcs/darcsweb/style.css')."'\n"
332 foreach ($list as $project) {
333 $unix_name = $project->getUnixName();
334 $toprepo = $this->getRootRepositories($project);
335 $repo_names = $this->getRepositories($project);
336 foreach ($repo_names as $repo_name) {
337 if ($repo_name == $unix_name) {
338 # Default repository name, we create a default entry for it
340 $this->darcswebRepository($project,
342 "$unix_name/$repo_name",
343 "$toprepo/$repo_name"));
346 $this->darcswebRepository($project,
347 "$unix_name/$repo_name",
348 "$unix_name/$repo_name",
349 "$toprepo/$repo_name"));
353 chmod($fname.'.new', 0644);
354 rename($fname.'.new', $fname);
357 function generateSnapshots($params) {
358 $us = forge_get_config('use_scm_snapshots');
359 $ut = forge_get_config('use_scm_tarballs');
364 $project = $this->checkParams($params);
369 $group_name = $project->getUnixName();
371 $snapshot = forge_get_config('scm_snapshots_path').'/'.$group_name.'-scm-latest.tar'.util_get_compressed_file_extension();
372 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar'.util_get_compressed_file_extension();
374 if (! $project->enableAnonSCM()) {
375 if (file_exists($tarball)) unlink($tarball);
380 $toprepo = forge_get_config('repos_path', 'scmdarcs');
381 $repo = $this->getRootRepositories($project);
383 if (!is_dir($repo)) {
384 if (file_exists($tarball)) unlink($tarball);
388 $tmp = trim(`mktemp -d`);
393 $today = date('Y-m-d');
394 $dir = $project->getUnixName()."-$today";
395 system("mkdir -p $tmp/$dir");
396 system("cd $tmp ; darcs $repo $dir > /dev/null 2>&1");
397 system("tar cCf $tmp - $dir |".forge_get_config('compression_method')."> $tmp/snapshot");
398 chmod("$tmp/snapshot", 0644);
399 copy("$tmp/snapshot", $snapshot);
400 unlink("$tmp/snapshot");
401 system("rm -rf $tmp/$dir");
405 system("tar cCf $toprepo - ".$project->getUnixName()."|".forge_get_config('compression_method')."> $tmp/tarball");
406 chmod("$tmp/tarball", 0644);
407 copy("$tmp/tarball", $tarball);
408 unlink("$tmp/tarball");
409 system("rm -rf $tmp");
413 function gatherStats($params) {
414 global $adds, $deletes, $updates, $commits,
415 $usr_adds, $usr_deletes, $usr_updates;
417 $project = $this->checkParams($params);
422 if ($params['mode'] == 'day') {
423 $year = $params['year'];
424 $month = $params['month'];
425 $day = $params['day'];
426 foreach ($this->getRepositories($project) as $repo_name) {
427 $this->gatherStatsRepo($project, $repo_name, $year, $month, $day);
432 function gatherStatsRepo($group, $project_reponame, $year, $month, $day) {
433 $month_string = sprintf("%04d%02d", $year, $month);
434 $start_time = gmmktime(0, 0, 0, $month, $day, $year);
435 $end_time = $start_time + 86400;
441 $usr_updates = array();
442 $usr_deletes = array();
444 $toprepo = $this->getRootRepositories($group);
445 $repo = $toprepo . '/' . $project_reponame;
446 if (!is_dir($repo) || !is_dir("$repo/_darcs")) {
447 echo "No repository $repo\n";
450 $from_date = date("c", $start_time);
451 $to_date = date("c", $end_time);
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 AND reponame = $4',
461 echo "Error while cleaning stats_cvs_group\n";
466 $res = db_query_params('DELETE FROM stats_cvs_user WHERE month = $1 AND day = $2 AND group_id = $3 AND reponame = $4',
472 echo "Error while cleaning stats_cvs_user\n";
477 $pipe = popen("darcs changes --repodir='$repo' --match 'date \"between $from_date and $to_date\"' --xml -s\n", 'r');
479 $xml_parser = xml_parser_create();
480 xml_set_element_handler($xml_parser, "DarcsPluginStartElement", "DarcsPluginEndElement");
482 // Analyzing history stream
483 while (!feof($pipe) && $data = fgets($pipe, 4096)) {
484 if (!xml_parse($xml_parser, $data, feof($pipe))) {
485 debug("Unable to parse XML with error ".
486 xml_error_string(xml_get_error_code($xml_parser)).
488 xml_get_current_line_number($xml_parser));
493 xml_parser_free($xml_parser);
495 // inserting group results in stats_cvs_groups
496 if (!db_query_params('INSERT INTO stats_cvs_group (month, day, group_id, checkouts, commits, adds, reponame)
497 VALUES ($1, $2, $3, $4, $5, $6, $7)',
504 $project_reponame))) {
505 echo "Error while inserting into stats_cvs_group\n";
510 // build map for email -> login
511 $email_login = array();
512 $email_login_fn = $repo."/_darcs/email-login.txt";
513 if (!file_exists($email_login_fn)) {
514 $email_login_fn = $repo."/.email-login.txt";
516 if (!file_exists($email_login_fn)) {
517 unset($email_login_fn);
520 if (isset($email_login_fn)) {
521 $fh = fopen($email_login_fn, 'r');
523 $a = explode(" ", fgets($fh));
525 $email_login[$a[0]] = rtrim($a[1]);
531 // building the user list
532 $user_list = array_unique(array_merge(array_keys($usr_adds), array_keys($usr_updates)));
534 foreach ($user_list as $user) {
535 // trying to get user id from darcs user name
537 $tmp_email = explode("<", $id, 2);
538 if (isset($tmp_email[1])) {
539 $tmp_email = explode(">", $tmp_email[1]);
542 if (isset($email_login[$id])) {
543 $id = $email_login[$id];
546 $u = user_get_object_by_name($id);
548 $user_id = $u->getID();
553 if (!db_query_params('INSERT INTO stats_cvs_user (month, day, group_id, user_id, commits, adds) VALUES ($1, $2, $3, $4, $5, $6, $7)',
558 isset($usr_updates[$user]) ? $usr_updates[$user] : 0,
559 isset($usr_adds[$user]) ? $usr_adds[$user] : 0),
560 $project_reponame)) {
561 echo "Error while inserting into stats_cvs_user\n";
569 function printAdminPage(&$params) {
570 parent::printAdminPage($params);
572 $project = $this->checkParams($params);
577 $result = db_query_params('SELECT repo_name FROM plugin_scmdarcs_create_repos WHERE group_id=$1',
578 array($project->getID()));
579 if ($result && db_numrows($result) > 0) {
581 while ($res = db_fetch_array($result)) {
582 array_push($nm, $res['repo_name']);
584 print '<p><strong>'._('Repository to be created')._(': ').'</strong>'.
585 implode(_(', '), $nm) . '</p>';
588 print '<p><strong>'._('Create new repository')._(': ').'</strong></p>';
589 print '<p>'._('Repository name')._(': ');
590 print '<input type="string" name="scm_create_repo_name" size=16 maxlength=128 /></p>';
591 print '<p>'._('Clone')._(': ').
592 '<select name="scm_clone_repo_name">';
593 print '<option value=""><none></option>';
594 foreach ($this->getRepositories($project) as $repo_name) {
595 print '<option value="'.$repo_name.'">'.$repo_name.'</option>';
597 print '</select></p>';
600 function adminUpdate($params) {
601 parent::adminUpdate($params);
603 $project = $this->checkParams($params);
608 if (!isset($params['scm_create_repo_name'])) {
611 $new_repo_name = $params['scm_create_repo_name'];
612 $clone_repo_name = $params['scm_clone_repo_name'];
613 if ($new_repo_name != '') {
614 $repo_names = $this->getRepositories($project);
615 if (in_array($new_repo_name, $repo_names)) {
616 html_error_top(_("Repository $new_repo_name already exists"));
620 if ($clone_repo_name != '' && !in_array($clone_repo_name, $repo_names)) {
621 html_error_top(_("Clone repository $clone_repo_name doesn't exist"));
624 if ($clone_repo_name == '<none>') {
625 $clone_repo_name = '';
628 if (!preg_match('/^[\w][-_\w\d\.]+$/', $new_repo_name)) {
629 html_error_top("Invalid repository name $new_repo_name");
634 if (!db_query_params('INSERT INTO plugin_scmdarcs_create_repos (group_id,repo_name,clone_repo_name)
636 array($project->getID(), $new_repo_name, $clone_repo_name))) {
637 html_error_top("SQL error while scheduling new repository $new_repo_name");
643 html_feedback_top(_("Repository $new_repo_name schedule for creation"));
647 function scm_admin_form(&$params) {
649 $project = $this->checkParams($params);
653 session_require_perm('project_admin', $params['group_id']);
655 if (forge_get_config('allow_multiple_scm') && ($params['allow_multiple_scm'] > 1)) {
656 echo html_ao('div', array('id' => 'tabber-'.$this->name, 'class' => 'tabbertab'));
658 if (forge_get_config('allow_multiple_scm') && ($params['allow_multiple_scm'] > 1)) {
659 echo html_ac(html_ap() - 1);
664 function DarcsPluginStartElement($parser, $name, $attrs) {
665 global $last_user, $commits,
666 $adds, $updates, $deletes,
667 $usr_adds, $usr_updates, $usr_deletes;
670 $last_user = $attrs['AUTHOR'];
671 if (!array_key_exists($last_user, $usr_deletes)) {
672 $usr_deletes[$last_user] = 0;
674 if (!array_key_exists($last_user, $usr_updates)) {
675 $usr_updates[$last_user] = 0;
677 if (!array_key_exists($last_user, $usr_adds)) {
678 $usr_adds[$last_user] = 0;
683 case "REMOVE_DIRECTORY":
686 $usr_deletes[$last_user]++;
693 $usr_updates[$last_user]++;
697 case "ADD_DIRECTORY":
700 $usr_adds[$last_user]++;
706 function DarcsPluginEndElement($parser, $name) {
710 // c-file-style: "bsd"