feat: 新增 git retime,把指定日期范围的提交时间改到 21:00-23:00 并强推

- 保留每个提交的原日期,只重排时分秒到 21:00-23:00;同日严格递增,
  间隔 5-40 分钟且秒位随机;窗口不足时自动压紧并在预览中标注
- 支持 --window/--since/--until、--interval(含 match-remote)、--seed、
  --remote、--branch、--no-push
- 安全措施:默认只预览(退出码 2)、脏工作区拒绝、改写前创建
  refs/kopsc-backup/<分支>-<时间戳> 备份、推送前比对 tree 哈希、
  只用 --force-with-lease
- BuiltinRequest 增加 Confirmed,使内置命令能获知 CLI 已确认 --yes
- 文档:新增 README.git.md,主 README 补充导航与示例
This commit is contained in:
2026-11-01 09:49:03 +08:00
parent f62c1f4872
commit f3b90d205c
19 changed files with 2581 additions and 4 deletions
+57
View File
@@ -0,0 +1,57 @@
package executor
import (
"context"
"strings"
"testing"
)
func TestTreesMatchComparesTreeHashes(t *testing.T) {
runner := &fakeGitRunner{responses: map[string]gitResponse{
"rev-parse old^{tree}": {out: "treeA\n"},
"rev-parse same^{tree}": {out: "treeA\n"},
"rev-parse other^{tree}": {out: "treeB\n"},
}}
ctx := context.Background()
same, err := treesMatch(ctx, runner, "old", "same")
if err != nil || !same {
t.Fatalf("same trees: match=%v err=%v", same, err)
}
same, err = treesMatch(ctx, runner, "old", "other")
if err != nil || same {
t.Fatalf("different trees: match=%v err=%v", same, err)
}
}
func TestTreesMatchReportsRevParseFailure(t *testing.T) {
runner := &fakeGitRunner{responses: map[string]gitResponse{}}
_, err := treesMatch(context.Background(), runner, "missing", "other")
if err == nil {
t.Fatal("expected an error when the commit cannot be read")
}
}
func TestVerifyRewritePassesOnIdenticalContent(t *testing.T) {
runner := &fakeGitRunner{responses: map[string]gitResponse{
"rev-parse oldtip^{tree}": {out: "treeX\n"},
"rev-parse newtip^{tree}": {out: "treeX\n"},
}}
if err := verifyRewrite(context.Background(), runner, "oldtip", "newtip"); err != nil {
t.Fatalf("identical content should pass: %v", err)
}
}
func TestVerifyRewriteStopsOnContentChange(t *testing.T) {
runner := &fakeGitRunner{responses: map[string]gitResponse{
"rev-parse oldtip^{tree}": {out: "treeX\n"},
"rev-parse newtip^{tree}": {out: "treeY\n"},
}}
err := verifyRewrite(context.Background(), runner, "oldtip", "newtip")
if err == nil || !strings.Contains(err.Error(), "内容与原提交不一致") {
t.Fatalf("err = %v, want a content-mismatch error", err)
}
if !strings.Contains(err.Error(), "回滚") {
t.Errorf("error should point at the backup: %v", err)
}
}