branch_actions_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. package git
  2. import (
  3. "errors"
  4. "path/filepath"
  5. "testing"
  6. gogit "github.com/go-git/go-git/v5"
  7. "github.com/go-git/go-git/v5/plumbing"
  8. )
  9. // newBareRemote creates an empty bare repository to act as a push target, so the
  10. // network code paths (delete / rename on a remote) can be exercised without a
  11. // real server.
  12. func newBareRemote(t *testing.T) string {
  13. t.Helper()
  14. path := filepath.Join(t.TempDir(), "remote.git")
  15. if _, err := gogit.PlainInit(path, true); err != nil {
  16. t.Fatalf("cannot create the bare remote: %v", err)
  17. }
  18. return path
  19. }
  20. // bareHasBranch reports whether the bare repository holds a given branch.
  21. func bareHasBranch(t *testing.T, barePath string, branch string) bool {
  22. t.Helper()
  23. repo, err := gogit.PlainOpen(barePath)
  24. if err != nil {
  25. t.Fatalf("cannot open the bare remote: %v", err)
  26. }
  27. _, err = repo.Reference(plumbing.NewBranchReferenceName(branch), false)
  28. return err == nil
  29. }
  30. // hasLocalRef reports whether a reference exists in a working repository.
  31. func hasLocalRef(t *testing.T, manager *Manager, repoPath string, name plumbing.ReferenceName) bool {
  32. t.Helper()
  33. repo, err := manager.open(repoPath)
  34. if err != nil {
  35. t.Fatalf("open() returned error: %v", err)
  36. }
  37. _, err = repo.Reference(name, false)
  38. return err == nil
  39. }
  40. /*
  41. newRepoWithRemote builds a working repository wired to a fresh bare remote, with
  42. master pushed and a second branch pushed under the given name. It returns the
  43. working repository path, the bare remote path and the default branch name.
  44. */
  45. func newRepoWithRemote(t *testing.T, manager *Manager, extraBranch string) (string, string, string) {
  46. t.Helper()
  47. bare := newBareRemote(t)
  48. repoPath := newTestRepo(t, manager)
  49. commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
  50. if err := manager.AddRemote(repoPath, "origin", bare); err != nil {
  51. t.Fatalf("AddRemote() returned error: %v", err)
  52. }
  53. branches, _ := manager.Branches(repoPath)
  54. defaultBranch := branches[0].Name
  55. if _, err := manager.Push(repoPath, &TransportRequest{SetUpstream: true}); err != nil {
  56. t.Fatalf("Push() of the default branch returned error: %v", err)
  57. }
  58. if extraBranch != "" {
  59. if err := manager.Checkout(repoPath, extraBranch, true); err != nil {
  60. t.Fatalf("Checkout(create %s) returned error: %v", extraBranch, err)
  61. }
  62. commitFile(t, manager, repoPath, "b.txt", "two\n", "on "+extraBranch)
  63. if _, err := manager.Push(repoPath, &TransportRequest{SetUpstream: true}); err != nil {
  64. t.Fatalf("Push() of %s returned error: %v", extraBranch, err)
  65. }
  66. //Return to the default branch so the extra one can be operated on
  67. if err := manager.Checkout(repoPath, defaultBranch, false); err != nil {
  68. t.Fatalf("Checkout(%s) returned error: %v", defaultBranch, err)
  69. }
  70. }
  71. return repoPath, bare, defaultBranch
  72. }
  73. /* ── Local branch deletion ────────────────────────────────────────────── */
  74. func TestDeleteBranchRemovesMergedBranch(t *testing.T) {
  75. manager := newTestManager(t)
  76. repoPath := newTestRepo(t, manager)
  77. commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
  78. branches, _ := manager.Branches(repoPath)
  79. defaultBranch := branches[0].Name
  80. //A branch created at HEAD with no extra commits is fully merged
  81. if err := manager.Checkout(repoPath, "scratch", true); err != nil {
  82. t.Fatalf("Checkout(create) returned error: %v", err)
  83. }
  84. if err := manager.Checkout(repoPath, defaultBranch, false); err != nil {
  85. t.Fatalf("Checkout(back) returned error: %v", err)
  86. }
  87. if err := manager.DeleteBranch(repoPath, "scratch", false); err != nil {
  88. t.Fatalf("DeleteBranch() returned error: %v", err)
  89. }
  90. if hasLocalRef(t, manager, repoPath, plumbing.NewBranchReferenceName("scratch")) {
  91. t.Errorf("the branch ref still exists after DeleteBranch()")
  92. }
  93. after, _ := manager.Branches(repoPath)
  94. for _, branch := range after {
  95. if branch.Name == "scratch" {
  96. t.Errorf("Branches() still lists the deleted branch")
  97. }
  98. }
  99. }
  100. func TestDeleteBranchRefusesCurrentBranch(t *testing.T) {
  101. manager := newTestManager(t)
  102. repoPath := newTestRepo(t, manager)
  103. commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
  104. branches, _ := manager.Branches(repoPath)
  105. current := branches[0].Name
  106. if err := manager.DeleteBranch(repoPath, current, false); err == nil {
  107. t.Errorf("DeleteBranch() on the current branch = nil error, want a refusal")
  108. }
  109. //Even forcing must not remove the branch HEAD points at
  110. if err := manager.DeleteBranch(repoPath, current, true); err == nil {
  111. t.Errorf("DeleteBranch(force) on the current branch = nil error, want a refusal")
  112. }
  113. if !hasLocalRef(t, manager, repoPath, plumbing.NewBranchReferenceName(current)) {
  114. t.Errorf("the current branch was deleted despite the refusal")
  115. }
  116. }
  117. func TestDeleteBranchUnmergedNeedsForce(t *testing.T) {
  118. manager := newTestManager(t)
  119. repoPath := newTestRepo(t, manager)
  120. commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
  121. branches, _ := manager.Branches(repoPath)
  122. defaultBranch := branches[0].Name
  123. //A commit that exists only on the feature branch makes it unmerged
  124. if err := manager.Checkout(repoPath, "feature", true); err != nil {
  125. t.Fatalf("Checkout(create) returned error: %v", err)
  126. }
  127. commitFile(t, manager, repoPath, "feature.txt", "work\n", "feature work")
  128. if err := manager.Checkout(repoPath, defaultBranch, false); err != nil {
  129. t.Fatalf("Checkout(back) returned error: %v", err)
  130. }
  131. err := manager.DeleteBranch(repoPath, "feature", false)
  132. if !errors.Is(err, ErrUnmergedBranch) {
  133. t.Fatalf("DeleteBranch() of an unmerged branch = %v, want ErrUnmergedBranch", err)
  134. }
  135. if !hasLocalRef(t, manager, repoPath, plumbing.NewBranchReferenceName("feature")) {
  136. t.Fatalf("the branch was deleted even though the call was refused")
  137. }
  138. //Forcing must go through
  139. if err := manager.DeleteBranch(repoPath, "feature", true); err != nil {
  140. t.Fatalf("DeleteBranch(force) returned error: %v", err)
  141. }
  142. if hasLocalRef(t, manager, repoPath, plumbing.NewBranchReferenceName("feature")) {
  143. t.Errorf("the branch survived a forced delete")
  144. }
  145. }
  146. func TestDeleteBranchDropsTrackingConfig(t *testing.T) {
  147. manager := newTestManager(t)
  148. repoPath, _, defaultBranch := newRepoWithRemote(t, manager, "feature")
  149. repo, err := manager.open(repoPath)
  150. if err != nil {
  151. t.Fatalf("open() returned error: %v", err)
  152. }
  153. cfg, _ := repo.Config()
  154. if _, ok := cfg.Branches["feature"]; !ok {
  155. t.Fatalf("the pushed branch has no tracking config to begin with")
  156. }
  157. //Force is needed: the feature branch carries its own commit
  158. if defaultBranch == "feature" {
  159. t.Fatalf("unexpected default branch name for this test")
  160. }
  161. if err := manager.DeleteBranch(repoPath, "feature", true); err != nil {
  162. t.Fatalf("DeleteBranch() returned error: %v", err)
  163. }
  164. repo, _ = manager.open(repoPath)
  165. cfg, _ = repo.Config()
  166. if _, ok := cfg.Branches["feature"]; ok {
  167. t.Errorf("the tracking config survived the branch delete")
  168. }
  169. }
  170. func TestDeleteBranchValidation(t *testing.T) {
  171. manager := newTestManager(t)
  172. repoPath := newTestRepo(t, manager)
  173. commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
  174. tests := []struct {
  175. name string
  176. branch string
  177. }{
  178. {name: "no such branch", branch: "nonexistent"},
  179. {name: "empty name", branch: ""},
  180. {name: "invalid name", branch: "bad name"},
  181. }
  182. for _, test := range tests {
  183. t.Run(test.name, func(t *testing.T) {
  184. if err := manager.DeleteBranch(repoPath, test.branch, false); err == nil {
  185. t.Errorf("DeleteBranch(%q) = nil error, want an error", test.branch)
  186. }
  187. })
  188. }
  189. }
  190. /* ── Local branch rename ──────────────────────────────────────────────── */
  191. func TestRenameBranchNotCurrent(t *testing.T) {
  192. manager := newTestManager(t)
  193. repoPath := newTestRepo(t, manager)
  194. commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
  195. branches, _ := manager.Branches(repoPath)
  196. defaultBranch := branches[0].Name
  197. if err := manager.Checkout(repoPath, "old-name", true); err != nil {
  198. t.Fatalf("Checkout(create) returned error: %v", err)
  199. }
  200. if err := manager.Checkout(repoPath, defaultBranch, false); err != nil {
  201. t.Fatalf("Checkout(back) returned error: %v", err)
  202. }
  203. if err := manager.RenameBranch(repoPath, "old-name", "new-name"); err != nil {
  204. t.Fatalf("RenameBranch() returned error: %v", err)
  205. }
  206. if hasLocalRef(t, manager, repoPath, plumbing.NewBranchReferenceName("old-name")) {
  207. t.Errorf("the old branch ref still exists")
  208. }
  209. if !hasLocalRef(t, manager, repoPath, plumbing.NewBranchReferenceName("new-name")) {
  210. t.Errorf("the new branch ref was not created")
  211. }
  212. //The current branch must not have changed
  213. status, _ := manager.Status(repoPath)
  214. if status.Branch != defaultBranch {
  215. t.Errorf("current branch = %q, want %q", status.Branch, defaultBranch)
  216. }
  217. }
  218. func TestRenameCurrentBranchFollowsHead(t *testing.T) {
  219. manager := newTestManager(t)
  220. repoPath := newTestRepo(t, manager)
  221. commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
  222. branches, _ := manager.Branches(repoPath)
  223. current := branches[0].Name
  224. if err := manager.RenameBranch(repoPath, current, "renamed-main"); err != nil {
  225. t.Fatalf("RenameBranch() returned error: %v", err)
  226. }
  227. status, err := manager.Status(repoPath)
  228. if err != nil {
  229. t.Fatalf("Status() returned error: %v", err)
  230. }
  231. if status.Branch != "renamed-main" {
  232. t.Errorf("current branch = %q, want %q", status.Branch, "renamed-main")
  233. }
  234. if status.Detached {
  235. t.Errorf("HEAD became detached after renaming the current branch")
  236. }
  237. if status.Head == nil {
  238. t.Errorf("HEAD no longer resolves to a commit after the rename")
  239. }
  240. //A commit must still be possible on the renamed branch
  241. commitFile(t, manager, repoPath, "b.txt", "two\n", "after rename")
  242. }
  243. func TestRenameBranchCarriesUpstreamConfig(t *testing.T) {
  244. manager := newTestManager(t)
  245. repoPath, _, _ := newRepoWithRemote(t, manager, "feature")
  246. if err := manager.RenameBranch(repoPath, "feature", "feature-renamed"); err != nil {
  247. t.Fatalf("RenameBranch() returned error: %v", err)
  248. }
  249. repo, err := manager.open(repoPath)
  250. if err != nil {
  251. t.Fatalf("open() returned error: %v", err)
  252. }
  253. cfg, _ := repo.Config()
  254. if _, ok := cfg.Branches["feature"]; ok {
  255. t.Errorf("the old branch config was left behind")
  256. }
  257. renamed, ok := cfg.Branches["feature-renamed"]
  258. if !ok {
  259. t.Fatalf("the renamed branch has no tracking config")
  260. }
  261. if renamed.Name != "feature-renamed" {
  262. t.Errorf("config Name = %q, want %q", renamed.Name, "feature-renamed")
  263. }
  264. if renamed.Remote != "origin" {
  265. t.Errorf("config Remote = %q, want origin", renamed.Remote)
  266. }
  267. }
  268. func TestRenameBranchValidation(t *testing.T) {
  269. manager := newTestManager(t)
  270. repoPath := newTestRepo(t, manager)
  271. commitFile(t, manager, repoPath, "a.txt", "one\n", "first")
  272. branches, _ := manager.Branches(repoPath)
  273. existing := branches[0].Name
  274. if err := manager.Checkout(repoPath, "other", true); err != nil {
  275. t.Fatalf("Checkout(create) returned error: %v", err)
  276. }
  277. if err := manager.Checkout(repoPath, existing, false); err != nil {
  278. t.Fatalf("Checkout(back) returned error: %v", err)
  279. }
  280. tests := []struct {
  281. name string
  282. oldName string
  283. newName string
  284. }{
  285. {name: "source missing", oldName: "nonexistent", newName: "whatever"},
  286. {name: "target already exists", oldName: "other", newName: existing},
  287. {name: "same name", oldName: "other", newName: "other"},
  288. {name: "invalid target", oldName: "other", newName: "bad name"},
  289. {name: "empty target", oldName: "other", newName: ""},
  290. {name: "empty source", oldName: "", newName: "fine"},
  291. }
  292. for _, test := range tests {
  293. t.Run(test.name, func(t *testing.T) {
  294. if err := manager.RenameBranch(repoPath, test.oldName, test.newName); err == nil {
  295. t.Errorf("RenameBranch(%q, %q) = nil error, want an error", test.oldName, test.newName)
  296. }
  297. })
  298. }
  299. //None of the refused calls may have changed anything
  300. if !hasLocalRef(t, manager, repoPath, plumbing.NewBranchReferenceName("other")) {
  301. t.Errorf("a refused rename removed the source branch")
  302. }
  303. }
  304. /* ── Remote branch deletion ───────────────────────────────────────────── */
  305. func TestDeleteRemoteBranch(t *testing.T) {
  306. manager := newTestManager(t)
  307. repoPath, bare, _ := newRepoWithRemote(t, manager, "feature")
  308. if !bareHasBranch(t, bare, "feature") {
  309. t.Fatalf("the remote does not have the feature branch to begin with")
  310. }
  311. if err := manager.DeleteRemoteBranch(repoPath, "origin", "feature", &TransportRequest{}); err != nil {
  312. t.Fatalf("DeleteRemoteBranch() returned error: %v", err)
  313. }
  314. if bareHasBranch(t, bare, "feature") {
  315. t.Errorf("the branch still exists on the remote")
  316. }
  317. //The local tracking ref must be pruned so the UI stops listing it
  318. if hasLocalRef(t, manager, repoPath, plumbing.NewRemoteReferenceName("origin", "feature")) {
  319. t.Errorf("the remote-tracking ref was not pruned")
  320. }
  321. after, _ := manager.Branches(repoPath)
  322. for _, branch := range after {
  323. if branch.IsRemote && branch.Short == "feature" {
  324. t.Errorf("Branches() still lists the deleted remote branch")
  325. }
  326. }
  327. //The local branch of the same name is a separate thing and must survive
  328. if !hasLocalRef(t, manager, repoPath, plumbing.NewBranchReferenceName("feature")) {
  329. t.Errorf("deleting the remote branch also removed the local branch")
  330. }
  331. }
  332. func TestDeleteRemoteBranchValidation(t *testing.T) {
  333. manager := newTestManager(t)
  334. repoPath, _, _ := newRepoWithRemote(t, manager, "feature")
  335. if err := manager.DeleteRemoteBranch(repoPath, "nosuchremote", "feature", &TransportRequest{}); !errors.Is(err, ErrNoRemote) {
  336. t.Errorf("DeleteRemoteBranch() with an unknown remote = %v, want ErrNoRemote", err)
  337. }
  338. if err := manager.DeleteRemoteBranch(repoPath, "origin", "bad name", &TransportRequest{}); err == nil {
  339. t.Errorf("DeleteRemoteBranch() with an invalid branch name = nil error, want an error")
  340. }
  341. if err := manager.DeleteRemoteBranch(repoPath, "origin", "", &TransportRequest{}); err == nil {
  342. t.Errorf("DeleteRemoteBranch() with an empty branch name = nil error, want an error")
  343. }
  344. }
  345. /* ── Remote branch rename ─────────────────────────────────────────────── */
  346. func TestRenameRemoteBranch(t *testing.T) {
  347. manager := newTestManager(t)
  348. repoPath, bare, _ := newRepoWithRemote(t, manager, "feature")
  349. if err := manager.RenameRemoteBranch(repoPath, "origin", "feature", "feature-v2", &TransportRequest{}); err != nil {
  350. t.Fatalf("RenameRemoteBranch() returned error: %v", err)
  351. }
  352. if !bareHasBranch(t, bare, "feature-v2") {
  353. t.Errorf("the new branch name was not created on the remote")
  354. }
  355. if bareHasBranch(t, bare, "feature") {
  356. t.Errorf("the old branch name still exists on the remote")
  357. }
  358. //The local tracking refs must mirror the change without needing a fetch
  359. if !hasLocalRef(t, manager, repoPath, plumbing.NewRemoteReferenceName("origin", "feature-v2")) {
  360. t.Errorf("the new remote-tracking ref was not created locally")
  361. }
  362. if hasLocalRef(t, manager, repoPath, plumbing.NewRemoteReferenceName("origin", "feature")) {
  363. t.Errorf("the old remote-tracking ref was not removed locally")
  364. }
  365. }
  366. func TestRenameRemoteBranchPreservesCommits(t *testing.T) {
  367. manager := newTestManager(t)
  368. repoPath, bare, _ := newRepoWithRemote(t, manager, "feature")
  369. //Record what the remote branch pointed at before the rename
  370. repo, _ := manager.open(repoPath)
  371. before, err := repo.Reference(plumbing.NewRemoteReferenceName("origin", "feature"), false)
  372. if err != nil {
  373. t.Fatalf("cannot read the remote-tracking ref: %v", err)
  374. }
  375. if err := manager.RenameRemoteBranch(repoPath, "origin", "feature", "feature-v2", &TransportRequest{}); err != nil {
  376. t.Fatalf("RenameRemoteBranch() returned error: %v", err)
  377. }
  378. bareRepo, err := gogit.PlainOpen(bare)
  379. if err != nil {
  380. t.Fatalf("cannot open the bare remote: %v", err)
  381. }
  382. after, err := bareRepo.Reference(plumbing.NewBranchReferenceName("feature-v2"), false)
  383. if err != nil {
  384. t.Fatalf("the renamed branch is missing on the remote: %v", err)
  385. }
  386. if after.Hash() != before.Hash() {
  387. t.Errorf("renamed branch points at %s, want the original commit %s", after.Hash(), before.Hash())
  388. }
  389. }
  390. func TestRenameRemoteBranchValidation(t *testing.T) {
  391. manager := newTestManager(t)
  392. repoPath, _, _ := newRepoWithRemote(t, manager, "feature")
  393. tests := []struct {
  394. name string
  395. remote string
  396. oldName string
  397. newName string
  398. }{
  399. {name: "unknown remote", remote: "nosuchremote", oldName: "feature", newName: "x"},
  400. {name: "source missing on remote", remote: "origin", oldName: "nonexistent", newName: "x"},
  401. {name: "same name", remote: "origin", oldName: "feature", newName: "feature"},
  402. {name: "invalid target", remote: "origin", oldName: "feature", newName: "bad name"},
  403. {name: "empty target", remote: "origin", oldName: "feature", newName: ""},
  404. }
  405. for _, test := range tests {
  406. t.Run(test.name, func(t *testing.T) {
  407. if err := manager.RenameRemoteBranch(repoPath, test.remote, test.oldName, test.newName, &TransportRequest{}); err == nil {
  408. t.Errorf("RenameRemoteBranch() with %s = nil error, want an error", test.name)
  409. }
  410. })
  411. }
  412. }
  413. /* ── Helpers ──────────────────────────────────────────────────────────── */
  414. func TestSplitRemoteRef(t *testing.T) {
  415. tests := []struct {
  416. name string
  417. fullRef string
  418. wantRemote string
  419. wantShort string
  420. }{
  421. {name: "simple remote branch", fullRef: "refs/remotes/origin/master", wantRemote: "origin", wantShort: "master"},
  422. {name: "nested branch name", fullRef: "refs/remotes/origin/feature/login", wantRemote: "origin", wantShort: "feature/login"},
  423. {name: "non-origin remote", fullRef: "refs/remotes/upstream/dev", wantRemote: "upstream", wantShort: "dev"},
  424. {name: "local branch", fullRef: "refs/heads/master", wantRemote: "", wantShort: "master"},
  425. {name: "local nested branch", fullRef: "refs/heads/feature/login", wantRemote: "", wantShort: "feature/login"},
  426. {name: "remote with no branch part", fullRef: "refs/remotes/origin", wantRemote: "", wantShort: "origin"},
  427. }
  428. for _, test := range tests {
  429. t.Run(test.name, func(t *testing.T) {
  430. remote, short := splitRemoteRef(test.fullRef)
  431. if remote != test.wantRemote {
  432. t.Errorf("splitRemoteRef(%q) remote = %q, want %q", test.fullRef, remote, test.wantRemote)
  433. }
  434. if short != test.wantShort {
  435. t.Errorf("splitRemoteRef(%q) short = %q, want %q", test.fullRef, short, test.wantShort)
  436. }
  437. })
  438. }
  439. }
  440. func TestRemoteOrName(t *testing.T) {
  441. tests := []struct {
  442. name string
  443. remote string
  444. want string
  445. }{
  446. {name: "explicit", remote: "upstream", want: "upstream"},
  447. {name: "empty defaults to origin", remote: "", want: "origin"},
  448. {name: "whitespace defaults to origin", remote: " ", want: "origin"},
  449. {name: "padded", remote: " upstream ", want: "upstream"},
  450. }
  451. for _, test := range tests {
  452. t.Run(test.name, func(t *testing.T) {
  453. if got := remoteOrName(test.remote); got != test.want {
  454. t.Errorf("remoteOrName(%q) = %q, want %q", test.remote, got, test.want)
  455. }
  456. })
  457. }
  458. }
  459. // TestBranchesReportRemoteAndShort checks the fields the front-end needs to
  460. // address a remote branch unambiguously.
  461. func TestBranchesReportRemoteAndShort(t *testing.T) {
  462. manager := newTestManager(t)
  463. repoPath, _, defaultBranch := newRepoWithRemote(t, manager, "feature")
  464. branches, err := manager.Branches(repoPath)
  465. if err != nil {
  466. t.Fatalf("Branches() returned error: %v", err)
  467. }
  468. sawLocal, sawRemote := false, false
  469. for _, branch := range branches {
  470. if !branch.IsRemote && branch.Name == defaultBranch {
  471. sawLocal = true
  472. if branch.Short != defaultBranch {
  473. t.Errorf("local branch Short = %q, want %q", branch.Short, defaultBranch)
  474. }
  475. if branch.Remote != "" {
  476. t.Errorf("local branch Remote = %q, want empty", branch.Remote)
  477. }
  478. }
  479. if branch.IsRemote && branch.Short == "feature" {
  480. sawRemote = true
  481. if branch.Remote != "origin" {
  482. t.Errorf("remote branch Remote = %q, want origin", branch.Remote)
  483. }
  484. if branch.Name != "origin/feature" {
  485. t.Errorf("remote branch Name = %q, want origin/feature", branch.Name)
  486. }
  487. }
  488. }
  489. if !sawLocal {
  490. t.Errorf("Branches() did not report the local default branch")
  491. }
  492. if !sawRemote {
  493. t.Errorf("Branches() did not report the remote feature branch")
  494. }
  495. }