Files
odboy f3b90d205c 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 补充导航与示例
2026-11-01 09:49:03 +08:00

123 lines
3.8 KiB
Go

package executor
import (
"context"
"strings"
"testing"
"time"
)
// rewriteFixture builds a fake git whose two commits c1 -> c2 live on main.
func rewriteFixture() *fakeGitRunner {
meta := func(hash, tree, parents string) map[string]gitResponse {
return map[string]gitResponse{
"log -1 --format=" + rawCommitFormat + " " + hash: {
out: strings.Join([]string{hash, tree, parents, "Odboy", "tianjun@odboy.cn", "Odboy", "tianjun@odboy.cn", "subject of " + hash}, "\t") + "\n",
},
"log -1 --format=%B " + hash: {out: "subject of " + hash + "\n\nbody line\n"},
}
}
responses := map[string]gitResponse{}
for _, entry := range []map[string]gitResponse{meta("c1", "tree1", "base"), meta("c2", "tree2", "c1")} {
for k, v := range entry {
responses[k] = v
}
}
// New commits are named deterministically by the tree they carry.
responses["commit-tree tree1 -p base -m subject of c1\n\nbody line"] = gitResponse{out: "n1\n"}
responses["commit-tree tree2 -p n1 -m subject of c2\n\nbody line"] = gitResponse{out: "n2\n"}
return &fakeGitRunner{responses: responses}
}
func TestRewriteChainRebuildsInOrder(t *testing.T) {
runner := rewriteFixture()
plan := retimePlan{
Commits: []retime{
{Hash: "c1", Short: "c1", NewTime: time.Date(2026, 10, 1, 21, 7, 43, 0, cst)},
{Hash: "c2", Short: "c2", NewTime: time.Date(2026, 10, 1, 21, 32, 11, 0, cst)},
},
Branch: "main",
Remote: "origin",
}
tip, rebuilt, err := rewriteChain(context.Background(), runner, plan)
if err != nil {
t.Fatalf("rewriteChain: %v", err)
}
if tip != "n2" {
t.Errorf("tip = %q, want n2", tip)
}
if len(rebuilt) != 2 || rebuilt[0].New != "n1" || rebuilt[1].New != "n2" {
t.Fatalf("rebuilt = %+v", rebuilt)
}
// The dates must be passed to git exactly as planned, with the original
// identities kept.
if len(runner.envs) != 2 {
t.Fatalf("expected env for two commits, got %d", len(runner.envs))
}
joined := strings.Join(runner.envs[0], " ")
for _, want := range []string{
"GIT_AUTHOR_NAME=Odboy",
"GIT_AUTHOR_EMAIL=tianjun@odboy.cn",
"GIT_AUTHOR_DATE=2026-10-01T21:07:43+08:00",
"GIT_COMMITTER_DATE=2026-10-01T21:07:43+08:00",
} {
if !strings.Contains(joined, want) {
t.Errorf("first commit env missing %q: %v", want, runner.envs[0])
}
}
if !strings.Contains(strings.Join(runner.envs[1], " "), "GIT_AUTHOR_DATE=2026-10-01T21:32:11+08:00") {
t.Errorf("second commit env = %v", runner.envs[1])
}
}
func TestRebuildCommitKeepsTreeParentsAndMessage(t *testing.T) {
var gotArgs []string
var gotEnv []string
runner := &fakeGitRunner{
responses: map[string]gitResponse{
"commit-tree T -p P1 -p P2 -m msg line 1\nline 2": {out: "newhash\n"},
},
}
orig := runner.RunEnv
_ = orig
meta := commitMeta{
Tree: "T", Parents: []string{"P1", "P2"},
AuthorName: "A", AuthorEmail: "a@e", CommitName: "C", CommitEmail: "c@e",
Body: "msg line 1\nline 2",
}
hash, err := rebuildCommit(context.Background(), runner, meta, time.Date(2026, 10, 1, 21, 0, 0, 0, cst))
if err != nil {
t.Fatalf("rebuildCommit: %v", err)
}
if hash != "newhash" {
t.Errorf("hash = %q", hash)
}
gotArgs = append(gotArgs, runner.calls...)
gotEnv = append(gotEnv, strings.Join(runner.envs[0], " "))
if !strings.Contains(gotEnv[0], "GIT_COMMITTER_EMAIL=c@e") {
t.Errorf("env = %v", gotEnv)
}
}
func TestMoveBranch(t *testing.T) {
runner := &fakeGitRunner{responses: map[string]gitResponse{
"update-ref refs/heads/main n2": {out: ""},
}}
if err := moveBranch(context.Background(), runner, "main", "n2"); err != nil {
t.Fatalf("moveBranch: %v", err)
}
if len(runner.calls) != 1 || runner.calls[0] != "update-ref refs/heads/main n2" {
t.Fatalf("calls = %v", runner.calls)
}
}
func TestRewriteChainEmptyPlan(t *testing.T) {
if _, _, err := rewriteChain(context.Background(), rewriteFixture(), retimePlan{}); err == nil {
t.Fatal("expected an error for an empty plan")
}
}