Skip to content

Commit 4423f84

Browse files
committed
Add support for finishing non-standard branches
1 parent bcf3444 commit 4423f84

File tree

4 files changed

+231
-11
lines changed

4 files changed

+231
-11
lines changed

cmd/finish.go

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import (
1515
)
1616

1717
// FinishCommand is the implementation of the finish command for topic branches
18-
func FinishCommand(branchType string, name string, continueOp bool, abortOp bool) {
19-
if err := executeFinish(branchType, name, continueOp, abortOp); err != nil {
18+
func FinishCommand(branchType string, name string, continueOp bool, abortOp bool, force bool) {
19+
if err := executeFinish(branchType, name, continueOp, abortOp, force); err != nil {
2020
var exitCode errors.ExitCode
2121
if flowErr, ok := err.(errors.Error); ok {
2222
exitCode = flowErr.ExitCode()
@@ -29,7 +29,7 @@ func FinishCommand(branchType string, name string, continueOp bool, abortOp bool
2929
}
3030

3131
// executeFinish performs the actual branch finishing logic and returns any errors
32-
func executeFinish(branchType string, name string, continueOp bool, abortOp bool) error {
32+
func executeFinish(branchType string, name string, continueOp bool, abortOp bool, force bool) error {
3333
// Get configuration early
3434
cfg, err := config.LoadConfig()
3535
if err != nil {
@@ -71,6 +71,52 @@ func executeFinish(branchType string, name string, continueOp bool, abortOp bool
7171
return &errors.NoMergeInProgressError{}
7272
}
7373

74+
// Check if the branch exists first
75+
branchExists := false
76+
if err := git.BranchExists(name); err == nil {
77+
branchExists = true
78+
} else if !strings.HasPrefix(name, branchConfig.Prefix) {
79+
// If not found as-is, try with prefix
80+
fullName := branchConfig.Prefix + name
81+
if err := git.BranchExists(fullName); err == nil {
82+
name = fullName
83+
branchExists = true
84+
}
85+
}
86+
87+
// Return early if branch doesn't exist
88+
if !branchExists {
89+
return &errors.BranchNotFoundError{BranchName: name}
90+
}
91+
92+
// If the branch exists but doesn't have the expected prefix
93+
if !strings.HasPrefix(name, branchConfig.Prefix) {
94+
if !force {
95+
// Get the short name for tag creation
96+
shortName := name
97+
if strings.Contains(name, "/") {
98+
parts := strings.Split(name, "/")
99+
shortName = parts[len(parts)-1]
100+
}
101+
102+
// Prompt user for confirmation
103+
fmt.Printf("Warning: Branch '%s' is not a standard %s branch (missing prefix '%s').\n", name, branchType, branchConfig.Prefix)
104+
fmt.Printf("Finishing this branch will:\n")
105+
fmt.Printf("1. Merge it into '%s' using the %s strategy\n", branchConfig.Parent, branchConfig.UpstreamStrategy)
106+
if branchConfig.Tag {
107+
fmt.Printf("2. Create a tag '%s%s'\n", branchConfig.TagPrefix, shortName)
108+
}
109+
fmt.Printf("3. Delete the branch after successful merge\n\n")
110+
fmt.Printf("Do you want to continue? [y/N]: ")
111+
112+
var response string
113+
fmt.Scanln(&response)
114+
if strings.ToLower(response) != "y" {
115+
return fmt.Errorf("operation cancelled by user")
116+
}
117+
}
118+
}
119+
74120
// Regular finish command flow
75121
return finishBranch(branchType, name, branchConfig)
76122
}
@@ -90,12 +136,19 @@ func finishBranch(branchType string, name string, branchConfig config.BranchConf
90136
return &errors.InvalidBranchNameError{Name: name}
91137
}
92138

93-
// Get full branch name
94-
fullBranchName := branchConfig.Prefix + name
139+
// Get the short name by removing the prefix if it exists
140+
shortName := name
141+
if strings.HasPrefix(name, branchConfig.Prefix) {
142+
shortName = strings.TrimPrefix(name, branchConfig.Prefix)
143+
} else if strings.Contains(name, "/") {
144+
// For non-standard branches, use the last part after the slash
145+
parts := strings.Split(name, "/")
146+
shortName = parts[len(parts)-1]
147+
}
95148

96149
// Check if branch exists
97-
if err := git.BranchExists(fullBranchName); err != nil {
98-
return &errors.BranchNotFoundError{BranchName: fullBranchName}
150+
if err := git.BranchExists(name); err != nil {
151+
return &errors.BranchNotFoundError{BranchName: name}
99152
}
100153

101154
// Get target branch (always the parent branch)
@@ -124,11 +177,11 @@ func finishBranch(branchType string, name string, branchConfig config.BranchConf
124177
state := &model.MergeState{
125178
Action: "finish",
126179
BranchType: branchType,
127-
BranchName: name,
180+
BranchName: shortName,
128181
CurrentStep: "merge",
129182
ParentBranch: targetBranch,
130183
MergeStrategy: branchConfig.UpstreamStrategy,
131-
FullBranchName: fullBranchName,
184+
FullBranchName: name,
132185
ChildBranches: childBranches,
133186
UpdatedBranches: []string{},
134187
}
@@ -220,6 +273,7 @@ func handleContinue(state *model.MergeState, branchConfig config.BranchConfig) e
220273
case "create_tag":
221274
// Create tag if enabled for this branch type
222275
if branchConfig.Tag {
276+
// Use BranchName for tag creation - it's already the correct short name
223277
tagName := state.BranchName
224278
if branchConfig.TagPrefix != "" {
225279
tagName = branchConfig.TagPrefix + state.BranchName

cmd/topicbranch.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,21 +84,23 @@ func registerBranchCommand(branchType string) {
8484
Use: "finish [name]",
8585
Short: fmt.Sprintf("Finish a %s branch", branchType),
8686
Long: fmt.Sprintf("Finish a %s branch by merging it into the appropriate base branch", branchType),
87-
Example: fmt.Sprintf(" git flow %s finish my-feature", branchType),
87+
Example: fmt.Sprintf(" git flow %s finish my-feature\n git flow %s finish other/branch -f", branchType, branchType),
8888
Args: cobra.ExactArgs(1),
8989
Run: func(cmd *cobra.Command, args []string) {
9090
// Get flags
9191
continueOp, _ := cmd.Flags().GetBool("continue")
9292
abortOp, _ := cmd.Flags().GetBool("abort")
93+
force, _ := cmd.Flags().GetBool("force")
9394

9495
// Call the generic finish command with the branch type and name
95-
FinishCommand(branchType, args[0], continueOp, abortOp)
96+
FinishCommand(branchType, args[0], continueOp, abortOp, force)
9697
},
9798
}
9899

99100
// Add flags
100101
finishCmd.Flags().BoolP("continue", "c", false, "Continue the finish operation after resolving conflicts")
101102
finishCmd.Flags().BoolP("abort", "a", false, "Abort the finish operation and return to the original state")
103+
finishCmd.Flags().BoolP("force", "f", false, "Force finish a non-standard branch using this branch type's strategy")
102104

103105
branchCmd.AddCommand(finishCmd)
104106

test/cmd/finish_test.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,3 +939,159 @@ func TestFinishReleaseWithMergeContinue(t *testing.T) {
939939
t.Error("Expected tag 'v1.0.0' to be created")
940940
}
941941
}
942+
943+
// TestFinishNonStandardBranchWithForce tests finishing a non-standard branch with force flag
944+
func TestFinishNonStandardBranchWithForce(t *testing.T) {
945+
// Setup
946+
dir := testutil.SetupTestRepo(t)
947+
defer testutil.CleanupTestRepo(t, dir)
948+
949+
// Initialize git-flow with defaults
950+
output, err := testutil.RunGitFlow(t, dir, "init", "-d", "-c")
951+
if err != nil {
952+
t.Fatalf("Failed to initialize git-flow: %v\nOutput: %s", err, output)
953+
}
954+
955+
// Create a non-standard branch from develop
956+
_, err = testutil.RunGit(t, dir, "checkout", "develop")
957+
if err != nil {
958+
t.Fatalf("Failed to checkout develop: %v", err)
959+
}
960+
_, err = testutil.RunGit(t, dir, "checkout", "-b", "custom/my-branch")
961+
if err != nil {
962+
t.Fatalf("Failed to create custom branch: %v", err)
963+
}
964+
965+
// Add some changes
966+
testutil.WriteFile(t, dir, "test.txt", "test content")
967+
_, err = testutil.RunGit(t, dir, "add", "test.txt")
968+
if err != nil {
969+
t.Fatalf("Failed to add file: %v", err)
970+
}
971+
_, err = testutil.RunGit(t, dir, "commit", "-m", "Add test file")
972+
if err != nil {
973+
t.Fatalf("Failed to commit file: %v", err)
974+
}
975+
976+
// Finish the branch using feature strategy with force flag
977+
output, err = testutil.RunGitFlow(t, dir, "feature", "finish", "-f", "custom/my-branch")
978+
if err != nil {
979+
t.Fatalf("Failed to finish custom branch: %v\nOutput: %s", err, output)
980+
}
981+
982+
// Verify branch was merged to develop
983+
_, err = testutil.RunGit(t, dir, "checkout", "develop")
984+
if err != nil {
985+
t.Fatalf("Failed to checkout develop: %v", err)
986+
}
987+
988+
// Check if test.txt exists in develop
989+
if !testutil.FileExists(t, dir, "test.txt") {
990+
t.Error("Expected test.txt to exist in develop branch")
991+
}
992+
993+
// Verify custom branch was deleted
994+
if testutil.BranchExists(t, dir, "custom/my-branch") {
995+
t.Error("Expected custom branch to be deleted")
996+
}
997+
}
998+
999+
// TestFinishNonStandardBranchWithoutForce tests finishing a non-standard branch without force flag
1000+
func TestFinishNonStandardBranchWithoutForce(t *testing.T) {
1001+
// Setup
1002+
dir := testutil.SetupTestRepo(t)
1003+
defer testutil.CleanupTestRepo(t, dir)
1004+
1005+
// Initialize git-flow with defaults
1006+
output, err := testutil.RunGitFlow(t, dir, "init", "-d", "-c")
1007+
if err != nil {
1008+
t.Fatalf("Failed to initialize git-flow: %v\nOutput: %s", err, output)
1009+
}
1010+
1011+
// Create a non-standard branch from develop
1012+
_, err = testutil.RunGit(t, dir, "checkout", "develop")
1013+
if err != nil {
1014+
t.Fatalf("Failed to checkout develop: %v", err)
1015+
}
1016+
_, err = testutil.RunGit(t, dir, "checkout", "-b", "custom/my-branch")
1017+
if err != nil {
1018+
t.Fatalf("Failed to create custom branch: %v", err)
1019+
}
1020+
1021+
// Add some changes
1022+
testutil.WriteFile(t, dir, "test.txt", "test content")
1023+
_, err = testutil.RunGit(t, dir, "add", "test.txt")
1024+
if err != nil {
1025+
t.Fatalf("Failed to add file: %v", err)
1026+
}
1027+
_, err = testutil.RunGit(t, dir, "commit", "-m", "Add test file")
1028+
if err != nil {
1029+
t.Fatalf("Failed to commit file: %v", err)
1030+
}
1031+
1032+
// Try to finish the branch without force flag (should fail)
1033+
output, err = testutil.RunGitFlow(t, dir, "feature", "finish", "custom/my-branch")
1034+
if err == nil {
1035+
t.Fatal("Expected finish to fail without force flag and user confirmation")
1036+
}
1037+
1038+
// Verify branch still exists
1039+
if !testutil.BranchExists(t, dir, "custom/my-branch") {
1040+
t.Error("Expected custom branch to still exist")
1041+
}
1042+
1043+
// Verify we're still on the custom branch
1044+
currentBranch := testutil.GetCurrentBranch(t, dir)
1045+
if currentBranch != "custom/my-branch" {
1046+
t.Errorf("Expected to be on custom/my-branch, got %s", currentBranch)
1047+
}
1048+
}
1049+
1050+
// TestFinishNonStandardBranchWithTag tests finishing a non-standard branch with tag creation
1051+
func TestFinishNonStandardBranchWithTag(t *testing.T) {
1052+
// Setup
1053+
dir := testutil.SetupTestRepo(t)
1054+
defer testutil.CleanupTestRepo(t, dir)
1055+
1056+
// Initialize git-flow with defaults and tag configuration
1057+
output, err := testutil.RunGitFlow(t, dir, "init", "-d", "-c")
1058+
if err != nil {
1059+
t.Fatalf("Failed to initialize git-flow: %v\nOutput: %s", err, output)
1060+
}
1061+
1062+
// Create a non-standard branch from develop
1063+
_, err = testutil.RunGit(t, dir, "checkout", "develop")
1064+
if err != nil {
1065+
t.Fatalf("Failed to checkout develop: %v", err)
1066+
}
1067+
_, err = testutil.RunGit(t, dir, "checkout", "-b", "custom/my-release")
1068+
if err != nil {
1069+
t.Fatalf("Failed to create custom branch: %v", err)
1070+
}
1071+
1072+
// Add some changes
1073+
testutil.WriteFile(t, dir, "release.txt", "release content")
1074+
_, err = testutil.RunGit(t, dir, "add", "release.txt")
1075+
if err != nil {
1076+
t.Fatalf("Failed to add file: %v", err)
1077+
}
1078+
_, err = testutil.RunGit(t, dir, "commit", "-m", "Add release file")
1079+
if err != nil {
1080+
t.Fatalf("Failed to commit file: %v", err)
1081+
}
1082+
1083+
// Finish the branch using release strategy with force flag
1084+
output, err = testutil.RunGitFlow(t, dir, "release", "finish", "-f", "custom/my-release")
1085+
if err != nil {
1086+
t.Fatalf("Failed to finish custom release branch: %v\nOutput: %s", err, output)
1087+
}
1088+
1089+
// Verify tag was created
1090+
tagExists, err := testutil.RunGit(t, dir, "tag", "-l", "my-release")
1091+
if err != nil {
1092+
t.Fatalf("Failed to list tags: %v", err)
1093+
}
1094+
if tagExists == "" {
1095+
t.Error("Expected tag 'my-release' to exist")
1096+
}
1097+
}

test/testutil/testutil.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,11 @@ func ReadFile(t *testing.T, dir string, name string) string {
5151
}
5252
return string(content)
5353
}
54+
55+
// FileExists checks if a file exists in the repository
56+
func FileExists(t *testing.T, dir string, path string) bool {
57+
t.Helper()
58+
fullPath := filepath.Join(dir, path)
59+
_, err := os.Stat(fullPath)
60+
return err == nil
61+
}

0 commit comments

Comments
 (0)