完成基本功能
This commit is contained in:
670
internal/app/service.go
Normal file
670
internal/app/service.go
Normal file
@@ -0,0 +1,670 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/zhuy1228/GitPilot/config"
|
||||
"github.com/zhuy1228/GitPilot/internal/git"
|
||||
)
|
||||
|
||||
// AppService 应用服务,暴露给前端调用
|
||||
type AppService struct {
|
||||
app *application.App
|
||||
config *config.AppConfig
|
||||
gitClient *git.GitClient
|
||||
}
|
||||
|
||||
func (a *AppService) SetApplication(app *application.App) {
|
||||
a.app = app
|
||||
}
|
||||
|
||||
func New() *AppService {
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
log.Printf("加载配置失败: %v, 使用默认配置", err)
|
||||
cfg = &config.AppConfig{
|
||||
Platforms: make(map[string]config.Platform),
|
||||
Settings: config.Settings{Concurrency: 6, NetworkCheck: true, LogLevel: "info"},
|
||||
}
|
||||
}
|
||||
return &AppService{
|
||||
config: cfg,
|
||||
gitClient: git.NewGitClient(),
|
||||
}
|
||||
}
|
||||
|
||||
// SelectDirectory 打开系统文件夹选择器,返回选中的路径
|
||||
func (s *AppService) SelectDirectory() (string, error) {
|
||||
if s.app == nil {
|
||||
return "", fmt.Errorf("应用未初始化")
|
||||
}
|
||||
path, err := s.app.Dialog.OpenFile().
|
||||
CanChooseDirectories(true).
|
||||
CanChooseFiles(false).
|
||||
SetTitle("选择项目文件夹").
|
||||
PromptForSingleSelection()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("打开文件夹选择器失败: %w", err)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// --- 平台/项目 树形结构 ---
|
||||
|
||||
// TreeNode 前端侧边栏树节点
|
||||
type TreeNode struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"` // platform, user, project
|
||||
Path string `json:"path,omitempty"`
|
||||
Children []TreeNode `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
// GetProjectTree 获取项目树,供前端侧边栏渲染
|
||||
func (s *AppService) GetProjectTree() []TreeNode {
|
||||
var tree []TreeNode
|
||||
for platformName, platform := range s.config.Platforms {
|
||||
platformNode := TreeNode{
|
||||
Key: platformName,
|
||||
Label: platformName,
|
||||
Type: "platform",
|
||||
}
|
||||
for _, user := range platform.Users {
|
||||
userNode := TreeNode{
|
||||
Key: platformName + "/" + user.Username,
|
||||
Label: user.Username,
|
||||
Type: "user",
|
||||
}
|
||||
for _, proj := range user.Projects {
|
||||
userNode.Children = append(userNode.Children, TreeNode{
|
||||
Key: platformName + "/" + user.Username + "/" + proj.Name,
|
||||
Label: proj.Name,
|
||||
Type: "project",
|
||||
Path: proj.Path,
|
||||
})
|
||||
}
|
||||
platformNode.Children = append(platformNode.Children, userNode)
|
||||
}
|
||||
tree = append(tree, platformNode)
|
||||
}
|
||||
return tree
|
||||
}
|
||||
|
||||
// --- 项目管理 ---
|
||||
|
||||
// AddProject 添加项目到指定平台/用户下
|
||||
func (s *AppService) AddProject(platform, username, name, path string) error {
|
||||
p, ok := s.config.Platforms[platform]
|
||||
if !ok {
|
||||
return fmt.Errorf("平台 %s 不存在", platform)
|
||||
}
|
||||
for i, user := range p.Users {
|
||||
if user.Username == username {
|
||||
// 检查重复
|
||||
for _, proj := range user.Projects {
|
||||
if proj.Name == name {
|
||||
return fmt.Errorf("项目 %s 已存在", name)
|
||||
}
|
||||
}
|
||||
s.config.Platforms[platform].Users[i].Projects = append(
|
||||
s.config.Platforms[platform].Users[i].Projects,
|
||||
config.Project{Name: name, Path: path},
|
||||
)
|
||||
return config.SaveConfig(s.config)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("用户 %s 不存在于平台 %s", username, platform)
|
||||
}
|
||||
|
||||
// RemoveProject 从指定平台/用户下删除项目
|
||||
func (s *AppService) RemoveProject(platform, username, name string) error {
|
||||
p, ok := s.config.Platforms[platform]
|
||||
if !ok {
|
||||
return fmt.Errorf("平台 %s 不存在", platform)
|
||||
}
|
||||
for i, user := range p.Users {
|
||||
if user.Username == username {
|
||||
projects := user.Projects
|
||||
for j, proj := range projects {
|
||||
if proj.Name == name {
|
||||
s.config.Platforms[platform].Users[i].Projects = append(projects[:j], projects[j+1:]...)
|
||||
return config.SaveConfig(s.config)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("项目 %s 不存在", name)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("用户 %s 不存在于平台 %s", username, platform)
|
||||
}
|
||||
|
||||
// --- 平台管理 ---
|
||||
|
||||
// AddPlatform 添加新平台
|
||||
func (s *AppService) AddPlatform(name, baseURL string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("平台名称不能为空")
|
||||
}
|
||||
if _, ok := s.config.Platforms[name]; ok {
|
||||
return fmt.Errorf("平台 %s 已存在", name)
|
||||
}
|
||||
s.config.Platforms[name] = config.Platform{
|
||||
BaseURL: baseURL,
|
||||
Users: []config.User{},
|
||||
}
|
||||
return config.SaveConfig(s.config)
|
||||
}
|
||||
|
||||
// UpdatePlatform 修改平台信息(base_url)
|
||||
func (s *AppService) UpdatePlatform(name, baseURL string) error {
|
||||
p, ok := s.config.Platforms[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("平台 %s 不存在", name)
|
||||
}
|
||||
p.BaseURL = baseURL
|
||||
s.config.Platforms[name] = p
|
||||
return config.SaveConfig(s.config)
|
||||
}
|
||||
|
||||
// RemovePlatform 删除平台
|
||||
func (s *AppService) RemovePlatform(name string) error {
|
||||
if _, ok := s.config.Platforms[name]; !ok {
|
||||
return fmt.Errorf("平台 %s 不存在", name)
|
||||
}
|
||||
delete(s.config.Platforms, name)
|
||||
return config.SaveConfig(s.config)
|
||||
}
|
||||
|
||||
// --- 用户管理 ---
|
||||
|
||||
// AddUser 添加用户到指定平台
|
||||
func (s *AppService) AddUser(platform, username, token string) error {
|
||||
p, ok := s.config.Platforms[platform]
|
||||
if !ok {
|
||||
return fmt.Errorf("平台 %s 不存在", platform)
|
||||
}
|
||||
if username == "" {
|
||||
return fmt.Errorf("用户名不能为空")
|
||||
}
|
||||
for _, user := range p.Users {
|
||||
if user.Username == username {
|
||||
return fmt.Errorf("用户 %s 已存在于平台 %s", username, platform)
|
||||
}
|
||||
}
|
||||
p.Users = append(p.Users, config.User{
|
||||
Username: username,
|
||||
Token: token,
|
||||
Projects: []config.Project{},
|
||||
})
|
||||
s.config.Platforms[platform] = p
|
||||
return config.SaveConfig(s.config)
|
||||
}
|
||||
|
||||
// UpdateUser 修改用户信息(用户名、token)
|
||||
func (s *AppService) UpdateUser(platform, oldUsername, newUsername, token string) error {
|
||||
p, ok := s.config.Platforms[platform]
|
||||
if !ok {
|
||||
return fmt.Errorf("平台 %s 不存在", platform)
|
||||
}
|
||||
if newUsername == "" {
|
||||
return fmt.Errorf("新用户名不能为空")
|
||||
}
|
||||
for i, user := range p.Users {
|
||||
if user.Username == oldUsername {
|
||||
// 如果改了用户名,检查新名字是否冲突
|
||||
if oldUsername != newUsername {
|
||||
for _, u := range p.Users {
|
||||
if u.Username == newUsername {
|
||||
return fmt.Errorf("用户 %s 已存在于平台 %s", newUsername, platform)
|
||||
}
|
||||
}
|
||||
}
|
||||
s.config.Platforms[platform].Users[i].Username = newUsername
|
||||
s.config.Platforms[platform].Users[i].Token = token
|
||||
return config.SaveConfig(s.config)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("用户 %s 不存在于平台 %s", oldUsername, platform)
|
||||
}
|
||||
|
||||
// RemoveUser 从平台删除用户
|
||||
func (s *AppService) RemoveUser(platform, username string) error {
|
||||
p, ok := s.config.Platforms[platform]
|
||||
if !ok {
|
||||
return fmt.Errorf("平台 %s 不存在", platform)
|
||||
}
|
||||
for i, user := range p.Users {
|
||||
if user.Username == username {
|
||||
s.config.Platforms[platform] = config.Platform{
|
||||
BaseURL: p.BaseURL,
|
||||
Users: append(p.Users[:i], p.Users[i+1:]...),
|
||||
}
|
||||
return config.SaveConfig(s.config)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("用户 %s 不存在于平台 %s", username, platform)
|
||||
}
|
||||
|
||||
// GetUserInfo 获取用户信息
|
||||
func (s *AppService) GetUserInfo(platform, username string) (*UserInfo, error) {
|
||||
p, ok := s.config.Platforms[platform]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("平台 %s 不存在", platform)
|
||||
}
|
||||
for _, user := range p.Users {
|
||||
if user.Username == username {
|
||||
return &UserInfo{
|
||||
Username: user.Username,
|
||||
Token: user.Token,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("用户 %s 不存在于平台 %s", username, platform)
|
||||
}
|
||||
|
||||
// GetPlatformInfo 获取平台信息
|
||||
func (s *AppService) GetPlatformInfo(name string) (*PlatformInfo, error) {
|
||||
p, ok := s.config.Platforms[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("平台 %s 不存在", name)
|
||||
}
|
||||
return &PlatformInfo{
|
||||
Name: name,
|
||||
BaseURL: p.BaseURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// --- Git 操作 ---
|
||||
|
||||
// ProjectStatus 项目状态信息
|
||||
type ProjectStatus struct {
|
||||
Branch string `json:"branch"`
|
||||
RemoteURL string `json:"remoteUrl"`
|
||||
ChangedFiles []FileInfo `json:"changedFiles"`
|
||||
}
|
||||
|
||||
// FileInfo 文件信息
|
||||
type FileInfo struct {
|
||||
Status string `json:"status"`
|
||||
StatusText string `json:"statusText"`
|
||||
FilePath string `json:"filePath"`
|
||||
Staged bool `json:"staged"`
|
||||
}
|
||||
|
||||
// UserInfo 用户信息
|
||||
type UserInfo struct {
|
||||
Username string `json:"username"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// PlatformInfo 平台信息
|
||||
type PlatformInfo struct {
|
||||
Name string `json:"name"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
}
|
||||
|
||||
// GetProjectStatus 获取项目 git 状态
|
||||
func (s *AppService) GetProjectStatus(path string) (*ProjectStatus, error) {
|
||||
// 先校验路径是否存在
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
|
||||
branch, err := s.gitClient.Branch(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取分支失败: %w", err)
|
||||
}
|
||||
|
||||
remoteURL, _ := s.gitClient.RemoteURL(path)
|
||||
|
||||
return &ProjectStatus{
|
||||
Branch: strings.TrimSpace(branch),
|
||||
RemoteURL: remoteURL,
|
||||
ChangedFiles: []FileInfo{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetProjectChangedFiles 获取项目变更文件列表(可能较慢)
|
||||
func (s *AppService) GetProjectChangedFiles(path string) ([]FileInfo, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
|
||||
changes, err := s.gitClient.ChangedFiles(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取变更文件失败: %w", err)
|
||||
}
|
||||
|
||||
var files []FileInfo
|
||||
for _, c := range changes {
|
||||
files = append(files, FileInfo{
|
||||
Status: c.Status,
|
||||
StatusText: c.StatusText(),
|
||||
FilePath: c.FilePath,
|
||||
Staged: c.Staged,
|
||||
})
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// GetFileContent 获取文件内容
|
||||
func (s *AppService) GetFileContent(projectPath, filePath string) (string, error) {
|
||||
fullPath := filepath.Join(projectPath, filePath)
|
||||
|
||||
// 检查文件大小,超过 2MB 不读取
|
||||
info, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取文件失败: %w", err)
|
||||
}
|
||||
if info.Size() > 2*1024*1024 {
|
||||
return "", fmt.Errorf("文件过大 (%.1f MB),不支持预览", float64(info.Size())/1024/1024)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 检测二进制文件:取前 8KB 检查是否含 NUL 字节
|
||||
checkLen := len(data)
|
||||
if checkLen > 8192 {
|
||||
checkLen = 8192
|
||||
}
|
||||
for _, b := range data[:checkLen] {
|
||||
if b == 0 {
|
||||
return "", fmt.Errorf("二进制文件,不支持预览")
|
||||
}
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// GetFileDiff 获取文件 diff
|
||||
func (s *AppService) GetFileDiff(projectPath, filePath string) (string, error) {
|
||||
return s.gitClient.DiffFile(projectPath, filePath)
|
||||
}
|
||||
|
||||
// StageFiles 暂存指定文件
|
||||
func (s *AppService) StageFiles(path string, files []string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未指定文件")
|
||||
}
|
||||
_, err := s.gitClient.Add(path, files...)
|
||||
return err
|
||||
}
|
||||
|
||||
// UnstageFiles 取消暂存指定文件
|
||||
func (s *AppService) UnstageFiles(path string, files []string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未指定文件")
|
||||
}
|
||||
_, err := s.gitClient.Reset(path, files...)
|
||||
return err
|
||||
}
|
||||
|
||||
// StageAll 暂存所有变更文件
|
||||
func (s *AppService) StageAll(path string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
_, err := s.gitClient.Add(path, ".")
|
||||
return err
|
||||
}
|
||||
|
||||
// UnstageAll 取消暂存所有文件
|
||||
func (s *AppService) UnstageAll(path string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
_, err := s.gitClient.Reset(path, ".")
|
||||
return err
|
||||
}
|
||||
|
||||
// CommitChanges 提交已暂存的更改
|
||||
func (s *AppService) CommitChanges(path, message string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
if strings.TrimSpace(message) == "" {
|
||||
return fmt.Errorf("提交信息不能为空")
|
||||
}
|
||||
_, err := s.gitClient.Commit(path, message)
|
||||
return err
|
||||
}
|
||||
|
||||
// DiscardFiles 丢弃工作区指定文件的更改
|
||||
func (s *AppService) DiscardFiles(path string, files []string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未指定文件")
|
||||
}
|
||||
|
||||
// 对每个文件判断:未跟踪的用 clean 删除,已跟踪的用 checkout 还原
|
||||
changes, err := s.gitClient.ChangedFiles(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取变更状态失败: %w", err)
|
||||
}
|
||||
untrackedMap := make(map[string]bool)
|
||||
for _, c := range changes {
|
||||
if c.Status == "?" {
|
||||
untrackedMap[c.FilePath] = true
|
||||
}
|
||||
}
|
||||
|
||||
var trackedFiles, untrackedFiles []string
|
||||
for _, f := range files {
|
||||
if untrackedMap[f] {
|
||||
untrackedFiles = append(untrackedFiles, f)
|
||||
} else {
|
||||
trackedFiles = append(trackedFiles, f)
|
||||
}
|
||||
}
|
||||
if len(trackedFiles) > 0 {
|
||||
if _, err := s.gitClient.Restore(path, trackedFiles...); err != nil {
|
||||
return fmt.Errorf("还原文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
if len(untrackedFiles) > 0 {
|
||||
if _, err := s.gitClient.CleanFiles(path, untrackedFiles...); err != nil {
|
||||
return fmt.Errorf("删除未跟踪文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFileDiffStaged 获取已暂存文件的 diff
|
||||
func (s *AppService) GetFileDiffStaged(projectPath, filePath string) (string, error) {
|
||||
return s.gitClient.DiffStagedFile(projectPath, filePath)
|
||||
}
|
||||
|
||||
// PullProject 拉取项目(当前分支)
|
||||
func (s *AppService) PullProject(path string) (string, error) {
|
||||
branch, err := s.gitClient.Branch(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取当前分支失败: %w", err)
|
||||
}
|
||||
return s.gitClient.Run(path, "pull", "origin", strings.TrimSpace(branch))
|
||||
}
|
||||
|
||||
// PushProject 推送项目(当前分支)
|
||||
func (s *AppService) PushProject(path string) (string, error) {
|
||||
branch, err := s.gitClient.Branch(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取当前分支失败: %w", err)
|
||||
}
|
||||
return s.gitClient.Run(path, "push", "origin", strings.TrimSpace(branch))
|
||||
}
|
||||
|
||||
// GetCommitDiff 获取指定提交的 diff
|
||||
func (s *AppService) GetCommitDiff(path, hash string) (string, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
return s.gitClient.CommitShow(path, hash)
|
||||
}
|
||||
|
||||
// CommitFileInfo 提交中的文件变更信息
|
||||
type CommitFileInfo struct {
|
||||
Status string `json:"status"`
|
||||
FilePath string `json:"filePath"`
|
||||
}
|
||||
|
||||
// GetCommitFiles 获取指定提交中变更的文件列表
|
||||
func (s *AppService) GetCommitFiles(path, hash string) ([]CommitFileInfo, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
out, err := s.gitClient.CommitFiles(path, hash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取提交文件列表失败: %w", err)
|
||||
}
|
||||
var files []CommitFileInfo
|
||||
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
files = append(files, CommitFileInfo{
|
||||
Status: parts[0],
|
||||
FilePath: parts[len(parts)-1],
|
||||
})
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// GetCommitFileDiff 获取指定提交中某个文件的 diff
|
||||
func (s *AppService) GetCommitFileDiff(path, hash, filePath string) (string, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
return s.gitClient.CommitFileDiff(path, hash, filePath)
|
||||
}
|
||||
|
||||
// FetchProject 拉取远程信息
|
||||
func (s *AppService) FetchProject(path string) (string, error) {
|
||||
return s.gitClient.Fetch(path)
|
||||
}
|
||||
|
||||
// CommitLog 提交记录
|
||||
type CommitLog struct {
|
||||
Hash string `json:"hash"`
|
||||
ShortHash string `json:"shortHash"`
|
||||
Author string `json:"author"`
|
||||
Email string `json:"email"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// BranchInfo 分支信息
|
||||
type BranchInfo struct {
|
||||
Name string `json:"name"`
|
||||
Current bool `json:"current"`
|
||||
}
|
||||
|
||||
// GetCommitLog 获取提交历史
|
||||
func (s *AppService) GetCommitLog(path string, count int) ([]CommitLog, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
if count <= 0 {
|
||||
count = 50
|
||||
}
|
||||
out, err := s.gitClient.Log(path, count)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取提交历史失败: %w", err)
|
||||
}
|
||||
return parseCommitLog(out), nil
|
||||
}
|
||||
|
||||
func parseCommitLog(output string) []CommitLog {
|
||||
var logs []CommitLog
|
||||
entries := strings.Split(output, "---END---")
|
||||
for _, entry := range entries {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
lines := strings.SplitN(entry, "\n", 6)
|
||||
if len(lines) < 6 {
|
||||
continue
|
||||
}
|
||||
var ts int64
|
||||
fmt.Sscanf(lines[4], "%d", &ts)
|
||||
logs = append(logs, CommitLog{
|
||||
Hash: lines[0],
|
||||
ShortHash: lines[1],
|
||||
Author: lines[2],
|
||||
Email: lines[3],
|
||||
Timestamp: ts,
|
||||
Message: lines[5],
|
||||
})
|
||||
}
|
||||
return logs
|
||||
}
|
||||
|
||||
// GetBranches 获取所有本地分支
|
||||
func (s *AppService) GetBranches(path string) ([]BranchInfo, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
out, err := s.gitClient.BranchList(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取分支列表失败: %w", err)
|
||||
}
|
||||
var branches []BranchInfo
|
||||
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "\t", 2)
|
||||
name := parts[0]
|
||||
current := len(parts) > 1 && strings.TrimSpace(parts[1]) == "*"
|
||||
branches = append(branches, BranchInfo{Name: name, Current: current})
|
||||
}
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
// ResetProject 版本回滚(git reset)
|
||||
// mode: "hard"(丢弃所有更改), "soft"(保留更改到暂存区), "mixed"(保留更改到工作区)
|
||||
func (s *AppService) ResetProject(path, hash, mode string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
if strings.TrimSpace(hash) == "" {
|
||||
return fmt.Errorf("提交哈希不能为空")
|
||||
}
|
||||
allowed := map[string]bool{"hard": true, "soft": true, "mixed": true}
|
||||
if !allowed[mode] {
|
||||
mode = "hard"
|
||||
}
|
||||
_, err := s.gitClient.ResetToCommit(path, strings.TrimSpace(hash), mode)
|
||||
return err
|
||||
}
|
||||
|
||||
// SwitchBranch 切换分支
|
||||
func (s *AppService) SwitchBranch(path, branch string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
if strings.TrimSpace(branch) == "" {
|
||||
return fmt.Errorf("分支名不能为空")
|
||||
}
|
||||
_, err := s.gitClient.Checkout(path, strings.TrimSpace(branch))
|
||||
return err
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zhuy1228/GitPilot/internal"
|
||||
@@ -21,6 +23,7 @@ func NewGitClient() *GitClient {
|
||||
return &GitClient{
|
||||
Enabled: proxy.Enabled,
|
||||
Proxy: proxy.Protocol + "://" + proxy.Server,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +31,12 @@ func NewGitClient() *GitClient {
|
||||
func (g *GitClient) Run(path string, args ...string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
|
||||
defer cancel()
|
||||
header := []string{"-C", path, "git"}
|
||||
header := []string{"-C", path, "-c", "core.quotePath=false"}
|
||||
if g.Enabled {
|
||||
header = append(header, "-c", "http.proxy="+g.Proxy, "-c", "https.proxy="+g.Proxy)
|
||||
}
|
||||
argsArr := append(header, args...)
|
||||
log.Println(argsArr)
|
||||
cmd := exec.CommandContext(ctx, "git", argsArr...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
@@ -46,3 +50,279 @@ func (g *GitClient) Run(path string, args ...string) (string, error) {
|
||||
}
|
||||
return stdout.String(), nil
|
||||
}
|
||||
|
||||
func (g *GitClient) Pull(path string) (string, error) {
|
||||
return g.Run(path, "pull")
|
||||
}
|
||||
|
||||
func (g *GitClient) Push(path string) (string, error) {
|
||||
return g.Run(path, "push")
|
||||
}
|
||||
|
||||
func (g *GitClient) Status(path string) (string, error) {
|
||||
return g.Run(path, "status")
|
||||
}
|
||||
|
||||
func (g *GitClient) Clone(repoURL, path string) (string, error) {
|
||||
return g.Run(".", "clone", repoURL, path)
|
||||
}
|
||||
|
||||
func (g *GitClient) Fetch(path string) (string, error) {
|
||||
return g.Run(path, "fetch", "--all")
|
||||
}
|
||||
|
||||
func (g *GitClient) RemoteURL(path string) (string, error) {
|
||||
out, err := g.Run(path, "config", "--get", "remote.origin.url")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(out), nil
|
||||
}
|
||||
|
||||
func (g *GitClient) Branch(path string) (string, error) {
|
||||
return g.Run(path, "rev-parse", "--abbrev-ref", "HEAD")
|
||||
}
|
||||
|
||||
func (g *GitClient) Add(path string, files ...string) (string, error) {
|
||||
args := append([]string{"add"}, files...)
|
||||
return g.Run(path, args...)
|
||||
}
|
||||
|
||||
// Reset 取消暂存指定文件 (git reset HEAD -- files...)
|
||||
func (g *GitClient) Reset(path string, files ...string) (string, error) {
|
||||
args := append([]string{"reset", "HEAD", "--"}, files...)
|
||||
return g.Run(path, args...)
|
||||
}
|
||||
|
||||
// Commit 提交暂存区的更改
|
||||
func (g *GitClient) Commit(path, message string) (string, error) {
|
||||
return g.Run(path, "commit", "-m", message)
|
||||
}
|
||||
|
||||
// Restore 丢弃工作区指定文件的更改 (git checkout -- files...)
|
||||
func (g *GitClient) Restore(path string, files ...string) (string, error) {
|
||||
args := append([]string{"checkout", "--"}, files...)
|
||||
return g.Run(path, args...)
|
||||
}
|
||||
|
||||
// CleanFiles 删除未跟踪的文件 (git clean -f -- files...)
|
||||
func (g *GitClient) CleanFiles(path string, files ...string) (string, error) {
|
||||
args := append([]string{"clean", "-f", "--"}, files...)
|
||||
return g.Run(path, args...)
|
||||
}
|
||||
|
||||
// Log 获取提交历史 (git log --oneline --format=... -n count)
|
||||
func (g *GitClient) Log(path string, count int) (string, error) {
|
||||
return g.Run(path, "log",
|
||||
fmt.Sprintf("--max-count=%d", count),
|
||||
"--format=%H%n%h%n%an%n%ae%n%at%n%s%n---END---",
|
||||
)
|
||||
}
|
||||
|
||||
// BranchList 获取所有本地分支
|
||||
func (g *GitClient) BranchList(path string) (string, error) {
|
||||
return g.Run(path, "branch", "--format=%(refname:short)\t%(HEAD)")
|
||||
}
|
||||
|
||||
// Checkout 切换分支
|
||||
func (g *GitClient) Checkout(path, branch string) (string, error) {
|
||||
return g.Run(path, "checkout", branch)
|
||||
}
|
||||
|
||||
// ResetToCommit 将 HEAD 重置到指定提交 (git reset --<mode> <hash>)
|
||||
// mode: hard / soft / mixed
|
||||
func (g *GitClient) ResetToCommit(path, hash, mode string) (string, error) {
|
||||
if mode == "" {
|
||||
mode = "hard"
|
||||
}
|
||||
return g.Run(path, "reset", "--"+mode, hash)
|
||||
}
|
||||
|
||||
// CommitShow 获取指定提交的详细 diff
|
||||
func (g *GitClient) CommitShow(path, hash string) (string, error) {
|
||||
return g.Run(path, "show", "--format=%b", hash)
|
||||
}
|
||||
|
||||
// CommitFiles 获取指定提交中变更的文件列表 (git diff-tree --root --no-commit-id -r --name-status <hash>)
|
||||
// --root 使根提交(第一次提交)也能与空树对比,列出所有新增文件
|
||||
func (g *GitClient) CommitFiles(path, hash string) (string, error) {
|
||||
return g.Run(path, "diff-tree", "--root", "--no-commit-id", "-r", "--name-status", hash)
|
||||
}
|
||||
|
||||
// CommitFileDiff 获取指定提交中某个文件的 diff (git show <hash> -- <file>)
|
||||
func (g *GitClient) CommitFileDiff(path, hash, filePath string) (string, error) {
|
||||
return g.Run(path, "show", "--format=", hash, "--", filePath)
|
||||
}
|
||||
|
||||
// FileChange 表示单个文件的变更信息
|
||||
type FileChange struct {
|
||||
// 变更状态: M(修改), A(新增), D(删除), R(重命名), C(复制), U(未合并), ?(未跟踪)
|
||||
Status string
|
||||
// 文件路径
|
||||
FilePath string
|
||||
// 重命名/复制时的原始路径
|
||||
OrigPath string
|
||||
// 是否为暂存区变更
|
||||
Staged bool
|
||||
}
|
||||
|
||||
// ChangedFiles 获取工作区中所有变更的文件列表(包含新增、修改、删除、重命名、未跟踪等)
|
||||
// 同一文件在暂存区和工作区都有变更时,会拆分为两条记录
|
||||
func (g *GitClient) ChangedFiles(path string) ([]FileChange, error) {
|
||||
out, err := g.Run(path, "status", "--porcelain", "-uall")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parsePorcelainStatus(out), nil
|
||||
}
|
||||
|
||||
// StagedFiles 获取已暂存的文件变更列表
|
||||
func (g *GitClient) StagedFiles(path string) ([]FileChange, error) {
|
||||
out, err := g.Run(path, "diff", "--cached", "--name-status")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseNameStatus(out), nil
|
||||
}
|
||||
|
||||
// DiffStat 获取工作区文件变更的统计信息(增删行数)
|
||||
func (g *GitClient) DiffStat(path string) (string, error) {
|
||||
return g.Run(path, "diff", "--stat")
|
||||
}
|
||||
|
||||
// DiffFile 获取指定文件的详细差异内容
|
||||
func (g *GitClient) DiffFile(path, filePath string) (string, error) {
|
||||
return g.Run(path, "diff", "--", filePath)
|
||||
}
|
||||
|
||||
// DiffStagedFile 获取指定已暂存文件的详细差异内容
|
||||
func (g *GitClient) DiffStagedFile(path, filePath string) (string, error) {
|
||||
return g.Run(path, "diff", "--cached", "--", filePath)
|
||||
}
|
||||
|
||||
// DiffCommit 获取两个提交之间的文件变更列表
|
||||
func (g *GitClient) DiffCommit(path, from, to string) ([]FileChange, error) {
|
||||
out, err := g.Run(path, "diff", "--name-status", from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseNameStatus(out), nil
|
||||
}
|
||||
|
||||
// parsePorcelainStatus 解析 git status --porcelain 的输出
|
||||
// porcelain 格式: XY filename, X=暂存区状态, Y=工作区状态
|
||||
// 同一文件若暂存区和工作区都有变更,会拆分为两条记录
|
||||
func parsePorcelainStatus(output string) []FileChange {
|
||||
var changes []FileChange
|
||||
// 注意: 不能用 TrimSpace,porcelain 格式中行首空格表示"暂存区无变更",TrimSpace 会错误地吃掉第一行的前导空格
|
||||
lines := strings.Split(strings.TrimRight(output, "\n\r "), "\n")
|
||||
for _, line := range lines {
|
||||
if len(line) < 3 {
|
||||
continue
|
||||
}
|
||||
indexStatus := string(line[0])
|
||||
workTreeStatus := string(line[1])
|
||||
filePath := strings.TrimSpace(line[2:])
|
||||
|
||||
// 处理重命名情况 "R old -> new"
|
||||
var origPath string
|
||||
if strings.Contains(filePath, " -> ") {
|
||||
parts := strings.SplitN(filePath, " -> ", 2)
|
||||
origPath = parts[0]
|
||||
filePath = parts[1]
|
||||
}
|
||||
|
||||
// 未跟踪文件 (??) 只产生一条记录
|
||||
if indexStatus == "?" {
|
||||
changes = append(changes, FileChange{
|
||||
Status: "?",
|
||||
FilePath: filePath,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// 暂存区有变更 (X 不为空格)
|
||||
if indexStatus != " " {
|
||||
changes = append(changes, FileChange{
|
||||
Status: indexStatus,
|
||||
FilePath: filePath,
|
||||
OrigPath: origPath,
|
||||
Staged: true,
|
||||
})
|
||||
}
|
||||
|
||||
// 工作区有变更 (Y 不为空格)
|
||||
if workTreeStatus != " " {
|
||||
changes = append(changes, FileChange{
|
||||
Status: workTreeStatus,
|
||||
FilePath: filePath,
|
||||
OrigPath: origPath,
|
||||
Staged: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
// parseNameStatus 解析 git diff --name-status 的输出
|
||||
func parseNameStatus(output string) []FileChange {
|
||||
var changes []FileChange
|
||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
change := FileChange{
|
||||
Status: parts[0],
|
||||
FilePath: parts[len(parts)-1],
|
||||
}
|
||||
// 重命名/复制时有原路径: R100 old_path new_path
|
||||
if len(parts) == 3 && (strings.HasPrefix(parts[0], "R") || strings.HasPrefix(parts[0], "C")) {
|
||||
change.OrigPath = parts[1]
|
||||
change.FilePath = parts[2]
|
||||
}
|
||||
changes = append(changes, change)
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
// StatusText 返回变更状态的中文描述
|
||||
func (fc FileChange) StatusText() string {
|
||||
switch {
|
||||
case strings.HasPrefix(fc.Status, "R"):
|
||||
return "重命名"
|
||||
case strings.HasPrefix(fc.Status, "C"):
|
||||
return "复制"
|
||||
default:
|
||||
statusMap := map[string]string{
|
||||
"M": "已修改",
|
||||
"A": "新增",
|
||||
"D": "已删除",
|
||||
"U": "未合并",
|
||||
"?": "未跟踪",
|
||||
}
|
||||
if text, ok := statusMap[fc.Status]; ok {
|
||||
return text
|
||||
}
|
||||
return fc.Status
|
||||
}
|
||||
}
|
||||
|
||||
// String 返回文件变更的可读字符串
|
||||
func (fc FileChange) String() string {
|
||||
area := "工作区"
|
||||
if fc.Staged {
|
||||
area = "暂存区"
|
||||
}
|
||||
if fc.Status == "?" {
|
||||
return fmt.Sprintf("[%s] %s", fc.StatusText(), fc.FilePath)
|
||||
}
|
||||
if fc.OrigPath != "" {
|
||||
return fmt.Sprintf("[%s] (%s) %s → %s", fc.StatusText(), area, fc.OrigPath, fc.FilePath)
|
||||
}
|
||||
return fmt.Sprintf("[%s] (%s) %s", fc.StatusText(), area, fc.FilePath)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user