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
20 * along with FusionForge; if not, write to the Free Software
21 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
25 forge_define_config_item ('default_server', 'scmgit', forge_get_config ('web_host')) ;
26 forge_define_config_item ('repos_path', 'scmgit', forge_get_config('chroot').'/scmrepos/git') ;
28 class GitPlugin extends SCMPlugin {
29 function GitPlugin () {
31 $this->name = 'scmgit';
33 $this->hooks[] = 'scm_browser_page';
34 $this->hooks[] = 'scm_update_repolist' ;
35 $this->hooks[] = 'scm_generate_snapshots' ;
36 $this->hooks[] = 'scm_gather_stats' ;
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)).")";
66 function getBlurb () {
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) {
108 if (session_loggedin()) {
109 $u =& user_get_object(user_getid()) ;
110 $d = $u->getUnixName() ;
111 if (forge_get_config('use_ssh', 'scmgit')) {
113 $b .= _('Developer GIT Access via SSH');
116 $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.');
118 $b .= '<p><tt>git clone git+ssh://'.$d.'@' . $project->getSCMBox() . '/'. forge_get_config('scm_root', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
119 } elseif (forge_get_config('use_dav', 'scmgit')) {
120 $protocol = forge_get_config('use_ssl', 'scmgit')? 'https' : 'http';
122 $b .= _('Developer GIT Access via HTTP');
125 $b .= _('Only project developers can access the GIT tree via this method. Enter your site password when prompted.');
127 $b .= '<p><tt>git clone '.$protocol.'://'.$d.'@' . $project->getSCMBox() . '/'. forge_get_config('scm_root', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
130 if (forge_get_config('use_ssh', 'scmgit')) {
132 $b .= _('Developer GIT Access via SSH');
135 $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.');
137 $b .= '<p><tt>git clone git+ssh://<i>'._('developername').'</i>@' . $project->getSCMBox() . '/'. forge_get_config('scm_root', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
138 } elseif (forge_get_config('use_dav', 'scmgit')) {
139 $protocol = forge_get_config('use_ssl', 'scmgit')? 'https' : 'http';
141 $b .= _('Developer GIT Access via HTTP');
144 $b .= _('Only project developers can access the GIT tree via this method. Enter your site password when prompted.');
146 $b .= '<p><tt>git clone '.$protocol.'://<i>'._('developername').'</i>@' . $project->getSCMBox() . '/'. forge_get_config('scm_root', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
150 if (session_loggedin()) {
151 $u =& user_get_object(user_getid()) ;
152 if ($u->getUnixStatus() == 'A') {
153 $result = db_query_params ('SELECT * FROM plugin_scmgit_personal_repos p WHERE p.group_id=$1 AND p.user_id=$2',
154 array ($project->getID(),
156 if ($result && db_numrows ($result) > 0) {
158 $b .= _('Access to your personal repository');
161 $b .= _('You have a personal repository for this project, accessible through SSH with the following method. Enter your site password when prompted.');
163 $b .= '<p><tt>git clone git+ssh://'.$u->getUnixName().'@' . $project->getSCMBox() . forge_get_config('scm_path', 'scmgit') .'/'. $project->getUnixName() .'/users/'. $u->getUnixName() .'.git</tt></p>' ;
165 $glist = $u->getGroups();
166 foreach ($glist as $g) {
167 if ($g->getID() == $project->getID()) {
169 $b .= _('Request a personal repository');
172 $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).');
175 $b .= sprintf (_('<a href="%s">Request a personal repository</a>.'),
176 util_make_url ('/plugins/scmgit/index.php?func=request-personal-repo&group_id='.$project->getID()));
187 function getSnapshotPara ($project) {
190 $filename = $project->getUnixName().'-scm-latest.tar.gz';
191 if (file_exists(forge_get_config('scm_snapshots_path').'/'.$filename)) {
193 $b .= util_make_link ("/snapshots.php?group_id=".$project->getID(),
194 _('Download the nightly snapshot')
201 function printBrowserPage ($params) {
204 $project = $this->checkParams ($params) ;
209 if ($project->usesPlugin ($this->name)) {
210 if ($this->browserDisplayable ($project)) {
211 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>' ;
216 function getBrowserLinkBlock ($project) {
218 $b = $HTML->boxMiddle(_('Git Repository Browser'));
220 $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.');
223 $b .= util_make_link ("/scm/browser.php?group_id=".$project->getID(),
224 _('Browse Git Repository')
230 // function getStatsBlock ($project) {
234 // $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',
235 // array ($project->getID()));
237 // if (db_numrows($result) > 0) {
238 // $b .= $HTML->boxMiddle(_('Repository Statistics'));
240 // $tableHeaders = array(
245 // $b .= $HTML->listTableTop($tableHeaders);
248 // $total = array('adds' => 0, 'commits' => 0);
250 // while($data = db_fetch_array($result)) {
251 // $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
252 // $b .= '<td width="50%">' ;
253 // $b .= util_make_link_u ($data['user_name'], $data['user_id'], $data['realname']) ;
254 // $b .= '</td><td width="25%" align="right">'.$data['adds']. '</td>'.
255 // '<td width="25%" align="right">'.$data['commits'].'</td></tr>';
256 // $total['adds'] += $data['adds'];
257 // $total['commits'] += $data['commits'];
260 // $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
261 // $b .= '<td width="50%"><strong>'._('Total').':</strong></td>'.
262 // '<td width="25%" align="right"><strong>'.$total['adds']. '</strong></td>'.
263 // '<td width="25%" align="right"><strong>'.$total['commits'].'</strong></td>';
265 // $b .= $HTML->listTableBottom();
271 function getStatsBlock ($project) {
275 function createOrUpdateRepo ($params) {
276 $project = $this->checkParams ($params) ;
281 if (! $project->usesPlugin ($this->name)) {
285 $project_name = $project->getUnixName() ;
286 $root = forge_get_config('repos_path', 'scmgit') . '/' . $project_name ;
287 $unix_group = 'scm_' . $project_name ;
288 system ("mkdir -p $root") ;
290 $main_repo = $root . '/' . $project_name . '.git' ;
291 if (!is_file ("$main_repo/HEAD") && !is_dir("$main_repo/objects") && !is_dir("$main_repo/refs")) {
292 system ("GIT_DIR=\"$main_repo\" git init --bare --shared=group") ;
293 system ("GIT_DIR=\"$main_repo\" git update-server-info") ;
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 system ("chgrp -R $unix_group $root") ;
310 system ("chmod g+s $root") ;
311 if ($project->enableAnonSCM()) {
312 system ("chmod g+wX,o+rX-w $root") ;
313 system ("chmod -R g+wX,o+rX-w $main_repo") ;
315 system ("chmod g+wX,o-rwx $root") ;
316 system ("chmod -R g+wX,o-rwx $main_repo") ;
319 $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',
320 array ($project->getID(),
322 $rows = db_numrows ($result) ;
323 for ($i=0; $i<$rows; $i++) {
324 system ("mkdir -p $root/users") ;
325 $user_name = db_result($result,$i,'user_name');
326 $repodir = $root . '/users/' . $user_name . '.git' ;
328 if (!is_file ("$repodir/HEAD") && !is_dir("$repodir/objects") && !is_dir("$repodir/refs")) {
329 system ("git clone --bare $main_repo $repodir") ;
330 system ("GIT_DIR=\"$repodir\" git update-server-info") ;
331 if (is_file ("$repodir/hooks/post-update.sample")) {
332 rename ("$repodir/hooks/post-update.sample",
333 "$repodir/hooks/post-update") ;
335 if (!is_file ("$repodir/hooks/post-update")) {
336 $f = fopen ("$repodir/hooks/post-update") ;
337 fwrite ($f, "exec git-update-server-info\n") ;
340 if (is_file ("$repodir/hooks/post-update")) {
341 system ("chmod +x $repodir/hooks/post-update") ;
343 system ("echo \"Git repository for user $owner in project $project_name\" > $repodir/description") ;
344 system ("chown -R $user_name:$unix_group $repodir") ;
347 if (is_dir ("$root/users")) {
348 if ($project->enableAnonSCM()) {
349 system ("chmod -R g+rX-w,o+rX-w $root/users") ;
351 system ("chmod -R g+rX-w,o-rwx $root/users") ;
356 function updateRepositoryList ($params) {
357 $groups = $this->getGroups () ;
359 foreach ($groups as $project) {
360 if ($this->browserDisplayable ($project)) {
365 $config_dir = '/etc/gforge/plugins/scmgit' ;
366 $fname = $config_dir . '/gitweb.conf' ;
367 $config_f = fopen ($fname.'.new', 'w') ;
368 $rootdir = forge_get_config('repos_path', 'scmgit');
369 fwrite($config_f, "\$projectroot = '$rootdir';\n");
370 fwrite($config_f, "\$projects_list = '$config_dir/gitweb.list';\n");
371 fwrite($config_f, "@git_base_url_list = ('". util_make_url ('/anonscm/git') . "');\n");
372 fwrite($config_f, "\$logo = '". util_make_url ('/plugins/scmgit/git-logo.png') . "';\n");
373 fwrite($config_f, "\$favicon = '". util_make_url ('/plugins/scmgit/git-favicon.png')."';\n");
374 fwrite($config_f, "\$stylesheet = '". util_make_url ('/plugins/scmgit/gitweb.css')."';\n");
375 fwrite($config_f, "\$prevent_xss = 'true';\n");
377 chmod ($fname.'.new', 0644) ;
378 rename ($fname.'.new', $fname) ;
380 $fname = $config_dir . '/gitweb.list' ;
382 $f = fopen ($fname.'.new', 'w') ;
383 foreach ($list as $project) {
384 $repos = $this->getRepositories($rootdir . "/" . $project->getUnixName());
385 foreach ($repos as $repo) {
386 $reldir = substr($repo, strlen($rootdir) + 1);
387 fwrite ($f, $reldir . "\n");
391 chmod ($fname.'.new', 0644) ;
392 rename ($fname.'.new', $fname) ;
395 function getRepositories($path) {
399 $entries = scandir($path);
400 foreach ($entries as $entry) {
401 $fullname = $path . "/" . $entry;
402 if (($entry == ".") or ($entry == ".."))
404 if (is_dir($fullname)) {
405 if (is_link($fullname))
407 $result = $this->getRepositories($fullname);
408 $list = array_merge($list, $result);
409 } else if ($entry == "HEAD") {
416 function gatherStats ($params) {
417 global $last_user, $usr_adds, $usr_deletes,
418 $usr_updates, $updates, $adds;
420 $project = $this->checkParams ($params) ;
425 if (! $project->usesPlugin ($this->name)) {
429 if ($params['mode'] == 'day') {
432 $year = $params ['year'] ;
433 $month = $params ['month'] ;
434 $day = $params ['day'] ;
435 $month_string = sprintf( "%04d%02d", $year, $month );
436 $start_time = gmmktime( 0, 0, 0, $month, $day, $year);
437 $end_time = $start_time + 86400;
439 $usr_adds = array () ;
440 $usr_updates = array () ;
441 $usr_deletes = array () ;
446 $repo = forge_get_config('repos_path', 'scmgit') . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git';
447 if (!is_dir ($repo) || !is_dir ("$repo/refs")) {
448 // echo "No repository\n" ;
453 $pipe = popen ("GIT_DIR=\"$repo\" git log --since=@$start_time --until=@$end_time --all --pretty='format:%n%an <%ae>' --name-status", 'r' ) ;
455 // cleaning stats_cvs_* table for the current day
456 $res = db_query_params ('DELETE FROM stats_cvs_group WHERE month=$1 AND day=$2 AND group_id=$3',
457 array ($month_string,
459 $project->getID())) ;
461 echo "Error while cleaning stats_cvs_group\n" ;
467 while (!feof($pipe) && $data = fgets ($pipe)) {
469 if (strlen($line) > 0) {
470 $result = preg_match("/^(?<name>.+) <(?<mail>.+)>/", $line, $matches);
473 $last_user = $matches['name'];
475 // Short-commit stats line
476 preg_match("/^(?<mode>[AM])\s+(?<file>.+)$/", $line, $matches);
477 if ($last_user == "") continue;
478 if ($matches['mode'] == 'A') {
479 $usr_adds[$last_user]++;
481 } elseif ($matches['mode'] == 'M') {
482 $usr_updates[$last_user]++;
484 } elseif ($matches['mode'] == 'D') {
485 $usr_deletes[$last_user]++;
491 // inserting group results in stats_cvs_groups
492 if ($updates > 0 || $adds > 0) {
493 if (!db_query_params ('INSERT INTO stats_cvs_group (month,day,group_id,checkouts,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
494 array ($month_string,
500 echo "Error while inserting into stats_cvs_group\n" ;
506 // building the user list
507 $user_list = array_unique( array_merge( array_keys( $usr_adds ), array_keys( $usr_updates ) ) );
509 foreach ( $user_list as $user ) {
510 // trying to get user id from user name
511 $u = &user_get_object_by_name ($user) ;
513 $user_id = $u->getID();
518 $uu = $usr_updates[$user] ? $usr_updates[$user] : 0 ;
519 $ua = $usr_adds[$user] ? $usr_adds[$user] : 0 ;
520 if ($uu > 0 || $ua > 0) {
521 if (!db_query_params ('INSERT INTO stats_cvs_user (month,day,group_id,user_id,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
522 array ($month_string,
528 echo "Error while inserting into stats_cvs_user\n" ;
538 function generateSnapshots ($params) {
540 $project = $this->checkParams ($params) ;
545 $group_name = $project->getUnixName() ;
547 $snapshot = forge_get_config('scm_snapshots_path').'/'.$group_name.'-scm-latest.tar.gz';
548 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar.gz';
550 if (! $project->usesPlugin ($this->name)) {
554 if (! $project->enableAnonSCM()) {
555 if (is_file($snapshot)) {
558 if (is_file($tarball)) {
564 // TODO: ideally we generate one snapshot per git repository
565 $toprepo = forge_get_config('repos_path', 'scmgit') ;
566 $repo = $toprepo . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git' ;
568 if (!is_dir ($repo)) {
569 if (is_file($snapshot)) {
572 if (is_file($tarball)) {
578 $tmp = trim (`mktemp -d`) ;
582 $today = date ('Y-m-d') ;
583 system ("GIT_DIR=\"$repo\" git archive --format=tar --prefix=$group_name-scm-$today/ HEAD | gzip > $tmp/snapshot.tar.gz");
584 chmod ("$tmp/snapshot.tar.gz", 0644) ;
585 copy ("$tmp/snapshot.tar.gz", $snapshot) ;
586 unlink ("$tmp/snapshot.tar.gz") ;
588 system ("tar czCf $toprepo $tmp/tarball.tar.gz " . $project->getUnixName()) ;
589 chmod ("$tmp/tarball.tar.gz", 0644) ;
590 copy ("$tmp/tarball.tar.gz", $tarball) ;
591 unlink ("$tmp/tarball.tar.gz") ;
592 system ("rm -rf $tmp") ;
598 // c-file-style: "bsd"