diff --git a/mob.go b/mob.go index f07c2a0..6b0e742 100644 --- a/mob.go +++ b/mob.go @@ -21,6 +21,7 @@ import ( "github.com/remotemobprogramming/mob/v5/help" "github.com/remotemobprogramming/mob/v5/open" "github.com/remotemobprogramming/mob/v5/say" + "github.com/remotemobprogramming/mob/v5/timer/localtimer" "github.com/remotemobprogramming/mob/v5/workdir" ) @@ -434,6 +435,24 @@ func determineBranches(currentBranch Branch, localBranches []string, configurati return } +func enrichConfigurationWithBranchQualifier(configuration config.Configuration) config.Configuration { + if !isGit() { + return configuration + } + + if configuration.WipBranchQualifier == "" { + currentBranch := gitCurrentBranch() + currentBaseBranch, _ := determineBranches(currentBranch, gitBranches(), configuration) + + if currentBranch.IsWipBranch(configuration) { + wipBranchWithoutWipPrefix := currentBranch.removeWipPrefix(configuration).Name + configuration.WipBranchQualifier = removePrefix(removePrefix(wipBranchWithoutWipPrefix, currentBaseBranch.Name), configuration.WipBranchQualifierSeparator) + } + } + + return configuration +} + func injectCommandWithMessage(command string, message string) string { placeHolders := strings.Count(command, "%s") if placeHolders > 1 { @@ -446,32 +465,13 @@ func injectCommandWithMessage(command string, message string) string { return fmt.Sprintf(command, message) } -func executeCommandsInBackgroundProcess(commands ...string) (err error) { - cmds := make([]string, 0) - for _, c := range commands { - if len(c) > 0 { - cmds = append(cmds, c) - } - } - say.Debug(fmt.Sprintf("Operating System %s", runtime.GOOS)) - switch runtime.GOOS { - case "windows": - _, err = startCommand("powershell", "-command", fmt.Sprintf("start-process powershell -NoNewWindow -ArgumentList '-command \"%s\"'", strings.Join(cmds, ";"))) - case "darwin", "linux": - _, err = startCommand("sh", "-c", fmt.Sprintf("(%s) &", strings.Join(cmds, ";"))) - default: - say.Warning(fmt.Sprintf("Cannot execute background commands on your os: %s", runtime.GOOS)) - } - return err -} - func currentTime() string { return time.Now().Format("15:04") } func moo(configuration config.Configuration) { voiceMessage := "moo" - err := executeCommandsInBackgroundProcess(getVoiceCommand(voiceMessage, configuration.VoiceCommand)) + err := localtimer.ExecuteCommandsInBackgroundProcess(localtimer.VoiceCommand(voiceMessage, configuration.VoiceCommand)) if err != nil { say.Warning(fmt.Sprintf("can't run voice command on your system (%s)", runtime.GOOS)) diff --git a/timer.go b/timer.go index 0896eec..bdcf943 100644 --- a/timer.go +++ b/timer.go @@ -1,17 +1,11 @@ package main import ( - "encoding/json" - "errors" - "fmt" - "runtime" - "strconv" - "time" - config "github.com/remotemobprogramming/mob/v5/configuration" "github.com/remotemobprogramming/mob/v5/exit" - "github.com/remotemobprogramming/mob/v5/httpclient" - "github.com/remotemobprogramming/mob/v5/say" + timerpkg "github.com/remotemobprogramming/mob/v5/timer" + _ "github.com/remotemobprogramming/mob/v5/timer/localtimer" + _ "github.com/remotemobprogramming/mob/v5/timer/webtimer" ) func StartTimer(timerInMinutes string, configuration config.Configuration) { @@ -21,71 +15,8 @@ func StartTimer(timerInMinutes string, configuration config.Configuration) { } func startTimer(timerInMinutes string, configuration config.Configuration) error { - err, timeoutInMinutes := toMinutes(timerInMinutes) - if err != nil { - return err - } - - timeoutInSeconds := timeoutInMinutes * 60 - timeOfTimeout := time.Now().Add(time.Minute * time.Duration(timeoutInMinutes)).Format("15:04") - say.Debug(fmt.Sprintf("Starting timer at %s for %d minutes = %d seconds (parsed from user input %s)", timeOfTimeout, timeoutInMinutes, timeoutInSeconds, timerInMinutes)) - - room := getMobTimerRoom(configuration) - startRemoteTimer := room != "" - startLocalTimer := configuration.TimerLocal - - if !startRemoteTimer && !startLocalTimer { - say.Error("No timer configured, not starting timer") - exit.Exit(1) - } - - if startRemoteTimer { - timerUser := getUserForMobTimer(configuration.TimerUser) - err := httpPutTimer(timeoutInMinutes, room, timerUser, configuration.TimerUrl, configuration.TimerInsecure) - if err != nil { - say.Error("remote timer couldn't be started") - say.Error(err.Error()) - exit.Exit(1) - } - } - - if startLocalTimer { - err := executeCommandsInBackgroundProcess(getSleepCommand(timeoutInSeconds), getVoiceCommand(configuration.VoiceMessage, configuration.VoiceCommand), getNotifyCommand(configuration.NotifyMessage, configuration.NotifyCommand), "echo \"mobTimer\"") - - if err != nil { - say.Error(fmt.Sprintf("timer couldn't be started on your system (%s)", runtime.GOOS)) - say.Error(err.Error()) - exit.Exit(1) - } - } - - say.Info("It's now " + currentTime() + ". " + fmt.Sprintf("%d min timer ends at approx. %s", timeoutInMinutes, timeOfTimeout) + ". Happy collaborating! :)") - return nil -} - -func getMobTimerRoom(configuration config.Configuration) string { - if !isGit() { - say.Debug("timer not in git repository, using MOB_TIMER_ROOM for room name") - return configuration.TimerRoom - } - - currentWipBranchQualifier := configuration.WipBranchQualifier - if currentWipBranchQualifier == "" { - currentBranch := gitCurrentBranch() - currentBaseBranch, _ := determineBranches(currentBranch, gitBranches(), configuration) - - if currentBranch.IsWipBranch(configuration) { - wipBranchWithoutWipPrefix := currentBranch.removeWipPrefix(configuration).Name - currentWipBranchQualifier = removePrefix(removePrefix(wipBranchWithoutWipPrefix, currentBaseBranch.Name), configuration.WipBranchQualifierSeparator) - } - } - - if configuration.TimerRoomUseWipBranchQualifier && currentWipBranchQualifier != "" { - say.Info("Using wip branch qualifier for room name") - return currentWipBranchQualifier - } - - return configuration.TimerRoom + configuration = enrichConfigurationWithBranchQualifier(configuration) + return timerpkg.RunTimer(timerInMinutes, configuration) } func StartBreakTimer(timerInMinutes string, configuration config.Configuration) { @@ -95,99 +26,6 @@ func StartBreakTimer(timerInMinutes string, configuration config.Configuration) } func startBreakTimer(timerInMinutes string, configuration config.Configuration) error { - err, timeoutInMinutes := toMinutes(timerInMinutes) - if err != nil { - return err - } - - timeoutInSeconds := timeoutInMinutes * 60 - timeOfTimeout := time.Now().Add(time.Minute * time.Duration(timeoutInMinutes)).Format("15:04") - say.Debug(fmt.Sprintf("Starting break timer at %s for %d minutes = %d seconds (parsed from user input %s)", timeOfTimeout, timeoutInMinutes, timeoutInSeconds, timerInMinutes)) - - room := getMobTimerRoom(configuration) - startRemoteTimer := room != "" - startLocalTimer := configuration.TimerLocal - - if !startRemoteTimer && !startLocalTimer { - say.Error("No break timer configured, not starting break timer") - exit.Exit(1) - } - - if startRemoteTimer { - timerUser := getUserForMobTimer(configuration.TimerUser) - err := httpPutBreakTimer(timeoutInMinutes, room, timerUser, configuration.TimerUrl, configuration.TimerInsecure) - - if err != nil { - say.Error("remote break timer couldn't be started") - say.Error(err.Error()) - exit.Exit(1) - } - } - - if startLocalTimer { - err := executeCommandsInBackgroundProcess(getSleepCommand(timeoutInSeconds), getVoiceCommand("mob start", configuration.VoiceCommand), getNotifyCommand("mob start", configuration.NotifyCommand), "echo \"mobTimer\"") - - if err != nil { - say.Error(fmt.Sprintf("break timer couldn't be started on your system (%s)", runtime.GOOS)) - say.Error(err.Error()) - exit.Exit(1) - } - } - - say.Info("It's now " + currentTime() + ". " + fmt.Sprintf("%d min break timer ends at approx. %s", timeoutInMinutes, timeOfTimeout) + ". So take a break now! :)") - return nil -} - -func getUserForMobTimer(userOverride string) string { - if userOverride == "" { - return gitUserName() - } - return userOverride -} - -func toMinutes(timerInMinutes string) (error, int) { - timeoutInMinutes, err := strconv.Atoi(timerInMinutes) - if err != nil || timeoutInMinutes < 1 { - say.Error(fmt.Sprintf("The parameter must be an integer number greater then zero")) - return errors.New("The parameter must be an integer number greater then zero"), 0 - } - return nil, timeoutInMinutes -} - -func httpPutTimer(timeoutInMinutes int, room string, user string, timerService string, disableSSLVerification bool) error { - putBody, _ := json.Marshal(map[string]interface{}{ - "timer": timeoutInMinutes, - "user": user, - }) - client := httpclient.CreateHttpClient(disableSSLVerification) - _, err := client.SendRequest(putBody, "PUT", timerService+room) - return err -} - -func httpPutBreakTimer(timeoutInMinutes int, room string, user string, timerService string, disableSSLVerification bool) error { - putBody, _ := json.Marshal(map[string]interface{}{ - "breaktimer": timeoutInMinutes, - "user": user, - }) - client := httpclient.CreateHttpClient(disableSSLVerification) - _, err := client.SendRequest(putBody, "PUT", timerService+room) - return err -} - -func getSleepCommand(timeoutInSeconds int) string { - return fmt.Sprintf("sleep %d", timeoutInSeconds) -} - -func getVoiceCommand(message string, voiceCommand string) string { - if len(voiceCommand) == 0 { - return "" - } - return injectCommandWithMessage(voiceCommand, message) -} - -func getNotifyCommand(message string, notifyCommand string) string { - if len(notifyCommand) == 0 { - return "" - } - return injectCommandWithMessage(notifyCommand, message) + configuration = enrichConfigurationWithBranchQualifier(configuration) + return timerpkg.RunBreakTimer(timerInMinutes, configuration) } diff --git a/timer/localtimer/localtimer.go b/timer/localtimer/localtimer.go new file mode 100644 index 0000000..34c50f9 --- /dev/null +++ b/timer/localtimer/localtimer.go @@ -0,0 +1,122 @@ +package localtimer + +import ( + "fmt" + "os/exec" + "runtime" + "strings" + + config "github.com/remotemobprogramming/mob/v5/configuration" + "github.com/remotemobprogramming/mob/v5/exit" + "github.com/remotemobprogramming/mob/v5/say" + "github.com/remotemobprogramming/mob/v5/timer" + "github.com/remotemobprogramming/mob/v5/workdir" +) + +func init() { + timer.Register(func(configuration config.Configuration) timer.Timer { + if !configuration.TimerLocal { + return nil + } + return NewProcessLocalTimer(configuration) + }) +} + +// ProcessLocalTimer is a Timer implementation that uses background OS processes. +type ProcessLocalTimer struct { + configuration config.Configuration +} + +func NewProcessLocalTimer(configuration config.Configuration) ProcessLocalTimer { + return ProcessLocalTimer{configuration: configuration} +} + +func (t ProcessLocalTimer) StartTimer(minutes int) error { + timeoutInSeconds := minutes * 60 + if err := ExecuteCommandsInBackgroundProcess( + sleepCommand(timeoutInSeconds), + VoiceCommand(t.configuration.VoiceMessage, t.configuration.VoiceCommand), + notifyCommand(t.configuration.NotifyMessage, t.configuration.NotifyCommand), + "echo \"mobTimer\"", + ); err != nil { + return fmt.Errorf("timer couldn't be started on your system (%s): %w", runtime.GOOS, err) + } + return nil +} + +func (t ProcessLocalTimer) StartBreakTimer(minutes int) error { + timeoutInSeconds := minutes * 60 + if err := ExecuteCommandsInBackgroundProcess( + sleepCommand(timeoutInSeconds), + VoiceCommand("mob start", t.configuration.VoiceCommand), + notifyCommand("mob start", t.configuration.NotifyCommand), + "echo \"mobTimer\"", + ); err != nil { + return fmt.Errorf("break timer couldn't be started on your system (%s): %w", runtime.GOOS, err) + } + return nil +} + +func sleepCommand(timeoutInSeconds int) string { + return fmt.Sprintf("sleep %d", timeoutInSeconds) +} + +// VoiceCommand builds the shell command string for the voice notification. +// Exported because it is also used by the moo feature in the main package. +func VoiceCommand(message string, voiceCommand string) string { + if len(voiceCommand) == 0 { + return "" + } + return injectCommandWithMessage(voiceCommand, message) +} + +func notifyCommand(message string, notifyCommand string) string { + if len(notifyCommand) == 0 { + return "" + } + return injectCommandWithMessage(notifyCommand, message) +} + +func injectCommandWithMessage(command string, message string) string { + placeHolders := strings.Count(command, "%s") + if placeHolders > 1 { + say.Error(fmt.Sprintf("Too many placeholders (%d) in format command string: %s", placeHolders, command)) + exit.Exit(1) + } + if placeHolders == 0 { + return fmt.Sprintf("%s %s", command, message) + } + return fmt.Sprintf(command, message) +} + +// ExecuteCommandsInBackgroundProcess runs the given shell commands in a background OS process. +// Exported because it is also used by the moo feature in the main package. +func ExecuteCommandsInBackgroundProcess(commands ...string) error { + cmds := make([]string, 0) + for _, c := range commands { + if len(c) > 0 { + cmds = append(cmds, c) + } + } + say.Debug(fmt.Sprintf("Operating System %s", runtime.GOOS)) + var err error + switch runtime.GOOS { + case "windows": + err = runInBackground("powershell", "-command", fmt.Sprintf("start-process powershell -NoNewWindow -ArgumentList '-command \"%s\"'", strings.Join(cmds, ";"))) + case "darwin", "linux": + err = runInBackground("sh", "-c", fmt.Sprintf("(%s) &", strings.Join(cmds, ";"))) + default: + say.Warning(fmt.Sprintf("Cannot execute background commands on your os: %s", runtime.GOOS)) + } + return err +} + +func runInBackground(name string, args ...string) error { + command := exec.Command(name, args...) + if len(workdir.Path) > 0 { + command.Dir = workdir.Path + } + commandString := strings.Join(command.Args, " ") + say.Debug("Starting command " + commandString) + return command.Start() +} diff --git a/timer/timer.go b/timer/timer.go new file mode 100644 index 0000000..9718094 --- /dev/null +++ b/timer/timer.go @@ -0,0 +1,109 @@ +package timer + +import ( + "errors" + "fmt" + "strconv" + "time" + + config "github.com/remotemobprogramming/mob/v5/configuration" + "github.com/remotemobprogramming/mob/v5/exit" + "github.com/remotemobprogramming/mob/v5/say" +) + +// Timer abstracts timer functionality so different implementations can be used. +type Timer interface { + StartTimer(minutes int) error + StartBreakTimer(minutes int) error +} + +// Factory creates a Timer for the given configuration. +// Returns nil if the timer should not be active. +type Factory func(configuration config.Configuration) Timer + +var factories []Factory + +// Register adds a Timer factory to the registry. +// Implementation packages call this in their init() function. +func Register(f Factory) { + factories = append(factories, f) +} + +// GetTimers returns all registered timers that are active for the given configuration. +func GetTimers(configuration config.Configuration) []Timer { + var timers []Timer + for _, createTimer := range factories { + t := createTimer(configuration) + if t != nil { + timers = append(timers, t) + } + } + return timers +} + +// RunTimer parses timerInMinutes, starts all active timers and returns any error. +func RunTimer(timerInMinutes string, configuration config.Configuration) error { + err, timeoutInMinutes := toMinutes(timerInMinutes) + if err != nil { + return err + } + + timeOfTimeout := time.Now().Add(time.Minute * time.Duration(timeoutInMinutes)).Format("15:04") + say.Debug(fmt.Sprintf("Starting timer at %s for %d minutes (parsed from user input %s)", timeOfTimeout, timeoutInMinutes, timerInMinutes)) + + timers := GetTimers(configuration) + if len(timers) == 0 { + say.Error("No timer configured, not starting timer") + exit.Exit(1) + } + + for _, t := range timers { + if err := t.StartTimer(timeoutInMinutes); err != nil { + say.Error(err.Error()) + exit.Exit(1) + } + } + + say.Info(fmt.Sprintf("It's now %s. %d min timer ends at approx. %s. Happy collaborating! :)", currentTime(), timeoutInMinutes, timeOfTimeout)) + return nil +} + +// RunBreakTimer parses timerInMinutes, starts all active break timers and returns any error. +func RunBreakTimer(timerInMinutes string, configuration config.Configuration) error { + err, timeoutInMinutes := toMinutes(timerInMinutes) + if err != nil { + return err + } + + timeOfTimeout := time.Now().Add(time.Minute * time.Duration(timeoutInMinutes)).Format("15:04") + say.Debug(fmt.Sprintf("Starting break timer at %s for %d minutes (parsed from user input %s)", timeOfTimeout, timeoutInMinutes, timerInMinutes)) + + timers := GetTimers(configuration) + if len(timers) == 0 { + say.Error("No break timer configured, not starting break timer") + exit.Exit(1) + } + + for _, t := range timers { + if err := t.StartBreakTimer(timeoutInMinutes); err != nil { + say.Error(err.Error()) + exit.Exit(1) + } + } + + say.Info(fmt.Sprintf("It's now %s. %d min break timer ends at approx. %s. So take a break now! :)", currentTime(), timeoutInMinutes, timeOfTimeout)) + return nil +} + +func toMinutes(timerInMinutes string) (error, int) { + timeoutInMinutes, err := strconv.Atoi(timerInMinutes) + if err != nil || timeoutInMinutes < 1 { + say.Error(fmt.Sprintf("The parameter must be an integer number greater then zero")) + return errors.New("The parameter must be an integer number greater then zero"), 0 + } + return nil, timeoutInMinutes +} + +func currentTime() string { + return time.Now().Format("15:04") +} diff --git a/timer/webtimer/webtimer.go b/timer/webtimer/webtimer.go new file mode 100644 index 0000000..86242f4 --- /dev/null +++ b/timer/webtimer/webtimer.go @@ -0,0 +1,85 @@ +package webtimer + +import ( + "encoding/json" + "fmt" + + config "github.com/remotemobprogramming/mob/v5/configuration" + "github.com/remotemobprogramming/mob/v5/git" + "github.com/remotemobprogramming/mob/v5/httpclient" + "github.com/remotemobprogramming/mob/v5/timer" +) + +func init() { + timer.Register(func(configuration config.Configuration) timer.Timer { + if configuration.TimerRoom == "" { + return nil + } + return NewWebTimer(configuration) + }) +} + +// WebTimer is a Timer implementation that notifies a remote timer service via HTTP. +type WebTimer struct { + room string + timerUser string + timerUrl string + timerInsecure bool +} + +func NewWebTimer(configuration config.Configuration) WebTimer { + // Determine the effective timer room + room := configuration.TimerRoom + if configuration.TimerRoomUseWipBranchQualifier && configuration.WipBranchQualifier != "" { + room = configuration.WipBranchQualifier + } + + return WebTimer{ + room: room, + timerUser: getUserForMobTimer(configuration.TimerUser), + timerUrl: configuration.TimerUrl, + timerInsecure: configuration.TimerInsecure, + } +} + +func getUserForMobTimer(userOverride string) string { + if userOverride == "" { + gitClient := &git.Client{} + return gitClient.UserName() + } + return userOverride +} + +func (t WebTimer) StartTimer(minutes int) error { + if err := httpPutTimer(minutes, t.room, t.timerUser, t.timerUrl, t.timerInsecure); err != nil { + return fmt.Errorf("remote timer couldn't be started: %w", err) + } + return nil +} + +func (t WebTimer) StartBreakTimer(minutes int) error { + if err := httpPutBreakTimer(minutes, t.room, t.timerUser, t.timerUrl, t.timerInsecure); err != nil { + return fmt.Errorf("remote break timer couldn't be started: %w", err) + } + return nil +} + +func httpPutTimer(timeoutInMinutes int, room string, user string, timerService string, disableSSLVerification bool) error { + putBody, _ := json.Marshal(map[string]interface{}{ + "timer": timeoutInMinutes, + "user": user, + }) + client := httpclient.CreateHttpClient(disableSSLVerification) + _, err := client.SendRequest(putBody, "PUT", timerService+room) + return err +} + +func httpPutBreakTimer(timeoutInMinutes int, room string, user string, timerService string, disableSSLVerification bool) error { + putBody, _ := json.Marshal(map[string]interface{}{ + "breaktimer": timeoutInMinutes, + "user": user, + }) + client := httpclient.CreateHttpClient(disableSSLVerification) + _, err := client.SendRequest(putBody, "PUT", timerService+room) + return err +}