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 () {
32 $this->name = 'scmgit';
34 $this->hooks[] = 'scm_update_repolist' ;
35 $this->hooks[] = 'scm_browser_page' ;
36 $this->hooks[] = 'scm_gather_stats' ;
37 $this->hooks[] = 'scm_generate_snapshots' ;
42 function getDefaultServer() {
43 return forge_get_config('default_server', 'scmgit') ;
46 function printShortStats ($params) {
47 $project = $this->checkParams ($params) ;
52 if ($project->usesPlugin($this->name)) {
53 $result = db_query_params('SELECT sum(commits) AS commits, sum(adds) AS adds FROM stats_cvs_group WHERE group_id=$1',
54 array ($project->getID())) ;
55 $commit_num = db_result($result,0,'commits');
56 $add_num = db_result($result,0,'adds');
63 echo ' (Git: '.sprintf(_('<strong>%1$s</strong> commits, <strong>%2$s</strong> adds'), number_format($commit_num, 0), number_format($add_num, 0)).")";
67 function getBlurb () {
68 return _('<p>Documentation for Git is available <a href="http://git-scm.com/">here</a>.</p>') ;
71 function getInstructionsForAnon ($project) {
72 $b = _('<p><b>Anonymous Git Access</b></p><p>This project\'s Git repository can be checked out through anonymous access with the following command.</p>');
74 $b .= '<tt>git clone '.util_make_url ('/anonscm/git/'.$project->getUnixName().'/'.$project->getUnixName().'.git').'</tt><br />';
77 $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',
78 array ($project->getID(),
80 $rows = db_numrows ($result) ;
83 $b .= ngettext ('<p><b>Developer\'s repository</b></p><p>One of this project\'s members also has a personal Git repository that can be checked out anonymously.</p>',
84 '<p><b>Developers\' repositories</b></p><p>Some of this project\'s members also have personal Git repositories that can be checked out anonymously.</p>',
87 for ($i=0; $i<$rows; $i++) {
88 $user_id = db_result($result,$i,'user_id');
89 $user_name = db_result($result,$i,'user_name');
90 $real_name = db_result($result,$i,'realname');
91 $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 />';
99 function getInstructionsForRW ($project) {
100 if (session_loggedin()) {
101 $u =& user_get_object(user_getid()) ;
102 $d = $u->getUnixName() ;
103 $b = _('<p><b>Developer GIT Access via SSH</b></p><p>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.</p>');
104 $b .= '<p><tt>git clone git+ssh://'.$d.'@' . $project->getSCMBox() . forge_get_config('repos_path', 'scmgit') .'/'. $project->getUnixName() .'/'. $project->getUnixName() .'.git</tt></p>' ;
106 $b = _('<p><b>Developer GIT Access via SSH</b></p><p>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.</p>');
107 $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>' ;
110 if (session_loggedin()) {
111 $u =& user_get_object(user_getid()) ;
112 if ($u->getUnixStatus() == 'A') {
113 $result = db_query_params ('SELECT * FROM plugin_scmgit_personal_repos p WHERE p.group_id=$1 AND p.user_id=$2',
114 array ($project->getID(),
116 if ($result && db_numrows ($result) > 0) {
117 $b .= _('<p><b>Access to your personal repository</b></p><p>You have a personal repository for this project, accessible through SSH with the following method. Enter your site password when prompted.</p>');
118 $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>' ;
120 $glist = $u->getGroups();
121 foreach ($glist as $g) {
122 if ($g->getID() == $project->getID()) {
123 $b .= sprintf (_('<p><b>Request a personal repository</b></p><p>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).</p><p><a href="%s">Request a personal repository</a>.</p>'),
124 util_make_url ('/plugins/scmgit/index.php?func=request-personal-repo&group_id='.$project->getID()));
134 function getSnapshotPara ($project) {
137 $filename = $project->getUnixName().'-scm-latest.tar.gz';
138 if (file_exists(forge_get_config('scm_snapshots_path').'/'.$filename)) {
140 $b .= util_make_link ("/snapshots.php?group_id=".$project->getID(),
141 _('Download the nightly snapshot')
148 function printBrowserPage ($params) {
151 $project = $this->checkParams ($params) ;
156 if ($project->usesPlugin ($this->name)) {
157 if ($this->browserDisplayable ($project)) {
158 print '<iframe src="'.util_make_url ("/plugins/scmgit/cgi-bin/gitweb.cgi?p=".$project->getUnixName().'/'.$project->getUnixName().'.git').'" frameborder="no" width=100% height=700></iframe>' ;
163 function getBrowserLinkBlock ($project) {
165 $b = $HTML->boxMiddle(_('Git Repository Browser'));
166 $b .= _('<p>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.</p>');
168 $b .= util_make_link ("/scm/browser.php?group_id=".$project->getID(),
169 _('Browse Git Repository')
175 // function getStatsBlock ($project) {
179 // $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',
180 // array ($project->getID()));
182 // if (db_numrows($result) > 0) {
183 // $b .= $HTML->boxMiddle(_('Repository Statistics'));
185 // $tableHeaders = array(
190 // $b .= $HTML->listTableTop($tableHeaders);
193 // $total = array('adds' => 0, 'commits' => 0);
195 // while($data = db_fetch_array($result)) {
196 // $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
197 // $b .= '<td width="50%">' ;
198 // $b .= util_make_link_u ($data['user_name'], $data['user_id'], $data['realname']) ;
199 // $b .= '</td><td width="25%" align="right">'.$data['adds']. '</td>'.
200 // '<td width="25%" align="right">'.$data['commits'].'</td></tr>';
201 // $total['adds'] += $data['adds'];
202 // $total['commits'] += $data['commits'];
205 // $b .= '<tr '. $HTML->boxGetAltRowStyle($i) .'>';
206 // $b .= '<td width="50%"><strong>'._('Total').':</strong></td>'.
207 // '<td width="25%" align="right"><strong>'.$total['adds']. '</strong></td>'.
208 // '<td width="25%" align="right"><strong>'.$total['commits'].'</strong></td>';
210 // $b .= $HTML->listTableBottom();
215 function getStatsBlock ($project) {
219 function createOrUpdateRepo ($params) {
220 $project = $this->checkParams ($params) ;
225 if (! $project->usesPlugin ($this->name)) {
229 $project_name = $project->getUnixName() ;
230 $root = forge_get_config('repos_path', 'scmgit') . '/' . $project_name ;
231 $unix_group = 'scm_' . $project_name ;
232 system ("mkdir -p $root") ;
234 $main_repo = $root . '/' . $project_name . '.git' ;
235 if (!is_file ("$main_repo/HEAD") && !is_dir("$main_repo/objects") && !is_dir("$main_repo/refs")) {
236 system ("GIT_DIR=\"$main_repo\" git init --bare --shared=group") ;
237 system ("GIT_DIR=\"$main_repo\" git update-server-info") ;
238 if (is_file ("$main_repo/hooks/post-update.sample")) {
239 rename ("$main_repo/hooks/post-update.sample",
240 "$main_repo/hooks/post-update") ;
242 if (!is_file ("$main_repo/hooks/post-update")) {
243 $f = fopen ("$main_repo/hooks/post-update") ;
244 fwrite ($f, "exec git-update-server-info\n") ;
247 if (is_file ("$main_repo/hooks/post-update")) {
248 system ("chmod +x $main_repo/hooks/post-update") ;
250 system ("echo \"Git repository for $project_name\" > $main_repo/description") ;
251 system ("find $main_repo -type d | xargs chmod g+s") ;
253 system ("chgrp -R $unix_group $root") ;
254 system ("chmod g+s $root") ;
255 if ($project->enableAnonSCM()) {
256 system ("chmod g+wX,o+rX-w $root") ;
257 system ("chmod -R g+wX,o+rX-w $main_repo") ;
259 system ("chmod g+wX,o-rwx $root") ;
260 system ("chmod -R g+wX,o-rwx $main_repo") ;
263 $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',
264 array ($project->getID(),
266 $rows = db_numrows ($result) ;
267 for ($i=0; $i<$rows; $i++) {
268 system ("mkdir -p $root/users") ;
269 $user_name = db_result($result,$i,'user_name');
270 $repodir = $root . '/users/' . $user_name . '.git' ;
272 if (!is_file ("$repodir/HEAD") && !is_dir("$repodir/objects") && !is_dir("$repodir/refs")) {
273 system ("git clone --bare $main_repo $repodir") ;
274 system ("GIT_DIR=\"$repodir\" git update-server-info") ;
275 if (is_file ("$repodir/hooks/post-update.sample")) {
276 rename ("$repodir/hooks/post-update.sample",
277 "$repodir/hooks/post-update") ;
279 if (!is_file ("$repodir/hooks/post-update")) {
280 $f = fopen ("$repodir/hooks/post-update") ;
281 fwrite ($f, "exec git-update-server-info\n") ;
284 if (is_file ("$repodir/hooks/post-update")) {
285 system ("chmod +x $repodir/hooks/post-update") ;
287 system ("echo \"Git repository for user $owner in project $project_name\" > $repodir/description") ;
288 system ("chown -R $user_name:$unix_group $repodir") ;
291 if (is_dir ("$root/users")) {
292 if ($project->enableAnonSCM()) {
293 system ("chmod -R g+rX-w,o+rX-w $root/users") ;
295 system ("chmod -R g+rX-w,o-rwx $root/users") ;
300 function updateRepositoryList ($params) {
301 $groups = $this->getGroups () ;
303 foreach ($groups as $project) {
304 if ($this->browserDisplayable ($project)) {
309 $config_dir = '/etc/gforge/plugins/scmgit' ;
310 $fname = $config_dir . '/gitweb.conf' ;
311 $config_f = fopen ($fname.'.new', 'w') ;
312 $rootdir = forge_get_config('repos_path', 'scmgit');
313 fwrite($config_f, "\$projectroot = '$rootdir';\n");
314 fwrite($config_f, "\$projects_list = '$config_dir/gitweb.list';\n");
315 fwrite($config_f, "@git_base_url_list = ('". util_make_url ('/anonscm/git') . "');\n");
316 fwrite($config_f, "\$logo = '". util_make_url ('/plugins/scmgit/git-logo.png') . "';\n");
317 fwrite($config_f, "\$favicon = '". util_make_url ('/plugins/scmgit/git-favicon.png')."';\n");
318 fwrite($config_f, "\$stylesheet = '". util_make_url ('/plugins/scmgit/gitweb.css')."';\n");
319 fwrite($config_f, "\$prevent_xss = 'true';\n");
321 chmod ($fname.'.new', 0644) ;
322 rename ($fname.'.new', $fname) ;
324 $fname = $config_dir . '/gitweb.list' ;
326 $f = fopen ($fname.'.new', 'w') ;
327 foreach ($list as $project) {
328 $repos = $this->getRepositories($rootdir . "/" . $project->getUnixName());
329 foreach ($repos as $repo) {
330 $reldir = substr($repo, strlen($rootdir) + 1);
331 fwrite ($f, $reldir . "\n");
335 chmod ($fname.'.new', 0644) ;
336 rename ($fname.'.new', $fname) ;
339 function getRepositories($path) {
343 $entries = scandir($path);
344 foreach ($entries as $entry) {
345 $fullname = $path . "/" . $entry;
346 if (($entry == ".") or ($entry == ".."))
348 if (is_dir($fullname)) {
349 if (is_link($fullname))
351 $result = $this->getRepositories($fullname);
352 $list = array_merge($list, $result);
353 } else if ($entry == "HEAD") {
360 function generateSnapshots ($params) {
363 $project = $this->checkParams ($params) ;
368 $group_name = $project->getUnixName() ;
370 $snapshot = forge_get_config('scm_snapshots_path').'/'.$group_name.'-scm-latest.tar.gz';
371 $tarball = forge_get_config('scm_tarballs_path').'/'.$group_name.'-scmroot.tar.gz';
373 if (! $project->usesPlugin ($this->name)) {
377 if (! $project->enableAnonSCM()) {
382 // TODO: ideally we generate one snapshot per git repository
383 $toprepo = forge_get_config('repos_path', 'scmgit') ;
384 $repo = $toprepo . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git' ;
386 if (!is_dir ($repo)) {
391 $today = date ('Y-m-d') ;
392 $tmp = trim (`mktemp -d`) ;
397 system ("GIT_DIR=\"$repo\" git archive --format=tar --prefix=$group_name-scm-$today/ HEAD | gzip > $tmp/snapshot.tar.gz");
398 chmod ("$tmp/snapshot.tar.gz", 0644) ;
399 copy ("$tmp/snapshot.tar.gz", $snapshot) ;
400 unlink ("$tmp/snapshot.tar.gz") ;
402 system ("tar czCf $toprepo $tmp/tarball.tar.gz " . $project->getUnixName()) ;
403 chmod ("$tmp/tarball.tar.gz", 0644) ;
404 copy ("$tmp/tarball.tar.gz", $tarball) ;
405 unlink ("$tmp/tarball.tar.gz") ;
406 system ("rm -rf $tmp") ;
409 function gatherStats ($params) {
410 global $last_user, $usr_adds, $usr_deletes,
411 $usr_updates, $updates, $adds;
413 $project = $this->checkParams ($params) ;
418 if (! $project->usesPlugin ($this->name)) {
422 if ($params['mode'] == 'day') {
425 $year = $params ['year'] ;
426 $month = $params ['month'] ;
427 $day = $params ['day'] ;
428 $month_string = sprintf( "%04d%02d", $year, $month );
429 $start_time = gmmktime( 0, 0, 0, $month, $day, $year);
430 $end_time = $start_time + 86400;
432 $usr_adds = array () ;
433 $usr_updates = array () ;
434 $usr_deletes = array () ;
439 $repo = forge_get_config('repos_path', 'scmgit') . '/' . $project->getUnixName() . '/' . $project->getUnixName() . '.git';
440 if (!is_dir ($repo) || !is_dir ("$repo/refs")) {
441 // echo "No repository\n" ;
446 $pipe = popen ("GIT_DIR=\"$repo\" git log --since=@$start_time --until=@$end_time --all --pretty='format:%n%an <%ae>' --name-status", 'r' ) ;
448 // cleaning stats_cvs_* table for the current day
449 $res = db_query_params ('DELETE FROM stats_cvs_group WHERE month=$1 AND day=$2 AND group_id=$3',
450 array ($month_string,
452 $project->getID())) ;
454 echo "Error while cleaning stats_cvs_group\n" ;
460 while (!feof($pipe) && $data = fgets ($pipe)) {
462 if (strlen($line) > 0) {
463 $result = preg_match("/^(?<name>.+) <(?<mail>.+)>/", $line, $matches);
466 $last_user = $matches['name'];
468 // Short-commit stats line
469 preg_match("/^(?<mode>[AM])\s+(?<file>.+)$/", $line, $matches);
470 if ($last_user == "") continue;
471 if ($matches['mode'] == 'A') {
472 $usr_adds[$last_user]++;
474 } elseif ($matches['mode'] == 'M') {
475 $usr_updates[$last_user]++;
477 } elseif ($matches['mode'] == 'D') {
478 $usr_deletes[$last_user]++;
484 // inserting group results in stats_cvs_groups
485 if ($updates > 0 || $adds > 0) {
486 if (!db_query_params ('INSERT INTO stats_cvs_group (month,day,group_id,checkouts,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
487 array ($month_string,
493 echo "Error while inserting into stats_cvs_group\n" ;
499 // building the user list
500 $user_list = array_unique( array_merge( array_keys( $usr_adds ), array_keys( $usr_updates ) ) );
502 foreach ( $user_list as $user ) {
503 // trying to get user id from user name
504 $u = &user_get_object_by_name ($user) ;
506 $user_id = $u->getID();
511 $uu = $usr_updates[$user] ? $usr_updates[$user] : 0 ;
512 $ua = $usr_adds[$user] ? $usr_adds[$user] : 0 ;
513 if ($uu > 0 || $ua > 0) {
514 if (!db_query_params ('INSERT INTO stats_cvs_user (month,day,group_id,user_id,commits,adds) VALUES ($1,$2,$3,$4,$5,$6)',
515 array ($month_string,
521 echo "Error while inserting into stats_cvs_user\n" ;
534 // c-file-style: "bsd"