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->hooks[] = 'scm_browser_page';
33 $this->hooks[] = 'scm_update_repolist' ;
34 $this->hooks[] = 'scm_generate_snapshots' ;
35 $this->hooks[] = 'scm_gather_stats' ;
40 function getDefaultServer() {
41 return forge_get_config('default_server', 'scmgit') ;
44 function printShortStats ($params) {
45 $project = $this->checkParams ($params) ;
50 if ($project->usesPlugin($this->name)) {
51 $result = db_query_params('SELECT sum(commits) AS commits, sum(adds) AS adds FROM stats_cvs_group WHERE group_id=$1',
52 array ($project->getID())) ;
53 $commit_num = db_result($result,0,'commits');
54 $add_num = db_result($result,0,'adds');
61 echo ' (Git: '.sprintf(_('<strong>%1$s</strong> commits, <strong>%2$s</strong> adds'), number_format($commit_num, 0), number_format($add_num, 0)).")";
65 function getBlurb () {
66 return '<p>' . _('Documentation for Git is available at <a href="http://git-scm.com/">http://git-scm.com/</a>.') . '</p>';
69 function getInstructionsForAnon ($project) {
70 $b = '<h2>' . _('Anonymous Git Access') . '</h2>';
72 $b .= _('This project\'s Git repository can be checked out through anonymous access with the following command.');
76 $b .= '<tt>git clone '.util_make_url ('/anonscm/git/'.$project->getUnixName().'/'.$project->getUnixName().'.git').'</tt><br />';
79 $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',
80 array ($project->getID(),
82 $rows = db_numrows ($result) ;
86 $b .= _('Developer\'s repository');
89 $b .= ngettext ('One of this project\'s members also has a personal Git repository that can be checked out anonymously.',
90 'Some of this project\'s members also have personal Git repositories that can be checked out anonymously.',
94 for ($i=0; $i<$rows; $i++) {
95 $user_id = db_result($result,$i,'user_id');
96 $user_name = db_result($result,$i,'user_name');
97 $real_name = db_result($result,$i,'realname');
98 $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 />';
106 function getInstructionsForRW ($project) {
107 if (session_loggedin()) {
108 $u =& user_get_object(user_getid()) ;
109 $d = $u->getUnixName() ;
110 if (forge_get_config('use_ssh', 'scmgit')) {
112 $b .= _('Developer GIT Access via SSH');
115 $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.');
117 $b .= '<p><tt>git clone git+ssh://'.$d.'@' . $project->getSCMBox() . '/'. forge_get_config('repos_path', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
118 } elseif (forge_get_config('use_dav', 'scmgit')) {
119 $protocol = forge_get_config('use_ssl', 'scmgit')? 'https' : 'http';
121 $b .= _('Developer GIT Access via HTTP');
124 $b .= _('Only project developers can access the GIT tree via this method. Enter your site password when prompted.');
126 $b .= '<p><tt>git clone '.$protocol.'://'.$d.'@' . $project->getSCMBox() . '/'. forge_get_config('repos_path', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
129 if (forge_get_config('use_ssh', 'scmgit')) {
131 $b .= _('Developer GIT Access via SSH');
134 $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.');
136 $b .= '<p><tt>git clone git+ssh://<i>'._('developername').'</i>@' . $project->getSCMBox() . '/'. forge_get_config('repos_path', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
137 } elseif (forge_get_config('use_dav', 'scmgit')) {
138 $protocol = forge_get_config('use_ssl', 'scmgit')? 'https' : 'http';
140 $b .= _('Developer GIT Access via HTTP');
143 $b .= _('Only project developers can access the GIT tree via this method. Enter your site password when prompted.');
145 $b .= '<p><tt>git clone '.$protocol.'://<i>'._('developername').'</i>@' . $project->getSCMBox() . '/'. forge_get_config('repos_path', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
149 if (session_loggedin()) {
150 $u =& user_get_object(user_getid()) ;
151 if ($u->getUnixStatus() == 'A') {
152 $result = db_query_params ('SELECT * FROM plugin_scmgit_personal_repos p WHERE p.group_id=$1 AND p.user_id=$2',
153 array ($project->getID(),
155 if ($result && db_numrows ($result) > 0) {
157 $b .= _('Access to your personal repository');
160 $b .= _('You have a personal repository for this project, accessible through SSH with the following method. Enter your site password when prompted.');
162 $b .= '<p><tt>git clone git+ssh://'.$u->getUnixName().'@' . $project->getSCMBox() . forge_get_config('repos_path', 'scmgit') .'/'. $project->getUnixName() .'/users/'. $u->getUnixName() .'.git</tt></p>' ;
164 $glist = $u->getGroups();
165 foreach ($glist as $g) {
166 if ($g->getID() == $project->getID()) {
168 $b .= _('Request a personal repository');
171 $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).');
174 $b .= sprintf (_('<a href="%s">Request a personal repository</a>.'),
175 util_make_url ('/plugins/scmgit/index.php?func=request-personal-repo&group_id='.$project->getID()));
186 function getSnapshotPara ($project) {
189 $filename = $project->getUnixName().'-scm-latest.tar.gz';
190 if (file_exists(forge_get_config('scm_snapshots_path').'/'.$filename)) {
192 $b .= util_make_link ("/snapshots.php?group_id=".$project->getID(),
193 _('Download the nightly snapshot')
200 function printBrowserPage ($params) {
203 $project = $this->checkParams ($params) ;
208 if ($project->usesPlugin ($this->name)) {
209 if ($this->browserDisplayable ($project)) {
210 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>' ;
215 function getBrowserLinkBlock ($project) {
217 $b = $HTML->boxMiddle(_('Git Repository Browser'));
219 $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.');
222 $b .= util_make_link ("/scm/browser.php?group_id=".$project->getID(),
223 _('Browse Git Repository')
229 // function getStatsBlock ($project) {
233 // $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',
234 // array ($project->getID()));
236 // if (db_numrows($result) > 0) {
237 // $b .= $HTML->boxMiddle(_('Repository Statistics'));
239 // $tableHeaders = array(
244 // $b .= $HTML->listTableTop($tableHeaders);
247 // $total = array('adds' => 0, 'commits' => 0);
249 // while($data = db_fetch_array($result)) {
250 // $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
251 // $b .= '<td width="50%">' ;
252 // $b .= util_make_link_u ($data['user_name'], $data['user_id'], $data['realname']) ;
253 // $b .= '</td><td width="25%" align="right">'.$data['adds']. '</td>'.
254 // '<td width="25%" align="right">'.$data['commits'].'</td></tr>';
255 // $total['adds'] += $data['adds'];
256 // $total['commits'] += $data['commits'];
259 // $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
260 // $b .= '<td width="50%"><strong>'._('Total').':</strong></td>'.
261 // '<td width="25%" align="right"><strong>'.$total['adds']. '</strong></td>'.
262 // '<td width="25%" align="right"><strong>'.$total['commits'].'</strong></td>';
264 // $b .= $HTML->listTableBottom();
270 function getStatsBlock ($project) {
274 static function createUserRepo ($params) {
275 $project = $params['project'];
276 $project_name = $project->getUnixName();
277 $user_name = $params['user_name'];
278 $unix_group = $params['unix_group'];
279 $main_repo = $params['main_repo'];
280 $root = $params['root'];
282 $repodir = $root . '/users/' . $user_name . '.git' ;
283 chgrp($repodir, $unix_group);
284 if ($project->enableAnonSCM()) {
285 chmod ($repodir, 02755);
287 chmod ($repodir, 02750);
289 if (!is_file ("$repodir/HEAD") && !is_dir("$repodir/objects") && !is_dir("$repodir/refs")) {
290 system ("git clone --bare $main_repo $repodir") ;
291 system ("GIT_DIR=\"$repodir\" git update-server-info") ;
292 if (is_file ("$repodir/hooks/post-update.sample")) {
293 rename ("$repodir/hooks/post-update.sample",
294 "$repodir/hooks/post-update") ;
296 if (!is_file ("$repodir/hooks/post-update")) {
297 $f = fopen ("$repodir/hooks/post-update","x+") ;
298 fwrite ($f, "exec git-update-server-info\n") ;
301 if (is_file ("$repodir/hooks/post-update")) {
302 system ("chmod +x $repodir/hooks/post-update") ;
304 system("echo \"Git repository for user $user_name in project $project_name\" > $repodir/description");
308 function createOrUpdateRepo ($params) {
309 $project = $this->checkParams ($params) ;
314 if (! $project->usesPlugin ($this->name)) {
318 $project_name = $project->getUnixName() ;
319 $root = forge_get_config('repos_path', 'scmgit') . '/' . $project_name ;
320 $unix_group = 'scm_' . $project_name ;
321 system ("mkdir -p $root") ;
323 $main_repo = $root . '/' . $project_name . '.git' ;
324 if (!is_dir($main_repo) || (!is_file("$main_repo/HEAD") &&
325 !is_dir("$main_repo/objects") && !is_dir("$main_repo/refs"))) {
326 $tmp_repo = util_mkdtemp('.git', $project_name);
327 if ($tmp_repo == false) {
330 system ("GIT_DIR=\"$tmp_repo\" git init --bare --shared=group") ;
331 system ("GIT_DIR=\"$tmp_repo\" git update-server-info") ;
332 if (is_file ("$tmp_repo/hooks/post-update.sample")) {
333 rename ("$tmp_repo/hooks/post-update.sample",
334 "$tmp_repo/hooks/post-update") ;
336 if (!is_file ("$tmp_repo/hooks/post-update")) {
337 $f = fopen ("$tmp_repo/hooks/post-update") ;
338 fwrite ($f, "exec git-update-server-info\n") ;
341 if (is_file ("$tmp_repo/hooks/post-update")) {
342 system ("chmod +x $tmp_repo/hooks/post-update") ;
344 system ("echo \"Git repository for $project_name\" > $tmp_repo/description") ;
345 system ("find $tmp_repo -type d | xargs chmod g+s") ;
346 system ("chgrp -R $unix_group $tmp_repo") ;
347 system ("chmod -R g+wX,o+rX-w $tmp_repo") ;
348 if ($project->enableAnonSCM()) {
349 system ("chmod g+wX,o+rX-w $root") ;
351 system ("chmod g+wX,o-rwx $root") ;
352 system ("chmod g+wX,o-rwx $tmp_repo") ;
356 * $main_repo can already exist, for example if it’s
357 * not a directory or doesn’t contain a HEAD file or
358 * an objects or refs subdirectory… move it out of
359 * the way in these cases
361 system("if test -e $main_repo || test -h $main_repo; then d=\$(mktemp -d $main_repo.scmgit-moved.XXXXXXXXXX) && mv -f $main_repo \$d/; fi");
362 /* here’s still a TOCTOU but we check $ret below */
363 system("mv $tmp_repo $main_repo", $ret);
368 system ("chgrp $unix_group $root") ;
369 system ("chmod g+s $root") ;
370 if ($project->enableAnonSCM()) {
371 system ("chmod g+wX,o+rX-w $root") ;
372 system ("chmod g+wX,o+rX-w $main_repo") ;
374 system ("chmod g+wX,o-rwx $root") ;
375 system ("chmod g+wX,o-rwx $main_repo") ;
378 $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',
379 array ($project->getID(),
381 $rows = db_numrows ($result) ;
382 for ($i=0; $i<$rows; $i++) {
383 system ("mkdir -p $root/users") ;
384 $user_name = db_result($result,$i,'user_name');
385 $repodir = $root . '/users/' . $user_name . '.git' ;
387 if (!is_dir($repodir) && mkdir ($repodir, 0700)) {
388 chown ($repodir, $user_name) ;
391 $params['project'] = $project;
392 $params['user_name'] = $user_name;
393 $params['unix_group'] = $unix_group;
394 $params['root'] = $root;
395 $params['main_repo'] = $main_repo;
397 util_sudo_effective_user($user_name,
398 array("GitPlugin","createUserRepo"),
402 if (is_dir ("$root/users")) {
403 if ($project->enableAnonSCM()) {
404 system ("chmod g+rX-w,o+rX-w $root/users") ;
406 system ("chmod g+rX-w,o-rwx $root/users") ;
411 function updateRepositoryList ($params) {
412 $groups = $this->getGroups () ;
414 foreach ($groups as $project) {
415 if ($this->browserDisplayable ($project)) {
420 $config_dir = '/etc/gforge/plugins/scmgit' ;
421 $fname = $config_dir . '/gitweb.conf' ;
422 $config_f = fopen ($fname.'.new', 'w') ;
423 $rootdir = forge_get_config('repos_path', 'scmgit');
424 fwrite($config_f, "\$projectroot = '$rootdir';\n");
425 fwrite($config_f, "\$projects_list = '$config_dir/gitweb.list';\n");
426 fwrite($config_f, "@git_base_url_list = ('". util_make_url ('/anonscm/git') . "');\n");
427 fwrite($config_f, "\$logo = '". util_make_url ('/plugins/scmgit/git-logo.png') . "';\n");
428 fwrite($config_f, "\$favicon = '". util_make_url ('/plugins/scmgit/git-favicon.png')."';\n");
429 fwrite($config_f, "\$stylesheet = '". util_make_url ('/plugins/scmgit/gitweb.css')."';\n");
430 fwrite($config_f, "\$prevent_xss = 'true';\n");
432 chmod ($fname.'.new', 0644) ;
433 rename ($fname.'.new', $fname) ;
435 $fname = $config_dir . '/gitweb.list' ;
437 $f = fopen ($fname.'.new', 'w') ;
438 foreach ($list as $project) {
439 $repos = $this->getRepositories($rootdir . "/" . $project->getUnixName());
440 foreach ($repos as $repo) {
441 $reldir = substr($repo, strlen($rootdir) + 1);
442 fwrite ($f, $reldir . "\n");
446 chmod ($fname.'.new', 0644) ;
447 rename ($fname.'.new', $fname) ;
450 function getRepositories($path) {
454 $entries = scandir($path);
455 foreach ($entries as $entry) {
456 $fullname = $path . "/" . $entry;
457 if (($entry == ".") or ($entry == ".."))
459 if (is_dir($fullname)) {
460 if (is_link($fullname))
462 $result = $this->getRepositories($fullname);
463 $list = array_merge($list, $result);
464 } else if ($entry == "HEAD") {
471 function gatherStats ($params) {
472 global $last_user, $usr_adds, $usr_deletes,
473 $usr_updates, $updates, $adds;
475 $project = $this->checkParams ($params) ;
480 if (! $project->usesPlugin ($this->name)) {
484 if ($params['mode'] == 'day') {
487 $year = $params ['year'] ;
488 $month = $params ['month'] ;
489 $day = $params ['day'] ;
490 $month_string = sprintf( "%04d%02d", $year, $month );
491 $start_time = gmmktime( 0, 0, 0, $month, $day, $year);
492 $end_time = $start_time + 86400;
494 $usr_adds = array () ;
495 $usr_updates = array () ;
496 $usr_deletes = array () ;
501 $repo = forge_get_config('repos_path', 'scmgit') . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git';
502 if (!is_dir ($repo) || !is_dir ("$repo/refs")) {
503 // echo "No repository\n" ;
508 $pipe = popen ("GIT_DIR=\"$repo\" git log --since=@$start_time --until=@$end_time --all --pretty='format:%n%an <%ae>' --name-status", 'r' ) ;
510 // cleaning stats_cvs_* table for the current day
511 $res = db_query_params ('DELETE FROM stats_cvs_group WHERE month=$1 AND day=$2 AND group_id=$3',
512 array ($month_string,
514 $project->getID())) ;
516 echo "Error while cleaning stats_cvs_group\n" ;
522 while (!feof($pipe) && $data = fgets ($pipe)) {
524 if (strlen($line) > 0) {
525 $result = preg_match("/^(?<name>.+) <(?<mail>.+)>/", $line, $matches);
528 $last_user = $matches['name'];
530 // Short-commit stats line
531 preg_match("/^(?<mode>[AM])\s+(?<file>.+)$/", $line, $matches);
532 if ($last_user == "") continue;
533 if ($matches['mode'] == 'A') {
534 $usr_adds[$last_user]++;
536 } elseif ($matches['mode'] == 'M') {
537 $usr_updates[$last_user]++;
539 } elseif ($matches['mode'] == 'D') {
540 $usr_deletes[$last_user]++;
546 // inserting group results in stats_cvs_groups
547 if ($updates > 0 || $adds > 0) {
548 if (!db_query_params ('INSERT INTO stats_cvs_group (month,day,group_id,checkouts,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
549 array ($month_string,
555 echo "Error while inserting into stats_cvs_group\n" ;
561 // building the user list
562 $user_list = array_unique( array_merge( array_keys( $usr_adds ), array_keys( $usr_updates ) ) );
564 foreach ( $user_list as $user ) {
565 // trying to get user id from user name
566 $u = &user_get_object_by_name ($user) ;
568 $user_id = $u->getID();
573 $uu = $usr_updates[$user] ? $usr_updates[$user] : 0 ;
574 $ua = $usr_adds[$user] ? $usr_adds[$user] : 0 ;
575 if ($uu > 0 || $ua > 0) {
576 if (!db_query_params ('INSERT INTO stats_cvs_user (month,day,group_id,user_id,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
577 array ($month_string,
583 echo "Error while inserting into stats_cvs_user\n" ;
593 function generateSnapshots ($params) {
595 $project = $this->checkParams ($params) ;
600 $group_name = $project->getUnixName() ;
602 $snapshot = forge_get_config('scm_snapshots_path').'/'.$group_name.'-scm-latest.tar.gz';
603 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar.gz';
605 if (! $project->usesPlugin ($this->name)) {
609 if (! $project->enableAnonSCM()) {
610 if (is_file($snapshot)) {
613 if (is_file($tarball)) {
619 // TODO: ideally we generate one snapshot per git repository
620 $toprepo = forge_get_config('repos_path', 'scmgit') ;
621 $repo = $toprepo . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git' ;
623 if (!is_dir ($repo)) {
624 if (is_file($snapshot)) {
627 if (is_file($tarball)) {
633 $tmp = trim (`mktemp -d`) ;
637 $today = date ('Y-m-d') ;
638 system ("GIT_DIR=\"$repo\" git archive --format=tar --prefix=$group_name-scm-$today/ HEAD | gzip > $tmp/snapshot.tar.gz");
639 chmod ("$tmp/snapshot.tar.gz", 0644) ;
640 copy ("$tmp/snapshot.tar.gz", $snapshot) ;
641 unlink ("$tmp/snapshot.tar.gz") ;
643 system ("tar czCf $toprepo $tmp/tarball.tar.gz " . $project->getUnixName()) ;
644 chmod ("$tmp/tarball.tar.gz", 0644) ;
645 copy ("$tmp/tarball.tar.gz", $tarball) ;
646 unlink ("$tmp/tarball.tar.gz") ;
647 system ("rm -rf $tmp") ;
653 // c-file-style: "bsd"