package git import ( "context" "strings" ) // HasChanges reports whether the working tree at workspaceRoot has any // uncommitted changes (staged, unstaged, or untracked). func HasChanges(ctx context.Context, workspaceRoot string) (bool, error) { out, _, err := runGit(ctx, workspaceRoot, "status", "--porcelain") if err != nil { return false, err } return strings.TrimSpace(out) != "", nil } // MergePreviewOpts carries options for MergePreview. type MergePreviewOpts struct { WorkspaceRoot string Branch string // branch to preview-merge into HEAD } // MergePreviewResult holds the outcome of a preview merge attempt. // When Clean is false, Conflicts lists the raw CONFLICT lines from git output. // Faithful to the TS implementation: the repo is left in merge-in-progress // state so the caller can inspect it; run `git merge --abort` to undo. type MergePreviewResult struct { Clean bool Conflicts []string Output string } // MergePreview runs git merge --no-commit --no-ff in WorkspaceRoot. // On success (no conflicts) Clean is true. On conflict, Clean is false and // Conflicts lists the CONFLICT lines from git output. func MergePreview(ctx context.Context, opts MergePreviewOpts) (MergePreviewResult, error) { stdout, stderr, err := runGit(ctx, opts.WorkspaceRoot, "merge", "--no-commit", "--no-ff", opts.Branch) combined := strings.TrimSpace(stdout + "\n" + stderr) if err == nil { return MergePreviewResult{Clean: true, Output: combined}, nil } var conflicts []string for _, line := range strings.Split(combined, "\n") { // git outputs "CONFLICT" in English or "冲突" in localized builds. if strings.HasPrefix(line, "CONFLICT") || strings.Contains(line, "冲突") { conflicts = append(conflicts, line) } } return MergePreviewResult{ Clean: false, Conflicts: conflicts, Output: combined, }, err }