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).')<br />';
110 function getInstructionsForRW($project) {
112 if (session_loggedin()) {
113 $u = user_get_object(user_getid());
114 $d = $u->getUnixName();
115 if (forge_get_config('use_ssh', 'scmgit')) {
117 $b .= _('Developer Git Access via SSH');
120 $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.');
122 $b .= '<p><tt>git clone git+ssh://'.$d.'@' . $this->getBoxForProject($project) . '/'. forge_get_config('repos_path', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
123 } elseif (forge_get_config('use_dav', 'scmgit')) {
124 $protocol = forge_get_config('use_ssl', 'scmgit')? 'https' : 'http';
126 $b .= _('Developer Git Access via HTTP');
129 $b .= _('Only project developers can access the Git tree via this method. Enter your site password when prompted.');
131 $b .= '<p><tt>git clone '.$protocol.'://'.$d.'@' . $this->getBoxForProject($project) . '/'. forge_get_config('scm_root', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
133 $b = '<p class="warning">'._('Missing configuration for access in scmgit.ini : use_ssh and use_dav disabled').'</p>';
136 if (forge_get_config('use_ssh', 'scmgit')) {
138 $b .= _('Developer Git Access via SSH');
141 $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.');
143 $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>' ;
144 } elseif (forge_get_config('use_dav', 'scmgit')) {
145 $protocol = forge_get_config('use_ssl', 'scmgit')? 'https' : 'http';
147 $b .= _('Developer Git Access via HTTP');
150 $b .= _('Only project developers can access the Git tree via this method. Enter your site password when prompted.');
152 $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>' ;
156 if (session_loggedin()) {
157 $u =& user_get_object(user_getid()) ;
158 if ($u->getUnixStatus() == 'A') {
159 $result = db_query_params('SELECT * FROM plugin_scmgit_personal_repos p WHERE p.group_id=$1 AND p.user_id=$2',
160 array ($project->getID(),
162 if ($result && db_numrows ($result) > 0) {
164 $b .= _('Access to your personal repository');
167 $b .= _('You have a personal repository for this project, accessible through SSH with the following method. Enter your site password when prompted.');
169 $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>' ;
171 $glist = $u->getGroups();
172 foreach ($glist as $g) {
173 if ($g->getID() == $project->getID()) {
175 $b .= _('Request a personal repository');
178 $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).');
181 $b .= sprintf (_('<a href="%s">Request a personal repository</a>.'),
182 util_make_url ('/plugins/scmgit/index.php?func=request-personal-repo&group_id='.$project->getID()));
192 function getSnapshotPara($project) {
195 $filename = $project->getUnixName().'-scm-latest.tar'.util_get_compressed_file_extension();
196 if (file_exists(forge_get_config('scm_snapshots_path').'/'.$filename)) {
198 $b .= util_make_link("/snapshots.php?group_id=".$project->getID(),
199 _('Download the nightly snapshot')
206 function printBrowserPage($params) {
207 $project = $this->checkParams($params);
212 if ($project->usesPlugin($this->name)) {
213 if ($this->browserDisplayable($project)) {
214 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>' ;
219 function getBrowserLinkBlock($project) {
221 $b = $HTML->boxMiddle(_('Git Repository Browser'));
223 $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.');
226 $b .= util_make_link ("/scm/browser.php?group_id=".$project->getID(),
227 _('Browse Git Repository')
233 function getStatsBlock ($project) {
237 $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',
238 array ($project->getID()));
240 if (db_numrows($result) > 0) {
241 // $b .= $HTML->boxMiddle(_('Repository Statistics'));
243 $tableHeaders = array(
248 $b .= $HTML->listTableTop($tableHeaders, false, '', 'repo-history');
251 $total = array('adds' => 0, 'commits' => 0);
253 while($data = db_fetch_array($result)) {
254 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
255 $b .= '<td class="halfwidth">' ;
256 $b .= util_make_link_u ($data['user_name'], $data['user_id'], $data['realname']) ;
257 $b .= '</td><td class="onequarterwidth align-right">'.$data['adds']. '</td>'.
258 '<td class="onequarterwidth align-right">'.$data['commits'].'</td></tr>';
259 $total['adds'] += $data['adds'];
260 $total['commits'] += $data['commits'];
263 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
264 $b .= '<td class="halfwidth"><strong>'._('Total').':</strong></td>'.
265 '<td class="onequarterwidth align-right"><strong>'.$total['adds']. '</strong></td>'.
266 '<td class="onequarterwidth align-right"><strong>'.$total['commits'].'</strong></td>';
268 $b .= $HTML->listTableBottom();
274 function createOrUpdateRepo($params) {
275 $project = $this->checkParams($params);
280 if (! $project->usesPlugin ($this->name)) {
284 $project_name = $project->getUnixName();
285 $root = forge_get_config('repos_path', 'scmgit') . '/' . $project_name ;
286 system ("mkdir -p $root");
289 $main_repo = $root . '/' . $project_name . '.git' ;
290 if (!is_file ("$main_repo/HEAD") && !is_dir("$main_repo/objects") && !is_dir("$main_repo/refs")) {
291 exec ("GIT_DIR=\"$main_repo\" git init --bare --shared=group", $result) ;
292 $output .= join("<br />", $result);
294 exec ("GIT_DIR=\"$main_repo\" git update-server-info", $result) ;
295 $output .= join("<br />", $result);
296 if (is_file ("$main_repo/hooks/post-update.sample")) {
297 rename ("$main_repo/hooks/post-update.sample",
298 "$main_repo/hooks/post-update") ;
300 if (!is_file ("$main_repo/hooks/post-update")) {
301 $f = fopen ("$main_repo/hooks/post-update") ;
302 fwrite ($f, "exec git-update-server-info\n") ;
305 if (is_file ("$main_repo/hooks/post-update")) {
306 system ("chmod +x $main_repo/hooks/post-update") ;
308 system ("echo \"Git repository for $project_name\" > $main_repo/description") ;
309 system ("find $main_repo -type d | xargs chmod g+s") ;
311 if (forge_get_config('use_ssh','scmgit')) {
312 $unix_group = 'scm_' . $project_name ;
313 system ("chgrp -R $unix_group $root") ;
314 system ("chmod g+s $root") ;
315 if ($project->enableAnonSCM()) {
316 system ("chmod g+wX,o+rX-w $root") ;
317 system ("chmod -R g+wX,o+rX-w $main_repo") ;
319 system ("chmod g+wX,o-rwx $root") ;
320 system ("chmod -R g+wX,o-rwx $main_repo") ;
323 $unix_user = forge_get_config('apache_user');
324 $unix_group = forge_get_config('apache_group');
325 system ("chown -R $unix_user:$unix_group $main_repo") ;
326 system ("chmod -R g-rwx,o-rwx $main_repo") ;
329 $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',
330 array ($project->getID(),
332 $rows = db_numrows ($result) ;
333 for ($i=0; $i<$rows; $i++) {
334 system ("mkdir -p $root/users") ;
335 $user_name = db_result($result,$i,'user_name');
336 $repodir = $root . '/users/' . $user_name . '.git' ;
338 if (!is_file ("$repodir/HEAD") && !is_dir("$repodir/objects") && !is_dir("$repodir/refs")) {
339 system ("git clone --bare $main_repo $repodir") ;
340 system ("GIT_DIR=\"$repodir\" git update-server-info") ;
341 if (is_file ("$repodir/hooks/post-update.sample")) {
342 rename ("$repodir/hooks/post-update.sample",
343 "$repodir/hooks/post-update") ;
345 if (!is_file ("$repodir/hooks/post-update")) {
346 $f = fopen ("$repodir/hooks/post-update") ;
347 fwrite ($f, "exec git-update-server-info\n") ;
350 if (is_file ("$repodir/hooks/post-update")) {
351 system ("chmod +x $repodir/hooks/post-update") ;
353 system("echo \"Git repository for user $user_name in project $project_name\" > $repodir/description");
354 system ("chown -R $user_name:$unix_group $repodir") ;
357 if (is_dir ("$root/users")) {
358 if ($project->enableAnonSCM()) {
359 system ("chmod -R g+rX-w,o+rX-w $root/users") ;
361 system ("chmod -R g+rX-w,o-rwx $root/users") ;
364 $params['output'] = $output;
367 function updateRepositoryList($params) {
368 $groups = $this->getGroups();
370 foreach ($groups as $project) {
371 if ($this->browserDisplayable($project)) {
376 $config_dir = forge_get_config('config_path').'/plugins/scmgit';
377 if (!is_dir($config_dir)) {
378 mkdir($config_dir, 0755, true);
380 $fname = $config_dir . '/gitweb.conf' ;
381 $config_f = fopen($fname.'.new', 'w') ;
382 $rootdir = forge_get_config('repos_path', 'scmgit');
383 fwrite($config_f, "\$projectroot = '$rootdir';\n");
384 fwrite($config_f, "\$projects_list = '$config_dir/gitweb.list';\n");
385 fwrite($config_f, "@git_base_url_list = ('". util_make_url('/anonscm/git') . "');\n");
386 fwrite($config_f, "\$logo = '". util_make_url('/plugins/scmgit/git-logo.png') . "';\n");
387 fwrite($config_f, "\$favicon = '". util_make_url('/plugins/scmgit/git-favicon.png')."';\n");
388 fwrite($config_f, "\$stylesheet = '". util_make_url('/plugins/scmgit/gitweb.css')."';\n");
389 fwrite($config_f, "\$prevent_xss = 'true';\n");
391 chmod ($fname.'.new', 0644) ;
392 rename ($fname.'.new', $fname) ;
394 $fname = $config_dir . '/gitweb.list' ;
396 $f = fopen ($fname.'.new', 'w');
397 foreach ($list as $project) {
398 $repos = $this->getRepositories($rootdir . "/" . $project->getUnixName());
399 foreach ($repos as $repo) {
400 $reldir = substr($repo, strlen($rootdir) + 1);
401 fwrite($f, $reldir . "\n");
405 chmod($fname.'.new', 0644);
406 rename($fname.'.new', $fname);
409 function getRepositories($path) {
410 if (! is_dir($path)) {
414 $entries = scandir($path);
415 foreach ($entries as $entry) {
416 $fullname = $path . "/" . $entry;
417 if (($entry == ".") or ($entry == ".."))
419 if (is_dir($fullname)) {
420 if (is_link($fullname))
422 $result = $this->getRepositories($fullname);
423 $list = array_merge($list, $result);
424 } else if ($entry == "HEAD") {
431 function gatherStats ($params) {
432 $project = $this->checkParams ($params) ;
437 if (! $project->usesPlugin ($this->name)) {
441 if ($params['mode'] == 'day') {
442 $year = $params ['year'] ;
443 $month = $params ['month'] ;
444 $day = $params ['day'] ;
445 $month_string = sprintf( "%04d%02d", $year, $month );
446 $start_time = gmmktime( 0, 0, 0, $month, $day, $year);
447 $end_time = $start_time + 86400;
449 $usr_adds = array () ;
450 $usr_updates = array () ;
451 $usr_deletes = array () ;
456 $repo = forge_get_config('repos_path', 'scmgit') . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git';
457 if (!is_dir ($repo) || !is_dir ("$repo/refs")) {
458 // echo "No repository\n" ;
462 $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' ) ;
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("/^(?P<name>.+) <(?P<mail>.+)>/", $line, $matches);
484 $last_user = $matches['name'];
485 $user2email[$last_user] = strtolower($matches['mail']);
487 // Short-commit stats line
488 preg_match("/^(?P<mode>[AM])\s+(?P<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 // Skip empty repo (no HEAD present in repository)
597 $ref = trim(`GIT_DIR=$repo git symbolic-ref HEAD`);
598 if (!file_exists($repo.'/'.$ref)) {
602 $tmp = trim (`mktemp -d`) ;
606 $today = date ('Y-m-d') ;
607 system ("GIT_DIR=\"$repo\" git archive --format=tar --prefix=$group_name-scm-$today/ HEAD |".forge_get_config('compression_method')." > $tmp/snapshot");
608 chmod ("$tmp/snapshot", 0644) ;
609 copy ("$tmp/snapshot", $snapshot) ;
610 unlink ("$tmp/snapshot") ;
612 system ("tar cCf $toprepo - ".$project->getUnixName() ."|".forge_get_config('compression_method')."> $tmp/tarball") ;
613 chmod ("$tmp/tarball", 0644) ;
614 copy ("$tmp/tarball", $tarball) ;
615 unlink ("$tmp/tarball") ;
616 system ("rm -rf $tmp") ;
620 * widgets - 'widgets' hook handler
621 * @param array $params
624 function widgets($params) {
625 require_once('common/widget/WidgetLayoutManager.class.php');
626 if ($params['owner_type'] == WidgetLayoutManager::OWNER_TYPE_GROUP) {
627 $params['fusionforge_widgets'][] = 'plugin_scmgit_project_latestcommits';
629 if ($params['owner_type'] == WidgetLayoutManager::OWNER_TYPE_USER) {
630 $params['fusionforge_widgets'][] = 'plugin_scmgit_user_myrepositories';
636 * Process the 'widget_instance' hook to create instances of the widgets
637 * @param array $params
639 function myPageBox($params) {
641 $user = UserManager::instance()->getCurrentUser();
642 require_once('common/widget/WidgetLayoutManager.class.php');
643 if ($params['widget'] == 'plugin_scmgit_user_myrepositories') {
644 require_once $gfplugins.$this->name.'/common/scmgit_Widget_MyRepositories.class.php';
645 $params['instance'] = new scmgit_Widget_MyRepositories(WidgetLayoutManager::OWNER_TYPE_USER, $user->getId());
652 // c-file-style: "bsd"