1package cmd
2
3import (
4	"fmt"
5
6	"github.com/git-town/git-town/src/cli"
7	"github.com/git-town/git-town/src/git"
8	"github.com/git-town/git-town/src/prompt"
9	"github.com/git-town/git-town/src/steps"
10	"github.com/spf13/cobra"
11)
12
13type killConfig struct {
14	initialBranch       string
15	previousBranch      string
16	targetBranchParent  string
17	targetBranch        string
18	childBranches       []string
19	isOffline           bool
20	isTargetBranchLocal bool
21	hasOpenChanges      bool
22	hasTrackingBranch   bool
23}
24
25var killCommand = &cobra.Command{
26	Use:   "kill [<branch>]",
27	Short: "Removes an obsolete feature branch",
28	Long: `Removes an obsolete feature branch
29
30Deletes the current or provided branch from the local and remote repositories.
31Does not delete perennial branches nor the main branch.`,
32	Run: func(cmd *cobra.Command, args []string) {
33		config, err := getKillConfig(args, prodRepo)
34		if err != nil {
35			cli.Exit(err)
36		}
37		stepList, err := getKillStepList(config, prodRepo)
38		if err != nil {
39			cli.Exit(err)
40		}
41		runState := steps.NewRunState("kill", stepList)
42		err = steps.Run(runState, prodRepo, nil)
43		if err != nil {
44			cli.Exit(err)
45		}
46	},
47	Args: cobra.MaximumNArgs(1),
48	PreRunE: func(cmd *cobra.Command, args []string) error {
49		if err := ValidateIsRepository(prodRepo); err != nil {
50			return err
51		}
52		return validateIsConfigured(prodRepo)
53	},
54}
55
56// nolint: funlen
57func getKillConfig(args []string, repo *git.ProdRepo) (result killConfig, err error) {
58	result.initialBranch, err = repo.Silent.CurrentBranch()
59	if err != nil {
60		return result, err
61	}
62	if len(args) == 0 {
63		result.targetBranch = result.initialBranch
64	} else {
65		result.targetBranch = args[0]
66	}
67	if !repo.Config.IsFeatureBranch(result.targetBranch) {
68		return result, fmt.Errorf("you can only kill feature branches")
69	}
70	result.isTargetBranchLocal, err = repo.Silent.HasLocalBranch(result.targetBranch)
71	if err != nil {
72		return result, err
73	}
74	if result.isTargetBranchLocal {
75		err = prompt.EnsureKnowsParentBranches([]string{result.targetBranch}, repo)
76		if err != nil {
77			return result, err
78		}
79		repo.Config.Reload()
80	}
81	hasOrigin, err := repo.Silent.HasRemote("origin")
82	if err != nil {
83		return result, err
84	}
85	result.isOffline = repo.Config.IsOffline()
86	if hasOrigin && !result.isOffline {
87		err := repo.Logging.Fetch()
88		if err != nil {
89			return result, err
90		}
91	}
92	if result.initialBranch != result.targetBranch {
93		hasTargetBranch, err := repo.Silent.HasLocalOrRemoteBranch(result.targetBranch)
94		if err != nil {
95			return result, err
96		}
97		if !hasTargetBranch {
98			return result, fmt.Errorf("there is no branch named %q", result.targetBranch)
99		}
100	}
101	result.hasTrackingBranch, err = repo.Silent.HasTrackingBranch(result.targetBranch)
102	if err != nil {
103		return result, err
104	}
105	result.targetBranchParent = repo.Config.GetParentBranch(result.targetBranch)
106	result.previousBranch, err = repo.Silent.PreviouslyCheckedOutBranch()
107	if err != nil {
108		return result, err
109	}
110	result.hasOpenChanges, err = repo.Silent.HasOpenChanges()
111	if err != nil {
112		return result, err
113	}
114	result.childBranches = repo.Config.GetChildBranches(result.targetBranch)
115	return result, nil
116}
117
118func getKillStepList(config killConfig, repo *git.ProdRepo) (result steps.StepList, err error) {
119	switch {
120	case config.isTargetBranchLocal:
121		if config.hasTrackingBranch && !config.isOffline {
122			result.Append(&steps.DeleteRemoteBranchStep{BranchName: config.targetBranch, IsTracking: true})
123		}
124		if config.initialBranch == config.targetBranch {
125			if config.hasOpenChanges {
126				result.Append(&steps.CommitOpenChangesStep{})
127			}
128			result.Append(&steps.CheckoutBranchStep{BranchName: config.targetBranchParent})
129		}
130		result.Append(&steps.DeleteLocalBranchStep{BranchName: config.targetBranch, Force: true})
131		for _, child := range config.childBranches {
132			result.Append(&steps.SetParentBranchStep{BranchName: child, ParentBranchName: config.targetBranchParent})
133		}
134		result.Append(&steps.DeleteParentBranchStep{BranchName: config.targetBranch})
135	case !repo.Config.IsOffline():
136		result.Append(&steps.DeleteRemoteBranchStep{BranchName: config.targetBranch, IsTracking: false})
137	default:
138		return result, fmt.Errorf("cannot delete remote branch %q in offline mode", config.targetBranch)
139	}
140	err = result.Wrap(steps.WrapOptions{
141		RunInGitRoot:     true,
142		StashOpenChanges: config.initialBranch != config.targetBranch && config.targetBranch == config.previousBranch,
143	}, repo)
144	return result, err
145}
146
147func init() {
148	RootCmd.AddCommand(killCommand)
149}
150