feat: 新增冲突处理、提交搜索、批量操作功能\n\n冲突处理:\n- 检测合并状态并显示冲突提示栏\n- 冲突文件列表展示和内容编辑器\n- 支持保存冲突文件、标记已解决、中止合并、完成合并\n\n提交搜索:\n- 支持按提交信息关键字和作者搜索\n- 搜索结果与完整历史切换显示\n\n批量操作:\n- 侧边栏新增批量操作入口\n- 展示所有项目概览(分支/变更/未推送状态)\n- 支持批量 Pull/Push 并显示执行结果"
This commit is contained in:
@@ -1049,3 +1049,224 @@ func (s *AppService) UpdateAppSettings(logLevel string) error {
|
||||
s.config.Settings.LogLevel = logLevel
|
||||
return config.SaveConfig(s.config)
|
||||
}
|
||||
|
||||
// --- 冲突处理 ---
|
||||
|
||||
// ConflictFileInfo 冲突文件信息
|
||||
type ConflictFileInfo struct {
|
||||
FilePath string `json:"filePath"`
|
||||
}
|
||||
|
||||
// GetConflictFiles 获取冲突文件列表
|
||||
func (s *AppService) GetConflictFiles(path string) ([]ConflictFileInfo, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
out, err := s.gitClient.ConflictFiles(path)
|
||||
if err != nil {
|
||||
// 没有冲突文件时命令可能返回错误,返回空列表
|
||||
return []ConflictFileInfo{}, nil
|
||||
}
|
||||
var files []ConflictFileInfo
|
||||
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
files = append(files, ConflictFileInfo{FilePath: line})
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// GetConflictFileContent 获取冲突文件内容(包含冲突标记)
|
||||
func (s *AppService) GetConflictFileContent(projectPath, filePath string) (string, error) {
|
||||
fullPath := filepath.Join(projectPath, filePath)
|
||||
data, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取冲突文件失败: %w", err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// ResolveConflictFile 将冲突文件标记为已解决
|
||||
func (s *AppService) ResolveConflictFile(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.MarkResolved(path, files...)
|
||||
return err
|
||||
}
|
||||
|
||||
// SaveConflictFile 保存冲突文件内容(手动解决冲突后保存)
|
||||
func (s *AppService) SaveConflictFile(projectPath, filePath, content string) error {
|
||||
fullPath := filepath.Join(projectPath, filePath)
|
||||
return os.WriteFile(fullPath, []byte(content), 0o644)
|
||||
}
|
||||
|
||||
// AbortMerge 中止合并
|
||||
func (s *AppService) AbortMerge(path string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
_, err := s.gitClient.AbortMerge(path)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsMerging 检查是否处于合并状态
|
||||
func (s *AppService) IsMerging(path string) bool {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return s.gitClient.MergeStatus(path)
|
||||
}
|
||||
|
||||
// --- 提交搜索 ---
|
||||
|
||||
// SearchCommitLog 搜索提交历史
|
||||
func (s *AppService) SearchCommitLog(path, keyword, author string, maxCount int) ([]CommitLog, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("项目路径不存在: %s", path)
|
||||
}
|
||||
if maxCount <= 0 {
|
||||
maxCount = 100
|
||||
}
|
||||
out, err := s.gitClient.SearchCommits(path, keyword, author, maxCount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("搜索提交历史失败: %w", err)
|
||||
}
|
||||
logs := parseCommitLog(out)
|
||||
// 标记推送状态
|
||||
branch, brErr := s.gitClient.Branch(path)
|
||||
if brErr == nil {
|
||||
branch = strings.TrimSpace(branch)
|
||||
unpushedOut, upErr := s.gitClient.UnpushedCommits(path, branch)
|
||||
unpushedSet := make(map[string]bool)
|
||||
if upErr == nil {
|
||||
for _, h := range strings.Split(strings.TrimSpace(unpushedOut), "\n") {
|
||||
if h != "" {
|
||||
unpushedSet[h] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range logs {
|
||||
logs[i].Pushed = !unpushedSet[logs[i].Hash]
|
||||
}
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// --- 批量操作 ---
|
||||
|
||||
// ProjectOverview 项目概览信息(轻量级)
|
||||
type ProjectOverview struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Branch string `json:"branch"`
|
||||
HasChanges bool `json:"hasChanges"`
|
||||
Unpushed int `json:"unpushed"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// GetAllProjectOverview 获取所有项目的概览状态
|
||||
func (s *AppService) GetAllProjectOverview() []ProjectOverview {
|
||||
var results []ProjectOverview
|
||||
for platformName, platform := range s.config.Platforms {
|
||||
for _, user := range platform.Users {
|
||||
for _, proj := range user.Projects {
|
||||
overview := ProjectOverview{
|
||||
Key: platformName + "/" + user.Username + "/" + proj.Name,
|
||||
Name: proj.Name,
|
||||
Path: proj.Path,
|
||||
}
|
||||
if _, err := os.Stat(proj.Path); os.IsNotExist(err) {
|
||||
overview.Error = "路径不存在"
|
||||
results = append(results, overview)
|
||||
continue
|
||||
}
|
||||
branch, hasChanges, unpushed, err := s.gitClient.QuickStatus(proj.Path)
|
||||
if err != nil {
|
||||
overview.Error = err.Error()
|
||||
} else {
|
||||
overview.Branch = branch
|
||||
overview.HasChanges = hasChanges
|
||||
overview.Unpushed = unpushed
|
||||
}
|
||||
results = append(results, overview)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// BatchPullResult 批量 pull 结果
|
||||
type BatchPullResult struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// BatchPull 批量拉取指定项目
|
||||
func (s *AppService) BatchPull(paths []string) []BatchPullResult {
|
||||
var results []BatchPullResult
|
||||
for _, path := range paths {
|
||||
result := BatchPullResult{Path: path}
|
||||
// 从路径推断名称
|
||||
result.Name = filepath.Base(path)
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
result.Message = "路径不存在"
|
||||
results = append(results, result)
|
||||
continue
|
||||
}
|
||||
branch, err := s.gitClient.Branch(path)
|
||||
if err != nil {
|
||||
result.Message = "获取分支失败: " + err.Error()
|
||||
results = append(results, result)
|
||||
continue
|
||||
}
|
||||
_, err = s.gitClient.Run(path, "pull", "origin", strings.TrimSpace(branch))
|
||||
if err != nil {
|
||||
result.Message = err.Error()
|
||||
} else {
|
||||
result.Success = true
|
||||
result.Message = "拉取成功"
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// BatchPush 批量推送指定项目
|
||||
func (s *AppService) BatchPush(paths []string) []BatchPullResult {
|
||||
var results []BatchPullResult
|
||||
for _, path := range paths {
|
||||
result := BatchPullResult{Path: path}
|
||||
result.Name = filepath.Base(path)
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
result.Message = "路径不存在"
|
||||
results = append(results, result)
|
||||
continue
|
||||
}
|
||||
branch, err := s.gitClient.Branch(path)
|
||||
if err != nil {
|
||||
result.Message = "获取分支失败: " + err.Error()
|
||||
results = append(results, result)
|
||||
continue
|
||||
}
|
||||
_, err = s.gitClient.Run(path, "push", "origin", strings.TrimSpace(branch))
|
||||
if err != nil {
|
||||
result.Message = err.Error()
|
||||
} else {
|
||||
result.Success = true
|
||||
result.Message = "推送成功"
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -443,6 +443,79 @@ func (g *GitClient) SetGitGlobalConfig(key, value string) (string, error) {
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
|
||||
// --- 冲突处理 ---
|
||||
|
||||
// ConflictFiles 获取冲突文件列表 (git diff --name-only --diff-filter=U)
|
||||
func (g *GitClient) ConflictFiles(path string) (string, error) {
|
||||
return g.Run(path, "diff", "--name-only", "--diff-filter=U")
|
||||
}
|
||||
|
||||
// GetConflictContent 获取冲突文件的完整内容(含冲突标记)
|
||||
func (g *GitClient) GetConflictContent(path, filePath string) (string, error) {
|
||||
return g.Run(path, "show", ":0:"+filePath)
|
||||
}
|
||||
|
||||
// MarkResolved 将冲突文件标记为已解决 (git add <file>)
|
||||
func (g *GitClient) MarkResolved(path string, files ...string) (string, error) {
|
||||
args := append([]string{"add"}, files...)
|
||||
return g.Run(path, args...)
|
||||
}
|
||||
|
||||
// AbortMerge 中止合并 (git merge --abort)
|
||||
func (g *GitClient) AbortMerge(path string) (string, error) {
|
||||
return g.Run(path, "merge", "--abort")
|
||||
}
|
||||
|
||||
// MergeStatus 检查是否处于合并状态
|
||||
func (g *GitClient) MergeStatus(path string) bool {
|
||||
// 检查 .git/MERGE_HEAD 文件是否存在
|
||||
_, err := g.Run(path, "rev-parse", "--verify", "MERGE_HEAD")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// --- 提交搜索 ---
|
||||
|
||||
// SearchCommits 搜索提交历史 (git log --grep / --author / --after / --before)
|
||||
func (g *GitClient) SearchCommits(path string, keyword, author string, maxCount int) (string, error) {
|
||||
args := []string{"log"}
|
||||
if maxCount > 0 {
|
||||
args = append(args, fmt.Sprintf("--max-count=%d", maxCount))
|
||||
}
|
||||
if keyword != "" {
|
||||
args = append(args, "--grep="+keyword, "-i")
|
||||
}
|
||||
if author != "" {
|
||||
args = append(args, "--author="+author)
|
||||
}
|
||||
args = append(args, "--format=%H%n%h%n%an%n%ae%n%at%n%s%n---END---")
|
||||
return g.Run(path, args...)
|
||||
}
|
||||
|
||||
// --- 批量操作 ---
|
||||
|
||||
// QuickStatus 快速获取分支名和是否有变更(轻量级)
|
||||
func (g *GitClient) QuickStatus(path string) (branch string, hasChanges bool, unpushed int, err error) {
|
||||
branchOut, err := g.Branch(path)
|
||||
if err != nil {
|
||||
return "", false, 0, err
|
||||
}
|
||||
branch = strings.TrimSpace(branchOut)
|
||||
|
||||
// 检查是否有变更
|
||||
statusOut, err := g.Run(path, "status", "--porcelain")
|
||||
if err == nil {
|
||||
hasChanges = strings.TrimSpace(statusOut) != ""
|
||||
}
|
||||
|
||||
// 检查未推送提交数
|
||||
unpushedOut, err := g.Run(path, "rev-list", "--count", "origin/"+branch+"..HEAD")
|
||||
if err == nil {
|
||||
fmt.Sscanf(strings.TrimSpace(unpushedOut), "%d", &unpushed)
|
||||
}
|
||||
|
||||
return branch, hasChanges, unpushed, nil
|
||||
}
|
||||
|
||||
// StatusText 返回变更状态的中文描述
|
||||
func (fc FileChange) StatusText() string {
|
||||
switch {
|
||||
|
||||
Reference in New Issue
Block a user