remove git excuteable need

This commit is contained in:
hanyixuanten
2026-08-14 17:55:21 +08:00
parent 5dac5e1dfe
commit 7ad2783be1
7 changed files with 37 additions and 376 deletions
+7 -9
View File
@@ -21,7 +21,7 @@ This project serves configured Git repositories through PHP. It supports:
`index.php` is the application entry point. Protocol responsibilities are split
into `operations/clone.php`, `operations/pull.php`, `operations/push.php`,
`operations/branch.php` and `operations/tag.php`; shared routing, repository,
HTTP and Git process code is under `lib/`.
HTTP and pure-PHP Git protocol code is under `lib/`.
Requirements
------------
@@ -30,23 +30,21 @@ Requirements
- Apache with `mod_rewrite` and `.htaccess` enabled for normal deployment.
- MySQL 5.7+/MariaDB 10.2+ and PHP PDO MySQL (`pdo_mysql`) when account
authentication is enabled.
- PHP zlib and hash extensions for the native Git protocol implementation.
- Git and PHP `proc_open` are optional; when available, Git remains the Smart
HTTP backend for full protocol and hook compatibility.
- PHP zlib and hash extensions are required for the Smart HTTP implementation.
- Read access to published repositories; write access is also required for push
and for the managed repository directory when home-page creation is enabled.
If Git or `proc_open` is unavailable, the native PHP backend supports ordinary
SHA-1 clone, fetch, pull and push, including deltas, branches and tags. It does
not currently support SHA-256 repositories, shallow or filtered fetches, signed
push certificates, Git hooks or protocol v2-only features.
The pure-PHP backend supports ordinary SHA-1 clone, fetch, pull and push,
including deltas, branches and tags. It does not currently support SHA-256
repositories, shallow or filtered fetches, signed push certificates, Git hooks
or protocol v2-only features.
Push is disabled by default and should be enabled only behind HTTPS.
Configuration
-------------
Copy `config.php.sample` to `config.php`, import `schema.mysql.sql`, then
configure `$url_base`, `$git_executable`, `$auth`, `$repos` and optionally
configure `$url_base`, `$auth`, `$repos` and optionally
`$managed_repositories`. A writable repository can be configured as:
```php
-5
View File
@@ -6,11 +6,6 @@
$url_base = '';
/* Git executable used by Smart HTTP when available. If it is unavailable,
* supported SHA-1 Smart HTTP operations run through the native PHP backend. */
$git_executable = 'git';
/* Interface language: 'en', 'zh', or empty to follow the browser. */
$language = '';
+1 -8
View File
@@ -242,7 +242,6 @@ function home_creation_result_notice($result) {
case 'already_exists': return array(409, 'error', t('notice.repository_exists', array('name' => $name)));
case 'create_busy': return array(409, 'error', t('notice.repository_create_busy'));
case 'root_unavailable': return array(503, 'error', t('notice.repository_root_unavailable'));
case 'git_unavailable': return array(503, 'error', t('notice.repository_git_unavailable'));
case 'metadata_unavailable': return array(503, 'error', t('notice.repository_metadata_unsaved'));
default: return array(500, 'error', t('notice.repository_create_failed'));
}
@@ -297,7 +296,6 @@ function home_create_repository(
$result = array('status' => 'already_exists', 'name' => $name);
} else {
$result = git_service_create_managed_repository(
$application,
$configuration,
$value,
$owner['id'],
@@ -918,9 +916,6 @@ if (!isset($repos) || !is_array($repos)) {
send_error(500, 'Internal Server Error', 'The repository configuration is invalid.');
}
if (!isset($git_executable)) {
$git_executable = 'git';
}
if (!isset($auth)) {
$auth = array();
@@ -938,9 +933,7 @@ if (!is_array($managed_repositories)) {
send_error(500, 'Internal Server Error', 'The managed repository configuration is invalid.');
}
$application = array(
'git_executable' => $git_executable,
'push_ref_rules' => array());
$application = array('push_ref_rules' => array());
$services = array();
register_branch_operation($application);
-11
View File
@@ -121,10 +121,6 @@ function install_validate($input) {
&& !preg_match('~^/[A-Za-z0-9._~/-]*[A-Za-z0-9._~-]$~D', $input['url_base'])) {
$errors[] = t('install.error_url_base');
}
if ($input['git_executable'] === '' || strlen($input['git_executable']) > 255
|| preg_match('~[\x00-\x1F\x7F]~', $input['git_executable'])) {
$errors[] = t('install.error_git_executable');
}
if (!install_host_is_valid($input['db_host'])) {
$errors[] = t('install.error_db_host');
}
@@ -223,7 +219,6 @@ function install_config_contents($input) {
return "<?php\n\n"
."/* Generated by install.php. See config.php.sample for every option. */\n\n"
."\$url_base = ".var_export($input['url_base'], TRUE).";\n"
."\$git_executable = ".var_export($input['git_executable'], TRUE).";\n"
."\$language = ".var_export($input['language'], TRUE).";\n\n"
."\$auth = array(\n"
." 'enabled' => TRUE,\n"
@@ -382,10 +377,6 @@ function install_send_form($input, $errors) {
.'<input id="url_base" name="url_base" type="text" value="'
.install_escape($input['url_base']).'" placeholder="/php-git-server">'
.'<p class="hint">'.t('install.hint_url_base').'</p></div>' ."\n";
echo '<div><label for="git_executable">'.install_escape(t('install.label_git_executable')).'</label>'
.'<input id="git_executable" name="git_executable" type="text" value="'
.install_escape($input['git_executable']).'" required>'
.'<p class="hint">'.t('install.hint_git_executable').'</p></div>' ."\n";
echo '<div><label for="language">'.install_escape(t('install.label_language')).'</label>'
.'<select id="language" name="language">'
.'<option value=""'.($input['language'] === '' ? ' selected' : '').'>'
@@ -489,7 +480,6 @@ if ($method !== 'GET' && $method !== 'HEAD' && $method !== 'POST') {
$input = array(
'url_base' => rtrim(str_replace('\\', '/', dirname(install_page_url())), '/'),
'git_executable' => 'git',
'registration_enabled' => TRUE,
'session_cookie_secure' => FALSE,
'language' => '',
@@ -523,7 +513,6 @@ if ($expected_token === FALSE
$input = array(
'url_base' => rtrim(trim(install_post_value('url_base')), '/'),
'git_executable' => trim(install_post_value('git_executable')),
'registration_enabled' => install_post_checked('registration_enabled'),
'session_cookie_secure' => install_post_checked('session_cookie_secure'),
'language' => install_post_value('language'),
+10 -305
View File
@@ -1,32 +1,5 @@
<?php
function git_service_executable_available($application) {
if (!isset($application['git_executable'])
|| !is_string($application['git_executable'])
|| $application['git_executable'] === '') {
return FALSE;
}
$executable = $application['git_executable'];
if (strpos($executable, DIRECTORY_SEPARATOR) !== FALSE) {
return is_file($executable) && is_executable($executable);
}
$path = getenv('PATH');
if ($path === FALSE) {
return FALSE;
}
foreach (explode(PATH_SEPARATOR, $path) as $directory) {
$candidate = rtrim($directory, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$executable;
if (is_file($candidate) && is_executable($candidate)) {
return TRUE;
}
}
return FALSE;
}
function git_service_native_available() {
return function_exists('inflate_init')
&& function_exists('inflate_add')
@@ -72,7 +45,7 @@ function git_service_update_unborn_head($git_path, $updated_refs) {
=== strlen($new_contents);
}
function git_service_init_bare_repository_with_php($path) {
function git_service_init_bare_repository($path) {
$directories = array(
'',
'/branches',
@@ -105,60 +78,7 @@ function git_service_init_bare_repository_with_php($path) {
return TRUE;
}
function git_service_init_bare_repository($application, $path) {
if (!function_exists('proc_open') || !git_service_executable_available($application)) {
return git_service_init_bare_repository_with_php($path);
}
$error = @tmpfile();
if ($error === FALSE) {
return FALSE;
}
$descriptor_spec = array(
0 => array('file', '/dev/null', 'r'),
1 => array('file', '/dev/null', 'w'),
2 => $error);
$pipes = array();
$command = array(
$application['git_executable'],
'init',
'--bare',
'--quiet',
$path);
$request = array('git_protocol' => NULL, 'user' => get_authenticated_user());
$process = @proc_open(
$command,
$descriptor_spec,
$pipes,
dirname($path),
git_service_environment($request));
if (!is_resource($process)) {
fclose($error);
return FALSE;
}
$exit_code = proc_close($process);
if ($exit_code !== 0) {
rewind($error);
$message = stream_get_contents($error);
error_log(
'Git repository initialization failed for '.$path.' with exit code '
.$exit_code.': '.trim($message));
}
fclose($error);
if ($exit_code !== 0) {
return FALSE;
}
$head = "ref: refs/heads/main\n";
return @file_put_contents($path.'/HEAD', $head, LOCK_EX) === strlen($head);
}
function git_service_create_managed_repository(
$application,
$configuration,
$value,
$owner_user_id,
@@ -213,7 +133,7 @@ function git_service_create_managed_repository(
}
$temporary_path = $root.DIRECTORY_SEPARATOR.'.create-'.$suffix.'.tmp';
if (!git_service_init_bare_repository($application, $temporary_path)
if (!git_service_init_bare_repository($temporary_path)
|| !managed_repository_is_bare($temporary_path)) {
return array('status' => 'create_failed', 'name' => $name);
}
@@ -255,212 +175,8 @@ function git_service_create_managed_repository(
}
}
function git_service_is_protocol_v2($request) {
if ($request['git_protocol'] === NULL) {
return FALSE;
}
return preg_match('~(?:^|:)version=2(?:$|:)~', $request['git_protocol']) === 1;
}
function git_service_environment($request) {
$environment = getenv();
if (!is_array($environment)) {
$environment = array();
}
unset($environment['GIT_DIR']);
unset($environment['GIT_WORK_TREE']);
unset($environment['GIT_PROTOCOL']);
if ($request['git_protocol'] !== NULL
&& strlen($request['git_protocol']) <= 1024
&& strpos($request['git_protocol'], "\0") === FALSE
&& strpos($request['git_protocol'], "\n") === FALSE
&& strpos($request['git_protocol'], "\r") === FALSE) {
$environment['GIT_PROTOCOL'] = $request['git_protocol'];
}
if ($request['user'] !== NULL) {
$environment['REMOTE_USER'] = $request['user'];
}
if (isset($_SERVER['REMOTE_ADDR'])) {
$environment['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'];
}
if (isset($_SERVER['HTTP_USER_AGENT'])) {
$environment['GIT_HTTP_USER_AGENT'] = $_SERVER['HTTP_USER_AGENT'];
}
return $environment;
}
function git_service_command($application, $service, $repository, $advertise) {
if ($service === 'git-upload-pack') {
$subcommand = 'upload-pack';
} else if ($service === 'git-receive-pack') {
$subcommand = 'receive-pack';
} else {
return FALSE;
}
$command = array(
$application['git_executable'],
$subcommand,
'--stateless-rpc');
if ($subcommand === 'upload-pack') {
$command[] = '--strict';
}
if ($advertise) {
$command[] = '--advertise-refs';
}
$command[] = $repository['path'];
return $command;
}
function git_service_pump_process($pipes, $input) {
stream_set_blocking($pipes[0], FALSE);
stream_set_blocking($pipes[1], FALSE);
$input_done = $input === NULL;
$input_buffer = '';
$stdin_open = TRUE;
$stdout_open = TRUE;
while ($stdin_open || $stdout_open) {
if (!$input_done && strlen($input_buffer) < 65536) {
$chunk = fread($input, 65536);
if ($chunk === FALSE) {
return FALSE;
}
if ($chunk !== '') {
$input_buffer .= $chunk;
}
if (feof($input)) {
$input_done = TRUE;
}
}
if ($stdin_open && $input_done && $input_buffer === '') {
fclose($pipes[0]);
$stdin_open = FALSE;
}
$read = $stdout_open ? array($pipes[1]) : array();
$write = $stdin_open && $input_buffer !== '' ? array($pipes[0]) : array();
$except = NULL;
if (empty($read) && empty($write)) {
continue;
}
$ready = @stream_select($read, $write, $except, 30);
if ($ready === FALSE) {
return FALSE;
}
if (!empty($write)) {
$written = fwrite($pipes[0], $input_buffer);
if ($written === FALSE) {
return FALSE;
}
if ($written > 0) {
$input_buffer = (string) substr($input_buffer, $written);
}
}
if (!empty($read)) {
$output = fread($pipes[1], 65536);
if ($output === FALSE) {
return FALSE;
}
if ($output !== '') {
echo $output;
}
if (feof($pipes[1])) {
fclose($pipes[1]);
$stdout_open = FALSE;
}
}
}
return TRUE;
}
function git_service_run(
$application,
$repository,
$request,
$service,
$advertise,
$input=NULL,
$prefix='') {
if (!function_exists('proc_open') || !git_service_executable_available($application)) {
send_error(503, 'Service Unavailable', 'Git Smart HTTP is not available.');
}
$command = git_service_command($application, $service, $repository, $advertise);
if ($command === FALSE) {
send_error(403, 'Forbidden', 'Unsupported Git service.');
}
$error = @tmpfile();
if ($error === FALSE) {
send_error(500, 'Internal Server Error', 'Unable to create a Git error stream.');
}
$descriptor_spec = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => $error);
$pipes = array();
$process = @proc_open(
$command,
$descriptor_spec,
$pipes,
dirname($repository['path']),
git_service_environment($request));
if (!is_resource($process)) {
fclose($error);
send_error(503, 'Service Unavailable', 'Unable to start the Git service.');
}
if ($prefix !== '') {
echo $prefix;
}
$stream_succeeded = git_service_pump_process($pipes, $input);
foreach ($pipes as $pipe) {
if (is_resource($pipe)) {
fclose($pipe);
}
}
$exit_code = proc_close($process);
if (!$stream_succeeded || $exit_code !== 0) {
rewind($error);
$message = stream_get_contents($error);
error_log(
'Git service '.$service.' failed for '.$repository['url'].' with exit code '
.$exit_code.': '.trim($message));
}
fclose($error);
return $stream_succeeded && $exit_code === 0 ? 0 : $exit_code;
}
function git_service_advertise($application, $repository, $request, $service) {
if ((!function_exists('proc_open') || !git_service_executable_available($application))
&& !git_service_native_available()) {
if (!git_service_native_available()) {
send_error(503, 'Service Unavailable', 'The native Git service requires PHP zlib and hash support.');
}
@@ -468,34 +184,25 @@ function git_service_advertise($application, $repository, $request, $service) {
header('Content-Type: application/x-'.$service.'-advertisement');
header('X-Content-Type-Options: nosniff');
if ((!function_exists('proc_open') || !git_service_executable_available($application))
&& $service === 'git-upload-pack') {
echo format_packet_line('# service='.$service."\n").'0000';
if ($service === 'git-upload-pack') {
if (!git_upload_pack_advertise_native($repository)) {
error_log('Native Git advertisement failed for '.$repository['url'].'.');
}
return;
}
if ((!function_exists('proc_open') || !git_service_executable_available($application))
&& $service === 'git-receive-pack') {
echo format_packet_line('# service='.$service."\n").'0000';
if ($service === 'git-receive-pack') {
if (!git_receive_pack_advertise_native($repository)) {
error_log('Native Git receive advertisement failed for '.$repository['url'].'.');
}
return;
}
$prefix = '';
if (!git_service_is_protocol_v2($request)) {
$prefix = format_packet_line('# service='.$service."\n").'0000';
}
git_service_run($application, $repository, $request, $service, TRUE, NULL, $prefix);
send_error(403, 'Forbidden', 'Unsupported Git service.');
}
function git_service_rpc($application, $repository, $request, $service, $input) {
if ((!function_exists('proc_open') || !git_service_executable_available($application))
&& !git_service_native_available()) {
if (!git_service_native_available()) {
send_error(503, 'Service Unavailable', 'The native Git service requires PHP zlib and hash support.');
}
@@ -503,15 +210,13 @@ function git_service_rpc($application, $repository, $request, $service, $input)
header('Content-Type: application/x-'.$service.'-result');
header('X-Content-Type-Options: nosniff');
if ((!function_exists('proc_open') || !git_service_executable_available($application))
&& $service === 'git-upload-pack') {
if ($service === 'git-upload-pack') {
if (!git_upload_pack_rpc_native($repository, $input)) {
error_log('Native Git upload-pack failed for '.$repository['url'].'.');
}
return;
}
if ((!function_exists('proc_open') || !git_service_executable_available($application))
&& $service === 'git-receive-pack') {
if ($service === 'git-receive-pack') {
if (!git_receive_pack_rpc_native($repository, $input)) {
error_log('Native Git receive-pack failed for '.$repository['url'].'.');
return 1;
@@ -519,5 +224,5 @@ function git_service_rpc($application, $repository, $request, $service, $input)
return 0;
}
return git_service_run($application, $repository, $request, $service, FALSE, $input);
send_error(403, 'Forbidden', 'Unsupported Git service.');
}
-10
View File
@@ -221,8 +221,6 @@ function i18n_catalog_en() {
.' shortly.',
'notice.repository_root_unavailable' => 'The repository directory is unavailable or'
.' not writable.',
'notice.repository_git_unavailable' => 'The Git initialization service is unavailable'
.' right now.',
'notice.repository_metadata_unsaved' => 'Repository ownership could not be saved.'
.' Try again shortly.',
'notice.repository_create_failed' => 'Repository creation failed. Check the server'
@@ -332,9 +330,6 @@ function i18n_catalog_en() {
'install.legend_application' => 'Application',
'install.label_url_base' => 'Base path',
'install.hint_url_base' => 'Leave empty when deploying at the domain root.',
'install.label_git_executable' => 'Git executable',
'install.hint_git_executable' => 'Falls back to the pure PHP implementation when'
.' unavailable.',
'install.label_language' => 'Interface language',
'install.language_auto' => 'Follow the browser (English fallback)',
'install.language_en' => 'English',
@@ -369,7 +364,6 @@ function i18n_catalog_en() {
'install.error_url_base' => 'The base path must start with / and must not end with'
.' /. Leave it empty when deploying at the domain root.',
'install.error_git_executable' => 'The Git executable path is not valid.',
'install.error_db_host' => 'The database host name is not valid.',
'install.error_db_port' => 'The database port is not valid.',
'install.error_db_name' => 'A database name may contain only letters, digits,'
@@ -559,7 +553,6 @@ function i18n_catalog_zh() {
'notice.repository_exists' => '仓库 {name} 已存在。',
'notice.repository_create_busy' => '另一个仓库正在创建,请稍后重试。',
'notice.repository_root_unavailable' => '仓库存放目录不可用或不可写。',
'notice.repository_git_unavailable' => 'Git 初始化服务当前不可用。',
'notice.repository_metadata_unsaved' => '仓库所有权信息无法保存,请稍后重试。',
'notice.repository_create_failed' => '仓库创建失败,请检查服务器日志。',
'notice.repository_created' => '仓库 {name} 已创建。',
@@ -646,8 +639,6 @@ function i18n_catalog_zh() {
'install.legend_application' => '应用',
'install.label_url_base' => '基础路径',
'install.hint_url_base' => '部署在域名根路径时留空。',
'install.label_git_executable' => 'Git 可执行文件',
'install.hint_git_executable' => '不可用时会回退到纯 PHP 实现。',
'install.label_language' => '界面语言',
'install.language_auto' => '跟随浏览器(默认英语)',
'install.language_en' => 'English',
@@ -676,7 +667,6 @@ function i18n_catalog_zh() {
'install.button_submit' => '开始安装',
'install.error_url_base' => '基础路径必须以 / 开头,且不能以 / 结尾;部署在域名根路径时请留空。',
'install.error_git_executable' => 'Git 可执行文件路径无效。',
'install.error_db_host' => '数据库主机名无效。',
'install.error_db_port' => '数据库端口无效。',
'install.error_db_name' => '数据库名只能包含字母、数字、下划线和短横线,最多 64 个字符。',
+18 -27
View File
@@ -2,12 +2,12 @@
本项目通过 PHP 发布显式配置的 Git 仓库,同时支持 **Dumb HTTP****Smart HTTP**
- `clone`支持 Smart HTTP;服务器没有 Git/`proc_open` 时由 PHP 原生 upload-pack 提供,也保留 Dumb HTTP 兼容路径。
- `pull` / `fetch`通过 `git-upload-pack --stateless-rpc` 提供。
- `push`有 Git 时通过 `git-receive-pack --stateless-rpc` 提供;无 Git 时由 PHP 原生 receive-pack 提供,默认关闭。
- `clone`由 PHP 实现 Smart HTTP upload-pack,并保留 Dumb HTTP 兼容路径。
- `pull` / `fetch`由 PHP 实现的 upload-pack 提供。
- `push`:由 PHP 实现的 receive-pack 提供,默认关闭。
- `branch`:远程分支以 `refs/heads/*` 表示,可通过 push 创建、更新和删除。
- `tag`:远程标签以 `refs/tags/*` 表示,可通过 push 创建、更新和删除。
- `create`:可从主界面创建受控目录内的 bare 仓库Git 不可用时由 PHP 直接初始化。
- `create`:可从主界面创建受控目录内的 bare 仓库由 PHP 直接初始化。
Git 协议不会向服务器发送名为“branch”或“tag”的独立命令:本地 `git branch``git tag` 不访问服务器;远程分支和标签通过 fetch/pull 获取,通过 push 更新。
@@ -21,7 +21,7 @@ lib/http.php HTTP 状态、响应头和认证用户读取
lib/auth.php MySQL 用户、网页登录会话与 Access Token 验证
lib/repository.php 仓库配置、安全路径和 Dumb HTTP refs
lib/router.php 请求路由
lib/git_service.php Smart HTTP Git 子进程与流式传输
lib/git_service.php Smart HTTP 服务分发与 bare 仓库初始化
operations/clone.php Dumb HTTP clone/object 资源
operations/pull.php upload-packclone/fetch/pull
operations/push.php receive-packpush 请求、大小及 refs 校验
@@ -38,14 +38,14 @@ operations/tag.php refs/tags/* 标签更新规则
- MySQL 5.7+/MariaDB 10.2+ 与 PHP PDO MySQL 扩展(`pdo_mysql`)。
- Apache `mod_rewrite` 模块。
- 允许项目目录中的 `.htaccess` 使用重写规则。
- Smart HTTP 需要服务器安装 Git,并允许 PHP 使用 `proc_open`
- Smart HTTP 需要 PHP zlib 与 hash 扩展
- Web 服务器进程对仓库具有读取权限;启用 push 时还需要写入权限。
项目没有 Composer 依赖,也不需要构建。若系统尚未启用 PDO MySQL,先安装对应 PHP 扩展并重启 Apache/PHP-FPM。
服务器可以不安装 Git。Git 或 `proc_open` 不可用时,应用使用纯 PHP 实现的 Smart HTTP 服务端协议,支持普通 SHA-1 仓库的 clone、fetch、pull、push、delta、分支和标签PHP 必须启用 zlib 与 hash 扩展
服务器不需要安装 Git。应用使用纯 PHP 实现的 Smart HTTP 服务端协议,支持普通 SHA-1 仓库的 clone、fetch、pull、push、delta、分支和标签。
原生 PHP 后端当前不支持 SHA-256 仓库、shallow clone、partial clone/filter、push certificate、Git hooks 和仅 protocol v2 提供的功能。需要这些能力时仍应安装 Git 并允许 `proc_open`
PHP 后端当前不支持 SHA-256 仓库、shallow clone、partial clone/filter、push certificate、Git hooks 和仅 protocol v2 提供的功能。
## 3. 安装
@@ -109,7 +109,6 @@ https://git.example.com/php-git-server/
<?php
$url_base = '/php-git-server';
$git_executable = 'git';
$auth = array(
'enabled' => TRUE,
@@ -191,7 +190,7 @@ $managed_repositories = array(
- 设置 `$managed_repositories = array();` 可完全关闭主界面创建功能。
- 仓库名称仅允许字母、数字、点、短横线和下划线,长度最多 64 个字符;`.git` 后缀可省略。
- 新仓库是 bare 仓库,默认分支为 `main`。应用内的创建请求使用锁、暂存目录和原子改名,不会互相覆盖;托管目录不应由其他进程同时写入。
- Git 或 `proc_open` 不可用时,应用以纯 PHP 创建标准 SHA-1 格式的空 bare 仓库;之后可以通过 Dumb HTTP clone,但首次写入仍需在其他具备 Git 的环境中生成仓库内容并同步到服务器
- 应用以纯 PHP 创建标准 SHA-1 格式的空 bare 仓库,可以直接通过 Smart HTTP 完成首次 push
静态 `$repos` 条目与托管目录中的仓库 URL 冲突时,以静态条目为准。仓库创建要求已登录 Session,所有修改表单还使用会话 CSRF 令牌。托管仓库的所有者和可见性保存在 `pgit_repositories`;缺少元数据的旧托管仓库按私有、无所有者处理,在完成迁移前不可 push。
@@ -215,7 +214,7 @@ $managed_repositories = array(
所有 push 都必须提供 access token,且 token 所属用户名必须与 `owner` 完全一致。`branches``tags``other_refs` 只控制 push 更新,不会隐藏已经存在的 refs。私有仓库的 Smart HTTP 与 Dumb HTTP 路径都会先验证 token,私有对象响应禁止共享缓存。
push 请求会先写入系统临时目录,以便在交给 `git-receive-pack` 前检查 ref 命名空间。启用较大的 push 时,应保证 PHP 系统临时目录具有足够空间;也可以使用 `max_request_bytes` 设置上限。
push 请求会先写入系统临时目录,以便 PHP 在处理对象前检查 ref 命名空间。启用较大的 push 时,应保证 PHP 系统临时目录具有足够空间;也可以使用 `max_request_bytes` 设置上限。
## 6. 身份认证
@@ -270,7 +269,7 @@ git push origin main
Git 收到私有读取或 push 的 `401` 响应后会提示输入用户名和密码。也可以使用操作系统的 Git Credential Manager 或其他安全凭据助手保存 token;不要把 token 写入远程 URL、shell 历史、仓库配置或脚本。
公开仓库允许匿名 clone/fetch/pull;私有仓库只接受 access token。浏览器登录 Session 只用于首页、创建仓库和显示私有仓库列表,不能代替 Git access token。Token 验证成功后,用户名会作为 `REMOTE_USER` 传给 Git 子进程和 hooks,现有 hooks 可以继续读取该变量
公开仓库允许匿名 clone/fetch/pull;私有仓库只接受 access token。浏览器登录 Session 只用于首页、创建仓库和显示私有仓库列表,不能代替 Git access token。Token 验证成功后,应用使用该用户名执行仓库所有者权限检查
### 现有数据库与仓库迁移
@@ -315,7 +314,7 @@ sudo find /var/www/php-git-server/repos/project.git -type f -exec chmod 660 {} \
权限策略应根据服务器实际用户、组和备份方案调整。不要使用 `chmod -R 777`
仓库中的 hooks 会在 `git-receive-pack` 处理 push 时以 PHP/Web 服务器用户身份执行。只能发布受信任的仓库和 hooks,并确保 hooks 不接受未经校验的外部参数去执行任意命令
PHP 服务端不会执行仓库中的 Git hooks
## 8. 操作示例
@@ -402,8 +401,8 @@ Smart HTTP 支持:
- `POST /git-upload-pack`
- `GET /info/refs?service=git-receive-pack`
- `POST /git-receive-pack`
- upload-pack 的 Git protocol v0/v1/v2 协商
- Git 自身支持的 SHA-1 或 SHA-256 仓库格式
- upload-pack 的 Git protocol v0/v1 基本协商
- SHA-1 仓库格式
Dumb HTTP 支持:
@@ -417,7 +416,7 @@ Dumb HTTP 支持:
- loose refs、packed refs、packed annotated tag 的 peeled refs
- SHA-1 和 SHA-256 长度的对象名称
安装 Git 时,Smart HTTP 的具体协商、对象校验、fast-forward 规则和仓库 hooks 由服务器 Git 负责。无 Git 时,PHP 后端执行 SHA-1 对象哈希、pack 校验、OFS/REF delta、对象连通性、ref 锁和默认 fast-forward 检查,但不会执行 hooks。
PHP 后端执行 SHA-1 对象哈希、pack 校验、OFS/REF delta、对象连通性、ref 锁和默认 fast-forward 检查,但不会执行 hooks。
## 10. 验证部署
@@ -480,14 +479,7 @@ curl -i -X POST https://git.example.com/php-git-server/project.git/HEAD
### clone / pull 返回 503
检查
1. `$git_executable` 是否指向可执行的 Git。
2. Web 服务器进程的 `PATH` 是否包含 Git。
3. PHP 是否允许 `proc_open`
4. Web 服务器进程是否可读取仓库。
若 Git 不可用,服务使用原生 PHP Smart HTTP 后端。确认 PHP 启用了 zlib 与 hash,并确认仓库是 SHA-1 格式;浅克隆、filter、SHA-256 或 hooks 需求必须改用 Git 后端。
检查 PHP 是否启用了 zlib 与 hash、仓库是否为 SHA-1 格式,以及 Web 服务器进程是否可读取仓库。当前服务不支持浅克隆、filter、SHA-256 或 hooks。
### push 返回 403
@@ -519,7 +511,7 @@ curl -i -X POST https://git.example.com/php-git-server/project.git/HEAD
### push 返回 500 或远端断开
检查 PHP/Apache 错误日志、Git hooks 输出、仓库写权限、磁盘空间和临时目录空间。还应确认 `max_request_bytes` 没有设置得过小。
检查 PHP/Apache 错误日志、仓库写权限、磁盘空间和临时目录空间。还应确认 `max_request_bytes` 没有设置得过小。
### non-fast-forward 被拒绝
@@ -529,7 +521,7 @@ curl -i -X POST https://git.example.com/php-git-server/project.git/HEAD
git push --force-with-lease origin main
```
仓库本地配置和 hooks 仍可进一步禁止 force push、删除或特定提交
应用的仓库选项还可禁止 force push、删除或特定 ref 命名空间
### 浏览器可以访问,但 Git 操作失败
@@ -554,6 +546,5 @@ GIT_TRACE=1 GIT_CURL_VERBOSE=1 git clone \
- 只给 Web 服务器最小必要的文件权限。
-`other_refs` 保持为 `FALSE`,除非确实需要 notes、replace 或自定义 refs。
- 使用 `max_request_bytes`、Web 服务器请求体限制和磁盘配额防止超大 push。
- 审查仓库 hookspush 会执行 receive-pack hooks。
- 不提交 `config.php`,也不要把敏感信息写入 Git 历史。
- 确保 `.htaccess` 或等价的虚拟主机规则生效,避免绕过 `index.php`