Repository: seachicken/gh-poi (go)

Issue:
Closed PRs are not associated with branches
**Describe the bug**
For example, PR A is not associated with a branch when we run the following.

1. Close PR A
2. Commit changes locally to the branch of PR A
3. Create a new PR B by pushing a commit
4. Run `gh poi`

**Your Environment**
 - OS: macOS monterey
 - gh --version: 2.4.0
 - gh-poi: 0.2.5


Pull request description:
Improve association of local branches with PR
Resolves #22 

<img width="474" alt="image" src="https://user-images.githubusercontent.com/5178598/150675501-ab8406b0-a832-4060-9645-99433e8f59ce.png">

- Fixe to be associated with multiple PRs
- Fix to associate with local commits after a PR has been submitted

Patch:
diff --git a/connmock/stub.go b/connmock/stub.go
index 9fa7a7c..dee9de1 100644
--- a/connmock/stub.go
+++ b/connmock/stub.go
@@ -22,6 +22,16 @@ type (
 	Conf struct {
 		Times *Times
 	}
+
+	AssociatedBranchNamesStub struct {
+		Oid      string
+		Filename string
+	}
+
+	LogStub struct {
+		BranchName string
+		Filename   string
+	}
 )
 
 var (
@@ -74,6 +84,32 @@ func (s *Stub) GetBranchNames(name string, err error, conf *Conf) *Stub {
 	return s
 }
 
+func (s *Stub) GetAssociatedBranchNames(stubs []AssociatedBranchNamesStub, err error, conf *Conf) *Stub {
+	s.t.Helper()
+	for _, stub := range stubs {
+		configure(
+			s.Conn.EXPECT().
+				GetAssociatedBranchNames(stub.Oid).
+				Return(s.readFile("git", "abranch", stub.Filename), err),
+			conf,
+		)
+	}
+	return s
+}
+
+func (s *Stub) GetLog(stubs []LogStub, err error, conf *Conf) *Stub {
+	s.t.Helper()
+	for _, stub := range stubs {
+		configure(
+			s.Conn.EXPECT().
+				GetLog(stub.BranchName).
+				Return(s.readFile("git", "log", stub.Filename), err),
+			conf,
+		)
+	}
+	return s
+}
+
 func (s *Stub) GetPullRequests(path string, err error, conf *Conf) *Stub {
 	s.t.Helper()
 	configure(
@@ -99,11 +135,7 @@ func (s *Stub) DeleteBranches(err error, conf *Conf) *Stub {
 }
 
 func configure(call *gomock.Call, conf *Conf) {
-	if conf == nil {
-		return
-	}
-
-	if conf.Times == nil {
+	if conf == nil || conf.Times == nil {
 		call.AnyTimes()
 	} else {
 		call.Times(conf.Times.N)
diff --git a/fixtures/gh/pr_issue1Closed.json b/fixtures/gh/pr_issue1Closed.json
index b2068a0..069e6b3 100644
--- a/fixtures/gh/pr_issue1Closed.json
+++ b/fixtures/gh/pr_issue1Closed.json
@@ -9,6 +9,15 @@
             "url": "https://github.com/owner/repo/pull/1",
             "state": "CLOSED",
             "headRefName": "issue1",
+            "commits": {
+              "nodes": [
+                {
+                  "commit": {
+                    "oid": "356a192b7913b04c54574d18c28d46e6395428ab"
+                  }
+                }
+              ]
+            },
             "author": {
               "login": "owner"
             }
diff --git a/fixtures/gh/pr_issue1Merged.json b/fixtures/gh/pr_issue1Merged.json
index b79e959..ad8724c 100644
--- a/fixtures/gh/pr_issue1Merged.json
+++ b/fixtures/gh/pr_issue1Merged.json
@@ -9,6 +9,15 @@
             "url": "https://github.com/owner/repo/pull/1",
             "state": "MERGED",
             "headRefName": "issue1",
+            "commits": {
+              "nodes": [
+                {
+                  "commit": {
+                    "oid": "356a192b7913b04c54574d18c28d46e6395428ab"
+                  }
+                }
+              ]
+            },
             "author": {
               "login": "owner"
             }
diff --git a/fixtures/gh/pr_issue1Merged_issue1Closed.json b/fixtures/gh/pr_issue1Merged_issue1Closed.json
index 902029a..718fdff 100644
--- a/fixtures/gh/pr_issue1Merged_issue1Closed.json
+++ b/fixtures/gh/pr_issue1Merged_issue1Closed.json
@@ -9,6 +9,15 @@
             "url": "https://github.com/owner/repo/pull/1",
             "state": "CLOSED",
             "headRefName": "issue1",
+            "commits": {
+              "nodes": [
+                {
+                  "commit": {
+                    "oid": "356a192b7913b04c54574d18c28d46e6395428ab"
+                  }
+                }
+              ]
+            },
             "author": {
               "login": "owner"
             }
@@ -20,6 +29,15 @@
             "url": "https://github.com/owner/repo/pull/2",
             "state": "MERGED",
             "headRefName": "issue1",
+            "commits": {
+              "nodes": [
+                {
+                  "commit": {
+                    "oid": "356a192b7913b04c54574d18c28d46e6395428ab"
+                  }
+                }
+              ]
+            },
             "author": {
               "login": "owner"
             }
diff --git a/fixtures/gh/pr_issue1UpMerged.json b/fixtures/gh/pr_issue1UpMerged.json
index 4868e76..dc6f3b1 100644
--- a/fixtures/gh/pr_issue1UpMerged.json
+++ b/fixtures/gh/pr_issue1UpMerged.json
@@ -9,6 +9,15 @@
             "url": "https://github.com/parent-owner/repo/pull/1",
             "state": "MERGED",
             "headRefName": "issue1",
+            "commits": {
+              "nodes": [
+                {
+                  "commit": {
+                    "oid": "356a192b7913b04c54574d18c28d46e6395428ab"
+                  }
+                }
+              ]
+            },
             "author": {
               "login": "owner"
             }
diff --git a/fixtures/git/abranch_issue1.txt b/fixtures/git/abranch_issue1.txt
new file mode 100644
index 0000000..147a92b
--- /dev/null
+++ b/fixtures/git/abranch_issue1.txt
@@ -0,0 +1,1 @@
+issue1
\ No newline at end of file
diff --git a/fixtures/git/abranch_main.txt b/fixtures/git/abranch_main.txt
new file mode 100644
index 0000000..88d050b
--- /dev/null
+++ b/fixtures/git/abranch_main.txt
@@ -0,0 +1,1 @@
+main
\ No newline at end of file
diff --git a/fixtures/git/abranch_main_issue1.txt b/fixtures/git/abranch_main_issue1.txt
new file mode 100644
index 0000000..8178107
--- /dev/null
+++ b/fixtures/git/abranch_main_issue1.txt
@@ -0,0 +1,2 @@
+issue1
+main
\ No newline at end of file
diff --git a/fixtures/git/branch_main_@issue1.txt b/fixtures/git/branch_main_@issue1.txt
index 4951f65..e7f94f8 100644
--- a/fixtures/git/branch_main_@issue1.txt
+++ b/fixtures/git/branch_main_@issue1.txt
@@ -1,3 +1,2 @@
 *,issue1,356a192b7913b04c54574d18c28d46e6395428ab
  ,main,b6589fc6ab0dc82cf12099d1c2d40ab994e8410c
- 
\ No newline at end of file
diff --git a/fixtures/git/log_issue1.txt b/fixtures/git/log_issue1.txt
new file mode 100644
index 0000000..dc53904
--- /dev/null
+++ b/fixtures/git/log_issue1.txt
@@ -0,0 +1,2 @@
+356a192b7913b04c54574d18c28d46e6395428ab
+b6589fc6ab0dc82cf12099d1c2d40ab994e8410c
\ No newline at end of file
diff --git a/fixtures/git/log_issue1CommitAfterMerge.txt b/fixtures/git/log_issue1CommitAfterMerge.txt
new file mode 100644
index 0000000..0d1f138
--- /dev/null
+++ b/fixtures/git/log_issue1CommitAfterMerge.txt
@@ -0,0 +1,3 @@
+b8a2645298053fb62ea03e27feea6c483d3fd27e
+356a192b7913b04c54574d18c28d46e6395428ab
+b6589fc6ab0dc82cf12099d1c2d40ab994e8410c
\ No newline at end of file
diff --git a/fixtures/git/log_main.txt b/fixtures/git/log_main.txt
new file mode 100644
index 0000000..f1322cd
--- /dev/null
+++ b/fixtures/git/log_main.txt
@@ -0,0 +1,1 @@
+b6589fc6ab0dc82cf12099d1c2d40ab994e8410c
\ No newline at end of file
diff --git a/fixtures/git/log_main_issue1Merged.txt b/fixtures/git/log_main_issue1Merged.txt
new file mode 100644
index 0000000..ea27f6e
--- /dev/null
+++ b/fixtures/git/log_main_issue1Merged.txt
@@ -0,0 +1,2 @@
+cb197ba87e4ad323b1008c611212deb7da2a4a49
+b6589fc6ab0dc82cf12099d1c2d40ab994e8410c
\ No newline at end of file
diff --git a/mocks/poi_mock.go b/mocks/poi_mock.go
index 9169e3b..5a127c6 100644
--- a/mocks/poi_mock.go
+++ b/mocks/poi_mock.go
@@ -62,6 +62,21 @@ 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) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetAssociatedBranchNames", arg0)
+	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 {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAssociatedBranchNames", reflect.TypeOf((*MockConnection)(nil).GetAssociatedBranchNames), arg0)
+}
+
 // GetBranchNames mocks base method.
 func (m *MockConnection) GetBranchNames() (string, error) {
 	m.ctrl.T.Helper()
@@ -77,6 +92,21 @@ func (mr *MockConnectionMockRecorder) GetBranchNames() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBranchNames", reflect.TypeOf((*MockConnection)(nil).GetBranchNames))
 }
 
+// GetLog mocks base method.
+func (m *MockConnection) GetLog(arg0 string) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetLog", arg0)
+	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 {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLog", reflect.TypeOf((*MockConnection)(nil).GetLog), arg0)
+}
+
 // GetPullRequests mocks base method.
 func (m *MockConnection) GetPullRequests(hostname string, repoNames []string, queryHashes string) (string, error) {
 	m.ctrl.T.Helper()
diff --git a/poi.go b/poi.go
index 81dd514..7ab763a 100644
--- a/poi.go
+++ b/poi.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"regexp"
+	"sort"
 	"strings"
 
 	"github.com/pkg/errors"
@@ -17,6 +18,8 @@ type (
 		CheckRepos(hostname string, repoNames []string) error
 		GetRepoNames() (string, error)
 		GetBranchNames() (string, error)
+		GetLog(string) (string, error)
+		GetAssociatedBranchNames(string) (string, error)
 		GetPullRequests(hostname string, repoNames []string, queryHashes string) (string, error)
 		DeleteBranches(branchNames []string) (string, error)
 	}
@@ -35,7 +38,7 @@ type (
 	Branch struct {
 		Head         bool
 		Name         string
-		LastObjectId string
+		Commits      []string
 		PullRequests []PullRequest
 		State        BranchState
 	}
@@ -43,11 +46,12 @@ type (
 	PullRequestState int
 
 	PullRequest struct {
-		Name   string
-		State  PullRequestState
-		Number int
-		Url    string
-		Author string
+		Name    string
+		State   PullRequestState
+		Number  int
+		Commits []string
+		Url     string
+		Author  string
 	}
 )
 
@@ -83,13 +87,17 @@ func GetBranches(conn Connection) ([]Branch, error) {
 
 	var branches []Branch
 	if names, err := conn.GetBranchNames(); err == nil {
-		branches = toBranch(strings.Split(names, "\n"))
+		branches = toBranch(splitLines(names))
+		branches, err = applyCommits(branches, defaultBranchName, conn)
+		if err != nil {
+			return nil, err
+		}
 	} else {
 		return nil, err
 	}
 
 	prs := []PullRequest{}
-	for _, queryHashes := range GetQueryHashes(branches, defaultBranchName) {
+	for _, queryHashes := range GetQueryHashes(branches) {
 		json, err := conn.GetPullRequests(hostname, repoNames, queryHashes)
 		if err != nil {
 			return nil, err
@@ -105,10 +113,77 @@ func GetBranches(conn Connection) ([]Branch, error) {
 	return branches, nil
 }
 
+func applyCommits(branches []Branch, defaultBranchName string, conn Connection) ([]Branch, error) {
+	results := []Branch{}
+
+	for _, branch := range branches {
+		if branch.Name == defaultBranchName {
+			results = append(results, branch)
+			continue
+		}
+
+		oids, err := conn.GetLog(branch.Name)
+		if err != nil {
+			return nil, err
+		}
+
+		trimmedOids, err := trimBranch(splitLines(oids), branch.Name, conn)
+		if err != nil {
+			return nil, err
+		}
+
+		branch.Commits = trimmedOids
+		results = append(results, branch)
+	}
+
+	return results, nil
+}
+
+func trimBranch(oids []string, branchName string, conn Connection) ([]string, error) {
+	results := []string{}
+	childNames := []string{}
+
+	for i, oid := range oids {
+		namesResult, err := conn.GetAssociatedBranchNames(oid)
+		if err != nil {
+			return nil, err
+		}
+		names := splitLines(namesResult)
+
+		if i == 0 {
+			for _, name := range names {
+				if name != branchName {
+					childNames = append(childNames, name)
+				}
+			}
+		}
+
+		isChild := func(name string) bool {
+			for _, childName := range childNames {
+				if name == childName {
+					return true
+				}
+			}
+			return false
+		}
+
+		for _, name := range names {
+			if name != branchName && !isChild(name) {
+				return results, nil
+			}
+		}
+
+		results = append(results, oid)
+	}
+
+	return results, nil
+}
+
 func applyPullRequest(branches []Branch, prs []PullRequest) []Branch {
 	results := []Branch{}
 	for _, branch := range branches {
 		prs := findMatchedPullRequest(branch.Name, prs)
+		sort.Slice(prs, func(i, j int) bool { return prs[i].Number < prs[j].Number })
 		branch.PullRequests = prs
 		results = append(results, branch)
 	}
@@ -117,8 +192,18 @@ func applyPullRequest(branches []Branch, prs []PullRequest) []Branch {
 
 func findMatchedPullRequest(branchName string, prs []PullRequest) []PullRequest {
 	results := []PullRequest{}
+
+	exists := func(pr PullRequest) bool {
+		for _, result := range results {
+			if pr.Number == result.Number {
+				return true
+			}
+		}
+		return false
+	}
+
 	for _, pr := range prs {
-		if pr.Name == branchName {
+		if pr.Name == branchName && !exists(pr) {
 			results = append(results, pr)
 		}
 	}
@@ -143,26 +228,40 @@ func getDeleteStatus(branch Branch) BranchState {
 		return NotDeletable
 	}
 
-	mergedCnt := 0
+	fullyMergedCnt := 0
 	for _, pr := range branch.PullRequests {
 		if pr.State == Open {
 			return NotDeletable
 		}
-		if pr.State == Merged {
-			mergedCnt++
+		if isFullyMerged(branch, pr) {
+			fullyMergedCnt++
 		}
 	}
-	if mergedCnt == 0 {
+	if fullyMergedCnt == 0 {
 		return NotDeletable
 	}
 
 	return Deletable
 }
 
-func toBranch(branchNames []string) []Branch {
-	branchNames = branchNames[:len(branchNames)-1]
+func isFullyMerged(branch Branch, pr PullRequest) bool {
+	if pr.State != Merged {
+		return false
+	}
 
+	localHeadOid := branch.Commits[0]
+	for _, oid := range pr.Commits {
+		if oid == localHeadOid {
+			return true
+		}
+	}
+
+	return false
+}
+
+func toBranch(branchNames []string) []Branch {
 	results := []Branch{}
+
 	for _, branchName := range branchNames {
 		splitedNames := strings.Split(branchName, ",")
 		head := false
@@ -172,7 +271,7 @@ func toBranch(branchNames []string) []Branch {
 		results = append(results, Branch{
 			head,
 			splitedNames[1],
-			splitedNames[2],
+			[]string{},
 			[]PullRequest{},
 			Unknown,
 		})
@@ -216,7 +315,7 @@ func getRepo(jsonResp string) (string, []string, string, error) {
 }
 
 func getHostname(url string) string {
-	r := regexp.MustCompile("//(.+?)/")
+	r := regexp.MustCompile(`//(.+?)/`)
 	found := r.FindSubmatch([]byte(url))
 	return string(found[1])
 }
@@ -230,9 +329,17 @@ func toPullRequests(jsonResp string) ([]PullRequest, error) {
 					Node struct {
 						Number      int
 						HeadRefName string
+						HeadRefOid  string
 						Url         string
 						State       string
-						Author      struct {
+						Commits     struct {
+							Nodes []struct {
+								Commit struct {
+									Oid string
+								}
+							}
+						}
+						Author struct {
 							Login string
 						}
 					}
@@ -253,10 +360,18 @@ func toPullRequests(jsonResp string) ([]PullRequest, error) {
 			return nil, fmt.Errorf("unexpected pull request state: %s", edge.Node.State)
 		}
 
+		commits := []string{}
+		for _, node := range edge.Node.Commits.Nodes {
+			commits = append(commits, node.Commit.Oid)
+		}
+
 		results = append(results, PullRequest{
 			edge.Node.HeadRefName,
 			state,
-			edge.Node.Number, edge.Node.Url, edge.Node.Author.Login,
+			edge.Node.Number,
+			commits,
+			edge.Node.Url,
+			edge.Node.Author.Login,
 		})
 	}
 
@@ -288,7 +403,7 @@ func DeleteBranches(branches []Branch, conn Connection) ([]Branch, error) {
 	if err != nil {
 		return nil, err
 	}
-	branchesAfter := toBranch(strings.Split(branchNamesAfter, "\n"))
+	branchesAfter := toBranch(splitLines(branchNamesAfter))
 
 	return checkDeleted(branches, branchesAfter), nil
 }
@@ -360,6 +475,21 @@ func (conn *ConnectionImpl) GetBranchNames() (string, error) {
 	return run("git", args)
 }
 
+func (conn *ConnectionImpl) GetLog(branchName string) (string, error) {
+	args := []string{
+		"log", "--first-parent", "--format=%H", branchName,
+	}
+	return run("git", args)
+}
+
+func (conn *ConnectionImpl) GetAssociatedBranchNames(oid string) (string, error) {
+	args := []string{
+		"branch", "--format=%(refname:lstrip=2)",
+		"--contains", oid,
+	}
+	return run("git", args)
+}
+
 func (conn *ConnectionImpl) GetPullRequests(
 	hostname string, repoNames []string, queryHashes string) (string, error) {
 	args := []string{
@@ -375,6 +505,13 @@ func (conn *ConnectionImpl) GetPullRequests(
           url
           state
           headRefName
+          commits(last: 10) {
+            nodes {
+              commit {
+                oid
+              }
+            }
+          }
           author { login }
         }
       }
@@ -403,6 +540,10 @@ func getQueryRepos(repoNames []string) string {
 	return repos.String()
 }
 
+func splitLines(text string) []string {
+	return strings.FieldsFunc(text, func(c rune) bool { return c == '\n' })
+}
+
 func run(name string, args []string) (string, error) {
 	var stdout, stderr bytes.Buffer
 	cmd := exec.Command(name, args...)
diff --git a/querygen.go b/querygen.go
index 457ae81..1785bf1 100644
--- a/querygen.go
+++ b/querygen.go
@@ -5,12 +5,12 @@ import (
 	"strings"
 )
 
-func GetQueryHashes(branches []Branch, defaultBranchName string) []string {
+func GetQueryHashes(branches []Branch) []string {
 	results := []string{}
 
 	var hashes strings.Builder
 	for i, branch := range branches {
-		if branch.Name == defaultBranchName {
+		if len(branch.Commits) == 0 {
 			continue
 		}
 
@@ -18,7 +18,7 @@ func GetQueryHashes(branches []Branch, defaultBranchName string) []string {
 		if i == len(branches)-1 {
 			separator = ""
 		}
-		hash := fmt.Sprintf("hash:%s%s", branch.LastObjectId, separator)
+		hash := fmt.Sprintf("hash:%s%s", branch.Commits[len(branch.Commits)-1], separator)
 
 		// https://docs.github.com/en/rest/reference/search#limitations-on-query-length
 		if len(hashes.String())+len(hash) > 256 {


Test patch:
diff --git a/poi_test.go b/poi_test.go
index 866e32f..40dff71 100644
--- a/poi_test.go
+++ b/poi_test.go
@@ -17,18 +17,37 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithMergedPR(t *testing.T) {
 		CheckRepos(nil, nil).
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
+		GetLog([]connmock.LogStub{
+			{"main", "main"}, {"issue1", "issue1"},
+		}, nil, nil).
+		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
+		}, nil, nil).
 		GetPullRequests("issue1Merged", nil, nil)
 
 	actual, _ := GetBranches(s.Conn)
 
 	assert.Equal(t, []Branch{
-		{false, "issue1", "356a192b7913b04c54574d18c28d46e6395428ab",
+		{
+			false, "issue1",
+			[]string{
+				"356a192b7913b04c54574d18c28d46e6395428ab",
+			},
 			[]PullRequest{
-				{"issue1", Merged, 1, "https://github.com/owner/repo/pull/1", "owner"},
+				{
+					"issue1", Merged, 1,
+					[]string{
+						"356a192b7913b04c54574d18c28d46e6395428ab",
+					},
+					"https://github.com/owner/repo/pull/1", "owner",
+				},
 			},
 			Deletable,
 		},
-		{true, "main", "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c",
+		{
+			true, "main",
+			[]string{},
 			[]PullRequest{},
 			NotDeletable,
 		},
@@ -43,18 +62,82 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithUpstreamMergedPR(t *testing
 		CheckRepos(nil, nil).
 		GetRepoNames("origin_upstream", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
+		GetLog([]connmock.LogStub{
+			{"main", "main"}, {"issue1", "issue1"},
+		}, nil, nil).
+		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
+		}, nil, nil).
 		GetPullRequests("issue1UpMerged", nil, nil)
 
 	actual, _ := GetBranches(s.Conn)
 
 	assert.Equal(t, []Branch{
-		{false, "issue1", "356a192b7913b04c54574d18c28d46e6395428ab",
+		{
+			false, "issue1",
+			[]string{
+				"356a192b7913b04c54574d18c28d46e6395428ab",
+			},
 			[]PullRequest{
-				{"issue1", Merged, 1, "https://github.com/parent-owner/repo/pull/1", "owner"},
+				{
+					"issue1", Merged, 1,
+					[]string{
+						"356a192b7913b04c54574d18c28d46e6395428ab",
+					},
+					"https://github.com/parent-owner/repo/pull/1", "owner",
+				},
 			},
 			Deletable,
 		},
-		{true, "main", "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c",
+		{
+			true, "main",
+			[]string{},
+			[]PullRequest{},
+			NotDeletable,
+		},
+	}, actual)
+}
+
+func Test_ShouldNotDeletableWhenBranchIsCheckedOut(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).
+		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
+		}, nil, nil).
+		GetPullRequests("issue1Merged", nil, nil)
+
+	actual, _ := GetBranches(s.Conn)
+
+	assert.Equal(t, []Branch{
+		{
+			true, "issue1",
+			[]string{
+				"356a192b7913b04c54574d18c28d46e6395428ab",
+			},
+			[]PullRequest{
+				{
+					"issue1", Merged, 1,
+					[]string{
+						"356a192b7913b04c54574d18c28d46e6395428ab",
+					},
+					"https://github.com/owner/repo/pull/1", "owner",
+				},
+			},
+			NotDeletable,
+		},
+		{
+			false, "main",
+			[]string{},
 			[]PullRequest{},
 			NotDeletable,
 		},
@@ -69,18 +152,37 @@ func Test_ShouldNotDeletableWhenBranchesAssociatedWithClosedPR(t *testing.T) {
 		CheckRepos(nil, nil).
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
+		GetLog([]connmock.LogStub{
+			{"main", "main"}, {"issue1", "issue1"},
+		}, nil, nil).
+		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
+		}, nil, nil).
 		GetPullRequests("issue1Closed", nil, nil)
 
 	actual, _ := GetBranches(s.Conn)
 
 	assert.Equal(t, []Branch{
-		{false, "issue1", "356a192b7913b04c54574d18c28d46e6395428ab",
+		{
+			false, "issue1",
+			[]string{
+				"356a192b7913b04c54574d18c28d46e6395428ab",
+			},
 			[]PullRequest{
-				{"issue1", Closed, 1, "https://github.com/owner/repo/pull/1", "owner"},
+				{
+					"issue1", Closed, 1,
+					[]string{
+						"356a192b7913b04c54574d18c28d46e6395428ab",
+					},
+					"https://github.com/owner/repo/pull/1", "owner",
+				},
 			},
 			NotDeletable,
 		},
-		{true, "main", "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c",
+		{
+			true, "main",
+			[]string{},
 			[]PullRequest{},
 			NotDeletable,
 		},
@@ -95,19 +197,92 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithMergedAndClosedPRs(t *testi
 		CheckRepos(nil, nil).
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
+		GetLog([]connmock.LogStub{
+			{"main", "main"}, {"issue1", "issue1"},
+		}, nil, nil).
+		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
+		}, nil, nil).
 		GetPullRequests("issue1Merged_issue1Closed", nil, nil)
 
 	actual, _ := GetBranches(s.Conn)
 
 	assert.Equal(t, []Branch{
-		{false, "issue1", "356a192b7913b04c54574d18c28d46e6395428ab",
+		{
+			false, "issue1",
+			[]string{
+				"356a192b7913b04c54574d18c28d46e6395428ab",
+			},
 			[]PullRequest{
-				{"issue1", Closed, 1, "https://github.com/owner/repo/pull/1", "owner"},
-				{"issue1", Merged, 2, "https://github.com/owner/repo/pull/2", "owner"},
+				{
+					"issue1", Closed, 1,
+					[]string{
+						"356a192b7913b04c54574d18c28d46e6395428ab",
+					},
+					"https://github.com/owner/repo/pull/1", "owner",
+				},
+				{
+					"issue1", Merged, 2,
+					[]string{
+						"356a192b7913b04c54574d18c28d46e6395428ab",
+					},
+					"https://github.com/owner/repo/pull/2", "owner",
+				},
 			},
 			Deletable,
 		},
-		{true, "main", "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c",
+		{
+			true, "main",
+			[]string{},
+			[]PullRequest{},
+			NotDeletable,
+		},
+	}, actual)
+}
+
+func Test_ShouldNotDeletableWhenBranchesAssociatedWithNotFullyMergedPR(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_issue1Merged"}, {"issue1", "issue1CommitAfterMerge"},
+		}, nil, nil).
+		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+			{"cb197ba87e4ad323b1008c611212deb7da2a4a49", "main"},
+			{"b8a2645298053fb62ea03e27feea6c483d3fd27e", "issue1"},
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
+		}, nil, nil).
+		GetPullRequests("issue1Merged", nil, nil)
+
+	actual, _ := GetBranches(s.Conn)
+
+	assert.Equal(t, []Branch{
+		{
+			false, "issue1",
+			[]string{
+				"b8a2645298053fb62ea03e27feea6c483d3fd27e",
+				"356a192b7913b04c54574d18c28d46e6395428ab",
+			},
+			[]PullRequest{
+				{
+					"issue1", Merged, 1,
+					[]string{
+						"356a192b7913b04c54574d18c28d46e6395428ab",
+					},
+					"https://github.com/owner/repo/pull/1", "owner",
+				},
+			},
+			NotDeletable,
+		},
+		{
+			true, "main",
+			[]string{},
 			[]PullRequest{},
 			NotDeletable,
 		},
@@ -153,6 +328,44 @@ func Test_ReturnsAnErrorWhenGetBranchNamesFails(t *testing.T) {
 	assert.NotNil(t, err)
 }
 
+func Test_ReturnsAnErrorWhenGetLogFails(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"},
+		}, errors.New("failed to run external command: git"), nil)
+
+	_, err := GetBranches(s.Conn)
+
+	assert.NotNil(t, err)
+}
+
+func Test_ReturnsAnErrorWhenGetAssociatedBranchNamesFails(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).
+		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
+		}, errors.New("failed to run external command: git"), nil)
+
+	_, err := GetBranches(s.Conn)
+
+	assert.NotNil(t, err)
+}
+
 func Test_ReturnsAnErrorWhenGetPullRequestsFails(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
@@ -161,6 +374,13 @@ func Test_ReturnsAnErrorWhenGetPullRequestsFails(t *testing.T) {
 		CheckRepos(nil, nil).
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
+		GetLog([]connmock.LogStub{
+			{"main", "main"}, {"issue1", "issue1"},
+		}, nil, nil).
+		GetAssociatedBranchNames([]connmock.AssociatedBranchNamesStub{
+			{"356a192b7913b04c54574d18c28d46e6395428ab", "issue1"},
+			{"b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "main_issue1"},
+		}, nil, nil).
 		GetPullRequests("issue1Merged", errors.New("failed to run external command: gh"), nil)
 
 	_, err := GetBranches(s.Conn)
@@ -177,15 +397,15 @@ func Test_DeletingDeletableBranches(t *testing.T) {
 		DeleteBranches(nil, connmock.NewConf(&connmock.Times{N: 1}))
 
 	branches := []Branch{
-		{false, "issue1", "", []PullRequest{}, Deletable},
-		{true, "main", "", []PullRequest{}, NotDeletable},
+		{false, "issue1", []string{}, []PullRequest{}, Deletable},
+		{true, "main", []string{}, []PullRequest{}, NotDeletable},
 	}
 
 	actual, _ := DeleteBranches(branches, s.Conn)
 
 	expected := []Branch{
-		{false, "issue1", "", []PullRequest{}, Deleted},
-		{true, "main", "", []PullRequest{}, NotDeletable},
+		{false, "issue1", []string{}, []PullRequest{}, Deleted},
+		{true, "main", []string{}, []PullRequest{}, NotDeletable},
 	}
 	assert.Equal(t, expected, actual)
 }
@@ -198,15 +418,15 @@ func Test_DoNotDeleteNotDeletableBranches(t *testing.T) {
 		DeleteBranches(nil, connmock.NewConf(&connmock.Times{N: 0}))
 
 	branches := []Branch{
-		{false, "issue1", "", []PullRequest{}, NotDeletable},
-		{true, "main", "", []PullRequest{}, NotDeletable},
+		{false, "issue1", []string{}, []PullRequest{}, NotDeletable},
+		{true, "main", []string{}, []PullRequest{}, NotDeletable},
 	}
 
 	actual, _ := DeleteBranches(branches, s.Conn)
 
 	expected := []Branch{
-		{false, "issue1", "", []PullRequest{}, NotDeletable},
-		{true, "main", "", []PullRequest{}, NotDeletable},
+		{false, "issue1", []string{}, []PullRequest{}, NotDeletable},
+		{true, "main", []string{}, []PullRequest{}, NotDeletable},
 	}
 	assert.Equal(t, expected, actual)
 }
diff --git a/querygen_test.go b/querygen_test.go
index b8a5380..8303c40 100644
--- a/querygen_test.go
+++ b/querygen_test.go
@@ -10,19 +10,53 @@ func Test_GetQueryHashes(t *testing.T) {
 	assert.Equal(t,
 		[]string{
 			"hash:356a192b7913b04c54574d18c28d46e6395428ab " +
-				"hash:da4b9237bacccdf19c0760cab7aec4a8359010b0 " +
+				"hash:08a2aaaadff191eb76974b9b3d8b71f202c0156e " +
 				"hash:77de68daecd823babbb58edb1c8e14d7106e83bb " +
 				"hash:1b6453892473a467d07372d45eb05abc2031647a " +
 				"hash:ac3478d69a3c81fa62e60f5c3696165a4e5e6ac4 ",
 			"hash:c1dfd96eea8cc2b62785275bca38ac261256e278",
 		},
 		GetQueryHashes([]Branch{
-			{false, "main", "b28b7af69320201d1cf206ebf28373980add1451", []PullRequest{}, Unknown},
-			{true, "issue1", "356a192b7913b04c54574d18c28d46e6395428ab", []PullRequest{}, Unknown},
-			{false, "issue2", "da4b9237bacccdf19c0760cab7aec4a8359010b0", []PullRequest{}, Unknown},
-			{false, "issue3", "77de68daecd823babbb58edb1c8e14d7106e83bb", []PullRequest{}, Unknown},
-			{false, "issue4", "1b6453892473a467d07372d45eb05abc2031647a", []PullRequest{}, Unknown},
-			{false, "issue5", "ac3478d69a3c81fa62e60f5c3696165a4e5e6ac4", []PullRequest{}, Unknown},
-			{false, "issue6", "c1dfd96eea8cc2b62785275bca38ac261256e278", []PullRequest{}, Unknown},
-		}, "main"))
+			{false, "main",
+				[]string{},
+				[]PullRequest{}, Unknown,
+			},
+			{true, "issue1",
+				[]string{
+					"356a192b7913b04c54574d18c28d46e6395428ab",
+				},
+				[]PullRequest{}, Unknown,
+			},
+			{false, "issue2",
+				[]string{
+					"da4b9237bacccdf19c0760cab7aec4a8359010b0",
+					"08a2aaaadff191eb76974b9b3d8b71f202c0156e",
+				},
+				[]PullRequest{}, Unknown,
+			},
+			{false, "issue3",
+				[]string{
+					"77de68daecd823babbb58edb1c8e14d7106e83bb",
+				},
+				[]PullRequest{}, Unknown,
+			},
+			{false, "issue4",
+				[]string{
+					"1b6453892473a467d07372d45eb05abc2031647a",
+				},
+				[]PullRequest{}, Unknown,
+			},
+			{false, "issue5",
+				[]string{
+					"ac3478d69a3c81fa62e60f5c3696165a4e5e6ac4",
+				},
+				[]PullRequest{}, Unknown,
+			},
+			{false, "issue6",
+				[]string{
+					"c1dfd96eea8cc2b62785275bca38ac261256e278",
+				},
+				[]PullRequest{}, Unknown,
+			},
+		}))
 }