3 * FusionForge Git plugin
5 * Copyright 2009, Roland Mas
6 * Copyright 2009, Mehdi Dogguy <mehdi@debian.org>
7 * Copyright 2012, Franck Villaume - TrivialDev
8 * http://fusionforge.org
10 * This file is part of FusionForge.
12 * FusionForge is free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published
14 * by the Free Software Foundation; either version 2 of the License,
15 * or (at your option) any later version.
17 * FusionForge is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
22 * You should have received a copy of the GNU General Public License along
23 * with this program; if not, write to the Free Software Foundation, Inc.,
24 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
27 forge_define_config_item('default_server', 'scmgit', forge_get_config ('web_host')) ;
28 forge_define_config_item('repos_path', 'scmgit', forge_get_config('chroot').'/scmrepos/git') ;
30 class GitPlugin extends SCMPlugin {
31 function GitPlugin() {
33 $this->name = 'scmgit';
35 $this->_addHook('scm_browser_page');
36 $this->_addHook('scm_update_repolist');
37 $this->_addHook('scm_generate_snapshots');
38 $this->_addHook('scm_gather_stats');
39 $this->_addHook('widget_instance', 'myPageBox', false);
40 $this->_addHook('widgets', 'widgets', false);
44 function getDefaultServer() {
45 return forge_get_config('default_server', 'scmgit') ;
48 function printShortStats ($params) {
49 $project = $this->checkParams($params);
54 if ($project->usesPlugin($this->name)) {
55 $result = db_query_params('SELECT sum(commits) AS commits, sum(adds) AS adds FROM stats_cvs_group WHERE group_id=$1',
56 array ($project->getID())) ;
57 $commit_num = db_result($result,0,'commits');
58 $add_num = db_result($result,0,'adds');
65 echo ' (Git: '.sprintf(_('<strong>%1$s</strong> commits, <strong>%2$s</strong> adds'), number_format($commit_num, 0), number_format($add_num, 0)).")";
70 return '<p>' . _('Documentation for Git is available at <a href="http://git-scm.com/">http://git-scm.com/</a>.') . '</p>';
73 function getInstructionsForAnon($project) {
74 $b = '<h2>' . _('Anonymous Git Access') . '</h2>';
76 $b .= _('This project\'s Git repository can be checked out through anonymous access with the following command.');
80 $b .= '<tt>git clone '.util_make_url ('/anonscm/git/'.$project->getUnixName().'/'.$project->getUnixName().'.git').'</tt><br />';
83 $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',
84 array ($project->getID(),
86 $rows = db_numrows($result);
90 $b .= _('Developer\'s repository');
93 $b .= ngettext('One of this project\'s members also has a personal Git repository that can be checked out anonymously.',
94 'Some of this project\'s members also have personal Git repositories that can be checked out anonymously.',
98 for ($i=0; $i<$rows; $i++) {
99 $user_id = db_result($result,$i,'user_id');
100 $user_name = db_result($result,$i,'user_name');
101 $real_name = db_result($result,$i,'realname');
102 $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).') ['.util_make_link('/scm/browser.php?group_id='.$project->getID().'&user_id='.$user_id, _('Browse Git Repository')).']<br />';
110 function getInstructionsForRW($project) {
112 if (session_loggedin()) {
113 $u = user_get_object(user_getid());
114 $d = $u->getUnixName();
117 if (forge_get_config('use_ssh', 'scmgit')) {
119 $b .= _('Developer Git Access via SSH');
122 $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.');
124 $b .= '<p><tt>git clone git+ssh://'.$d.'@' . $this->getBoxForProject($project) . '/'. forge_get_config('repos_path', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
127 if (forge_get_config('use_dav', 'scmgit')) {
128 $protocol = forge_get_config('use_ssl', 'scmgit')? 'https' : 'http';
130 $b .= _('Developer Git Access via HTTP');
133 $b .= _('Only project developers can access the Git tree via this method. Enter your site password when prompted.');
135 $b .= '<p><tt>git clone '.$protocol.'://'.$d.'@' . $this->getBoxForProject($project) . '/'. forge_get_config('scm_root', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
138 if ($validSetup == 0) {
139 $b = '<p class="warning">'._('Missing configuration for access in scmgit.ini : use_ssh and use_dav disabled').'</p>';
142 if (forge_get_config('use_ssh', 'scmgit')) {
144 $b .= _('Developer Git Access via SSH');
147 $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.');
149 $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>' ;
151 if (forge_get_config('use_dav', 'scmgit')) {
152 $protocol = forge_get_config('use_ssl', 'scmgit')? 'https' : 'http';
154 $b .= _('Developer Git Access via HTTP');
157 $b .= _('Only project developers can access the Git tree via this method. Enter your site password when prompted.');
159 $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>' ;
163 if (session_loggedin()) {
164 $u =& user_get_object(user_getid()) ;
165 if ($u->getUnixStatus() == 'A') {
166 $result = db_query_params('SELECT * FROM plugin_scmgit_personal_repos p WHERE p.group_id=$1 AND p.user_id=$2',
167 array ($project->getID(),
169 if ($result && db_numrows ($result) > 0) {
171 $b .= _('Access to your personal repository');
174 $b .= _('You have a personal repository for this project, accessible through SSH with the following method. Enter your site password when prompted.');
176 $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>' ;
178 $glist = $u->getGroups();
179 foreach ($glist as $g) {
180 if ($g->getID() == $project->getID()) {
182 $b .= _('Request a personal repository');
185 $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).');
188 $b .= sprintf (_('<a href="%s">Request a personal repository</a>.'),
189 util_make_url ('/plugins/scmgit/index.php?func=request-personal-repo&group_id='.$project->getID()));
199 function getSnapshotPara($project) {
202 $filename = $project->getUnixName().'-scm-latest.tar'.util_get_compressed_file_extension();
203 if (file_exists(forge_get_config('scm_snapshots_path').'/'.$filename)) {
205 $b .= util_make_link("/snapshots.php?group_id=".$project->getID(),
206 _('Download the nightly snapshot')
213 function printBrowserPage($params) {
214 $project = $this->checkParams($params);
219 if ($project->usesPlugin($this->name)) {
220 if ($params['user_id']) {
221 $user = user_get_object($params['user_id']);
222 echo $project->getUnixName().'/users/'.$user->getUnixName();
223 print '<iframe src="'.util_make_url("/plugins/scmgit/cgi-bin/gitweb.cgi?p=".$project->getUnixName().'/users/'.$user->getUnixName().'.git').'" frameborder="0" width=100% height=700></iframe>' ;
224 } else if ($this->browserDisplayable($project)) {
225 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>' ;
230 function getBrowserLinkBlock($project) {
232 $b = $HTML->boxMiddle(_('Git Repository Browser'));
234 $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.');
237 $b .= util_make_link ("/scm/browser.php?group_id=".$project->getID(),
238 _('Browse Git Repository')
244 function getStatsBlock ($project) {
248 $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',
249 array ($project->getID()));
251 if (db_numrows($result) > 0) {
252 // $b .= $HTML->boxMiddle(_('Repository Statistics'));
254 $tableHeaders = array(
259 $b .= $HTML->listTableTop($tableHeaders, false, '', 'repo-history');
262 $total = array('adds' => 0, 'commits' => 0);
264 while($data = db_fetch_array($result)) {
265 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
266 $b .= '<td class="halfwidth">' ;
267 $b .= util_make_link_u ($data['user_name'], $data['user_id'], $data['realname']) ;
268 $b .= '</td><td class="onequarterwidth align-right">'.$data['adds']. '</td>'.
269 '<td class="onequarterwidth align-right">'.$data['commits'].'</td></tr>';
270 $total['adds'] += $data['adds'];
271 $total['commits'] += $data['commits'];
274 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
275 $b .= '<td class="halfwidth"><strong>'._('Total').':</strong></td>'.
276 '<td class="onequarterwidth align-right"><strong>'.$total['adds']. '</strong></td>'.
277 '<td class="onequarterwidth align-right"><strong>'.$total['commits'].'</strong></td>';
279 $b .= $HTML->listTableBottom();
285 function createOrUpdateRepo($params) {
286 $project = $this->checkParams($params);
291 if (! $project->usesPlugin($this->name)) {
295 $project_name = $project->getUnixName();
296 $root = forge_get_config('repos_path', 'scmgit') . '/' . $project_name;
297 if (!is_dir($root)) {
298 system("mkdir -p $root");
302 $main_repo = $root . '/' . $project_name . '.git' ;
303 if (!is_file("$main_repo/HEAD") && !is_dir("$main_repo/objects") && !is_dir("$main_repo/refs")) {
304 exec("GIT_DIR=\"$main_repo\" git init --bare --shared=group", $result) ;
305 $output .= join("<br />", $result);
307 exec("GIT_DIR=\"$main_repo\" git update-server-info", $result) ;
308 $output .= join("<br />", $result);
309 if (is_file("$main_repo/hooks/post-update.sample")) {
310 rename("$main_repo/hooks/post-update.sample",
311 "$main_repo/hooks/post-update");
313 if (!is_file("$main_repo/hooks/post-update")) {
314 $f = fopen("$main_repo/hooks/post-update");
315 fwrite($f, "exec git-update-server-info\n");
318 if (is_file ("$main_repo/hooks/post-update")) {
319 system ("chmod +x $main_repo/hooks/post-update") ;
321 system ("echo \"Git repository for $project_name\" > $main_repo/description") ;
322 system ("find $main_repo -type d | xargs chmod g+s") ;
324 if (forge_get_config('use_ssh','scmgit')) {
325 $unix_group = 'scm_' . $project_name ;
326 system ("chgrp -R $unix_group $root") ;
327 system ("chmod g+s $root") ;
328 if ($project->enableAnonSCM()) {
329 system ("chmod g+wX,o+rX-w $root") ;
330 system ("chmod -R g+wX,o+rX-w $main_repo") ;
332 system ("chmod g+wX,o-rwx $root") ;
333 system ("chmod -R g+wX,o-rwx $main_repo") ;
336 $unix_user = forge_get_config('apache_user');
337 $unix_group = forge_get_config('apache_group');
338 system ("chown -R $unix_user:$unix_group $main_repo") ;
339 system ("chmod -R g-rwx,o-rwx $main_repo") ;
342 $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',
343 array ($project->getID(),
345 $rows = db_numrows ($result) ;
346 for ($i=0; $i<$rows; $i++) {
347 system ("mkdir -p $root/users") ;
348 $user_name = db_result($result,$i,'user_name');
349 $repodir = $root . '/users/' . $user_name . '.git' ;
351 if (!is_file ("$repodir/HEAD") && !is_dir("$repodir/objects") && !is_dir("$repodir/refs")) {
352 system ("git clone --bare $main_repo $repodir") ;
353 system ("GIT_DIR=\"$repodir\" git update-server-info") ;
354 if (is_file ("$repodir/hooks/post-update.sample")) {
355 rename ("$repodir/hooks/post-update.sample",
356 "$repodir/hooks/post-update") ;
358 if (!is_file ("$repodir/hooks/post-update")) {
359 $f = fopen ("$repodir/hooks/post-update") ;
360 fwrite ($f, "exec git-update-server-info\n") ;
363 if (is_file ("$repodir/hooks/post-update")) {
364 system ("chmod +x $repodir/hooks/post-update") ;
366 system("echo \"Git repository for user $user_name in project $project_name\" > $repodir/description");
367 system ("chown -R $user_name:$unix_group $repodir") ;
370 if (is_dir ("$root/users")) {
371 if ($project->enableAnonSCM()) {
372 system ("chmod -R g+rX-w,o+rX-w $root/users") ;
374 system ("chmod -R g+rX-w,o-rwx $root/users") ;
377 $params['output'] = $output;
380 function updateRepositoryList($params) {
381 $groups = $this->getGroups();
383 foreach ($groups as $project) {
384 if ($this->browserDisplayable($project)) {
389 $config_dir = forge_get_config('config_path').'/plugins/scmgit';
390 if (!is_dir($config_dir)) {
391 mkdir($config_dir, 0755, true);
393 $fname = $config_dir . '/gitweb.conf' ;
394 $config_f = fopen($fname.'.new', 'w') ;
395 $rootdir = forge_get_config('repos_path', 'scmgit');
396 fwrite($config_f, "\$projectroot = '$rootdir';\n");
397 fwrite($config_f, "\$projects_list = '$config_dir/gitweb.list';\n");
398 fwrite($config_f, "@git_base_url_list = ('". util_make_url('/anonscm/git') . "');\n");
399 fwrite($config_f, "\$logo = '". util_make_url('/plugins/scmgit/git-logo.png') . "';\n");
400 fwrite($config_f, "\$favicon = '". util_make_url('/plugins/scmgit/git-favicon.png')."';\n");
401 fwrite($config_f, "\$stylesheet = '". util_make_url('/plugins/scmgit/gitweb.css')."';\n");
402 fwrite($config_f, "\$prevent_xss = 'true';\n");
404 chmod ($fname.'.new', 0644) ;
405 rename ($fname.'.new', $fname) ;
407 $fname = $config_dir . '/gitweb.list' ;
409 $f = fopen ($fname.'.new', 'w');
410 foreach ($list as $project) {
411 $repos = $this->getRepositories($rootdir . "/" . $project->getUnixName());
412 foreach ($repos as $repo) {
413 $reldir = substr($repo, strlen($rootdir) + 1);
414 fwrite($f, $reldir . "\n");
418 chmod($fname.'.new', 0644);
419 rename($fname.'.new', $fname);
422 function getRepositories($path) {
423 if (! is_dir($path)) {
427 $entries = scandir($path);
428 foreach ($entries as $entry) {
429 $fullname = $path . "/" . $entry;
430 if (($entry == ".") or ($entry == ".."))
432 if (is_dir($fullname)) {
433 if (is_link($fullname))
435 $result = $this->getRepositories($fullname);
436 $list = array_merge($list, $result);
437 } else if ($entry == "HEAD") {
444 function gatherStats ($params) {
445 $project = $this->checkParams ($params) ;
450 if (! $project->usesPlugin ($this->name)) {
454 if ($params['mode'] == 'day') {
455 $year = $params ['year'] ;
456 $month = $params ['month'] ;
457 $day = $params ['day'] ;
458 $month_string = sprintf( "%04d%02d", $year, $month );
459 $start_time = gmmktime( 0, 0, 0, $month, $day, $year);
460 $end_time = $start_time + 86400;
462 $usr_adds = array () ;
463 $usr_updates = array () ;
464 $usr_deletes = array () ;
469 $repo = forge_get_config('repos_path', 'scmgit') . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git';
470 if (!is_dir ($repo) || !is_dir ("$repo/refs")) {
471 // echo "No repository\n" ;
475 $pipe = popen ("GIT_DIR=\"$repo\" git log --since=@$start_time --until=@$end_time --all --pretty='format:%n%an <%ae>' --name-status 2>/dev/null", 'r' ) ;
479 // cleaning stats_cvs_* table for the current day
480 $res = db_query_params ('DELETE FROM stats_cvs_group WHERE month=$1 AND day=$2 AND group_id=$3',
481 array ($month_string,
483 $project->getID())) ;
485 echo "Error while cleaning stats_cvs_group\n" ;
491 while (!feof($pipe) && $data = fgets ($pipe)) {
493 if (strlen($line) > 0) {
494 $result = preg_match("/^(?P<name>.+) <(?P<mail>.+)>/", $line, $matches);
497 $last_user = $matches['name'];
498 $user2email[$last_user] = strtolower($matches['mail']);
500 // Short-commit stats line
501 preg_match("/^(?P<mode>[AM])\s+(?P<file>.+)$/", $line, $matches);
502 if ($last_user == "") continue;
503 if ($matches['mode'] == 'A') {
504 $usr_adds[$last_user]++;
506 } elseif ($matches['mode'] == 'M') {
507 $usr_updates[$last_user]++;
509 } elseif ($matches['mode'] == 'D') {
510 $usr_deletes[$last_user]++;
516 // inserting group results in stats_cvs_groups
517 if ($updates > 0 || $adds > 0) {
518 if (!db_query_params ('INSERT INTO stats_cvs_group (month,day,group_id,checkouts,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
519 array ($month_string,
525 echo "Error while inserting into stats_cvs_group\n" ;
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 user name or email
536 $u = &user_get_object_by_name ($user) ;
538 $user_id = $u->getID();
540 $res=db_query_params('SELECT user_id FROM users WHERE lower(realname)=$1 OR email=$2',
541 array (strtolower($user), $user2email[$user]));
542 if ($res && db_numrows($res) > 0) {
543 $user_id = db_result($res,0,'user_id');
549 $uu = $usr_updates[$user] ? $usr_updates[$user] : 0 ;
550 $ua = $usr_adds[$user] ? $usr_adds[$user] : 0 ;
551 if ($uu > 0 || $ua > 0) {
552 if (!db_query_params ('INSERT INTO stats_cvs_user (month,day,group_id,user_id,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
553 array ($month_string,
559 echo "Error while inserting into stats_cvs_user\n" ;
569 function generateSnapshots ($params) {
571 $project = $this->checkParams ($params) ;
576 $group_name = $project->getUnixName() ;
578 $snapshot = forge_get_config('scm_snapshots_path').'/'.$group_name.'-scm-latest.tar'.util_get_compressed_file_extension();
579 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar'.util_get_compressed_file_extension();
581 if (! $project->usesPlugin ($this->name)) {
585 if (! $project->enableAnonSCM()) {
586 if (is_file($snapshot)) {
589 if (is_file($tarball)) {
595 // TODO: ideally we generate one snapshot per git repository
596 $toprepo = forge_get_config('repos_path', 'scmgit') ;
597 $repo = $toprepo . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git' ;
599 if (!is_dir ($repo)) {
600 if (is_file($snapshot)) {
603 if (is_file($tarball)) {
609 // Skip empty repo (no HEAD present in repository)
610 $ref = trim(`GIT_DIR=$repo git symbolic-ref HEAD`);
611 if (!file_exists($repo.'/'.$ref)) {
615 $tmp = trim (`mktemp -d`) ;
619 $today = date ('Y-m-d') ;
620 system ("GIT_DIR=\"$repo\" git archive --format=tar --prefix=$group_name-scm-$today/ HEAD |".forge_get_config('compression_method')." > $tmp/snapshot");
621 chmod ("$tmp/snapshot", 0644) ;
622 copy ("$tmp/snapshot", $snapshot) ;
623 unlink ("$tmp/snapshot") ;
625 system ("tar cCf $toprepo - ".$project->getUnixName() ."|".forge_get_config('compression_method')."> $tmp/tarball") ;
626 chmod ("$tmp/tarball", 0644) ;
627 copy ("$tmp/tarball", $tarball) ;
628 unlink ("$tmp/tarball") ;
629 system ("rm -rf $tmp") ;
633 * widgets - 'widgets' hook handler
634 * @param array $params
637 function widgets($params) {
638 require_once('common/widget/WidgetLayoutManager.class.php');
639 if ($params['owner_type'] == WidgetLayoutManager::OWNER_TYPE_GROUP) {
640 $params['fusionforge_widgets'][] = 'plugin_scmgit_project_latestcommits';
642 if ($params['owner_type'] == WidgetLayoutManager::OWNER_TYPE_USER) {
643 $params['fusionforge_widgets'][] = 'plugin_scmgit_user_myrepositories';
649 * Process the 'widget_instance' hook to create instances of the widgets
650 * @param array $params
652 function myPageBox($params) {
654 $user = UserManager::instance()->getCurrentUser();
655 require_once('common/widget/WidgetLayoutManager.class.php');
656 if ($params['widget'] == 'plugin_scmgit_user_myrepositories') {
657 require_once $gfplugins.$this->name.'/common/scmgit_Widget_MyRepositories.class.php';
658 $params['instance'] = new scmgit_Widget_MyRepositories(WidgetLayoutManager::OWNER_TYPE_USER, $user->getId());
662 function weekly(&$params) {
663 $res = db_query_params('SELECT group_id FROM groups WHERE status=$1 AND use_scm=1 ORDER BY group_id DESC',
666 $params['output'] .= 'ScmGit Plugin: Unable to get list of projects using SCM: '.db_error();
670 $params['output'] .= 'ScmGit Plugin: Running "git gc --quiet" on '.db_numrows($res).' repositories.'."\n";
671 while ($row = db_fetch_array($res)) {
672 $project = group_get_object($row['group_id']);
673 if (!$project || !is_object($project)) {
675 } elseif ($project->isError()) {
678 if (!$project->usesPlugin($this->name)) {
682 $project_name = $project->getUnixName();
683 $repo = forge_get_config('repos_path', 'scmgit') . '/' . $project_name . '/' . $project_name .'.git';
686 $params['output'] .= $project_name.': '.`git gc --quiet 2>&1`;
694 // c-file-style: "bsd"