Files
toolbox/app/Services/ProjectService.php

303 lines
7.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services;
use App\Models\Project;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use RuntimeException;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class ProjectService
{
private string $projectsPath;
public function __construct(private readonly ConfigService $configService)
{
$this->projectsPath = $this->resolveProjectsPath();
}
private function resolveProjectsPath(): string
{
$path = $this->configService->get('workspace.projects_path');
if (empty($path)) {
throw new RuntimeException('configs 表未设置 workspace.projects_path。');
}
return rtrim($path, '/');
}
/**
* 获取项目根目录路径
*/
public function getProjectsPath(): string
{
return $this->projectsPath;
}
/**
* 获取所有项目
*/
public function getAllProjects(): Collection
{
return Project::query()->orderBy('name')->get();
}
/**
* 根据 slug 获取项目
*/
public function getBySlug(string $slug): ?Project
{
return Project::findBySlug($slug);
}
/**
* 创建项目
*/
public function create(array $data): Project
{
return Project::query()->create($data);
}
/**
* 更新项目
*/
public function update(Project $project, array $data): Project
{
$project->update($data);
return $project->refresh();
}
/**
* 删除项目
*/
public function delete(Project $project): bool
{
return $project->delete();
}
/**
* 获取项目状态
*/
public function getProjectStatus(Project $project): array
{
$fullPath = $project->getFullPath($this->projectsPath);
$pathValid = $project->isPathValid($this->projectsPath);
$isGitRepo = $pathValid && $project->isGitRepository($this->projectsPath);
$status = [
'path_valid' => $pathValid,
'full_path' => $fullPath,
'is_git_repo' => $isGitRepo,
'current_branch' => null,
'has_uncommitted_changes' => null,
];
if ($isGitRepo) {
try {
$status['current_branch'] = $this->getCurrentBranch($fullPath);
$status['has_uncommitted_changes'] = $this->hasUncommittedChanges($fullPath);
} catch (\Exception $e) {
// 忽略 Git 命令错误
}
}
return $status;
}
/**
* 发现新项目(扫描 projects_path 目录)
*/
public function discoverProjects(): array
{
if (!File::exists($this->projectsPath)) {
return [];
}
$existingSlugs = Project::query()->pluck('slug')->toArray();
$discovered = [];
$directories = File::directories($this->projectsPath);
foreach ($directories as $directory) {
$dirName = basename($directory);
// 跳过已存在的项目
if (in_array($dirName, $existingSlugs, true)) {
continue;
}
// 跳过隐藏目录
if (str_starts_with($dirName, '.')) {
continue;
}
$isGitRepo = is_dir($directory . DIRECTORY_SEPARATOR . '.git');
$discovered[] = [
'slug' => $dirName,
'name' => ucfirst(str_replace(['-', '_'], ' ', $dirName)),
'directory' => $dirName,
'path' => $directory,
'is_git_repo' => $isGitRepo,
];
}
return $discovered;
}
/**
* 批量添加发现的项目
*/
public function addDiscoveredProjects(array $slugs): array
{
$discovered = $this->discoverProjects();
$added = [];
foreach ($discovered as $project) {
if (in_array($project['slug'], $slugs, true)) {
$created = Project::query()->create([
'slug' => $project['slug'],
'name' => $project['name'],
'directory' => $project['directory'],
]);
$added[] = $created;
}
}
return $added;
}
/**
* 同步项目的 Git 信息
*/
public function syncGitInfo(Project $project): Project
{
$fullPath = $project->getFullPath($this->projectsPath);
if (!$project->isGitRepository($this->projectsPath)) {
return $project;
}
try {
// 获取当前版本(从 version.txt
$version = $this->getMasterVersion($fullPath);
if ($version !== null) {
$project->git_current_version = $version;
}
// 获取当前分支
$branch = $this->getCurrentBranch($fullPath);
if ($branch !== null) {
$project->git_release_branch = $branch;
}
$project->git_version_cached_at = now();
$project->save();
} catch (\Exception $e) {
// 忽略错误
}
return $project->refresh();
}
/**
* 获取 master 分支的版本号
*/
private function getMasterVersion(string $path): ?string
{
try {
$this->runGit($path, ['git', 'fetch', 'origin', 'master']);
$version = $this->runGit($path, ['git', 'show', 'origin/master:version.txt']);
return trim($version) ?: null;
} catch (ProcessFailedException $e) {
return null;
}
}
/**
* 获取当前分支
*/
private function getCurrentBranch(string $path): ?string
{
try {
$branch = $this->runGit($path, ['git', 'rev-parse', '--abbrev-ref', 'HEAD']);
return trim($branch) ?: null;
} catch (ProcessFailedException $e) {
return null;
}
}
/**
* 检查是否有未提交的更改
*/
private function hasUncommittedChanges(string $path): bool
{
try {
$output = $this->runGit($path, ['git', 'status', '--porcelain']);
return !empty(trim($output));
} catch (ProcessFailedException $e) {
return false;
}
}
/**
* 运行 Git 命令
*/
private function runGit(string $cwd, array $command): string
{
$process = new Process($command, $cwd);
$process->setTimeout(60);
$process->mustRun();
return $process->getOutput();
}
/**
* 获取所有启用 Git 监控的项目
*/
public function getGitMonitorEnabledProjects(): Collection
{
return Project::getGitMonitorEnabled();
}
/**
* 根据 app 名称查找项目
*/
public function findByAppName(string $appName): ?Project
{
return Project::findByAppName($appName);
}
/**
* 一键同步所有项目的 Git 信息
*/
public function syncAllGitInfo(): array
{
$projects = $this->getAllProjects();
$synced = [];
$failed = [];
foreach ($projects as $project) {
try {
if ($project->isGitRepository($this->projectsPath)) {
$this->syncGitInfo($project);
$synced[] = $project->slug;
}
} catch (\Exception $e) {
$failed[] = [
'slug' => $project->slug,
'error' => $e->getMessage(),
];
}
}
return [
'synced' => $synced,
'failed' => $failed,
];
}
}