worktree_test.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. package git
  2. import (
  3. "os"
  4. "path/filepath"
  5. "testing"
  6. )
  7. func TestCleanRepoPath(t *testing.T) {
  8. tests := []struct {
  9. name string
  10. input string
  11. want string
  12. wantError bool
  13. }{
  14. {name: "plain file", input: "main.go", want: "main.go"},
  15. {name: "nested file", input: "src/mod/git/main.go", want: "src/mod/git/main.go"},
  16. {name: "leading dot slash", input: "./main.go", want: "main.go"},
  17. {name: "backslash separators", input: "src\\mod\\main.go", want: "src/mod/main.go"},
  18. {name: "surrounding spaces", input: " main.go ", want: "main.go"},
  19. {name: "redundant segments", input: "src/./mod/../mod/main.go", want: "src/mod/main.go"},
  20. {name: "empty string", input: "", wantError: true},
  21. {name: "current folder", input: ".", wantError: true},
  22. {name: "parent escape", input: "../secret.txt", wantError: true},
  23. {name: "nested parent escape", input: "src/../../secret.txt", wantError: true},
  24. {name: "absolute unix path", input: "/absolute/secret.txt", wantError: true},
  25. }
  26. for _, test := range tests {
  27. t.Run(test.name, func(t *testing.T) {
  28. got, err := cleanRepoPath(test.input)
  29. if test.wantError {
  30. if err == nil {
  31. t.Fatalf("cleanRepoPath(%q) = %q, want an error", test.input, got)
  32. }
  33. return
  34. }
  35. if err != nil {
  36. t.Fatalf("cleanRepoPath(%q) returned error: %v", test.input, err)
  37. }
  38. if got != test.want {
  39. t.Errorf("cleanRepoPath(%q) = %q, want %q", test.input, got, test.want)
  40. }
  41. })
  42. }
  43. }
  44. func TestCommitRequiresMessage(t *testing.T) {
  45. manager := newTestManager(t)
  46. repoPath := newTestRepo(t, manager)
  47. writeFile(t, repoPath, "a.txt", "content\n")
  48. tests := []struct {
  49. name string
  50. request *CommitRequest
  51. }{
  52. {name: "nil request", request: nil},
  53. {name: "empty message", request: &CommitRequest{Message: "", Files: []string{"a.txt"}}},
  54. {name: "whitespace message", request: &CommitRequest{Message: " ", Files: []string{"a.txt"}}},
  55. }
  56. for _, test := range tests {
  57. t.Run(test.name, func(t *testing.T) {
  58. if _, err := manager.Commit(repoPath, test.request); err == nil {
  59. t.Errorf("Commit() with %s = nil error, want an error", test.name)
  60. }
  61. })
  62. }
  63. }
  64. func TestCommitOnlySelectedFiles(t *testing.T) {
  65. manager := newTestManager(t)
  66. repoPath := newTestRepo(t, manager)
  67. writeFile(t, repoPath, "included.txt", "in\n")
  68. writeFile(t, repoPath, "excluded.txt", "out\n")
  69. if _, err := manager.Commit(repoPath, &CommitRequest{
  70. Message: "add only the selected file",
  71. Files: []string{"included.txt"},
  72. Name: "Test User",
  73. Email: "test@arozos.local",
  74. }); err != nil {
  75. t.Fatalf("Commit() returned error: %v", err)
  76. }
  77. status, err := manager.Status(repoPath)
  78. if err != nil {
  79. t.Fatalf("Status() returned error: %v", err)
  80. }
  81. if len(status.Changes) != 1 || status.Changes[0].Path != "excluded.txt" {
  82. t.Errorf("Changes after selective commit = %+v, want only excluded.txt", status.Changes)
  83. }
  84. }
  85. func TestCommitEmptySelectionFails(t *testing.T) {
  86. manager := newTestManager(t)
  87. repoPath := newTestRepo(t, manager)
  88. commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
  89. //Nothing changed since the last commit
  90. if _, err := manager.Commit(repoPath, &CommitRequest{
  91. Message: "empty",
  92. Name: "Test User",
  93. Email: "test@arozos.local",
  94. }); err == nil {
  95. t.Errorf("Commit() with no changes = nil error, want an error")
  96. }
  97. }
  98. func TestCommitRejectsEscapingPath(t *testing.T) {
  99. manager := newTestManager(t)
  100. repoPath := newTestRepo(t, manager)
  101. if _, err := manager.Commit(repoPath, &CommitRequest{
  102. Message: "escape attempt",
  103. Files: []string{"../outside.txt"},
  104. Name: "Test User",
  105. Email: "test@arozos.local",
  106. }); err == nil {
  107. t.Errorf("Commit() with an escaping path = nil error, want an error")
  108. }
  109. }
  110. func TestAddAllStagesEverything(t *testing.T) {
  111. manager := newTestManager(t)
  112. repoPath := newTestRepo(t, manager)
  113. writeFile(t, repoPath, "a.txt", "a\n")
  114. writeFile(t, repoPath, "nested/b.txt", "b\n")
  115. if err := manager.AddAll(repoPath); err != nil {
  116. t.Fatalf("AddAll() returned error: %v", err)
  117. }
  118. status, err := manager.Status(repoPath)
  119. if err != nil {
  120. t.Fatalf("Status() returned error: %v", err)
  121. }
  122. if len(status.Changes) != 2 {
  123. t.Fatalf("Changes = %d entries, want 2", len(status.Changes))
  124. }
  125. for _, change := range status.Changes {
  126. if !change.Staged {
  127. t.Errorf("%s Staged = false after AddAll(), want true", change.Path)
  128. }
  129. }
  130. }
  131. func TestUnstageRemovesFromIndexOnly(t *testing.T) {
  132. manager := newTestManager(t)
  133. repoPath := newTestRepo(t, manager)
  134. commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
  135. writeFile(t, repoPath, "a.txt", "one changed\n")
  136. if err := manager.Add(repoPath, []string{"a.txt"}); err != nil {
  137. t.Fatalf("Add() returned error: %v", err)
  138. }
  139. if err := manager.Unstage(repoPath, []string{"a.txt"}); err != nil {
  140. t.Fatalf("Unstage() returned error: %v", err)
  141. }
  142. status, err := manager.Status(repoPath)
  143. if err != nil {
  144. t.Fatalf("Status() returned error: %v", err)
  145. }
  146. if len(status.Changes) != 1 {
  147. t.Fatalf("Changes = %d entries, want 1", len(status.Changes))
  148. }
  149. if status.Changes[0].Staged {
  150. t.Errorf("Staged = true after Unstage(), want false")
  151. }
  152. //The working tree edit must survive
  153. content, err := os.ReadFile(filepath.Join(repoPath, "a.txt"))
  154. if err != nil {
  155. t.Fatalf("cannot read file: %v", err)
  156. }
  157. if string(content) != "one changed\n" {
  158. t.Errorf("file content = %q, want the working tree edit to be preserved", string(content))
  159. }
  160. }
  161. func TestUnstageWithNoFilesFails(t *testing.T) {
  162. manager := newTestManager(t)
  163. repoPath := newTestRepo(t, manager)
  164. if err := manager.Unstage(repoPath, []string{}); err == nil {
  165. t.Errorf("Unstage() with no files = nil error, want an error")
  166. }
  167. }
  168. func TestDiscardRestoresTrackedFile(t *testing.T) {
  169. manager := newTestManager(t)
  170. repoPath := newTestRepo(t, manager)
  171. commitFile(t, manager, repoPath, "a.txt", "original\n", "first")
  172. writeFile(t, repoPath, "a.txt", "vandalised\n")
  173. if err := manager.Discard(repoPath, []string{"a.txt"}); err != nil {
  174. t.Fatalf("Discard() returned error: %v", err)
  175. }
  176. content, err := os.ReadFile(filepath.Join(repoPath, "a.txt"))
  177. if err != nil {
  178. t.Fatalf("cannot read file: %v", err)
  179. }
  180. if string(content) != "original\n" {
  181. t.Errorf("file content = %q, want %q", string(content), "original\n")
  182. }
  183. }
  184. func TestDiscardDeletesUntrackedFile(t *testing.T) {
  185. manager := newTestManager(t)
  186. repoPath := newTestRepo(t, manager)
  187. commitFile(t, manager, repoPath, "tracked.txt", "keep\n", "first")
  188. writeFile(t, repoPath, "junk.txt", "delete me\n")
  189. if err := manager.Discard(repoPath, []string{"junk.txt"}); err != nil {
  190. t.Fatalf("Discard() returned error: %v", err)
  191. }
  192. if _, err := os.Stat(filepath.Join(repoPath, "junk.txt")); !os.IsNotExist(err) {
  193. t.Errorf("untracked file still exists after Discard(), want it removed")
  194. }
  195. }
  196. func TestSanitiseLocalEmail(t *testing.T) {
  197. tests := []struct {
  198. name string
  199. input string
  200. want string
  201. }{
  202. {name: "simple name", input: "toby", want: "toby@arozos.local"},
  203. {name: "mixed case", input: "Toby Chui", want: "tobychui@arozos.local"},
  204. {name: "punctuation stripped", input: "a.b-c_d!", want: "a.b-c_d@arozos.local"},
  205. {name: "non ascii only", input: "中文", want: "user@arozos.local"},
  206. {name: "empty", input: "", want: "user@arozos.local"},
  207. }
  208. for _, test := range tests {
  209. t.Run(test.name, func(t *testing.T) {
  210. if got := sanitiseLocalEmail(test.input); got != test.want {
  211. t.Errorf("sanitiseLocalEmail(%q) = %q, want %q", test.input, got, test.want)
  212. }
  213. })
  214. }
  215. }
  216. func TestCommitFallsBackToRepositoryConfigIdentity(t *testing.T) {
  217. manager := newTestManager(t)
  218. repoPath := newTestRepo(t, manager)
  219. repo, err := manager.open(repoPath)
  220. if err != nil {
  221. t.Fatalf("open() returned error: %v", err)
  222. }
  223. cfg, err := repo.Config()
  224. if err != nil {
  225. t.Fatalf("Config() returned error: %v", err)
  226. }
  227. cfg.User.Name = "Config User"
  228. cfg.User.Email = "config@example.com"
  229. if err := repo.SetConfig(cfg); err != nil {
  230. t.Fatalf("SetConfig() returned error: %v", err)
  231. }
  232. writeFile(t, repoPath, "a.txt", "content\n")
  233. if _, err := manager.Commit(repoPath, &CommitRequest{
  234. Message: "identity from config",
  235. Files: []string{"a.txt"},
  236. }); err != nil {
  237. t.Fatalf("Commit() returned error: %v", err)
  238. }
  239. status, err := manager.Status(repoPath)
  240. if err != nil {
  241. t.Fatalf("Status() returned error: %v", err)
  242. }
  243. if status.Head.AuthorName != "Config User" {
  244. t.Errorf("AuthorName = %q, want %q", status.Head.AuthorName, "Config User")
  245. }
  246. if status.Head.AuthorEmail != "config@example.com" {
  247. t.Errorf("AuthorEmail = %q, want %q", status.Head.AuthorEmail, "config@example.com")
  248. }
  249. }