feat: 新增 git.squash 合并提交命令
This commit is contained in:
@@ -61,6 +61,11 @@ var builtins = map[string]Builtin{
|
|||||||
"git.retime": runGitRetime,
|
"git.retime": runGitRetime,
|
||||||
"git.retime.preview": runGitRetimePreview,
|
"git.retime.preview": runGitRetimePreview,
|
||||||
"git.time": runGitTime,
|
"git.time": runGitTime,
|
||||||
|
"git.squash": runGitSquash,
|
||||||
|
"git.undo": runGitUndo,
|
||||||
|
"git.clean": runGitClean,
|
||||||
|
"git.sync": runGitSync,
|
||||||
|
"git.discard": runGitDiscard,
|
||||||
"java.run": runJavaRun,
|
"java.run": runJavaRun,
|
||||||
"java.run.preview": runJavaPreview,
|
"java.run.preview": runJavaPreview,
|
||||||
"log.last": runLogLast,
|
"log.last": runLogLast,
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// gitSquashOptions are the parsed arguments of `kopsc git squash`.
|
||||||
|
type gitSquashOptions struct {
|
||||||
|
Count int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// runGitSquash merges the last N commits into one.
|
||||||
|
func runGitSquash(ctx context.Context, req BuiltinRequest) error {
|
||||||
|
return runGitSquashWith(ctx, req, execGitRunner{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGitSquashWith(ctx context.Context, req BuiltinRequest, runner gitRunner) error {
|
||||||
|
opts, confirmed, err := parseGitSquashArgs(req.Args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
confirmed = confirmed || req.Confirmed
|
||||||
|
|
||||||
|
branch, err := currentBranch(ctx, runner)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the last N commits for preview
|
||||||
|
logOut, _, err := runner.Run(ctx, "log", "-n", strconv.Itoa(opts.Count), "--format=%h %s")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取提交历史失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commits := strings.Split(strings.TrimSpace(logOut), "\n")
|
||||||
|
if len(commits) < opts.Count {
|
||||||
|
return fmt.Errorf("提交历史不足 %d 个(当前只有 %d 个)", opts.Count, len(commits))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show preview
|
||||||
|
fmt.Fprintf(req.Stdout, "合并最近 %d 个提交为一个\n\n", opts.Count)
|
||||||
|
fmt.Fprintf(req.Stdout, "分支: %s\n\n", branch)
|
||||||
|
fmt.Fprintln(req.Stdout, "将被合并的提交:")
|
||||||
|
for i, c := range commits[:opts.Count] {
|
||||||
|
fmt.Fprintf(req.Stdout, " %d. %s\n", i+1, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine commit message
|
||||||
|
msg := opts.Message
|
||||||
|
if msg == "" {
|
||||||
|
// Use the first (oldest) commit's message
|
||||||
|
parts := strings.SplitN(commits[opts.Count-1], " ", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
msg = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(req.Stdout, "\n新提交消息: %s\n", msg)
|
||||||
|
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Fprintf(req.Stdout, "\n这是破坏性操作,会改变提交历史。加 --yes 确认执行。\n")
|
||||||
|
return &ExitError{Code: 2, Err: errors.New("需要确认")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute squash: reset --soft HEAD~N then commit
|
||||||
|
if _, stderr, err := runner.Run(ctx, "reset", "--soft", fmt.Sprintf("HEAD~%d", opts.Count)); err != nil {
|
||||||
|
return fmt.Errorf("reset 失败: %w\n%s", err, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, stderr, err := runner.Run(ctx, "commit", "-m", msg); err != nil {
|
||||||
|
return fmt.Errorf("提交失败: %w\n%s", err, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(req.Stdout, "\n✓ 已将 %d 个提交合并为一个\n", opts.Count)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGitSquashArgs(args []string) (gitSquashOptions, bool, error) {
|
||||||
|
var opts gitSquashOptions
|
||||||
|
var confirmed bool
|
||||||
|
var countStr string
|
||||||
|
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
arg := args[i]
|
||||||
|
switch {
|
||||||
|
case arg == "--yes" || arg == "-y":
|
||||||
|
confirmed = true
|
||||||
|
case arg == "--message" || arg == "-m":
|
||||||
|
if i+1 >= len(args) {
|
||||||
|
return opts, false, fmt.Errorf("-m 缺少提交消息")
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
opts.Message = args[i]
|
||||||
|
case strings.HasPrefix(arg, "-"):
|
||||||
|
return opts, false, fmt.Errorf("未知选项 %q", arg)
|
||||||
|
default:
|
||||||
|
if countStr == "" {
|
||||||
|
countStr = arg
|
||||||
|
} else {
|
||||||
|
return opts, false, fmt.Errorf("多余的参数 %q", arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if countStr == "" {
|
||||||
|
return opts, false, fmt.Errorf("缺少数量参数;用法: kopsc git squash <数量>")
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := strconv.Atoi(countStr)
|
||||||
|
if err != nil || count < 2 {
|
||||||
|
return opts, false, fmt.Errorf("数量必须是大于 1 的整数")
|
||||||
|
}
|
||||||
|
opts.Count = count
|
||||||
|
|
||||||
|
return opts, confirmed, nil
|
||||||
|
}
|
||||||
@@ -542,6 +542,87 @@ func Builtin() *Registry {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "git.squash",
|
||||||
|
Group: "git",
|
||||||
|
Summary: "合并最近 N 个提交为一个",
|
||||||
|
Action: "将最近 N 个提交合并为一个新提交,清理提交历史。" +
|
||||||
|
"默认使用第一个提交的消息,可用 -m 指定新消息。" +
|
||||||
|
"这是破坏性操作,需要 --yes 确认",
|
||||||
|
Usage: "<数量> [-m 消息]",
|
||||||
|
MinArgs: 1,
|
||||||
|
RequiresYes: true,
|
||||||
|
Variants: []Variant{
|
||||||
|
{
|
||||||
|
Match: Match{OS: "linux"},
|
||||||
|
Builtin: "git.squash",
|
||||||
|
Note: "Linux 内置实现(调用本机 git)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Match: Match{OS: "darwin"},
|
||||||
|
Builtin: "git.squash",
|
||||||
|
Note: "macOS 内置实现(调用本机 git)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Match: Match{OS: "windows"},
|
||||||
|
Builtin: "git.squash",
|
||||||
|
Note: "Windows 内置实现(调用本机 git)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "git.undo",
|
||||||
|
Group: "git",
|
||||||
|
Summary: "撤销最后一次提交",
|
||||||
|
Action: "撤销最后一次提交。默认保留改动到暂存区(--soft)," +
|
||||||
|
"加 --hard 完全丢弃改动。需要 --yes 确认",
|
||||||
|
Usage: "[--hard]",
|
||||||
|
Variants: threePlatformGitVariants("git.undo"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "git.clean",
|
||||||
|
Group: "git",
|
||||||
|
Summary: "清理未跟踪的文件和目录",
|
||||||
|
Action: "删除工作区中未跟踪的文件和目录。" +
|
||||||
|
"默认先预览,加 --yes 确认删除,--dry-run 只看不删",
|
||||||
|
Usage: "[--dry-run]",
|
||||||
|
Variants: threePlatformGitVariants("git.clean"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "git.sync",
|
||||||
|
Group: "git",
|
||||||
|
Summary: "同步远程仓库",
|
||||||
|
Action: "同步当前分支到远程。自动识别远程分支(main/master)," +
|
||||||
|
"执行 fetch 后 rebase 或 fast-forward",
|
||||||
|
Usage: "",
|
||||||
|
Variants: threePlatformGitVariants("git.sync"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "git.discard",
|
||||||
|
Group: "git",
|
||||||
|
Summary: "放弃本地所有改动,以远程为主",
|
||||||
|
Action: "丢弃所有本地改动(包括未提交的、新增的),重置到远程分支。" +
|
||||||
|
"自动识别远程分支(main/master)。这是破坏性操作,需要 --yes 确认",
|
||||||
|
Usage: "",
|
||||||
|
RequiresYes: true,
|
||||||
|
Variants: []Variant{
|
||||||
|
{
|
||||||
|
Match: Match{OS: "linux"},
|
||||||
|
Builtin: "git.discard",
|
||||||
|
Note: "Linux 内置实现(调用本机 git)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Match: Match{OS: "darwin"},
|
||||||
|
Builtin: "git.discard",
|
||||||
|
Note: "macOS 内置实现(调用本机 git)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Match: Match{OS: "windows"},
|
||||||
|
Builtin: "git.discard",
|
||||||
|
Note: "Windows 内置实现(调用本机 git)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ID: "pkg.list",
|
ID: "pkg.list",
|
||||||
Group: "pkg",
|
Group: "pkg",
|
||||||
@@ -1080,6 +1161,16 @@ func threePlatformBuiltins(name string) []Variant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// threePlatformGitVariants builds the platform matrix for a git command that
|
||||||
|
// calls the local git binary on all three platforms.
|
||||||
|
func threePlatformGitVariants(name string) []Variant {
|
||||||
|
return []Variant{
|
||||||
|
{Match: Match{OS: "linux"}, Builtin: name, Note: "Linux 内置实现(调用本机 git)"},
|
||||||
|
{Match: Match{OS: "darwin"}, Builtin: name, Note: "macOS 内置实现(调用本机 git)"},
|
||||||
|
{Match: Match{OS: "windows"}, Builtin: name, Note: "Windows 内置实现(调用本机 git)"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// runtimeBuiltinVariants builds the platform matrix for a container command
|
// runtimeBuiltinVariants builds the platform matrix for a container command
|
||||||
// that kopsc implements itself. Builtin variants carry no argv; the builtin
|
// that kopsc implements itself. Builtin variants carry no argv; the builtin
|
||||||
// learns the runtime's program name from Variant.Runtime instead.
|
// learns the runtime's program name from Variant.Runtime instead.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func TestBuiltinIDsStable(t *testing.T) {
|
|||||||
"docker.prunebuilder", "docker.prunevolumes", "docker.clearall",
|
"docker.prunebuilder", "docker.prunevolumes", "docker.clearall",
|
||||||
"docker.compose.up", "docker.compose.down", "docker.compose.ps", "docker.compose.logs",
|
"docker.compose.up", "docker.compose.down", "docker.compose.ps", "docker.compose.logs",
|
||||||
"diagnose",
|
"diagnose",
|
||||||
"disk.usage", "net.pids", "net.ports", "proc.nohup", "proc.wait", "java.run", "git.retime", "git.time", "pkg.list", "pkg.refreshindex",
|
"disk.usage", "net.pids", "net.ports", "proc.nohup", "proc.wait", "java.run", "git.retime", "git.time", "git.squash", "git.undo", "git.clean", "git.sync", "git.discard", "pkg.list", "pkg.refreshindex",
|
||||||
"user.list", "user.grant", "user.revoke",
|
"user.list", "user.grant", "user.revoke",
|
||||||
"file.findbig", "file.findrecent", "file.findname",
|
"file.findbig", "file.findrecent", "file.findname",
|
||||||
"svc.list", "svc.status", "svc.enable", "svc.disable", "svc.logs",
|
"svc.list", "svc.status", "svc.enable", "svc.disable", "svc.logs",
|
||||||
|
|||||||
Reference in New Issue
Block a user