2 /** FusionForge Git plugin
4 * Copyright 2009, Roland Mas
5 * Copyright 2009, Mehdi Dogguy <mehdi@debian.org>
7 * This file is part of FusionForge.
9 * FusionForge is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published
11 * by the Free Software Foundation; either version 2 of the License,
12 * or (at your option) any later version.
14 * FusionForge is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * General Public License for more details.
19 * You should have received a copy of the GNU General Public License along
20 * with this program; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 forge_define_config_item('default_server', 'scmgit', forge_get_config ('web_host')) ;
25 forge_define_config_item('repos_path', 'scmgit', forge_get_config('chroot').'/scmrepos/git') ;
27 class GitPlugin extends SCMPlugin {
28 function GitPlugin() {
30 $this->name = 'scmgit';
32 $this->_addHook('scm_browser_page');
33 $this->_addHook('scm_update_repolist');
34 $this->_addHook('scm_generate_snapshots');
35 $this->_addHook('scm_gather_stats');
36 $this->_addHook('widget_instance', 'myPageBox', false);
37 $this->_addHook('widgets', 'widgets', false);
41 function getDefaultServer() {
42 return forge_get_config('default_server', 'scmgit') ;
45 function printShortStats ($params) {
46 $project = $this->checkParams($params);
51 if ($project->usesPlugin($this->name)) {
52 $result = db_query_params('SELECT sum(commits) AS commits, sum(adds) AS adds FROM stats_cvs_group WHERE group_id=$1',
53 array ($project->getID())) ;
54 $commit_num = db_result($result,0,'commits');
55 $add_num = db_result($result,0,'adds');
62 echo ' (Git: '.sprintf(_('<strong>%1$s</strong> commits, <strong>%2$s</strong> adds'), number_format($commit_num, 0), number_format($add_num, 0)).")";
67 return '<p>' . _('Documentation for Git is available at <a href="http://git-scm.com/">http://git-scm.com/</a>.') . '</p>';
70 function getInstructionsForAnon($project) {
71 $b = '<h2>' . _('Anonymous Git Access') . '</h2>';
73 $b .= _('This project\'s Git repository can be checked out through anonymous access with the following command.');
77 $b .= '<tt>git clone '.util_make_url ('/anonscm/git/'.$project->getUnixName().'/'.$project->getUnixName().'.git').'</tt><br />';
80 $result = db_query_params('SELECT u.user_id, u.user_name, u.realname FROM plugin_scmgit_personal_repos p, users u WHERE p.group_id=$1 AND u.user_id=p.user_id AND u.unix_status=$2',
81 array ($project->getID(),
83 $rows = db_numrows($result);
87 $b .= _('Developer\'s repository');
90 $b .= ngettext('One of this project\'s members also has a personal Git repository that can be checked out anonymously.',
91 'Some of this project\'s members also have personal Git repositories that can be checked out anonymously.',
95 for ($i=0; $i<$rows; $i++) {
96 $user_id = db_result($result,$i,'user_id');
97 $user_name = db_result($result,$i,'user_name');
98 $real_name = db_result($result,$i,'realname');
99 $b .= '<tt>git clone '.util_make_url('/anonscm/git/'.$project->getUnixName().'/users/'.$user_name.'.git').'</tt> ('.util_make_link_u ($user_name, $user_id, $real_name).')<br />';
107 function getInstructionsForRW($project) {
109 if (session_loggedin()) {
110 $u =& user_get_object(user_getid());
111 $d = $u->getUnixName();
112 if (forge_get_config('use_ssh', 'scmgit')) {
114 $b .= _('Developer GIT Access via SSH');
117 $b .= _('Only project developers can access the GIT tree via this method. SSH must be installed on your client machine. Enter your site password when prompted.');
119 $b .= '<p><tt>git clone git+ssh://'.$d.'@' . $this->getBoxForProject($project) . '/'. forge_get_config('repos_path', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
120 } elseif (forge_get_config('use_dav', 'scmgit')) {
121 $protocol = forge_get_config('use_ssl', 'scmgit')? 'https' : 'http';
123 $b .= _('Developer GIT Access via HTTP');
126 $b .= _('Only project developers can access the GIT tree via this method. Enter your site password when prompted.');
128 $b .= '<p><tt>git clone '.$protocol.'://'.$d.'@' . $this->getBoxForProject($project) . '/'. forge_get_config('scm_root', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
130 $b = '<p class="warning">'._('Missing configuration for access in scmgit.ini : use_ssh and use_dav disabled').'</p>';
133 if (forge_get_config('use_ssh', 'scmgit')) {
135 $b .= _('Developer GIT Access via SSH');
138 $b .= _('Only project developers can access the GIT 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.');
140 $b .= '<p><tt>git clone git+ssh://<i>'._('developername').'</i>@' . $this->getBoxForProject($project) . '/'. forge_get_config('repos_path', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
141 } elseif (forge_get_config('use_dav', 'scmgit')) {
142 $protocol = forge_get_config('use_ssl', 'scmgit')? 'https' : 'http';
144 $b .= _('Developer GIT Access via HTTP');
147 $b .= _('Only project developers can access the GIT tree via this method. Enter your site password when prompted.');
149 $b .= '<p><tt>git clone '.$protocol.'://<i>'._('developername').'</i>@' . $this->getBoxForProject($project) . '/'. forge_get_config('scm_root', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
153 if (session_loggedin()) {
154 $u =& user_get_object(user_getid()) ;
155 if ($u->getUnixStatus() == 'A') {
156 $result = db_query_params('SELECT * FROM plugin_scmgit_personal_repos p WHERE p.group_id=$1 AND p.user_id=$2',
157 array ($project->getID(),
159 if ($result && db_numrows ($result) > 0) {
161 $b .= _('Access to your personal repository');
164 $b .= _('You have a personal repository for this project, accessible through SSH with the following method. Enter your site password when prompted.');
166 $b .= '<p><tt>git clone git+ssh://'.$u->getUnixName().'@' . $this->getBoxForProject($project) . forge_get_config('repos_path', 'scmgit') .'/'. $project->getUnixName() .'/users/'. $u->getUnixName() .'.git</tt></p>' ;
168 $glist = $u->getGroups();
169 foreach ($glist as $g) {
170 if ($g->getID() == $project->getID()) {
172 $b .= _('Request a personal repository');
175 $b .= _('You can clone the project repository into a personal one into which you alone will be able to write. Other members of the project will only have read access. Access for non-members will follow the same rules as for the project\'s main repository. Note that the personal repository may take some time before it is created (less than an hour in most situations).');
178 $b .= sprintf (_('<a href="%s">Request a personal repository</a>.'),
179 util_make_url ('/plugins/scmgit/index.php?func=request-personal-repo&group_id='.$project->getID()));
189 function getSnapshotPara($project) {
192 $filename = $project->getUnixName().'-scm-latest.tar'.util_get_compressed_file_extension();
193 if (file_exists(forge_get_config('scm_snapshots_path').'/'.$filename)) {
195 $b .= util_make_link("/snapshots.php?group_id=".$project->getID(),
196 _('Download the nightly snapshot')
203 function printBrowserPage($params) {
206 $project = $this->checkParams($params);
211 if ($project->usesPlugin($this->name)) {
212 if ($this->browserDisplayable($project)) {
213 print '<iframe src="'.util_make_url("/plugins/scmgit/cgi-bin/gitweb.cgi?p=".$project->getUnixName().'/'.$project->getUnixName().'.git').'" frameborder="0" width=100% height=700></iframe>' ;
218 function getBrowserLinkBlock($project) {
220 $b = $HTML->boxMiddle(_('Git Repository Browser'));
222 $b .= _('Browsing the Git 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.');
225 $b .= util_make_link ("/scm/browser.php?group_id=".$project->getID(),
226 _('Browse Git Repository')
232 function getStatsBlock ($project) {
236 $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',
237 array ($project->getID()));
239 if (db_numrows($result) > 0) {
240 // $b .= $HTML->boxMiddle(_('Repository Statistics'));
242 $tableHeaders = array(
247 $b .= $HTML->listTableTop($tableHeaders, false, '', 'repo-history');
250 $total = array('adds' => 0, 'commits' => 0);
252 while($data = db_fetch_array($result)) {
253 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
254 $b .= '<td class="halfwidth">' ;
255 $b .= util_make_link_u ($data['user_name'], $data['user_id'], $data['realname']) ;
256 $b .= '</td><td class="onequarterwidth align-right">'.$data['adds']. '</td>'.
257 '<td class="onequarterwidth align-right">'.$data['commits'].'</td></tr>';
258 $total['adds'] += $data['adds'];
259 $total['commits'] += $data['commits'];
262 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
263 $b .= '<td class="halfwidth"><strong>'._('Total').':</strong></td>'.
264 '<td class="onequarterwidth align-right"><strong>'.$total['adds']. '</strong></td>'.
265 '<td class="onequarterwidth align-right"><strong>'.$total['commits'].'</strong></td>';
267 $b .= $HTML->listTableBottom();
273 function createOrUpdateRepo($params) {
274 $project = $this->checkParams($params);
279 if (! $project->usesPlugin ($this->name)) {
283 $project_name = $project->getUnixName();
284 $root = forge_get_config('repos_path', 'scmgit') . '/' . $project_name ;
285 system ("mkdir -p $root");
287 $main_repo = $root . '/' . $project_name . '.git' ;
288 if (!is_file ("$main_repo/HEAD") && !is_dir("$main_repo/objects") && !is_dir("$main_repo/refs")) {
289 exec ("GIT_DIR=\"$main_repo\" git init --bare --shared=group", $result) ;
290 $output .= join("<br />", $result);
292 exec ("GIT_DIR=\"$main_repo\" git update-server-info", $result) ;
293 $output .= join("<br />", $result);
294 if (is_file ("$main_repo/hooks/post-update.sample")) {
295 rename ("$main_repo/hooks/post-update.sample",
296 "$main_repo/hooks/post-update") ;
298 if (!is_file ("$main_repo/hooks/post-update")) {
299 $f = fopen ("$main_repo/hooks/post-update") ;
300 fwrite ($f, "exec git-update-server-info\n") ;
303 if (is_file ("$main_repo/hooks/post-update")) {
304 system ("chmod +x $main_repo/hooks/post-update") ;
306 system ("echo \"Git repository for $project_name\" > $main_repo/description") ;
307 system ("find $main_repo -type d | xargs chmod g+s") ;
309 if (forge_get_config('use_ssh','scmgit')) {
310 $unix_group = 'scm_' . $project_name ;
311 system ("chgrp -R $unix_group $root") ;
312 system ("chmod g+s $root") ;
313 if ($project->enableAnonSCM()) {
314 system ("chmod g+wX,o+rX-w $root") ;
315 system ("chmod -R g+wX,o+rX-w $main_repo") ;
317 system ("chmod g+wX,o-rwx $root") ;
318 system ("chmod -R g+wX,o-rwx $main_repo") ;
321 $unix_user = forge_get_config('apache_user');
322 $unix_group = forge_get_config('apache_group');
323 system ("chown -R $unix_user:$unix_group $main_repo") ;
324 system ("chmod -R g-rwx,o-rwx $main_repo") ;
327 $result = db_query_params ('SELECT u.user_name FROM plugin_scmgit_personal_repos p, users u WHERE p.group_id=$1 AND u.user_id=p.user_id AND u.unix_status=$2',
328 array ($project->getID(),
330 $rows = db_numrows ($result) ;
331 for ($i=0; $i<$rows; $i++) {
332 system ("mkdir -p $root/users") ;
333 $user_name = db_result($result,$i,'user_name');
334 $repodir = $root . '/users/' . $user_name . '.git' ;
336 if (!is_file ("$repodir/HEAD") && !is_dir("$repodir/objects") && !is_dir("$repodir/refs")) {
337 system ("git clone --bare $main_repo $repodir") ;
338 system ("GIT_DIR=\"$repodir\" git update-server-info") ;
339 if (is_file ("$repodir/hooks/post-update.sample")) {
340 rename ("$repodir/hooks/post-update.sample",
341 "$repodir/hooks/post-update") ;
343 if (!is_file ("$repodir/hooks/post-update")) {
344 $f = fopen ("$repodir/hooks/post-update") ;
345 fwrite ($f, "exec git-update-server-info\n") ;
348 if (is_file ("$repodir/hooks/post-update")) {
349 system ("chmod +x $repodir/hooks/post-update") ;
351 system("echo \"Git repository for user $user_name in project $project_name\" > $repodir/description");
352 system ("chown -R $user_name:$unix_group $repodir") ;
355 if (is_dir ("$root/users")) {
356 if ($project->enableAnonSCM()) {
357 system ("chmod -R g+rX-w,o+rX-w $root/users") ;
359 system ("chmod -R g+rX-w,o-rwx $root/users") ;
362 $params['output'] = $output;
365 function updateRepositoryList($params) {
366 $groups = $this->getGroups();
368 foreach ($groups as $project) {
369 if ($this->browserDisplayable($project)) {
374 $config_dir = forge_get_config('config_path').'/plugins/scmgit';
375 $fname = $config_dir . '/gitweb.conf' ;
376 $config_f = fopen($fname.'.new', 'w') ;
377 $rootdir = forge_get_config('repos_path', 'scmgit');
378 fwrite($config_f, "\$projectroot = '$rootdir';\n");
379 fwrite($config_f, "\$projects_list = '$config_dir/gitweb.list';\n");
380 fwrite($config_f, "@git_base_url_list = ('". util_make_url('/anonscm/git') . "');\n");
381 fwrite($config_f, "\$logo = '". util_make_url('/plugins/scmgit/git-logo.png') . "';\n");
382 fwrite($config_f, "\$favicon = '". util_make_url('/plugins/scmgit/git-favicon.png')."';\n");
383 fwrite($config_f, "\$stylesheet = '". util_make_url('/plugins/scmgit/gitweb.css')."';\n");
384 fwrite($config_f, "\$prevent_xss = 'true';\n");
386 chmod ($fname.'.new', 0644) ;
387 rename ($fname.'.new', $fname) ;
389 $fname = $config_dir . '/gitweb.list' ;
391 $f = fopen ($fname.'.new', 'w');
392 foreach ($list as $project) {
393 $repos = $this->getRepositories($rootdir . "/" . $project->getUnixName());
394 foreach ($repos as $repo) {
395 $reldir = substr($repo, strlen($rootdir) + 1);
396 fwrite($f, $reldir . "\n");
400 chmod($fname.'.new', 0644);
401 rename($fname.'.new', $fname);
404 function getRepositories($path) {
405 if (! is_dir($path)) {
406 echo 'pas de path ?';
410 $entries = scandir($path);
411 foreach ($entries as $entry) {
412 $fullname = $path . "/" . $entry;
413 if (($entry == ".") or ($entry == ".."))
415 if (is_dir($fullname)) {
416 if (is_link($fullname))
418 $result = $this->getRepositories($fullname);
419 $list = array_merge($list, $result);
420 } else if ($entry == "HEAD") {
427 function gatherStats ($params) {
428 global $last_user, $usr_adds, $usr_deletes,
429 $usr_updates, $updates, $adds;
431 $project = $this->checkParams ($params) ;
436 if (! $project->usesPlugin ($this->name)) {
440 if ($params['mode'] == 'day') {
443 $year = $params ['year'] ;
444 $month = $params ['month'] ;
445 $day = $params ['day'] ;
446 $month_string = sprintf( "%04d%02d", $year, $month );
447 $start_time = gmmktime( 0, 0, 0, $month, $day, $year);
448 $end_time = $start_time + 86400;
450 $usr_adds = array () ;
451 $usr_updates = array () ;
452 $usr_deletes = array () ;
457 $repo = forge_get_config('repos_path', 'scmgit') . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git';
458 if (!is_dir ($repo) || !is_dir ("$repo/refs")) {
459 // echo "No repository\n" ;
464 $pipe = popen ("GIT_DIR=\"$repo\" git log --since=@$start_time --until=@$end_time --all --pretty='format:%n%an <%ae>' --name-status", 'r' ) ;
466 // cleaning stats_cvs_* table for the current day
467 $res = db_query_params ('DELETE FROM stats_cvs_group WHERE month=$1 AND day=$2 AND group_id=$3',
468 array ($month_string,
470 $project->getID())) ;
472 echo "Error while cleaning stats_cvs_group\n" ;
478 while (!feof($pipe) && $data = fgets ($pipe)) {
480 if (strlen($line) > 0) {
481 $result = preg_match("/^(?<name>.+) <(?<mail>.+)>/", $line, $matches);
484 $last_user = $matches['name'];
485 $user2email[$last_user] = strtolower($matches['mail']);
487 // Short-commit stats line
488 preg_match("/^(?<mode>[AM])\s+(?<file>.+)$/", $line, $matches);
489 if ($last_user == "") continue;
490 if ($matches['mode'] == 'A') {
491 $usr_adds[$last_user]++;
493 } elseif ($matches['mode'] == 'M') {
494 $usr_updates[$last_user]++;
496 } elseif ($matches['mode'] == 'D') {
497 $usr_deletes[$last_user]++;
503 // inserting group results in stats_cvs_groups
504 if ($updates > 0 || $adds > 0) {
505 if (!db_query_params ('INSERT INTO stats_cvs_group (month,day,group_id,checkouts,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
506 array ($month_string,
512 echo "Error while inserting into stats_cvs_group\n" ;
518 // building the user list
519 $user_list = array_unique( array_merge( array_keys( $usr_adds ), array_keys( $usr_updates ) ) );
521 foreach ( $user_list as $user ) {
522 // Trying to get user id from user name or email
523 $u = &user_get_object_by_name ($user) ;
525 $user_id = $u->getID();
527 $res=db_query_params('SELECT user_id FROM users WHERE lower(realname)=$1 OR email=$2',
528 array (strtolower($user), $user2email[$user]));
529 if ($res && db_numrows($res) > 0) {
530 $user_id = db_result($res,0,'user_id');
536 $uu = $usr_updates[$user] ? $usr_updates[$user] : 0 ;
537 $ua = $usr_adds[$user] ? $usr_adds[$user] : 0 ;
538 if ($uu > 0 || $ua > 0) {
539 if (!db_query_params ('INSERT INTO stats_cvs_user (month,day,group_id,user_id,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
540 array ($month_string,
546 echo "Error while inserting into stats_cvs_user\n" ;
556 function generateSnapshots ($params) {
558 $project = $this->checkParams ($params) ;
563 $group_name = $project->getUnixName() ;
565 $snapshot = forge_get_config('scm_snapshots_path').'/'.$group_name.'-scm-latest.tar'.util_get_compressed_file_extension();
566 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar'.util_get_compressed_file_extension();
568 if (! $project->usesPlugin ($this->name)) {
572 if (! $project->enableAnonSCM()) {
573 if (is_file($snapshot)) {
576 if (is_file($tarball)) {
582 // TODO: ideally we generate one snapshot per git repository
583 $toprepo = forge_get_config('repos_path', 'scmgit') ;
584 $repo = $toprepo . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git' ;
586 if (!is_dir ($repo)) {
587 if (is_file($snapshot)) {
590 if (is_file($tarball)) {
596 $tmp = trim (`mktemp -d`) ;
600 $today = date ('Y-m-d') ;
601 system ("GIT_DIR=\"$repo\" git archive --format=tar --prefix=$group_name-scm-$today/ HEAD |".forge_get_config('compression_method')." > $tmp/snapshot");
602 chmod ("$tmp/snapshot", 0644) ;
603 copy ("$tmp/snapshot", $snapshot) ;
604 unlink ("$tmp/snapshot") ;
606 system ("tar cCf $toprepo - ".$project->getUnixName() ."|".forge_get_config('compression_method')."> $tmp/tarball") ;
607 chmod ("$tmp/tarball", 0644) ;
608 copy ("$tmp/tarball", $tarball) ;
609 unlink ("$tmp/tarball") ;
610 system ("rm -rf $tmp") ;
614 * widgets - 'widgets' hook handler
615 * @param array $params
618 function widgets($params) {
619 require_once('common/widget/WidgetLayoutManager.class.php');
620 if ($params['owner_type'] == WidgetLayoutManager::OWNER_TYPE_GROUP) {
621 $params['fusionforge_widgets'][] = 'plugin_scmgit_project_latestcommits';
623 if ($params['owner_type'] == WidgetLayoutManager::OWNER_TYPE_USER) {
624 $params['fusionforge_widgets'][] = 'plugin_scmgit_user_myrepositories';
630 * Process the 'widget_instance' hook to create instances of the widgets
631 * @param array $params
633 function myPageBox($params) {
635 $user = UserManager::instance()->getCurrentUser();
636 require_once('common/widget/WidgetLayoutManager.class.php');
637 if ($params['widget'] == 'plugin_scmgit_user_myrepositories') {
638 require_once $gfplugins.$this->name.'/common/scmgit_Widget_MyRepositories.class.php';
639 $params['instance'] = new scmgit_Widget_MyRepositories(WidgetLayoutManager::OWNER_TYPE_USER, $user->getId());
643 function weekly(&$params) {
644 $res = db_query_params('SELECT group_id FROM groups WHERE status=$1 AND use_scm=1 ORDER BY group_id DESC',
647 $params['output'] .= 'ScmGit Plugin: Unable to get list of projects using SCM: '.db_error();
651 $params['output'] .= 'ScmGit Plugin: Running "git gc --quiet" on '.db_numrows($res).' repositories.'."\n";
652 while ($row = db_fetch_array($res)) {
653 $project = group_get_object($row['group_id']);
654 if (!$project || !is_object($project)) {
656 } elseif ($project->isError()) {
659 if (!$project->usesPlugin($this->name)) {
663 $project_name = $project->getUnixName();
664 $repo = forge_get_config('repos_path', 'scmgit') . '/' . $project_name . '/' . $project_name .'.git';
667 $params['output'] .= $project_name.': '.`git gc --quiet 2>&1`;
675 // c-file-style: "bsd"