package gitx import ( "context" "errors" "os" "path/filepath" "strings" "testing" "time" ) func TestValidRef(t *testing.T) { ok := []string{"main", "v1.2.3", "feature/login", "refs/heads/x", "HEAD", "a1b2c3d", "release-2024", "_underscore"} for _, r := range ok { if !ValidRef(r) { t.Errorf("%q 应为合法 ref", r) } } bad := []string{"", "-x", "--upload-pack=evil", "a b", "a..b", "a~1", "a^", "a:b", "a?b", "a*b", "a[b", `a\b`, "a@{0}", "!bang", "a\nb", strings.Repeat("x", 256)} for _, r := range bad { if ValidRef(r) { t.Errorf("%q 应被判为非法 ref", r) } } } func TestValidRepoName(t *testing.T) { for _, n := range []string{"demo", "a", "my-project", "a.b_c", "Repo123"} { if !ValidRepoName(n) { t.Errorf("%q 应为合法仓库名", n) } } for _, n := range []string{"", ".hidden", "-lead", "a/b", `a\b`, "..", "a..b", "x.git", "X.GIT", "a b", strings.Repeat("x", 65)} { if ValidRepoName(n) { t.Errorf("%q 应为非法仓库名", n) } } } func TestCommandTimeoutIsApplied(t *testing.T) { old := commandTimeout() SetCommandTimeout(150 * time.Millisecond) defer SetCommandTimeout(old) dir := t.TempDir() if err := InitBare(dir, "main"); err != nil { t.Fatalf("InitBare 失败: %v", err) } // 不存在的对象上跑 log,正常会立刻返回;这里主要验证超时机制 // 不会让 run 退化成无超时。 start := time.Now() _, _ = Log(filepath.Join(dir, "missing.git"), "HEAD", 5, 0) if d := time.Since(start); d > 5*time.Second { t.Errorf("命令耗时 %v,明显超出预期", d) } } func TestRunLimitRejectsOversizedOutput(t *testing.T) { dir := t.TempDir() if err := InitBare(dir, "main"); err != nil { t.Fatalf("InitBare 失败: %v", err) } // 造一个 1MB 的文件 big := strings.Repeat("A", 1<<20) blob := runStdin(t, dir, big, "hash-object", "-w", "--stdin") tree := runStdin(t, dir, "100644 blob "+blob+"\tlarge.txt\n", "mktree") commit := runStdin(t, dir, "", "commit-tree", tree, "-m", "big") _, _ = run(dir, "update-ref", "refs/heads/main", commit) if n, err := LineCount(dir, "main", "large.txt"); err != nil || n != 1 { t.Errorf("单行 1MB 文件的行数 = %d (err=%v),期望 1", n, err) } if _, err := BlobLimited(dir, "main", "large.txt", 1024); !errors.Is(err, ErrTooLarge) { t.Errorf("超出上限应返回 ErrTooLarge,实际 %v", err) } if b, err := BlobLimited(dir, "main", "large.txt", 2<<20); err != nil || len(b) != 1<<20 { t.Errorf("未超限时读取失败: len=%d err=%v", len(b), err) } } // TestLineCountNoDeadlock 回归:scanner 提前放弃时若不关闭读端, // cmd.Wait 会一直等到命令超时(曾表现为单次请求 60 秒)。 func TestLineCountNoDeadlock(t *testing.T) { dir := t.TempDir() if err := InitBare(dir, "main"); err != nil { t.Fatalf("InitBare 失败: %v", err) } // 无换行的 3MB 内容:scanner 会因超长行中止 huge := strings.Repeat("A", 3<<20) blob := runStdin(t, dir, huge, "hash-object", "-w", "--stdin") tree := runStdin(t, dir, "100644 blob "+blob+"\thuge.txt\n", "mktree") commit := runStdin(t, dir, "", "commit-tree", tree, "-m", "huge") _, _ = run(dir, "update-ref", "refs/heads/main", commit) done := make(chan struct{}) go func() { _, _ = LineCount(dir, "main", "huge.txt") close(done) }() select { case <-done: case <-time.After(10 * time.Second): t.Fatal("LineCount 卡死(读端未关闭)") } } func TestIsBinary(t *testing.T) { if !IsBinary([]byte("abc\x00def")) { t.Error("含 NUL 应判为二进制") } if IsBinary([]byte("普通文本内容")) { t.Error("纯文本不应判为二进制") } } func TestIsBareRepo(t *testing.T) { dir := filepath.Join(t.TempDir(), "r.git") if IsBareRepo(dir) { t.Error("不存在的目录不应被识别为裸仓库") } if err := InitBare(dir, "main"); err != nil { t.Fatalf("InitBare 失败: %v", err) } if !IsBareRepo(dir) { t.Error("初始化后应被识别为裸仓库") } if _, err := os.Stat(filepath.Join(dir, "HEAD")); err != nil { t.Errorf("缺少 HEAD: %v", err) } } func TestSetDefaultBranchRejectsBadRef(t *testing.T) { dir := t.TempDir() if err := InitBare(dir, "main"); err != nil { t.Fatalf("InitBare 失败: %v", err) } if err := SetDefaultBranch(dir, "--upload-pack=evil"); err == nil { t.Error("非法分支名应被拒绝") } if err := SetDefaultBranch(dir, "dev"); err != nil { t.Errorf("合法分支名被拒: %v", err) } if got := CurrentBranch(dir); got != "dev" { t.Errorf("当前分支 = %q,期望 dev", got) } } var _ = context.Background