Repository: seachicken/gh-poi
Pull request #89 (2022-11-20 12:44:08)

Improve performance for very large repositories
I was using `git branch --all --contains` to find the oid associated with a PR, but I found it to be very slow when the repository has many remote branches and a long history.
Substituting oid from the remotes branch speeds things up, so I changed it.


Patch:
diff --git a/conn/command.go b/conn/command.go
index 37c3674..ee8402b 100644
--- a/conn/command.go
+++ b/conn/command.go
@@ -79,6 +79,20 @@ func (conn *Connection) GetMergedBranchNames(ctx context.Context, remoteName str
 	return conn.run(ctx, "git", args, None)
 }
 
+func (conn *Connection) GetRemoteHeadOid(ctx context.Context, remoteName string, branchName string) (string, error) {
+	args := []string{
+		"rev-parse", fmt.Sprintf("%s/%s", remoteName, branchName),
+	}
+	return conn.run(ctx, "git", args, None)
+}
+
+func (conn *Connection) GetLsRemoteHeadOid(ctx context.Context, url string, branchName string) (string, error) {
+	args := []string{
+		"ls-remote", url, branchName,
+	}
+	return conn.run(ctx, "git", args, None)
+}
+
 func (conn *Connection) GetLog(ctx context.Context, branchName string) (string, error) {
 	args := []string{
 		"log", "--first-parent", "--max-count=30", "--format=%H", branchName, "--",
diff --git a/conn/fixtures/git/configMerge_forkMain.txt b/conn/fixtures/git/config_mergeForkMain.txt
similarity index 100%
rename from conn/fixtures/git/configMerge_forkMain.txt
rename to conn/fixtures/git/config_mergeForkMain.txt
diff --git a/conn/fixtures/git/configMerge_issue1.txt b/conn/fixtures/git/config_mergeIssue1.txt
similarity index 100%
rename from conn/fixtures/git/configMerge_issue1.txt
rename to conn/fixtures/git/config_mergeIssue1.txt
diff --git a/conn/fixtures/git/configMerge_main.txt b/conn/fixtures/git/config_mergeMain.txt
similarity index 100%
rename from conn/fixtures/git/configMerge_main.txt
rename to conn/fixtures/git/config_mergeMain.txt
diff --git a/conn/fixtures/git/config_remote.txt b/conn/fixtures/git/config_remote.txt
new file mode 100644
index 0000000..5e4e788
--- /dev/null
+++ b/conn/fixtures/git/config_remote.txt
@@ -0,0 +1,1 @@
+git@github.com:owner/repo.git
diff --git a/conn/fixtures/git/lsRemoteHead_issue1.txt b/conn/fixtures/git/lsRemoteHead_issue1.txt
new file mode 100644
index 0000000..a3633d9
--- /dev/null
+++ b/conn/fixtures/git/lsRemoteHead_issue1.txt
@@ -0,0 +1,1 @@
+a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0	refs/heads/issue1
diff --git a/conn/fixtures/git/remoteHead_issue1.txt b/conn/fixtures/git/remoteHead_issue1.txt
new file mode 100644
index 0000000..4915d74
--- /dev/null
+++ b/conn/fixtures/git/remoteHead_issue1.txt
@@ -0,0 +1,1 @@
+a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0
diff --git a/conn/stub.go b/conn/stub.go
index 5110e57..2acd4f5 100644
--- a/conn/stub.go
+++ b/conn/stub.go
@@ -23,6 +23,16 @@ type (
 		Times *Times
 	}
 
+	RemoteHeadStub struct {
+		BranchName string
+		Filename   string
+	}
+
+	LsRemoteHeadStub struct {
+		BranchName string
+		Filename   string
+	}
+
 	AssociatedBranchNamesStub struct {
 		Oid      string
 		Filename string
@@ -124,6 +134,50 @@ func (s *Stub) GetMergedBranchNames(filename string, err error, conf *Conf) *Stu
 	return s
 }
 
+func (s *Stub) GetRemoteHeadOid(stubs []RemoteHeadStub, err error, conf *Conf) *Stub {
+	s.t.Helper()
+	if stubs == nil {
+		configure(
+			s.Conn.EXPECT().
+				GetRemoteHeadOid(gomock.Any(), gomock.Any(), gomock.Any()).
+				Return("", err),
+			conf,
+		)
+	} else {
+		for _, stub := range stubs {
+			configure(
+				s.Conn.EXPECT().
+					GetRemoteHeadOid(gomock.Any(), gomock.Any(), stub.BranchName).
+					Return(s.readFile("git", "remoteHead", stub.Filename), err),
+				conf,
+			)
+		}
+	}
+	return s
+}
+
+func (s *Stub) GetLsRemoteHeadOid(stubs []LsRemoteHeadStub, err error, conf *Conf) *Stub {
+	s.t.Helper()
+	if stubs == nil {
+		configure(
+			s.Conn.EXPECT().
+				GetLsRemoteHeadOid(gomock.Any(), gomock.Any(), gomock.Any()).
+				Return("", err),
+			conf,
+		)
+	} else {
+		for _, stub := range stubs {
+			configure(
+				s.Conn.EXPECT().
+					GetLsRemoteHeadOid(gomock.Any(), gomock.Any(), stub.BranchName).
+					Return(s.readFile("git", "lsRemoteHead", stub.Filename), err),
+				conf,
+			)
+		}
+	}
+	return s
+}
+
 func (s *Stub) GetAssociatedRefNames(stubs []AssociatedBranchNamesStub, err error, conf *Conf) *Stub {
 	s.t.Helper()
 	for _, stub := range stubs {
@@ -181,7 +235,7 @@ func (s *Stub) GetConfig(stubs []ConfigStub, err error, conf *Conf) *Stub {
 			s.Conn.
 				EXPECT().
 				GetConfig(gomock.Any(), stub.BranchName).
-				Return(s.readFile("git", "configMerge", stub.Filename), err),
+				Return(s.readFile("git", "config", stub.Filename), err),
 			conf,
 		)
 	}
diff --git a/mocks/poi_mock.go b/mocks/poi_mock.go
index 55c87b2..00032b6 100644
--- a/mocks/poi_mock.go
+++ b/mocks/poi_mock.go
@@ -138,6 +138,21 @@ func (mr *MockConnectionMockRecorder) GetLog(ctx, branchName interface{}) *gomoc
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLog", reflect.TypeOf((*MockConnection)(nil).GetLog), ctx, branchName)
 }
 
+// GetLsRemoteHeadOid mocks base method.
+func (m *MockConnection) GetLsRemoteHeadOid(ctx context.Context, url, branchName string) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetLsRemoteHeadOid", ctx, url, branchName)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetLsRemoteHeadOid indicates an expected call of GetLsRemoteHeadOid.
+func (mr *MockConnectionMockRecorder) GetLsRemoteHeadOid(ctx, url, branchName interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLsRemoteHeadOid", reflect.TypeOf((*MockConnection)(nil).GetLsRemoteHeadOid), ctx, url, branchName)
+}
+
 // GetMergedBranchNames mocks base method.
 func (m *MockConnection) GetMergedBranchNames(ctx context.Context, remoteName, branchName string) (string, error) {
 	m.ctrl.T.Helper()
@@ -168,6 +183,21 @@ func (mr *MockConnectionMockRecorder) GetPullRequests(ctx, hostname, repoNames,
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPullRequests", reflect.TypeOf((*MockConnection)(nil).GetPullRequests), ctx, hostname, repoNames, queryHashes)
 }
 
+// GetRemoteHeadOid mocks base method.
+func (m *MockConnection) GetRemoteHeadOid(ctx context.Context, remoteName, branchName string) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetRemoteHeadOid", ctx, remoteName, branchName)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetRemoteHeadOid indicates an expected call of GetRemoteHeadOid.
+func (mr *MockConnectionMockRecorder) GetRemoteHeadOid(ctx, remoteName, branchName interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRemoteHeadOid", reflect.TypeOf((*MockConnection)(nil).GetRemoteHeadOid), ctx, remoteName, branchName)
+}
+
 // GetRemoteNames mocks base method.
 func (m *MockConnection) GetRemoteNames(ctx context.Context) (string, error) {
 	m.ctrl.T.Helper()
diff --git a/poi.go b/poi.go
index f15f778..b67ffe6 100644
--- a/poi.go
+++ b/poi.go
@@ -21,6 +21,8 @@ type (
 		GetRepoNames(ctx context.Context, hostname string, repoName string) (string, error)
 		GetBranchNames(ctx context.Context) (string, error)
 		GetMergedBranchNames(ctx context.Context, remoteName string, branchName string) (string, error)
+		GetRemoteHeadOid(ctx context.Context, remoteName string, branchName string) (string, error)
+		GetLsRemoteHeadOid(ctx context.Context, url string, branchName string) (string, error)
 		GetLog(ctx context.Context, branchName string) (string, error)
 		GetAssociatedRefNames(ctx context.Context, oid string) (string, error)
 		GetPullRequests(ctx context.Context, hostname string, repoNames []string, queryHashes string) (string, error)
@@ -39,12 +41,13 @@ type (
 	BranchState int
 
 	Branch struct {
-		Head         bool
-		Name         string
-		IsMerged     bool
-		Commits      []string
-		PullRequests []PullRequest
-		State        BranchState
+		Head          bool
+		Name          string
+		IsMerged      bool
+		RemoteHeadOid string
+		Commits       []string
+		PullRequests  []PullRequest
+		State         BranchState
 	}
 
 	PullRequestState int
@@ -130,7 +133,7 @@ func GetBranches(ctx context.Context, remote Remote, connection Connection, dryR
 			return nil, err
 		}
 		branches = applyMerged(branches, extractMergedBranchNames(splitLines(mergedNames)))
-		branches, err = applyCommits(ctx, branches, defaultBranchName, connection)
+		branches, err = applyCommits(ctx, remote, branches, defaultBranchName, connection)
 		if err != nil {
 			return nil, err
 		}
@@ -185,6 +188,7 @@ func GetBranches(ctx context.Context, remote Remote, connection Connection, dryR
 			result = append(result, Branch{
 				true, defaultBranchName,
 				false,
+				"",
 				[]string{},
 				[]PullRequest{},
 				NotDeletable,
@@ -285,7 +289,7 @@ func nameExists(name string, names []string) bool {
 	return false
 }
 
-func applyCommits(ctx context.Context, branches []Branch, defaultBranchName string, connection Connection) ([]Branch, error) {
+func applyCommits(ctx context.Context, remote Remote, branches []Branch, defaultBranchName string, connection Connection) ([]Branch, error) {
 	results := []Branch{}
 
 	for _, branch := range branches {
@@ -294,12 +298,29 @@ func applyCommits(ctx context.Context, branches []Branch, defaultBranchName stri
 			continue
 		}
 
+		if remoteHeadOid, err := connection.GetRemoteHeadOid(ctx, remote.Name, branch.Name); err == nil {
+			branch.RemoteHeadOid = splitLines(remoteHeadOid)[0]
+		} else {
+			result, _ := connection.GetConfig(ctx, fmt.Sprintf("branch.%s.remote", branch.Name))
+			splitResults := splitLines(result)
+			if len(splitResults) > 0 {
+				remoteUrl := splitResults[0]
+				if result, err := connection.GetLsRemoteHeadOid(ctx, remoteUrl, branch.Name); err == nil {
+					splitResults := strings.Fields(result)
+					if len(splitResults) > 0 {
+						branch.RemoteHeadOid = splitResults[0]
+					}
+				}
+			}
+		}
+
 		oids, err := connection.GetLog(ctx, branch.Name)
 		if err != nil {
 			return nil, err
 		}
 
-		trimmedOids, err := trimBranch(ctx, splitLines(oids), branch.IsMerged,
+		trimmedOids, err := trimBranch(
+			ctx, splitLines(oids), branch.RemoteHeadOid, branch.IsMerged,
 			branch.Name, defaultBranchName, connection)
 		if err != nil {
 			return nil, err
@@ -312,13 +333,13 @@ func applyCommits(ctx context.Context, branches []Branch, defaultBranchName stri
 	return results, nil
 }
 
-func trimBranch(ctx context.Context, oids []string, isMerged bool,
+func trimBranch(ctx context.Context, oids []string, remoteHeadOid string, isMerged bool,
 	branchName string, defaultBranchName string, connection Connection) ([]string, error) {
 	results := []string{}
 	childNames := []string{}
 
 	for i, oid := range oids {
-		if isMerged {
+		if len(remoteHeadOid) > 0 || isMerged {
 			results = append(results, oid)
 			break
 		}
@@ -522,6 +543,7 @@ func toBranch(branchNames []string) []Branch {
 			splitedNames[0] == "*",
 			splitedNames[1],
 			false,
+			"",
 			[]string{},
 			[]PullRequest{},
 			Unknown,
diff --git a/querygen.go b/querygen.go
index 802df14..6a79a71 100644
--- a/querygen.go
+++ b/querygen.go
@@ -10,7 +10,7 @@ func getQueryHashes(branches []Branch) []string {
 
 	var hashes strings.Builder
 	for i, branch := range branches {
-		if len(branch.Commits) == 0 {
+		if branch.RemoteHeadOid == "" && len(branch.Commits) == 0 {
 			continue
 		}
 
@@ -18,7 +18,13 @@ func getQueryHashes(branches []Branch) []string {
 		if i == len(branches)-1 {
 			separator = ""
 		}
-		hash := fmt.Sprintf("hash:%s%s", branch.Commits[len(branch.Commits)-1], separator)
+		oid := ""
+		if branch.RemoteHeadOid == "" {
+			oid = branch.Commits[len(branch.Commits)-1]
+		} else {
+			oid = branch.RemoteHeadOid
+		}
+		hash := fmt.Sprintf("hash:%s%s", oid, 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/conn/command_test.go b/conn/command_test.go
index c0b20e5..f6af4af 100644
--- a/conn/command_test.go
+++ b/conn/command_test.go
@@ -79,7 +79,7 @@ func Test_RepoBasic(t *testing.T) {
 	t.Run("GetConfig", func(t *testing.T) {
 		actual, _ := conn.GetConfig(context.Background(), "branch.main.merge")
 		assert.Equal(t,
-			stub.readFile("git", "configMerge", "main"),
+			stub.readFile("git", "config", "mergeMain"),
 			actual,
 		)
 	})
diff --git a/poi_test.go b/poi_test.go
index f08b97d..1e65186 100644
--- a/poi_test.go
+++ b/poi_test.go
@@ -10,6 +10,120 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+var ErrCommand = errors.New("failed to run external command")
+
+func Test_ShouldBeDeletableWhenRemoteBranchesAssociatedWithMergedPR(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	s := conn.Setup(ctrl).
+		CheckRepos(nil, nil).
+		GetRemoteNames("origin", nil, nil).
+		GetSshConfig("github.com", nil, nil).
+		GetRepoNames("origin", nil, nil).
+		GetBranchNames("@main_issue1", nil, nil).
+		GetMergedBranchNames("@main_issue1", nil, nil).
+		GetRemoteHeadOid([]conn.RemoteHeadStub{
+			{BranchName: "issue1", Filename: "issue1"},
+		}, nil, nil).
+		GetLog([]conn.LogStub{
+			{BranchName: "main", Filename: "main_issue1Merged"}, {BranchName: "issue1", Filename: "issue1Merged"},
+		}, nil, nil).
+		GetPullRequests("issue1Merged", nil, nil).
+		GetUncommittedChanges("", nil, nil).
+		GetConfig([]conn.ConfigStub{
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+		}, nil, nil)
+	remote, _ := GetRemote(context.Background(), s.Conn)
+
+	actual, _ := GetBranches(context.Background(), remote, s.Conn, false)
+
+	assert.Equal(t, []Branch{
+		{
+			false, "issue1", true,
+			"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
+			[]string{
+				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
+			},
+			[]PullRequest{
+				{
+					"issue1", Merged, false, 1,
+					[]string{
+						"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
+					},
+					"https://github.com/owner/repo/pull/1", "owner",
+				},
+			},
+			Deletable,
+		},
+		{
+			true, "main", true,
+			"",
+			[]string{},
+			[]PullRequest{},
+			NotDeletable,
+		},
+	}, actual)
+}
+
+func Test_ShouldBeDeletableWhenLsRemoteBranchesAssociatedWithMergedPR(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	s := conn.Setup(ctrl).
+		CheckRepos(nil, nil).
+		GetRemoteNames("origin", nil, nil).
+		GetSshConfig("github.com", nil, nil).
+		GetRepoNames("origin", nil, nil).
+		GetBranchNames("@main_issue1", nil, nil).
+		GetMergedBranchNames("@main_issue1", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid([]conn.LsRemoteHeadStub{
+			{BranchName: "issue1", Filename: "issue1"},
+		}, nil, nil).
+		GetLog([]conn.LogStub{
+			{BranchName: "main", Filename: "main_issue1Merged"}, {BranchName: "issue1", Filename: "issue1Merged"},
+		}, nil, nil).
+		GetPullRequests("issue1Merged", nil, nil).
+		GetUncommittedChanges("", nil, nil).
+		GetConfig([]conn.ConfigStub{
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
+		}, nil, nil)
+	remote, _ := GetRemote(context.Background(), s.Conn)
+
+	actual, _ := GetBranches(context.Background(), remote, s.Conn, false)
+
+	assert.Equal(t, []Branch{
+		{
+			false, "issue1", true,
+			"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
+			[]string{
+				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
+			},
+			[]PullRequest{
+				{
+					"issue1", Merged, false, 1,
+					[]string{
+						"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
+					},
+					"https://github.com/owner/repo/pull/1", "owner",
+				},
+			},
+			Deletable,
+		},
+		{
+			true, "main", true,
+			"",
+			[]string{},
+			[]PullRequest{},
+			NotDeletable,
+		},
+	}, actual)
+}
+
 func Test_ShouldBeDeletableWhenBranchesAssociatedWithMergedPR(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
@@ -21,6 +135,8 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithMergedPR(t *testing.T) {
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main_issue1", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main_issue1Merged"}, {BranchName: "issue1", Filename: "issue1Merged"},
 		}, nil, nil).
@@ -32,8 +148,9 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithMergedPR(t *testing.T) {
 		GetPullRequests("issue1Merged", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -42,6 +159,7 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithMergedPR(t *testing.T) {
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", true,
+			"",
 			[]string{
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
 			},
@@ -58,6 +176,7 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithMergedPR(t *testing.T) {
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -76,6 +195,8 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithSquashAndMergedPR(t *testin
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -86,8 +207,9 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithSquashAndMergedPR(t *testin
 		GetPullRequests("issue1Merged", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -96,6 +218,7 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithSquashAndMergedPR(t *testin
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", false,
+			"",
 			[]string{
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
 			},
@@ -112,6 +235,7 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithSquashAndMergedPR(t *testin
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -130,6 +254,8 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithUpstreamSquashAndMergedPR(t
 		GetRepoNames("origin_upstream", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -140,8 +266,9 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithUpstreamSquashAndMergedPR(t
 		GetPullRequests("issue1UpMerged", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -150,6 +277,7 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithUpstreamSquashAndMergedPR(t
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", false,
+			"",
 			[]string{
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
 			},
@@ -166,6 +294,7 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithUpstreamSquashAndMergedPR(t
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -184,6 +313,8 @@ func Test_ShouldBeDeletableWhenPRCheckoutBranchesAssociatedWithUpstreamSquashAnd
 		GetRepoNames("origin_upstream", nil, nil).
 		GetBranchNames("@main_forkMain", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "fork/main", Filename: "issue1"},
 		}, nil, nil).
@@ -194,8 +325,9 @@ func Test_ShouldBeDeletableWhenPRCheckoutBranchesAssociatedWithUpstreamSquashAnd
 		GetPullRequests("forkMainUpMerged", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.fork/main.merge", Filename: "forkMain"},
-			{BranchName: "branch.main.merge", Filename: "main"},
+			{BranchName: "branch.fork/main.merge", Filename: "mergeForkMain"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.fork/main.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -204,6 +336,7 @@ func Test_ShouldBeDeletableWhenPRCheckoutBranchesAssociatedWithUpstreamSquashAnd
 	assert.Equal(t, []Branch{
 		{
 			false, "fork/main", false,
+			"",
 			[]string{
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
 			},
@@ -220,6 +353,7 @@ func Test_ShouldBeDeletableWhenPRCheckoutBranchesAssociatedWithUpstreamSquashAnd
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -238,6 +372,8 @@ func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithTheCheckIsFalse(t *testing.
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("main_@issue1", nil, nil).
 		GetMergedBranchNames("main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -248,8 +384,9 @@ func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithTheCheckIsFalse(t *testing.
 		GetPullRequests("issue1Merged", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil).
 		CheckoutBranch(nil, conn.NewConf(&conn.Times{N: 1}))
 	remote, _ := GetRemote(context.Background(), s.Conn)
@@ -259,6 +396,7 @@ func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithTheCheckIsFalse(t *testing.
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", false,
+			"",
 			[]string{
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
 			},
@@ -275,6 +413,7 @@ func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithTheCheckIsFalse(t *testing.
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -293,6 +432,8 @@ func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithTheCheckIsTrue(t *testing.T
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("main_@issue1", nil, nil).
 		GetMergedBranchNames("main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -303,8 +444,9 @@ func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithTheCheckIsTrue(t *testing.T
 		GetPullRequests("issue1Merged", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil).
 		CheckoutBranch(nil, conn.NewConf(&conn.Times{N: 0}))
 	remote, _ := GetRemote(context.Background(), s.Conn)
@@ -314,6 +456,7 @@ func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithTheCheckIsTrue(t *testing.T
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", false,
+			"",
 			[]string{
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
 			},
@@ -330,6 +473,7 @@ func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithTheCheckIsTrue(t *testing.T
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -348,6 +492,8 @@ func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithoutADefaultBranch(t *testin
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@issue1", nil, nil).
 		GetMergedBranchNames("empty", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -358,7 +504,8 @@ func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithoutADefaultBranch(t *testin
 		GetPullRequests("issue1Merged", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil).
 		CheckoutBranch(nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
@@ -368,6 +515,7 @@ func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithoutADefaultBranch(t *testin
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", false,
+			"",
 			[]string{
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
 			},
@@ -384,6 +532,7 @@ func Test_ShouldBeDeletableWhenBranchIsCheckedOutWithoutADefaultBranch(t *testin
 		},
 		{
 			true, "main", false,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -402,6 +551,8 @@ func Test_ShouldNotDeletableWhenBranchHasModifiedUncommittedChanges(t *testing.T
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("main_@issue1", nil, nil).
 		GetMergedBranchNames("main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -412,8 +563,9 @@ func Test_ShouldNotDeletableWhenBranchHasModifiedUncommittedChanges(t *testing.T
 		GetPullRequests("issue1Merged", nil, nil).
 		GetUncommittedChanges(" M README.md", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil).
 		CheckoutBranch(nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
@@ -423,6 +575,7 @@ func Test_ShouldNotDeletableWhenBranchHasModifiedUncommittedChanges(t *testing.T
 	assert.Equal(t, []Branch{
 		{
 			true, "issue1", false,
+			"",
 			[]string{
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
 			},
@@ -439,6 +592,7 @@ func Test_ShouldNotDeletableWhenBranchHasModifiedUncommittedChanges(t *testing.T
 		},
 		{
 			false, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -457,6 +611,8 @@ func Test_ShouldBeDeletableWhenBranchHasUntrackedUncommittedChanges(t *testing.T
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("main_@issue1", nil, nil).
 		GetMergedBranchNames("main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -467,8 +623,9 @@ func Test_ShouldBeDeletableWhenBranchHasUntrackedUncommittedChanges(t *testing.T
 		GetPullRequests("issue1Merged", nil, nil).
 		GetUncommittedChanges("?? new.txt", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil).
 		CheckoutBranch(nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
@@ -478,6 +635,7 @@ func Test_ShouldBeDeletableWhenBranchHasUntrackedUncommittedChanges(t *testing.T
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", false,
+			"",
 			[]string{
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
 			},
@@ -494,6 +652,7 @@ func Test_ShouldBeDeletableWhenBranchHasUntrackedUncommittedChanges(t *testing.T
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -512,6 +671,8 @@ func Test_ShouldNotDeletableWhenBranchesAssociatedWithClosedPR(t *testing.T) {
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -522,8 +683,9 @@ func Test_ShouldNotDeletableWhenBranchesAssociatedWithClosedPR(t *testing.T) {
 		GetPullRequests("issue1Closed", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -532,6 +694,7 @@ func Test_ShouldNotDeletableWhenBranchesAssociatedWithClosedPR(t *testing.T) {
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", false,
+			"",
 			[]string{
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
 			},
@@ -548,6 +711,7 @@ func Test_ShouldNotDeletableWhenBranchesAssociatedWithClosedPR(t *testing.T) {
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -566,6 +730,8 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithSquashAndMergedAndClosedPRs
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -576,8 +742,9 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithSquashAndMergedAndClosedPRs
 		GetPullRequests("issue1Merged_issue1Closed", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -586,6 +753,7 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithSquashAndMergedAndClosedPRs
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", false,
+			"",
 			[]string{
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
 			},
@@ -609,6 +777,7 @@ func Test_ShouldBeDeletableWhenBranchesAssociatedWithSquashAndMergedAndClosedPRs
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -627,6 +796,8 @@ func Test_ShouldNotDeletableWhenBranchesAssociatedWithNotFullyMergedPR(t *testin
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main_issue1SquashAndMerged"}, {BranchName: "issue1", Filename: "issue1CommitAfterMerge"},
 		}, nil, nil).
@@ -639,8 +810,9 @@ func Test_ShouldNotDeletableWhenBranchesAssociatedWithNotFullyMergedPR(t *testin
 		GetPullRequests("issue1Merged", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -649,6 +821,7 @@ func Test_ShouldNotDeletableWhenBranchesAssociatedWithNotFullyMergedPR(t *testin
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", false,
+			"",
 			[]string{
 				"b8a2645298053fb62ea03e27feea6c483d3fd27e",
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
@@ -666,6 +839,7 @@ func Test_ShouldNotDeletableWhenBranchesAssociatedWithNotFullyMergedPR(t *testin
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -684,6 +858,8 @@ func Test_ShouldNotDeletableWhenDefaultBranchAssociatedWithMergedPR(t *testing.T
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -694,8 +870,9 @@ func Test_ShouldNotDeletableWhenDefaultBranchAssociatedWithMergedPR(t *testing.T
 		GetPullRequests("mainMerged", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -704,6 +881,7 @@ func Test_ShouldNotDeletableWhenDefaultBranchAssociatedWithMergedPR(t *testing.T
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", false,
+			"",
 			[]string{
 				"a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0",
 			},
@@ -712,6 +890,7 @@ func Test_ShouldNotDeletableWhenDefaultBranchAssociatedWithMergedPR(t *testing.T
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{
 				{
@@ -738,6 +917,8 @@ func Test_BranchesAndPRsAreNotAssociatedWhenManyLocalCommitsAreAhead(t *testing.
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"},
 			{BranchName: "issue1", Filename: "issue1ManyCommits"}, // return with '--max-count=3'
@@ -750,8 +931,9 @@ func Test_BranchesAndPRsAreNotAssociatedWhenManyLocalCommitsAreAhead(t *testing.
 		GetPullRequests("notFound", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -760,6 +942,7 @@ func Test_BranchesAndPRsAreNotAssociatedWhenManyLocalCommitsAreAhead(t *testing.
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", false,
+			"",
 			[]string{
 				"62d5d8280031f607f1db058da959a97f6a8e6d90",
 				"b8a2645298053fb62ea03e27feea6c483d3fd27e",
@@ -770,6 +953,7 @@ func Test_BranchesAndPRsAreNotAssociatedWhenManyLocalCommitsAreAhead(t *testing.
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -788,6 +972,8 @@ func Test_ShouldBeNoCommitHistoryWhenTheFirstCommitOfATopicBranchIsAssociatedWit
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "main"},
 		}, nil, nil).
@@ -797,8 +983,9 @@ func Test_ShouldBeNoCommitHistoryWhenTheFirstCommitOfATopicBranchIsAssociatedWit
 		GetPullRequests("notFound", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -807,12 +994,14 @@ func Test_ShouldBeNoCommitHistoryWhenTheFirstCommitOfATopicBranchIsAssociatedWit
 	assert.Equal(t, []Branch{
 		{
 			false, "issue1", false,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
 		},
 		{
 			true, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -831,6 +1020,8 @@ func Test_ShouldBeNoCommitHistoryWhenDetachedBranch(t *testing.T) {
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("main_@detached", nil, nil).
 		GetMergedBranchNames("main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"},
 		}, nil, nil).
@@ -840,7 +1031,7 @@ func Test_ShouldBeNoCommitHistoryWhenDetachedBranch(t *testing.T) {
 		GetPullRequests("notFound", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -849,12 +1040,14 @@ func Test_ShouldBeNoCommitHistoryWhenDetachedBranch(t *testing.T) {
 	assert.Equal(t, []Branch{
 		{
 			true, "(HEAD detached at a97e963)", false,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
 		},
 		{
 			false, "main", true,
+			"",
 			[]string{},
 			[]PullRequest{},
 			NotDeletable,
@@ -867,7 +1060,7 @@ func Test_ReturnsAnErrorWhenGetRemoteNamesFails(t *testing.T) {
 	defer ctrl.Finish()
 
 	s := conn.Setup(ctrl).
-		GetRemoteNames("origin", errors.New("failed to run external command: git"), nil)
+		GetRemoteNames("origin", ErrCommand, nil)
 
 	_, err := GetRemote(context.Background(), s.Conn)
 
@@ -881,10 +1074,12 @@ func Test_DoesNotReturnsAnErrorWhenGetSshConfigFails(t *testing.T) {
 	s := conn.Setup(ctrl).
 		CheckRepos(nil, nil).
 		GetRemoteNames("origin", nil, nil).
-		GetSshConfig("github.com", errors.New("failed to run external command: ssh"), nil).
+		GetSshConfig("github.com", ErrCommand, nil).
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -895,8 +1090,9 @@ func Test_DoesNotReturnsAnErrorWhenGetSshConfigFails(t *testing.T) {
 		GetPullRequests("issue1Merged", nil, nil).
 		GetUncommittedChanges("", nil, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -912,7 +1108,7 @@ func Test_ReturnsAnErrorWhenGetRepoNamesFails(t *testing.T) {
 	s := conn.Setup(ctrl).
 		GetRemoteNames("origin", nil, nil).
 		GetSshConfig("github.com", nil, nil).
-		GetRepoNames("origin", errors.New("failed to run external command: gh"), nil)
+		GetRepoNames("origin", ErrCommand, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
 	_, err := GetBranches(context.Background(), remote, s.Conn, false)
@@ -925,7 +1121,7 @@ func Test_ReturnsAnErrorWhenCheckReposFails(t *testing.T) {
 	defer ctrl.Finish()
 
 	s := conn.Setup(ctrl).
-		CheckRepos(errors.New("failed to run external command: gh"), nil).
+		CheckRepos(ErrCommand, nil).
 		GetRemoteNames("origin", nil, nil).
 		GetSshConfig("github.com", nil, nil).
 		GetRepoNames("origin", nil, nil)
@@ -945,7 +1141,7 @@ func Test_ReturnsAnErrorWhenGetBranchNamesFails(t *testing.T) {
 		GetRemoteNames("origin", nil, nil).
 		GetSshConfig("github.com", nil, nil).
 		GetRepoNames("origin", nil, nil).
-		GetBranchNames("@main_issue1", errors.New("failed to run external command: git"), nil)
+		GetBranchNames("@main_issue1", ErrCommand, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
 	_, err := GetBranches(context.Background(), remote, s.Conn, false)
@@ -963,7 +1159,7 @@ func Test_ReturnsAnErrorWhenGetMergedBranchNames(t *testing.T) {
 		GetSshConfig("github.com", nil, nil).
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
-		GetMergedBranchNames("@main", errors.New("failed to run external command: git"), nil)
+		GetMergedBranchNames("@main", ErrCommand, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
 	_, err := GetBranches(context.Background(), remote, s.Conn, false)
@@ -982,9 +1178,14 @@ func Test_ReturnsAnErrorWhenGetLogFails(t *testing.T) {
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
-		}, errors.New("failed to run external command: git"), nil)
+		}, ErrCommand, nil).
+		GetConfig([]conn.ConfigStub{
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
+		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
 	_, err := GetBranches(context.Background(), remote, s.Conn, false)
@@ -1003,13 +1204,18 @@ func Test_ReturnsAnErrorWhenGetAssociatedRefNamesFails(t *testing.T) {
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
 		GetAssociatedRefNames([]conn.AssociatedBranchNamesStub{
 			{Oid: "a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0", Filename: "issue1"},
 			{Oid: "6ebe3d30d23531af56bd23b5a098d3ccae2a534a", Filename: "main_issue1"},
-		}, errors.New("failed to run external command: git"), nil)
+		}, ErrCommand, nil).
+		GetConfig([]conn.ConfigStub{
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
+		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
 	_, err := GetBranches(context.Background(), remote, s.Conn, false)
@@ -1028,6 +1234,8 @@ func Test_ReturnsAnErrorWhenGetPullRequestsFails(t *testing.T) {
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -1035,7 +1243,10 @@ func Test_ReturnsAnErrorWhenGetPullRequestsFails(t *testing.T) {
 			{Oid: "a97e9630426df5d34ca9ee77ae1159bdfd5ff8f0", Filename: "issue1"},
 			{Oid: "6ebe3d30d23531af56bd23b5a098d3ccae2a534a", Filename: "main_issue1"},
 		}, nil, nil).
-		GetPullRequests("issue1Merged", errors.New("failed to run external command: gh"), nil)
+		GetPullRequests("issue1Merged", ErrCommand, nil).
+		GetConfig([]conn.ConfigStub{
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
+		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
 	_, err := GetBranches(context.Background(), remote, s.Conn, false)
@@ -1054,6 +1265,8 @@ func Test_ReturnsAnErrorWhenGetUncommittedChangesFails(t *testing.T) {
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("@main_issue1", nil, nil).
 		GetMergedBranchNames("@main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -1062,10 +1275,11 @@ func Test_ReturnsAnErrorWhenGetUncommittedChangesFails(t *testing.T) {
 			{Oid: "6ebe3d30d23531af56bd23b5a098d3ccae2a534a", Filename: "main_issue1"},
 		}, nil, nil).
 		GetPullRequests("issue1Merged", nil, nil).
-		GetUncommittedChanges("", errors.New("failed to run external command: git"), nil).
+		GetUncommittedChanges("", ErrCommand, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -1085,6 +1299,8 @@ func Test_ReturnsAnErrorWhenCheckoutBranchFails(t *testing.T) {
 		GetRepoNames("origin", nil, nil).
 		GetBranchNames("main_@issue1", nil, nil).
 		GetMergedBranchNames("main", nil, nil).
+		GetRemoteHeadOid(nil, ErrCommand, nil).
+		GetLsRemoteHeadOid(nil, nil, nil).
 		GetLog([]conn.LogStub{
 			{BranchName: "main", Filename: "main"}, {BranchName: "issue1", Filename: "issue1"},
 		}, nil, nil).
@@ -1094,10 +1310,11 @@ func Test_ReturnsAnErrorWhenCheckoutBranchFails(t *testing.T) {
 		}, nil, nil).
 		GetPullRequests("issue1Merged", nil, nil).
 		GetUncommittedChanges("", nil, nil).
-		CheckoutBranch(errors.New("failed to run external command: git"), nil).
+		CheckoutBranch(ErrCommand, nil).
 		GetConfig([]conn.ConfigStub{
-			{BranchName: "branch.main.merge", Filename: "main"},
-			{BranchName: "branch.issue1.merge", Filename: "issue1"},
+			{BranchName: "branch.main.merge", Filename: "mergeMain"},
+			{BranchName: "branch.issue1.merge", Filename: "mergeIssue1"},
+			{BranchName: "branch.issue1.remote", Filename: "remote"},
 		}, nil, nil)
 	remote, _ := GetRemote(context.Background(), s.Conn)
 
@@ -1115,15 +1332,15 @@ func Test_DeletingDeletableBranches(t *testing.T) {
 		DeleteBranches(nil, conn.NewConf(&conn.Times{N: 1}))
 
 	branches := []Branch{
-		{false, "issue1", false, []string{}, []PullRequest{}, Deletable},
-		{true, "main", true, []string{}, []PullRequest{}, NotDeletable},
+		{false, "issue1", false, "", []string{}, []PullRequest{}, Deletable},
+		{true, "main", true, "", []string{}, []PullRequest{}, NotDeletable},
 	}
 
 	actual, _ := DeleteBranches(context.Background(), branches, s.Conn)
 
 	expected := []Branch{
-		{false, "issue1", false, []string{}, []PullRequest{}, Deleted},
-		{true, "main", true, []string{}, []PullRequest{}, NotDeletable},
+		{false, "issue1", false, "", []string{}, []PullRequest{}, Deleted},
+		{true, "main", true, "", []string{}, []PullRequest{}, NotDeletable},
 	}
 	assert.Equal(t, expected, actual)
 }
@@ -1136,15 +1353,15 @@ func Test_DoNotDeleteNotDeletableBranches(t *testing.T) {
 		DeleteBranches(nil, conn.NewConf(&conn.Times{N: 0}))
 
 	branches := []Branch{
-		{false, "issue1", false, []string{}, []PullRequest{}, NotDeletable},
-		{true, "main", true, []string{}, []PullRequest{}, NotDeletable},
+		{false, "issue1", false, "", []string{}, []PullRequest{}, NotDeletable},
+		{true, "main", true, "", []string{}, []PullRequest{}, NotDeletable},
 	}
 
 	actual, _ := DeleteBranches(context.Background(), branches, s.Conn)
 
 	expected := []Branch{
-		{false, "issue1", false, []string{}, []PullRequest{}, NotDeletable},
-		{true, "main", true, []string{}, []PullRequest{}, NotDeletable},
+		{false, "issue1", false, "", []string{}, []PullRequest{}, NotDeletable},
+		{true, "main", true, "", []string{}, []PullRequest{}, NotDeletable},
 	}
 	assert.Equal(t, expected, actual)
 }
diff --git a/querygen_test.go b/querygen_test.go
index 74cf42d..69c161c 100644
--- a/querygen_test.go
+++ b/querygen_test.go
@@ -18,16 +18,19 @@ func Test_GetQueryHashes(t *testing.T) {
 		},
 		getQueryHashes([]Branch{
 			{false, "main", false,
+				"",
 				[]string{},
 				[]PullRequest{}, Unknown,
 			},
 			{true, "issue1", false,
+				"",
 				[]string{
 					"356a192b7913b04c54574d18c28d46e6395428ab",
 				},
 				[]PullRequest{}, Unknown,
 			},
 			{false, "issue2", false,
+				"",
 				[]string{
 					"da4b9237bacccdf19c0760cab7aec4a8359010b0",
 					"08a2aaaadff191eb76974b9b3d8b71f202c0156e",
@@ -35,24 +38,28 @@ func Test_GetQueryHashes(t *testing.T) {
 				[]PullRequest{}, Unknown,
 			},
 			{false, "issue3", false,
+				"",
 				[]string{
 					"77de68daecd823babbb58edb1c8e14d7106e83bb",
 				},
 				[]PullRequest{}, Unknown,
 			},
 			{false, "issue4", false,
+				"",
 				[]string{
 					"1b6453892473a467d07372d45eb05abc2031647a",
 				},
 				[]PullRequest{}, Unknown,
 			},
 			{false, "issue5", false,
+				"",
 				[]string{
 					"ac3478d69a3c81fa62e60f5c3696165a4e5e6ac4",
 				},
 				[]PullRequest{}, Unknown,
 			},
 			{false, "issue6", false,
+				"",
 				[]string{
 					"c1dfd96eea8cc2b62785275bca38ac261256e278",
 				},