Repository: seachicken/gh-poi (go)

Issue:
Delete checked out PR branches
**Description**
When a PR branch is currently checked out, `gh-poi` will not delete the branch. A way to fix this would be to checkout the default branch and _then_ delete the branch. 

**Additional context**

[hub-sync](https://hub.github.com/hub-sync.1.html) implemented this same functionality and the way I described above is how this tool dealt with the problem.

Pull request:
Delete merged branches that have been checked out
Resolves #25 

Patch:
diff --git a/connmock/stub.go b/connmock/stub.go
index dee9de1..ba70597 100644
--- a/connmock/stub.go
+++ b/connmock/stub.go
@@ -61,35 +61,35 @@ func (s *Stub) CheckRepos(err error, conf *Conf) *Stub {
 	return s
 }
 
-func (s *Stub) GetRepoNames(path string, err error, conf *Conf) *Stub {
+func (s *Stub) GetRepoNames(filename string, err error, conf *Conf) *Stub {
 	s.t.Helper()
 	configure(
 		s.Conn.
 			EXPECT().
 			GetRepoNames().
-			Return(s.readFile("gh", "repo", path), err),
+			Return(s.readFile("gh", "repo", filename), err),
 		conf,
 	)
 	return s
 }
 
-func (s *Stub) GetBranchNames(name string, err error, conf *Conf) *Stub {
+func (s *Stub) GetBranchNames(filename string, err error, conf *Conf) *Stub {
 	s.t.Helper()
 	configure(
 		s.Conn.EXPECT().
 			GetBranchNames().
-			Return(s.readFile("git", "branch", name), err),
+			Return(s.readFile("git", "branch", filename), err),
 		conf,
 	)
 	return s
 }
 
-func (s *Stub) GetAssociatedBranchNames(stubs []AssociatedBranchNamesStub, err error, conf *Conf) *Stub {
+func (s *Stub) GetAssociatedRefNames(stubs []AssociatedBranchNamesStub, err error, conf *Conf) *Stub {
 	s.t.Helper()
 	for _, stub := range stubs {
 		configure(
 			s.Conn.EXPECT().
-				GetAssociatedBranchNames(stub.Oid).
+				GetAssociatedRefNames(stub.Oid).
 				Return(s.readFile("git", "abranch", stub.Filename), err),
 			conf,
 		)
@@ -110,13 +110,37 @@ func (s *Stub) GetLog(stubs []LogStub, err error, conf *Conf) *Stub {
 	return s
 }
 
-func (s *Stub) GetPullRequests(path string, err error, conf *Conf) *Stub {
+func (s *Stub) GetPullRequests(filename string, err error, conf *Conf) *Stub {
 	s.t.Helper()
 	configure(
 		s.Conn.
 			EXPECT().
 			GetPullRequests(gomock.Any(), gomock.Any(), gomock.Any()).
-			Return(s.readFile("gh", "pr", path), err),
+			Return(s.readFile("gh", "pr", filename), err),
+		conf,
+	)
+	return s
+}
+
+func (s *Stub) GetUncommittedChanges(uncommittedChanges string, err error, conf *Conf) *Stub {
+	s.t.Helper()
+	configure(
+		s.Conn.
+			EXPECT().
+			GetUncommittedChanges().
+			Return(uncommittedChanges, err),
+		conf,
+	)
+	return s
+}
+
+func (s *Stub) CheckoutBranch(err error, conf *Conf) *Stub {
+	s.t.Helper()
+	configure(
+		s.Conn.
+			EXPECT().
+			CheckoutBranch(gomock.Any()).
+			Return("", err),
 		conf,
 	)
 	return s
diff --git a/fixtures/git/abranch_issue1.txt b/fixtures/git/abranch_issue1.txt
index 147a92b..64dff49 100644
--- a/fixtures/git/abranch_issue1.txt
+++ b/fixtures/git/abranch_issue1.txt
@@ -1,1 +1,1 @@
-issue1
\ No newline at end of file
+refs/heads/issue1
\ No newline at end of file
diff --git a/fixtures/git/abranch_issue1_origin-main.txt b/fixtures/git/abranch_issue1_origin-main.txt
new file mode 100644
index 0000000..3f196fd
--- /dev/null
+++ b/fixtures/git/abranch_issue1_origin-main.txt
@@ -0,0 +1,2 @@
+refs/heads/issue1
+refs/remotes/origin/main
\ No newline at end of file
diff --git a/fixtures/git/abranch_main.txt b/fixtures/git/abranch_main.txt
index 88d050b..e607e0d 100644
--- a/fixtures/git/abranch_main.txt
+++ b/fixtures/git/abranch_main.txt
@@ -1,1 +1,1 @@
-main
\ No newline at end of file
+refs/heads/main
\ No newline at end of file
diff --git a/fixtures/git/abranch_main_issue1.txt b/fixtures/git/abranch_main_issue1.txt
index 8178107..fb8cb2e 100644
--- a/fixtures/git/abranch_main_issue1.txt
+++ b/fixtures/git/abranch_main_issue1.txt
@@ -1,2 +1,2 @@
-issue1
-main
\ No newline at end of file
+refs/heads/issue1
+refs/heads/main
\ No newline at end of file
diff --git a/fixtures/git/branch_@issue1.txt b/fixtures/git/branch_@issue1.txt
new file mode 100644
index 0000000..68fdf59
--- /dev/null
+++ b/fixtures/git/branch_@issue1.txt
@@ -0,0 +1,1 @@
+*,issue1,356a192b7913b04c54574d18c28d46e6395428ab
diff --git a/main.go b/main.go
index 232a9e3..6c2165f 100644
--- a/main.go
+++ b/main.go
@@ -39,7 +39,7 @@ func runMain(check bool) {
 	sp.Start()
 	var fetchingErr error
 
-	branches, fetchingErr := GetBranches(conn)
+	branches, fetchingErr := GetBranches(conn, check)
 
 	sp.Stop()
 
diff --git a/mocks/poi_mock.go b/mocks/poi_mock.go
index 5a127c6..764fa09 100644
--- a/mocks/poi_mock.go
+++ b/mocks/poi_mock.go
@@ -47,6 +47,21 @@ func (mr *MockConnectionMockRecorder) CheckRepos(hostname, repoNames interface{}
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckRepos", reflect.TypeOf((*MockConnection)(nil).CheckRepos), hostname, repoNames)
 }
 
+// CheckoutBranch mocks base method.
+func (m *MockConnection) CheckoutBranch(branchName string) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CheckoutBranch", branchName)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CheckoutBranch indicates an expected call of CheckoutBranch.
+func (mr *MockConnectionMockRecorder) CheckoutBranch(branchName interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckoutBranch", reflect.TypeOf((*MockConnection)(nil).CheckoutBranch), branchName)
+}
+
 // DeleteBranches mocks base method.
 func (m *MockConnection) DeleteBranches(branchNames []string) (string, error) {
 	m.ctrl.T.Helper()
@@ -62,19 +77,19 @@ func (mr *MockConnectionMockRecorder) DeleteBranches(branchNames interface{}) *g
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBranches", reflect.TypeOf((*MockConnection)(nil).DeleteBranches), branchNames)
 }
 
-// GetAssociatedBranchNames mocks base method.
-func (m *MockConnection) GetAssociatedBranchNames(arg0 string) (string, error) {
+// GetAssociatedRefNames mocks base method.
+func (m *MockConnection) GetAssociatedRefNames(oid string) (string, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetAssociatedBranchNames", arg0)
+	ret := m.ctrl.Call(m, "GetAssociatedRefNames", oid)
 	ret0, _ := ret[0].(string)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// GetAssociatedBranchNames indicates an expected call of GetAssociatedBranchNames.
-func (mr *MockConnectionMockRecorder) GetAssociatedBranchNames(arg0 interface{}) *gomock.Call {
+// GetAssociatedRefNames indicates an expected call of GetAssociatedRefNames.
+func (mr *MockConnectionMockRecorder) GetAssociatedRefNames(oid interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAssociatedBranchNames", reflect.TypeOf((*MockConnection)(nil).GetAssociatedBranchNames), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAssociatedRefNames", reflect.TypeOf((*MockConnection)(nil).GetAssociatedRefNames), oid)
 }
 
 // GetBranchNames mocks base method.
@@ -93,18 +108,18 @@ func (mr *MockConnectionMockRecorder) GetBranchNames() *gomock.Call {
 }
 
 // GetLog mocks base method.
-func (m *MockConnection) GetLog(arg0 string) (string, error) {
+func (m *MockConnection) GetLog(branchName string) (string, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetLog", arg0)
+	ret := m.ctrl.Call(m, "GetLog", branchName)
 	ret0, _ := ret[0].(string)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // GetLog indicates an expected call of GetLog.
-func (mr *MockConnectionMockRecorder) GetLog(arg0 interface{}) *gomock.Call {
+func (mr *MockConnectionMockRecorder) GetLog(branchName interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLog", reflect.TypeOf((*MockConnection)(nil).GetLog), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLog", reflect.TypeOf((*MockConnection)(nil).GetLog), branchName)
 }
 
 // GetPullRequests mocks base method.
@@ -136,3 +151,18 @@ func (mr *MockConnectionMockRecorder) GetRepoNames() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepoNames", reflect.TypeOf((*MockConnection)(nil).GetRepoNames))
 }
+
+// GetUncommittedChanges mocks base method.
+func (m *MockConnection) GetUncommittedChanges() (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetUncommittedChanges")
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetUncommittedChanges indicates an expected call of GetUncommittedChanges.
+func (mr *MockConnectionMockRecorder) GetUncommittedChanges() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUncommittedChanges", reflect.TypeOf((*MockConnection)(nil).GetUncommittedChanges))
+}
diff --git a/poi.go b/poi.go
index eff67fc..f752699 100644
--- a/poi.go
+++ b/poi.go
@@ -18,9 +18,11 @@ type (
 		CheckRepos(hostname string, repoNames []string) error
 		GetRepoNames() (string, error)
 		GetBranchNames() (string, error)
-		GetLog(string) (string, error)
-		GetAssociatedBranchNames(string) (string, error)
+		GetLog(branchName string) (string, error)
+		GetAssociatedRefNames(oid string) (string, error)
 		GetPullRequests(hostname string, repoNames []string, queryHashes string) (string, error)
+		GetUncommittedChanges() (string, error)
+		CheckoutBranch(branchName string) (string, error)
 		DeleteBranches(branchNames []string) (string, error)
 	}
 
@@ -70,7 +72,7 @@ const (
 
 var ErrNotFound = errors.New("not found")
 
-func GetBranches(conn Connection) ([]Branch, error) {
+func GetBranches(conn Connection, check bool) ([]Branch, error) {
 	var hostname string
 	var repoNames []string
 	var defaultBranchName string
@@ -109,7 +111,55 @@ func GetBranches(conn Connection) ([]Branch, error) {
 	}
 
 	branches = applyPullRequest(branches, prs)
-	branches = checkDeletion(branches)
+
+	uncommittedChanges, err := conn.GetUncommittedChanges()
+	if err != nil {
+		return nil, err
+	}
+
+	branches = checkDeletion(branches, uncommittedChanges)
+
+	needsCheckout := false
+	for _, branch := range branches {
+		if branch.Head && branch.State == Deletable {
+			needsCheckout = true
+			break
+		}
+	}
+
+	if needsCheckout {
+		result := []Branch{}
+
+		if !check {
+			_, err := conn.CheckoutBranch(defaultBranchName)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		if !branchNameExists(defaultBranchName, branches) {
+			result = append(result, Branch{
+				true, defaultBranchName,
+				[]string{},
+				[]PullRequest{},
+				NotDeletable,
+			})
+		}
+
+		for _, branch := range branches {
+			if branch.Name == defaultBranchName {
+				branch.Head = true
+			} else {
+				branch.Head = false
+			}
+			result = append(result, branch)
+		}
+
+		branches = result
+	}
+
+	sort.Slice(branches, func(i, j int) bool { return branches[i].Name < branches[j].Name })
+
 	return branches, nil
 }
 
@@ -144,11 +194,11 @@ func trimBranch(oids []string, branchName string, conn Connection) ([]string, er
 	childNames := []string{}
 
 	for i, oid := range oids {
-		namesResult, err := conn.GetAssociatedBranchNames(oid)
+		refNames, err := conn.GetAssociatedRefNames(oid)
 		if err != nil {
 			return nil, err
 		}
-		names := splitLines(namesResult)
+		names := extractBranchNames(splitLines(refNames))
 
 		if i == 0 {
 			for _, name := range names {
@@ -179,6 +229,15 @@ func trimBranch(oids []string, branchName string, conn Connection) ([]string, er
 	return results, nil
 }
 
+func extractBranchNames(refNames []string) []string {
+	result := []string{}
+	r := regexp.MustCompile(`^refs/(?:heads|remotes/.+?)/`)
+	for _, name := range refNames {
+		result = append(result, r.ReplaceAllString(name, ""))
+	}
+	return result
+}
+
 func applyPullRequest(branches []Branch, prs []PullRequest) []Branch {
 	results := []Branch{}
 	for _, branch := range branches {
@@ -210,17 +269,17 @@ func findMatchedPullRequest(branchName string, prs []PullRequest) []PullRequest
 	return results
 }
 
-func checkDeletion(branches []Branch) []Branch {
+func checkDeletion(branches []Branch, uncommittedChanges string) []Branch {
 	results := []Branch{}
 	for _, branch := range branches {
-		branch.State = getDeleteStatus(branch)
+		branch.State = getDeleteStatus(branch, uncommittedChanges)
 		results = append(results, branch)
 	}
 	return results
 }
 
-func getDeleteStatus(branch Branch) BranchState {
-	if branch.Head {
+func getDeleteStatus(branch Branch, uncommittedChanges string) BranchState {
+	if branch.Head && len(uncommittedChanges) > 0 {
 		return NotDeletable
 	}
 
@@ -482,9 +541,9 @@ func (conn *ConnectionImpl) GetLog(branchName string) (string, error) {
 	return run("git", args)
 }
 
-func (conn *ConnectionImpl) GetAssociatedBranchNames(oid string) (string, error) {
+func (conn *ConnectionImpl) GetAssociatedRefNames(oid string) (string, error) {
 	args := []string{
-		"branch", "--format=%(refname:lstrip=2)",
+		"branch", "--all", "--format=%(refname)",
 		"--contains", oid,
 	}
 	return run("git", args)
@@ -525,6 +584,18 @@ func (conn *ConnectionImpl) GetPullRequests(
 	return run("gh", args)
 }
 
+func (conn *ConnectionImpl) GetUncommittedChanges() (string, error) {
+	args := append([]string{
+		"status", "--short"})
+	return run("git", args)
+}
+
+func (conn *ConnectionImpl) CheckoutBranch(branchName string) (string, error) {
+	args := append([]string{
+		"checkout", "--quiet", branchName})
+	return run("git", args)
+}
+
 func (conn *ConnectionImpl) DeleteBranches(branchNames []string) (string, error) {
 	args := append([]string{
 		"branch", "-D"},


Test patch:
diff --git a/poi_test.go b/poi_test.go
index 40dff71..94f294f 100644
--- a/poi_test.go
+++ b/poi_test.go
@@ -20,13 +20,14 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithMergedPR(t *testing.T) {
 		GetLog([]connmock.LogStub{
 			{"main", "main"}, {"issue1", "issue1"},
 		}, nil, nil).
-		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
 			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
 			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
 		}, nil, nil).
-		GetPullRequests("issue1Merged", nil, nil)
+		GetPullRequests("issue1Merged", nil, nil).
+		GetUncommittedChanges("", nil, nil)
 
-	actual, _ := GetBranches(s.Conn)
+	actual, _ := GetBranches(s.Conn, false)
 
 	assert.Equal(t, []Branch{
 		{
@@ -65,13 +66,14 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithUpstreamMergedPR(t *testing
 		GetLog([]connmock.LogStub{
 			{"main", "main"}, {"issue1", "issue1"},
 		}, nil, nil).
-		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
 			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
 			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
 		}, nil, nil).
-		GetPullRequests("issue1UpMerged", nil, nil)
+		GetPullRequests("issue1UpMerged", nil, nil).
+		GetUncommittedChanges("", nil, nil)
 
-	actual, _ := GetBranches(s.Conn)
+	actual, _ := GetBranches(s.Conn, false)
 
 	assert.Equal(t, []Branch{
 		{
@@ -99,7 +101,7 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithUpstreamMergedPR(t *testing
 	}, actual)
 }
 
-func Test_ShouldNotDeletableWhenBranchIsCheckedOut(t *testing.T) {
+func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithTheCheckIsFalse(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
@@ -110,13 +112,156 @@ func Test_ShouldNotDeletableWhenBranchIsCheckedOut(t *testing.T) {
 		GetLog([]connmock.LogStub{
 			{"main", "main"}, {"issue1", "issue1"},
 		}, nil, nil).
-		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
 			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
 			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
 		}, nil, nil).
-		GetPullRequests("issue1Merged", nil, nil)
+		GetPullRequests("issue1Merged", nil, nil).
+		GetUncommittedChanges("", nil, nil).
+		CheckoutBranch(nil, connmock.NewConf(&connmock.Times{N: 1}))
 
-	actual, _ := GetBranches(s.Conn)
+	actual, _ := GetBranches(s.Conn, false)
+
+	assert.Equal(t, []Branch{
+		{
+			false, "issue1",
+			[]string{
+				"356a192b7913b04c54574d18c28d46e6395428ab",
+			},
+			[]PullRequest{
+				{
+					"issue1", Merged, 1,
+					[]string{
+						"356a192b7913b04c54574d18c28d46e6395428ab",
+					},
+					"https://github.com/owner/repo/pull/1", "owner",
+				},
+			},
+			Deletable,
+		},
+		{
+			true, "main",
+			[]string{},
+			[]PullRequest{},
+			NotDeletable,
+		},
+	}, actual)
+}
+
+func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithTheCheckIsTrue(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	s := connmock.Setup(ctrl).
+		CheckRepos(nil, nil).
+		GetRepoNames("origin", nil, nil).
+		GetBranchNames("main_@issue1", nil, nil).
+		GetLog([]connmock.LogStub{
+			{"main", "main"}, {"issue1", "issue1"},
+		}, nil, nil).
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
+		}, nil, nil).
+		GetPullRequests("issue1Merged", nil, nil).
+		GetUncommittedChanges("", nil, nil).
+		CheckoutBranch(nil, connmock.NewConf(&connmock.Times{N: 0}))
+
+	actual, _ := GetBranches(s.Conn, true)
+
+	assert.Equal(t, []Branch{
+		{
+			false, "issue1",
+			[]string{
+				"356a192b7913b04c54574d18c28d46e6395428ab",
+			},
+			[]PullRequest{
+				{
+					"issue1", Merged, 1,
+					[]string{
+						"356a192b7913b04c54574d18c28d46e6395428ab",
+					},
+					"https://github.com/owner/repo/pull/1", "owner",
+				},
+			},
+			Deletable,
+		},
+		{
+			true, "main",
+			[]string{},
+			[]PullRequest{},
+			NotDeletable,
+		},
+	}, actual)
+}
+
+func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithoutADefaultBranch(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	s := connmock.Setup(ctrl).
+		CheckRepos(nil, nil).
+		GetRepoNames("origin", nil, nil).
+		GetBranchNames("@issue1", nil, nil).
+		GetLog([]connmock.LogStub{
+			{"issue1", "issue1"},
+		}, nil, nil).
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "issue1_origin-main"},
+		}, nil, nil).
+		GetPullRequests("issue1Merged", nil, nil).
+		GetUncommittedChanges("", nil, nil).
+		CheckoutBranch(nil, nil)
+
+	actual, _ := GetBranches(s.Conn, false)
+
+	assert.Equal(t, []Branch{
+		{
+			false, "issue1",
+			[]string{
+				"356a192b7913b04c54574d18c28d46e6395428ab",
+			},
+			[]PullRequest{
+				{
+					"issue1", Merged, 1,
+					[]string{
+						"356a192b7913b04c54574d18c28d46e6395428ab",
+					},
+					"https://github.com/owner/repo/pull/1", "owner",
+				},
+			},
+			Deletable,
+		},
+		{
+			true, "main",
+			[]string{},
+			[]PullRequest{},
+			NotDeletable,
+		},
+	}, actual)
+}
+
+func Test_ShouldNotDeletableWhenBranchHasUncommittedChanges(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	s := connmock.Setup(ctrl).
+		CheckRepos(nil, nil).
+		GetRepoNames("origin", nil, nil).
+		GetBranchNames("main_@issue1", nil, nil).
+		GetLog([]connmock.LogStub{
+			{"main", "main"}, {"issue1", "issue1"},
+		}, nil, nil).
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
+		}, nil, nil).
+		GetPullRequests("issue1Merged", nil, nil).
+		GetUncommittedChanges(" M README.md", nil, nil).
+		CheckoutBranch(nil, nil)
+
+	actual, _ := GetBranches(s.Conn, false)
 
 	assert.Equal(t, []Branch{
 		{
@@ -155,13 +300,14 @@ func Test_ShouldNotDeletableWhenBranchesAssociatedWithClosedPR(t *testing.T) {
 		GetLog([]connmock.LogStub{
 			{"main", "main"}, {"issue1", "issue1"},
 		}, nil, nil).
-		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
 			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
 			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
 		}, nil, nil).
-		GetPullRequests("issue1Closed", nil, nil)
+		GetPullRequests("issue1Closed", nil, nil).
+		GetUncommittedChanges("", nil, nil)
 
-	actual, _ := GetBranches(s.Conn)
+	actual, _ := GetBranches(s.Conn, false)
 
 	assert.Equal(t, []Branch{
 		{
@@ -200,13 +346,14 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithMergedAndClosedPRs(t *testi
 		GetLog([]connmock.LogStub{
 			{"main", "main"}, {"issue1", "issue1"},
 		}, nil, nil).
-		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
 			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
 			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
 		}, nil, nil).
-		GetPullRequests("issue1Merged_issue1Closed", nil, nil)
+		GetPullRequests("issue1Merged_issue1Closed", nil, nil).
+		GetUncommittedChanges("", nil, nil)
 
-	actual, _ := GetBranches(s.Conn)
+	actual, _ := GetBranches(s.Conn, false)
 
 	assert.Equal(t, []Branch{
 		{
@@ -252,15 +399,16 @@ func Test_ShouldNotDeletableWhenBranchesAssociatedWithNotFullyMergedPR(t *testin
 		GetLog([]connmock.LogStub{
 			{"main", "main_issue1Merged"}, {"issue1", "issue1CommitAfterMerge"},
 		}, nil, nil).
-		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
 			{"cb197ba87e4ad323b1008c611212deb7da2a4a49", "main"},
 			{"b8a2645298053fb62ea03e27feea6c483d3fd27e", "issue1"},
 			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
 			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
 		}, nil, nil).
-		GetPullRequests("issue1Merged", nil, nil)
+		GetPullRequests("issue1Merged", nil, nil).
+		GetUncommittedChanges("", nil, nil)
 
-	actual, _ := GetBranches(s.Conn)
+	actual, _ := GetBranches(s.Conn, false)
 
 	assert.Equal(t, []Branch{
 		{
@@ -296,7 +444,7 @@ func Test_ReturnsAnErrorWhenGetRepoNamesFails(t *testing.T) {
 	s := connmock.Setup(ctrl).
 		GetRepoNames("origin", errors.New("failed to run external command: git"), nil)
 
-	_, err := GetBranches(s.Conn)
+	_, err := GetBranches(s.Conn, false)
 
 	assert.NotNil(t, err)
 }
@@ -309,7 +457,7 @@ func Test_ReturnsAnErrorWhenCheckReposFails(t *testing.T) {
 		CheckRepos(errors.New("failed to run external command: gh"), nil).
 		GetRepoNames("origin", nil, nil)
 
-	_, err := GetBranches(s.Conn)
+	_, err := GetBranches(s.Conn, false)
 
 	assert.NotNil(t, err)
 }
@@ -323,7 +471,7 @@ func Test_ReturnsAnErrorWhenGetBranchNamesFails(t *testing.T) {
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", errors.New("failed to run external command: gh"), nil)
 
-	_, err := GetBranches(s.Conn)
+	_, err := GetBranches(s.Conn, false)
 
 	assert.NotNil(t, err)
 }
@@ -340,12 +488,12 @@ func Test_ReturnsAnErrorWhenGetLogFails(t *testing.T) {
 			{"main", "main"}, {"issue1", "issue1"},
 		}, errors.New("failed to run external command: git"), nil)
 
-	_, err := GetBranches(s.Conn)
+	_, err := GetBranches(s.Conn, false)
 
 	assert.NotNil(t, err)
 }
 
-func Test_ReturnsAnErrorWhenGetAssociatedBranchNamesFails(t *testing.T) {
+func Test_ReturnsAnErrorWhenGetAssociatedRefNamesFails(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
@@ -356,12 +504,12 @@ func Test_ReturnsAnErrorWhenGetAssociatedBranchNamesFails(t *testing.T) {
 		GetLog([]connmock.LogStub{
 			{"main", "main"}, {"issue1", "issue1"},
 		}, nil, nil).
-		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
 			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
 			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
 		}, errors.New("failed to run external command: git"), nil)
 
-	_, err := GetBranches(s.Conn)
+	_, err := GetBranches(s.Conn, false)
 
 	assert.NotNil(t, err)
 }
@@ -377,13 +525,60 @@ func Test_ReturnsAnErrorWhenGetPullRequestsFails(t *testing.T) {
 		GetLog([]connmock.LogStub{
 			{"main", "main"}, {"issue1", "issue1"},
 		}, nil, nil).
-		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
 			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
 			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
 		}, nil, nil).
 		GetPullRequests("issue1Merged", errors.New("failed to run external command: gh"), nil)
 
-	_, err := GetBranches(s.Conn)
+	_, err := GetBranches(s.Conn, false)
+
+	assert.NotNil(t, err)
+}
+
+func Test_ReturnsAnErrorWhenGetUncommittedChangesFails(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	s := connmock.Setup(ctrl).
+		CheckRepos(nil, nil).
+		GetRepoNames("origin", nil, nil).
+		GetBranchNames("@main_issue1", nil, nil).
+		GetLog([]connmock.LogStub{
+			{"main", "main"}, {"issue1", "issue1"},
+		}, nil, nil).
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
+		}, nil, nil).
+		GetPullRequests("issue1Merged", nil, nil).
+		GetUncommittedChanges("", errors.New("failed to run external command: git"), nil)
+
+	_, err := GetBranches(s.Conn, false)
+
+	assert.NotNil(t, err)
+}
+
+func Test_ReturnsAnErrorWhenCheckoutBranchFails(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	s := connmock.Setup(ctrl).
+		CheckRepos(nil, nil).
+		GetRepoNames("origin", nil, nil).
+		GetBranchNames("main_@issue1", nil, nil).
+		GetLog([]connmock.LogStub{
+			{"main", "main"}, {"issue1", "issue1"},
+		}, nil, nil).
+		GetAssociatedRefNames([]connmock.AssociatedBranchNamesStub{
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
+		}, nil, nil).
+		GetPullRequests("issue1Merged", nil, nil).
+		GetUncommittedChanges("", nil, nil).
+		CheckoutBranch(errors.New("failed to run external command: git"), nil)
+
+	_, err := GetBranches(s.Conn, false)
 
 	assert.NotNil(t, err)
 }