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:
@@ -0,0 +1,88 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newStateRunner() *fakeGitRunner {
|
||||
return &fakeGitRunner{responses: map[string]gitResponse{
|
||||
"rev-parse --abbrev-ref HEAD": {out: "main\n"},
|
||||
"remote": {out: "origin\nupstream\n"},
|
||||
"status --porcelain": {out: ""},
|
||||
"for-each-ref --format=%(upstream) refs/heads/main": {out: "refs/remotes/origin/main\n"},
|
||||
"for-each-ref --format=%(upstream) refs/heads/feature": {out: "\n"},
|
||||
}}
|
||||
}
|
||||
|
||||
func TestCurrentBranch(t *testing.T) {
|
||||
branch, err := currentBranch(context.Background(), newStateRunner())
|
||||
if err != nil || branch != "main" {
|
||||
t.Fatalf("branch = %q, err = %v", branch, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentBranchRejectsDetachedHead(t *testing.T) {
|
||||
r := &fakeGitRunner{responses: map[string]gitResponse{
|
||||
"rev-parse --abbrev-ref HEAD": {out: "HEAD\n"},
|
||||
}}
|
||||
_, err := currentBranch(context.Background(), r)
|
||||
if err == nil || !strings.Contains(err.Error(), "detached HEAD") {
|
||||
t.Fatalf("err = %v, want a detached HEAD error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemotes(t *testing.T) {
|
||||
got, err := remotes(context.Background(), newStateRunner())
|
||||
if err != nil || strings.Join(got, ",") != "origin,upstream" {
|
||||
t.Fatalf("remotes = %v, err = %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsWorktreeClean(t *testing.T) {
|
||||
clean, err := isWorktreeClean(context.Background(), newStateRunner())
|
||||
if err != nil || !clean {
|
||||
t.Fatalf("clean = %v, err = %v", clean, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsWorktreeDirty(t *testing.T) {
|
||||
r := &fakeGitRunner{responses: map[string]gitResponse{
|
||||
"status --porcelain": {out: " M internal/cli/cli.go\n?? new.txt\n"},
|
||||
}}
|
||||
clean, err := isWorktreeClean(context.Background(), r)
|
||||
if err != nil || clean {
|
||||
t.Fatalf("clean = %v, err = %v", clean, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpstreamRef(t *testing.T) {
|
||||
ref, err := upstreamRef(context.Background(), newStateRunner(), "main")
|
||||
if err != nil || ref != "refs/remotes/origin/main" {
|
||||
t.Fatalf("ref = %q, err = %v", ref, err)
|
||||
}
|
||||
// A branch without an upstream returns "" rather than an error.
|
||||
ref, err = upstreamRef(context.Background(), newStateRunner(), "feature")
|
||||
if err != nil || ref != "" {
|
||||
t.Fatalf("ref = %q, err = %v; want no upstream", ref, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitStateHelpersPropagateErrors(t *testing.T) {
|
||||
r := &fakeGitRunner{responses: map[string]gitResponse{
|
||||
"rev-parse --abbrev-ref HEAD": {errOut: "fatal: not a repo", err: errors.New("exit 128")},
|
||||
"remote": {errOut: "boom", err: errors.New("exit 1")},
|
||||
"status --porcelain": {errOut: "boom", err: errors.New("exit 1")},
|
||||
}}
|
||||
if _, err := currentBranch(context.Background(), r); err == nil || !strings.Contains(err.Error(), "not a repo") {
|
||||
t.Errorf("currentBranch err = %v", err)
|
||||
}
|
||||
if _, err := remotes(context.Background(), r); err == nil {
|
||||
t.Error("remotes should fail")
|
||||
}
|
||||
if _, err := isWorktreeClean(context.Background(), r); err == nil {
|
||||
t.Error("isWorktreeClean should fail")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user