├── .htaccess ├── README.md ├── config.php.sample └── index.php /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | RewriteCond %{REQUEST_URI} !index\.php$ 4 | RewriteRule .* index.php [L,QSA] 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | PHP Git server 3 | ============== 4 | 5 | This is an implementation of the simple Git HTTP protocol in PHP. 6 | It allows you to serve Git repositories from a simple PHP-enabled server 7 | in situations where the `git` command is not available. 8 | 9 | Git is _not_ required to be installed on the server. There is no dependency 10 | on the `git` command, thus it can be installed on a server where `git` 11 | is not available. 12 | 13 | It serves most files directly from the files system, and also generates 14 | the files `/info/refs` and `/objects/info/packs`, without the need for 15 | executing `git update-server-info` on the server. 16 | -------------------------------------------------------------------------------- /config.php.sample: -------------------------------------------------------------------------------- 1 | 9 | by Jon Lund Steffensen, July 2011. Licenced under GPL2. */ 10 | 11 | function str_endswith($s, $test) { 12 | $strlen = strlen($s); 13 | $testlen = strlen($test); 14 | if ($testlen > $strlen) return FALSE; 15 | return substr_compare($s, $test, -$testlen) === 0; 16 | } 17 | 18 | function header_nocache() { 19 | header('Expires: Fri, 01 Jan 1980 00:00:00 GMT'); 20 | header('Pragma: no-cache'); 21 | header('Cache-Control: no-cache, max-age=0, must-revalidate'); 22 | } 23 | 24 | function header_cache_forever() { 25 | header('Expires: '.date('r', time() + 31536000)); 26 | header('Cache-Control: public, max-age=31536000'); 27 | } 28 | 29 | function send_local_file($type, $path) { 30 | $f = @fopen($path, 'rb'); 31 | if (!$f) { 32 | header('Status: 404 Not Found'); 33 | die(); 34 | } 35 | 36 | $stat = fstat($f); 37 | header('Content-Type: '.$type); 38 | header('Last-Modified: '.date('r', $stat['mtime'])); 39 | 40 | fpassthru($f); 41 | fclose($f); 42 | } 43 | 44 | function get_text_file($git_path, $name) { 45 | header_nocache(); 46 | send_local_file('text/plain', $git_path.$name); 47 | } 48 | 49 | function get_loose_object($git_path, $name) { 50 | header_cache_forever(); 51 | send_local_file('application/x-git-loose-object', $git_path.$name); 52 | } 53 | 54 | function get_pack_file($git_path, $name) { 55 | header_cache_forever(); 56 | send_local_file('application/x-git-packed-objects', $git_path.$name); 57 | } 58 | 59 | function get_idx_file($git_path, $name) { 60 | header_cache_forever(); 61 | send_local_file('application/x-git-packed-objects-toc', $git_path.$name); 62 | } 63 | 64 | 65 | function ref_entry_cmp($a, $b) { 66 | return strcmp($a[0], $b[0]); 67 | } 68 | 69 | function read_packed_refs($f) { 70 | $list = array(); 71 | 72 | while (($line = fgets($f)) !== FALSE) { 73 | if (preg_match('~^([0-9a-f]{40})\s(\S+)~', $line, $matches)) { 74 | $list[] = array($matches[2], $matches[1]); 75 | } 76 | } 77 | 78 | usort($list, 'ref_entry_cmp'); 79 | return $list; 80 | } 81 | 82 | function get_packed_refs($git_path) { 83 | $packed_refs_path = $git_path.'/packed-refs'; 84 | $f = @fopen($packed_refs_path, 'r'); 85 | 86 | $list = array(); 87 | 88 | if ($f) { 89 | $list = read_packed_refs($f); 90 | fclose($f); 91 | } 92 | 93 | return $list; 94 | } 95 | 96 | function resolve_ref($git_path, $ref) { 97 | $depth = 5; 98 | 99 | while (TRUE) { 100 | $depth -= 1; 101 | if ($depth < 0) { 102 | return array(NULL, '0000000000000000000000000000000000000000'); 103 | } 104 | 105 | $path = $git_path.'/'.$ref; 106 | if (!@lstat($path)) { 107 | foreach (get_packed_refs($git_path) as $pref) { 108 | if (!strcmp($pref[0], $ref)) { 109 | return array($ref, $pref[1]); 110 | } 111 | } 112 | return array(NULL, '0000000000000000000000000000000000000000'); 113 | } 114 | 115 | if (is_link($path)) { 116 | $dest = readlink($path); 117 | if (strlen($dest) >= 5 && !strcmp('refs/', substr($dest, 0, 5))) { 118 | $ref = $dest; 119 | continue; 120 | } 121 | } 122 | 123 | if (is_dir($path)) { 124 | return array(NULL, '0000000000000000000000000000000000000000'); 125 | } 126 | 127 | $buffer = file_get_contents($path); 128 | if (!preg_match('~ref:\s*(.*)~', $buffer, $matches)) { 129 | if (strlen($buffer) < 40) { 130 | return array(NULL, '0000000000000000000000000000000000000000'); 131 | } 132 | 133 | return array($ref, substr($buffer, 0, 40)); 134 | } 135 | 136 | $ref = $matches[1]; 137 | } 138 | } 139 | 140 | function get_ref_dir($git_path, $base, $list=array()) { 141 | $path = $git_path.'/'.$base; 142 | $dir = dir($path); 143 | 144 | while (($entry = $dir->read()) !== FALSE) { 145 | if ($entry[0] == '.') continue; 146 | if (strlen($entry) > 255) continue; 147 | if (str_endswith($entry, '.lock')) continue; 148 | 149 | $entry_path = $path.'/'.$entry; 150 | 151 | if (is_dir($entry_path)) { 152 | $list = get_ref_dir($git_path, $base.'/'.$entry, $list); 153 | } else { 154 | $r = resolve_ref($git_path, $base.'/'.$entry); 155 | $list[] = array($base.'/'.$entry, $r[1]); 156 | } 157 | } 158 | 159 | usort($list, 'ref_entry_cmp'); 160 | return $list; 161 | } 162 | 163 | function get_loose_refs($git_path) { 164 | return get_ref_dir($git_path, 'refs'); 165 | } 166 | 167 | function get_refs($git_path) { 168 | $list = array_merge(get_loose_refs($git_path), get_packed_refs($git_path)); 169 | usort($list, 'ref_entry_cmp'); 170 | return $list; 171 | } 172 | 173 | function get_info_refs($git_path, $name) { 174 | header_nocache(); 175 | header('Content-Type: text/plain'); 176 | 177 | /* TODO Are dereferenced tags needed in this 178 | list, or just a convenience? */ 179 | 180 | foreach (get_refs($git_path) as $ref) { 181 | echo $ref[1]."\t".$ref[0]."\n"; 182 | } 183 | } 184 | 185 | function get_info_packs($git_path, $name) { 186 | header_nocache(); 187 | header('Content-Type: text/plain; charset=utf-8'); 188 | 189 | $pack_dir = $git_path.'/objects/pack'; 190 | $dir = dir($pack_dir); 191 | 192 | while (($entry = $dir->read()) !== FALSE) { 193 | if (str_endswith($entry, '.idx')) { 194 | $name = substr($entry, 0, -4); 195 | if (is_file($pack_dir.'/'.$name.'.pack')) { 196 | echo 'P '.$name.'.pack'."\n"; 197 | } 198 | } 199 | } 200 | } 201 | 202 | 203 | $services = array( 204 | array('GET', '/HEAD$', 'get_text_file'), 205 | array('GET', '/info/refs$', 'get_info_refs'), 206 | array('GET', '/objects/info/alternates$', 'get_text_file'), 207 | array('GET', '/objects/info/http-alternates$', 'get_text_file'), 208 | array('GET', '/objects/info/packs$', 'get_info_packs'), 209 | array('GET', '/objects/[0-9a-f]{2}/[0-9a-f]{38}$', 'get_loose_object'), 210 | array('GET', '/objects/pack/pack-[0-9a-f]{40}\\.pack$', 'get_pack_file'), 211 | array('GET', '/objects/pack/pack-[0-9a-f]{40}\\.idx$', 'get_idx_file')); 212 | 213 | 214 | foreach ($repos as $repo) { 215 | if (preg_match('~^'.$url_base.$repo[0].'(/.*)~', $url_path, $matches)) { 216 | $repo_path = $matches[1]; 217 | 218 | foreach ($services as $service) { 219 | if (preg_match('~^'.$service[1].'~', $repo_path)) { 220 | 221 | if ($_SERVER['REQUEST_METHOD'] != $service[0]) { 222 | header('Status: 405 Method Not Allowed'); 223 | header('Allow: '.$service[0]); 224 | echo 'Method Not Allowed'; 225 | die(); 226 | } 227 | 228 | call_user_func($service[2], $repo[1], $repo_path); 229 | die(); 230 | } 231 | } 232 | 233 | header('Status: 404 Not Found'); 234 | die(); 235 | } 236 | } 237 | 238 | header('Status: 404 Not Found'); 239 | die(); 240 | --------------------------------------------------------------------------------