vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
| 1 | package git |
| 2 | |
| 3 | // TouchTimes answers "when was this file last changed?" for a whole subtree in |
| 4 | // one `git log`. These tests pin the two properties the issue list depends on: |
| 5 | // the newest commit wins, and a commit range only names the paths it touched. |
| 6 | |
| 7 | import ( |
| 8 | "os" |
| 9 | "os/exec" |
| 10 | "path/filepath" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | "time" |
| 14 | ) |
| 15 | |
| 16 | func touchRepo(t *testing.T) *Repo { |
| 17 | t.Helper() |
| 18 | dir := t.TempDir() |
| 19 | run := func(at time.Time, args ...string) { |
| 20 | t.Helper() |
| 21 | cmd := exec.Command("git", args...) |
| 22 | cmd.Dir = dir |
| 23 | date := at.Format(time.RFC3339) |
| 24 | cmd.Env = append(os.Environ(), |
| 25 | "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", |
| 26 | "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.com", |
| 27 | "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.com", |
| 28 | "GIT_AUTHOR_DATE="+date, "GIT_COMMITTER_DATE="+date) |
| 29 | if out, err := cmd.CombinedOutput(); err != nil { |
| 30 | t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, out) |
| 31 | } |
| 32 | } |
| 33 | write := func(name, content string) { |
| 34 | t.Helper() |
| 35 | p := filepath.Join(dir, name) |
| 36 | if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | if err := os.WriteFile(p, []byte(content), 0o644); err != nil { |
| 40 | t.Fatal(err) |
| 41 | } |
| 42 | } |
| 43 | first := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) |
| 44 | second := first.Add(24 * time.Hour) |
| 45 | third := second.Add(24 * time.Hour) |
| 46 | |
| 47 | run(first, "init", "-q", "-b", "main") |
| 48 | write("issues/a/issue.md", "a\n") |
| 49 | write("issues/b/issue.md", "b\n") |
| 50 | run(first, "add", "-A") |
| 51 | run(first, "commit", "-qm", "open both") |
| 52 | |
| 53 | write("issues/b/issue.md", "b, edited\n") |
| 54 | run(second, "add", "-A") |
| 55 | run(second, "commit", "-qm", "edit b") |
| 56 | |
| 57 | write("issues/b/comments/c.md", "a comment\n") |
| 58 | run(third, "add", "-A") |
| 59 | run(third, "commit", "-qm", "comment on b") |
| 60 | return open(t, dir) |
| 61 | } |
| 62 | |
| 63 | func TestTouchTimesKeepsTheNewestCommitPerPath(t *testing.T) { |
| 64 | t.Parallel() |
| 65 | r := touchRepo(t) |
| 66 | first := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) |
| 67 | second := first.Add(24 * time.Hour) |
| 68 | third := second.Add(24 * time.Hour) |
| 69 | |
| 70 | times, err := r.TouchTimes("HEAD", "issues") |
| 71 | if err != nil { |
| 72 | t.Fatalf("TouchTimes: %v", err) |
| 73 | } |
| 74 | want := map[string]int64{ |
| 75 | "issues/a/issue.md": first.Unix(), |
| 76 | "issues/b/issue.md": second.Unix(), |
| 77 | "issues/b/comments/c.md": third.Unix(), |
| 78 | } |
| 79 | if len(times) != len(want) { |
| 80 | t.Fatalf("TouchTimes = %v, want %v", times, want) |
| 81 | } |
| 82 | for path, when := range want { |
| 83 | if got, ok := times[path]; !ok || got != when { |
| 84 | t.Fatalf("%s = %d (present %v), want %d", path, got, ok, when) |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | func TestTouchTimesReadsOnlyTheRangeItIsGiven(t *testing.T) { |
| 90 | t.Parallel() |
| 91 | r := touchRepo(t) |
| 92 | third := time.Date(2026, 1, 3, 12, 0, 0, 0, time.UTC) |
| 93 | |
| 94 | times, err := r.TouchTimes("HEAD~1..HEAD", "issues") |
| 95 | if err != nil { |
| 96 | t.Fatalf("TouchTimes: %v", err) |
| 97 | } |
| 98 | if len(times) != 1 || times["issues/b/comments/c.md"] != third.Unix() { |
| 99 | t.Fatalf("range TouchTimes = %v, want only the comment at %d", times, third.Unix()) |
| 100 | } |
| 101 | |
| 102 | empty, err := r.TouchTimes("", "issues") |
| 103 | if err != nil || empty != nil { |
| 104 | t.Fatalf("TouchTimes with no rev = %v, %v; want no rows and no error", empty, err) |
| 105 | } |
| 106 | } |
| 107 |