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('scm_root', '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('scm_root', '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('scm_root', '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) {
209 $project = $this->checkParams($params);
214 if ($project->usesPlugin($this->name)) {
215 if ($this->browserDisplayable($project)) {
216 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>' ;
221 function getBrowserLinkBlock($project) {
223 $b = $HTML->boxMiddle(_('Git Repository Browser'));
225 $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.');
228 $b .= util_make_link ("/scm/browser.php?group_id=".$project->getID(),
229 _('Browse Git Repository')
235 function getStatsBlock ($project) {
239 $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',
240 array ($project->getID()));
242 if (db_numrows($result) > 0) {
243 // $b .= $HTML->boxMiddle(_('Repository Statistics'));
245 $tableHeaders = array(
250 $b .= $HTML->listTableTop($tableHeaders, false, '', 'repo-history');
253 $total = array('adds' => 0, 'commits' => 0);
255 while($data = db_fetch_array($result)) {
256 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
257 $b .= '<td class="halfwidth">' ;
258 $b .= util_make_link_u ($data['user_name'], $data['user_id'], $data['realname']) ;
259 $b .= '</td><td class="onequarterwidth align-right">'.$data['adds']. '</td>'.
260 '<td class="onequarterwidth align-right">'.$data['commits'].'</td></tr>';
261 $total['adds'] += $data['adds'];
262 $total['commits'] += $data['commits'];
265 $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
266 $b .= '<td class="halfwidth"><strong>'._('Total').':</strong></td>'.
267 '<td class="onequarterwidth align-right"><strong>'.$total['adds']. '</strong></td>'.
268 '<td class="onequarterwidth align-right"><strong>'.$total['commits'].'</strong></td>';
270 $b .= $HTML->listTableBottom();
276 function createOrUpdateRepo($params) {
277 $project = $this->checkParams($params);
282 if (! $project->usesPlugin ($this->name)) {
286 $project_name = $project->getUnixName();
287 $root = forge_get_config('repos_path', 'scmgit') . '/' . $project_name ;
288 system ("mkdir -p $root");
291 $main_repo = $root . '/' . $project_name . '.git' ;
292 if (!is_file ("$main_repo/HEAD") && !is_dir("$main_repo/objects") && !is_dir("$main_repo/refs")) {
293 exec ("GIT_DIR=\"$main_repo\" git init --bare --shared=group", $result) ;
294 $output .= join("<br />", $result);
296 exec ("GIT_DIR=\"$main_repo\" git update-server-info", $result) ;
297 $output .= join("<br />", $result);
298 if (is_file ("$main_repo/hooks/post-update.sample")) {
299 rename ("$main_repo/hooks/post-update.sample",
300 "$main_repo/hooks/post-update") ;
302 if (!is_file ("$main_repo/hooks/post-update")) {
303 $f = fopen ("$main_repo/hooks/post-update") ;
304 fwrite ($f, "exec git-update-server-info\n") ;
307 if (is_file ("$main_repo/hooks/post-update")) {
308 system ("chmod +x $main_repo/hooks/post-update") ;
310 system ("echo \"Git repository for $project_name\" > $main_repo/description") ;
311 system ("find $main_repo -type d | xargs chmod g+s") ;
313 if (forge_get_config('use_ssh','scmgit')) {
314 $unix_group = 'scm_' . $project_name ;
315 system ("chgrp -R $unix_group $root") ;
316 system ("chmod g+s $root") ;
317 if ($project->enableAnonSCM()) {
318 system ("chmod g+wX,o+rX-w $root") ;
319 system ("chmod -R g+wX,o+rX-w $main_repo") ;
321 system ("chmod g+wX,o-rwx $root") ;
322 system ("chmod -R g+wX,o-rwx $main_repo") ;
325 $unix_user = forge_get_config('apache_user');
326 $unix_group = forge_get_config('apache_group');
327 system ("chown -R $unix_user:$unix_group $main_repo") ;
328 system ("chmod -R g-rwx,o-rwx $main_repo") ;
331 $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',
332 array ($project->getID(),
334 $rows = db_numrows ($result) ;
335 for ($i=0; $i<$rows; $i++) {
336 system ("mkdir -p $root/users") ;
337 $user_name = db_result($result,$i,'user_name');
338 $repodir = $root . '/users/' . $user_name . '.git' ;
340 if (!is_file ("$repodir/HEAD") && !is_dir("$repodir/objects") && !is_dir("$repodir/refs")) {
341 system ("git clone --bare $main_repo $repodir") ;
342 system ("GIT_DIR=\"$repodir\" git update-server-info") ;
343 if (is_file ("$repodir/hooks/post-update.sample")) {
344 rename ("$repodir/hooks/post-update.sample",
345 "$repodir/hooks/post-update") ;
347 if (!is_file ("$repodir/hooks/post-update")) {
348 $f = fopen ("$repodir/hooks/post-update") ;
349 fwrite ($f, "exec git-update-server-info\n") ;
352 if (is_file ("$repodir/hooks/post-update")) {
353 system ("chmod +x $repodir/hooks/post-update") ;
355 system("echo \"Git repository for user $user_name in project $project_name\" > $repodir/description");
356 system ("chown -R $user_name:$unix_group $repodir") ;
359 if (is_dir ("$root/users")) {
360 if ($project->enableAnonSCM()) {
361 system ("chmod -R g+rX-w,o+rX-w $root/users") ;
363 system ("chmod -R g+rX-w,o-rwx $root/users") ;
366 $params['output'] = $output;
369 function updateRepositoryList($params) {
370 $groups = $this->getGroups();
372 foreach ($groups as $project) {
373 if ($this->browserDisplayable($project)) {
378 $config_dir = forge_get_config('config_path').'/plugins/scmgit';
379 $fname = $config_dir . '/gitweb.conf' ;
380 $config_f = fopen($fname.'.new', 'w') ;
381 $rootdir = forge_get_config('repos_path', 'scmgit');
382 fwrite($config_f, "\$projectroot = '$rootdir';\n");
383 fwrite($config_f, "\$projects_list = '$config_dir/gitweb.list';\n");
384 fwrite($config_f, "@git_base_url_list = ('". util_make_url('/anonscm/git') . "');\n");
385 fwrite($config_f, "\$logo = '". util_make_url('/plugins/scmgit/git-logo.png') . "';\n");
386 fwrite($config_f, "\$favicon = '". util_make_url('/plugins/scmgit/git-favicon.png')."';\n");
387 fwrite($config_f, "\$stylesheet = '". util_make_url('/plugins/scmgit/gitweb.css')."';\n");
388 fwrite($config_f, "\$prevent_xss = 'true';\n");
390 chmod ($fname.'.new', 0644) ;
391 rename ($fname.'.new', $fname) ;
393 $fname = $config_dir . '/gitweb.list' ;
395 $f = fopen ($fname.'.new', 'w');
396 foreach ($list as $project) {
397 $repos = $this->getRepositories($rootdir . "/" . $project->getUnixName());
398 foreach ($repos as $repo) {
399 $reldir = substr($repo, strlen($rootdir) + 1);
400 fwrite($f, $reldir . "\n");
404 chmod($fname.'.new', 0644);
405 rename($fname.'.new', $fname);
408 function getRepositories($path) {
409 if (! is_dir($path)) {
413 $entries = scandir($path);
414 foreach ($entries as $entry) {
415 $fullname = $path . "/" . $entry;
416 if (($entry == ".") or ($entry == ".."))
418 if (is_dir($fullname)) {
419 if (is_link($fullname))
421 $result = $this->getRepositories($fullname);
422 $list = array_merge($list, $result);
423 } else if ($entry == "HEAD") {
430 function gatherStats ($params) {
431 global $last_user, $usr_adds, $usr_deletes,
432 $usr_updates, $updates, $adds;
434 $project = $this->checkParams ($params) ;
439 if (! $project->usesPlugin ($this->name)) {
443 if ($params['mode'] == 'day') {
446 $year = $params ['year'] ;
447 $month = $params ['month'] ;
448 $day = $params ['day'] ;
449 $month_string = sprintf( "%04d%02d", $year, $month );
450 $start_time = gmmktime( 0, 0, 0, $month, $day, $year);
451 $end_time = $start_time + 86400;
453 $usr_adds = array () ;
454 $usr_updates = array () ;
455 $usr_deletes = array () ;
460 $repo = forge_get_config('repos_path', 'scmgit') . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git';
461 if (!is_dir ($repo) || !is_dir ("$repo/refs")) {
462 // echo "No repository\n" ;
467 $pipe = popen ("GIT_DIR=\"$repo\" git log --since=@$start_time --until=@$end_time --all --pretty='format:%n%an <%ae>' --name-status", 'r' ) ;
469 // cleaning stats_cvs_* table for the current day
470 $res = db_query_params ('DELETE FROM stats_cvs_group WHERE month=$1 AND day=$2 AND group_id=$3',
471 array ($month_string,
473 $project->getID())) ;
475 echo "Error while cleaning stats_cvs_group\n" ;
481 while (!feof($pipe) && $data = fgets ($pipe)) {
483 if (strlen($line) > 0) {
484 $result = preg_match("/^(?P<name>.+) <(?P<mail>.+)>/", $line, $matches);
487 $last_user = $matches['name'];
488 $user2email[$last_user] = strtolower($matches['mail']);
490 // Short-commit stats line
491 preg_match("/^(?P<mode>[AM])\s+(?P<file>.+)$/", $line, $matches);
492 if ($last_user == "") continue;
493 if ($matches['mode'] == 'A') {
494 $usr_adds[$last_user]++;
496 } elseif ($matches['mode'] == 'M') {
497 $usr_updates[$last_user]++;
499 } elseif ($matches['mode'] == 'D') {
500 $usr_deletes[$last_user]++;
506 // inserting group results in stats_cvs_groups
507 if ($updates > 0 || $adds > 0) {
508 if (!db_query_params ('INSERT INTO stats_cvs_group (month,day,group_id,checkouts,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
509 array ($month_string,
515 echo "Error while inserting into stats_cvs_group\n" ;
521 // building the user list
522 $user_list = array_unique( array_merge( array_keys( $usr_adds ), array_keys( $usr_updates ) ) );
524 foreach ( $user_list as $user ) {
525 // Trying to get user id from user name or email
526 $u = &user_get_object_by_name ($user) ;
528 $user_id = $u->getID();
530 $res=db_query_params('SELECT user_id FROM users WHERE lower(realname)=$1 OR email=$2',
531 array (strtolower($user), $user2email[$user]));
532 if ($res && db_numrows($res) > 0) {
533 $user_id = db_result($res,0,'user_id');
539 $uu = $usr_updates[$user] ? $usr_updates[$user] : 0 ;
540 $ua = $usr_adds[$user] ? $usr_adds[$user] : 0 ;
541 if ($uu > 0 || $ua > 0) {
542 if (!db_query_params ('INSERT INTO stats_cvs_user (month,day,group_id,user_id,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
543 array ($month_string,
549 echo "Error while inserting into stats_cvs_user\n" ;
559 function generateSnapshots ($params) {
561 $project = $this->checkParams ($params) ;
566 $group_name = $project->getUnixName() ;
568 $snapshot = forge_get_config('scm_snapshots_path').'/'.$group_name.'-scm-latest.tar'.util_get_compressed_file_extension();
569 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar'.util_get_compressed_file_extension();
571 if (! $project->usesPlugin ($this->name)) {
575 if (! $project->enableAnonSCM()) {
576 if (is_file($snapshot)) {
579 if (is_file($tarball)) {
585 // TODO: ideally we generate one snapshot per git repository
586 $toprepo = forge_get_config('repos_path', 'scmgit') ;
587 $repo = $toprepo . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git' ;
589 if (!is_dir ($repo)) {
590 if (is_file($snapshot)) {
593 if (is_file($tarball)) {
599 // Skip empty repo (no HEAD present in repository)
600 $ref = trim(`GIT_DIR=$repo git symbolic-ref HEAD`);
601 if (!file_exists($repo.'/'.$ref)) {
605 $tmp = trim (`mktemp -d`) ;
609 $today = date ('Y-m-d') ;
610 system ("GIT_DIR=\"$repo\" git archive --format=tar --prefix=$group_name-scm-$today/ HEAD |".forge_get_config('compression_method')." > $tmp/snapshot");
611 chmod ("$tmp/snapshot", 0644) ;
612 copy ("$tmp/snapshot", $snapshot) ;
613 unlink ("$tmp/snapshot") ;
615 system ("tar cCf $toprepo - ".$project->getUnixName() ."|".forge_get_config('compression_method')."> $tmp/tarball") ;
616 chmod ("$tmp/tarball", 0644) ;
617 copy ("$tmp/tarball", $tarball) ;
618 unlink ("$tmp/tarball") ;
619 system ("rm -rf $tmp") ;
623 * widgets - 'widgets' hook handler
624 * @param array $params
627 function widgets($params) {
628 require_once('common/widget/WidgetLayoutManager.class.php');
629 if ($params['owner_type'] == WidgetLayoutManager::OWNER_TYPE_GROUP) {
630 $params['fusionforge_widgets'][] = 'plugin_scmgit_project_latestcommits';
632 if ($params['owner_type'] == WidgetLayoutManager::OWNER_TYPE_USER) {
633 $params['fusionforge_widgets'][] = 'plugin_scmgit_user_myrepositories';
639 * Process the 'widget_instance' hook to create instances of the widgets
640 * @param array $params
642 function myPageBox($params) {
644 $user = UserManager::instance()->getCurrentUser();
645 require_once('common/widget/WidgetLayoutManager.class.php');
646 if ($params['widget'] == 'plugin_scmgit_user_myrepositories') {
647 require_once $gfplugins.$this->name.'/common/scmgit_Widget_MyRepositories.class.php';
648 $params['instance'] = new scmgit_Widget_MyRepositories(WidgetLayoutManager::OWNER_TYPE_USER, $user->getId());
655 // c-file-style: "bsd"