Repository: remotemobprogramming/mob (go)

Issue:
Exits with 0 on at least some failures
Here's one: `cannot start; clean working tree required`

But exits with 0.

Pull request:
Exit
Closes #402. I needed to add the mock exit functionality here as the exiting leads to the test failing. If you have a better idea please let me know. 

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index b2b44bb..90587a9 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -12,7 +12,7 @@ jobs:
             git-version: 2.20.0
     runs-on: ${{ matrix.os }}
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v4
     - name: Install old git version (macOS)
       if: ${{ matrix.os == 'macos-latest' && matrix.git-version != 'latest'}}
       run: |
@@ -33,7 +33,7 @@ jobs:
         sudo make install install-info
     - name: Show git version
       run: git version
-    - name: Use Go 1.20.x
+    - name: Use Go 1.22.x
       uses: actions/setup-go@v5
       with:
         go-version: '1.22'
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 72b3040..af49768 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -16,8 +16,8 @@ jobs:
     outputs:
       sha_linux: ${{ steps.shasum.outputs.sha_linux }}
     steps:
-      - uses: actions/checkout@main
-      - name: Use Go 1.20.x
+      - uses: actions/checkout@v4
+      - name: Use Go 1.22.x
         uses: actions/setup-go@v5
         with:
           go-version: '1.22'
diff --git a/.github/workflows/virustotal.yaml b/.github/workflows/virustotal.yaml
index 739aa06..cba9acb 100644
--- a/.github/workflows/virustotal.yaml
+++ b/.github/workflows/virustotal.yaml
@@ -8,7 +8,7 @@ jobs:
     steps:
       -
         name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       -
         name: Set up Go
         uses: actions/setup-go@v5
diff --git a/README.md b/README.md
index ac750ab..9f7a30f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # Fast git handover with mob
 
-![mob Logo](logo.svg)
+![mob Logo](logo_def.svg)
 <p align="center">
   <a href="https://github.com/remotemobprogramming/mob/actions/workflows/ci.yaml?query=branch%3Amain">
     <img alt="Test Workflow" src="https://img.shields.io/github/actions/workflow/status/remotemobprogramming/mob/ci.yaml?branch=main"></a>
@@ -12,6 +12,7 @@
     <img  alt="Downloads of latest" src="https://img.shields.io/github/downloads/remotemobprogramming/mob/latest/total" /></a>
   <a href="https://img.shields.io/github/stars/remotemobprogramming/mob">
     <img alt="Stars" src="https://img.shields.io/github/stars/remotemobprogramming/mob" /></a>
+  <a href="https://join.slack.com/t/mobsh/shared_invite/zt-2gxr4zvli-NnYvxvI32sjs9TjXUvnydA" rel="nofollow"><img src="https://camo.githubusercontent.com/5ade1fd1e76a6ab860802cdd2941fe2501e2ca2cb534e5d8968dbf864c13d33d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f736c61636b2d6a6f696e5f636861742d77686974652e7376673f6c6f676f3d736c61636b267374796c653d736f6369616c" alt="Slack Status" data-canonical-src="https://img.shields.io/badge/slack-join_chat-white.svg?logo=slack&amp;style=social" style="max-width: 100%;"></a>
 </p>
 
 **#StandWithUkraine [donate here](https://www.savethechildren.org.uk/where-we-work/europe/ukraine) #StandWithUkraine**
@@ -25,7 +26,7 @@ Fast [git handover](https://www.remotemobprogramming.org/#git-handover) for remo
 - **mob** is on 'assess' in the [Thoughtworks Technology Radar](https://twitter.com/simonharrer/status/1453372354097205253?s=20)
 - **mob** has [VSCode integration](https://marketplace.visualstudio.com/items?itemName=alessandrosangalli.mob-vscode-gui)
 
-![diagram how mob works](diagram.svg)
+![diagram how mob works](diagram_def.svg)
 
 ## What people say about mob
 
@@ -472,6 +473,7 @@ go test -coverprofile=cover.out && go tool cover -html=cover.out
 - [BLUME2000](https://twitter.com/slashBene/status/1337329356637687811?s=20)
 - [REWE Digital](https://www.rewe-digital.com/)
 - [Amadeus IT Group](https://amadeus.com/)
+- [DB Systel GmbH](https://www.dbsystel.de/)
 - [FlixMobility TECH (FlixBus)](https://www.flixbus.com/)
 - And probably many others who shall not be named.
 
diff --git a/configuration/configuration.go b/configuration/configuration.go
index 7f7a48b..2753595 100644
--- a/configuration/configuration.go
+++ b/configuration/configuration.go
@@ -4,7 +4,6 @@ import (
 	"bufio"
 	"github.com/remotemobprogramming/mob/v4/say"
 	"os"
-	"os/user"
 	"runtime"
 	"strconv"
 	"strings"
@@ -21,6 +20,7 @@ type Configuration struct {
 	RemoteName                     string // override with MOB_REMOTE_NAME
 	WipCommitMessage               string // override with MOB_WIP_COMMIT_MESSAGE
 	StartCommitMessage             string // override with MOB_START_COMMIT_MESSAGE
+	SkipCiPushOptionEnabled        bool   // override with MOB_SKIP_CI_PUSH_OPTION_ENABLED
 	GitHooksEnabled                bool   // override with MOB_GIT_HOOKS_ENABLED
 	RequireCommitMessage           bool   // override with MOB_REQUIRE_COMMIT_MESSAGE
 	VoiceCommand                   string // override with MOB_VOICE_COMMAND
@@ -75,6 +75,7 @@ func Config(c Configuration) {
 	say.Say("MOB_REMOTE_NAME" + "=" + quote(c.RemoteName))
 	say.Say("MOB_WIP_COMMIT_MESSAGE" + "=" + quote(c.WipCommitMessage))
 	say.Say("MOB_START_COMMIT_MESSAGE" + "=" + quote(c.StartCommitMessage))
+	say.Say("MOB_SKIP_CI_PUSH_OPTION_ENABLED" + "=" + strconv.FormatBool(c.SkipCiPushOptionEnabled))
 	say.Say("MOB_GIT_HOOKS_ENABLED" + "=" + strconv.FormatBool(c.GitHooksEnabled))
 	say.Say("MOB_REQUIRE_COMMIT_MESSAGE" + "=" + strconv.FormatBool(c.RequireCommitMessage))
 	say.Say("MOB_VOICE_COMMAND" + "=" + quote(c.VoiceCommand))
@@ -101,8 +102,8 @@ func ReadConfiguration(gitRootDir string) Configuration {
 	configuration := GetDefaultConfiguration()
 	configuration = parseEnvironmentVariables(configuration)
 
-	currentUser, _ := user.Current()
-	userConfigurationPath := currentUser.HomeDir + "/.mob"
+	userHomeDir, _ := os.UserHomeDir()
+	userConfigurationPath := userHomeDir + "/.mob"
 	configuration = parseUserConfiguration(configuration, userConfigurationPath)
 	if gitRootDir != "" {
 		configuration = parseProjectConfiguration(configuration, gitRootDir+"/.mob")
@@ -181,6 +182,7 @@ func GetDefaultConfiguration() Configuration {
 		RemoteName:                     "origin",
 		WipCommitMessage:               "mob next [ci-skip] [ci skip] [skip ci]",
 		StartCommitMessage:             "mob start [ci-skip] [ci skip] [skip ci]",
+		SkipCiPushOptionEnabled:        true,
 		GitHooksEnabled:                false,
 		VoiceCommand:                   voiceCommand,
 		VoiceMessage:                   "mob next",
@@ -237,6 +239,8 @@ func parseUserConfiguration(configuration Configuration, path string) Configurat
 			setUnquotedString(&configuration.WipCommitMessage, key, value)
 		case "MOB_START_COMMIT_MESSAGE":
 			setUnquotedString(&configuration.StartCommitMessage, key, value)
+		case "MOB_SKIP_CI_PUSH_OPTION_ENABLED":
+			setBoolean(&configuration.SkipCiPushOptionEnabled, key, value)
 		case "MOB_GIT_HOOKS_ENABLED":
 			setBoolean(&configuration.GitHooksEnabled, key, value)
 		case "MOB_REQUIRE_COMMIT_MESSAGE":
@@ -328,6 +332,8 @@ func parseProjectConfiguration(configuration Configuration, path string) Configu
 			setUnquotedString(&configuration.WipCommitMessage, key, value)
 		case "MOB_START_COMMIT_MESSAGE":
 			setUnquotedString(&configuration.StartCommitMessage, key, value)
+		case "MOB_SKIP_CI_PUSH_OPTION_ENABLED":
+			setBoolean(&configuration.SkipCiPushOptionEnabled, key, value)
 		case "MOB_GIT_HOOKS_ENABLED":
 			setBoolean(&configuration.GitHooksEnabled, key, value)
 		case "MOB_REQUIRE_COMMIT_MESSAGE":
@@ -420,10 +426,12 @@ func parseEnvironmentVariables(configuration Configuration) Configuration {
 	removed("MOB_WIP_BRANCH", "Use '"+configuration.Mob("start --branch <branch>")+"' instead.")
 	removed("MOB_START_INCLUDE_UNCOMMITTED_CHANGES", "Use the parameter --include-uncommitted-changes instead.")
 	experimental("MOB_WIP_BRANCH_PREFIX")
+	deprecated("MOB_START_COMMIT_MESSAGE", "Please check that everybody you work with uses version 5.0.0 or higher. Then this environment variable can be unset, as it will not have an impact anymore.")
 
 	setStringFromEnvVariable(&configuration.RemoteName, "MOB_REMOTE_NAME")
 	setStringFromEnvVariable(&configuration.WipCommitMessage, "MOB_WIP_COMMIT_MESSAGE")
 	setStringFromEnvVariable(&configuration.StartCommitMessage, "MOB_START_COMMIT_MESSAGE")
+	setBoolFromEnvVariable(&configuration.SkipCiPushOptionEnabled, "MOB_SKIP_CI_PUSH_OPTION_ENABLED")
 	setBoolFromEnvVariable(&configuration.GitHooksEnabled, "MOB_GIT_HOOKS_ENABLED")
 	setBoolFromEnvVariable(&configuration.RequireCommitMessage, "MOB_REQUIRE_COMMIT_MESSAGE")
 	setOptionalStringFromEnvVariable(&configuration.VoiceCommand, "MOB_VOICE_COMMAND")
diff --git a/help/help.go b/help/help.go
index d14d690..501e2d7 100644
--- a/help/help.go
+++ b/help/help.go
@@ -59,6 +59,7 @@ Other
   moo                Moo!
 
 Add '--debug' to any option to enable verbose logging.
+Need more help? Join the community at slack.mob.sh
 `
 	say.Say(output)
 }
diff --git a/mob.go b/mob.go
index cf61e19..c550dea 100644
--- a/mob.go
+++ b/mob.go
@@ -20,7 +20,7 @@ import (
 )
 
 const (
-	versionNumber     = "4.5.1"
+	versionNumber     = "5.0.0"
 	minimumGitVersion = "2.13.0"
 )
 
@@ -240,14 +240,14 @@ func run(osArgs []string) {
 	if versionString == "" {
 		say.Error("'git' command was not found in PATH. It may be not installed. " +
 			"To learn how to install 'git' refer to https://git-scm.com/book/en/v2/Getting-Started-Installing-Git.")
-		exit(1)
+		Exit(1)
 	}
 
 	currentVersion := parseGitVersion(versionString)
 	if currentVersion.Less(parseGitVersion(minimumGitVersion)) {
 		say.Error(fmt.Sprintf("'git' command version '%s' is lower than the required minimum version (%s). "+
 			"Please update your 'git' installation!", versionString, minimumGitVersion))
-		exit(1)
+		Exit(1)
 	}
 
 	projectRootDir := ""
@@ -255,7 +255,7 @@ func run(osArgs []string) {
 		projectRootDir = gitRootDir()
 		if !hasCommits() {
 			say.Error("Git repository does not have any commits yet. Please create an initial commit.")
-			exit(1)
+			Exit(1)
 		}
 	}
 
@@ -300,7 +300,7 @@ func execute(command string, parameter []string, configuration config.Configurat
 	case "s", "start":
 		err := start(configuration)
 		if !isMobProgramming(configuration) || err != nil {
-			return
+			Exit(1)
 		}
 		if len(parameter) > 0 {
 			timer := parameter[0]
@@ -455,7 +455,7 @@ 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(1)
+		Exit(1)
 	}
 	if placeHolders == 0 {
 		return fmt.Sprintf("%s %s", command, message)
@@ -528,7 +528,7 @@ func start(configuration config.Configuration) error {
 		say.Info("cannot start; clean working tree required")
 		sayUnstagedChangesInfo()
 		sayUntrackedFilesInfo()
-		sayFixUncommitedChanges(configuration)
+		sayFixUncommittedChanges(configuration)
 		return errors.New("cannot start; clean working tree required")
 	}
 
@@ -586,21 +586,33 @@ func start(configuration config.Configuration) error {
 	return nil // no error
 }
 
-func sayFixUncommitedChanges(configuration config.Configuration) {
-	fixCommand := configuration.CliName + " start"
-	instruction := "To start, including uncommitted changes, use"
+func sayFixUncommittedChanges(configuration config.Configuration) {
+	var instruction string
 	if configuration.StartCreate {
 		instruction = "To start, including uncommitted changes and create the remote branch, use"
-		fixCommand += " --create"
-	}
-	if containsAny(args, "-b", "--branch") && configuration.WipBranchQualifier != "" {
-		fixCommand += " --branch " + configuration.WipBranchQualifier
+	} else {
+		instruction = "To start, including uncommitted changes, use"
 	}
-	fixCommand += " --include-uncommitted-changes"
+
+	fixCommand := configuration.CliName + " start" + createFix(configuration) + branchFix(configuration) + " --include-uncommitted-changes"
 
 	say.Fix(instruction, fixCommand)
 }
 
+func createFix(configuration config.Configuration) string {
+	if configuration.StartCreate {
+		return " --create"
+	}
+	return ""
+}
+
+func branchFix(configuration config.Configuration) string {
+	if branchParameter(configuration) {
+		return " --branch " + configuration.WipBranchQualifier
+	}
+	return ""
+}
+
 func branchParameter(configuration config.Configuration) bool {
 	return containsAny(args, "-b", "--branch") && configuration.WipBranchQualifier != ""
 }
@@ -735,16 +747,16 @@ func startNewMobSession(configuration config.Configuration) {
 
 	say.Info("starting new session from " + currentBaseBranch.remote(configuration).String())
 	git("checkout", "-B", currentWipBranch.Name, currentBaseBranch.remote(configuration).Name)
-	git("commit", gitHooksOption(configuration), "--allow-empty", "-m", configuration.StartCommitMessage)
-	gitPush(gitHooksOption(configuration), "--set-upstream", configuration.RemoteName, currentWipBranch.Name+":"+currentWipBranch.Name)
+	gitWithoutEmptyStrings(append(gitPushArgs(configuration), gitHooksOption(configuration), "--set-upstream", configuration.RemoteName, currentWipBranch.Name+":"+currentWipBranch.Name)...)
 }
 
-func gitPush(args ...string) {
-	gitWithoutEmptyStrings(pushArgs(args)...)
-}
-
-func pushArgs(args []string) []string {
-	return append([]string{"push"}, deleteEmptyStrings(args)...)
+func gitPushArgs(c config.Configuration) []string {
+	pushArgs := []string{"push"}
+	if c.SkipCiPushOptionEnabled {
+		return pushArgs
+	} else {
+		return append(pushArgs, "--push-option", "ci.skip")
+	}
 }
 
 func getUntrackedFiles() string {
@@ -1096,7 +1108,7 @@ func silentgit(args ...string) string {
 			say.Error(output)
 			say.Error(err.Error())
 		}
-		exit(1)
+		Exit(1)
 	}
 	return strings.TrimSpace(output)
 }
@@ -1141,7 +1153,7 @@ func git(args ...string) {
 			say.Error(output)
 			say.Error(err.Error())
 		}
-		exit(1)
+		Exit(1)
 	} else {
 		say.Indented(commandString)
 	}
@@ -1158,7 +1170,7 @@ func gitIgnoreFailure(args ...string) error {
 	if err != nil {
 		if !isGit() {
 			say.Error("expecting the current working directory to be a git repository.")
-			exit(1)
+			Exit(1)
 		} else {
 			say.Warning(commandString)
 			say.Warning(output)
@@ -1257,6 +1269,6 @@ func startCommand(name string, args ...string) (string, error) {
 	return commandString, err
 }
 
-var exit = func(code int) {
+var Exit = func(code int) {
 	os.Exit(code)
 }
diff --git a/timer.go b/timer.go
index ecfb188..c7e1e78 100644
--- a/timer.go
+++ b/timer.go
@@ -19,7 +19,7 @@ import (
 
 func StartTimer(timerInMinutes string, configuration config.Configuration) {
 	if err := startTimer(configuration.Timer, configuration); err != nil {
-		exit(1)
+		Exit(1)
 	}
 }
 
@@ -39,7 +39,7 @@ func startTimer(timerInMinutes string, configuration config.Configuration) error
 
 	if !startRemoteTimer && !startLocalTimer {
 		say.Error("No timer configured, not starting timer")
-		exit(1)
+		Exit(1)
 	}
 
 	if startRemoteTimer {
@@ -48,7 +48,7 @@ func startTimer(timerInMinutes string, configuration config.Configuration) error
 		if err != nil {
 			say.Error("remote timer couldn't be started")
 			say.Error(err.Error())
-			exit(1)
+			Exit(1)
 		}
 	}
 
@@ -58,7 +58,7 @@ func startTimer(timerInMinutes string, configuration config.Configuration) error
 		if err != nil {
 			say.Error(fmt.Sprintf("timer couldn't be started on your system (%s)", runtime.GOOS))
 			say.Error(err.Error())
-			exit(1)
+			Exit(1)
 		}
 	}
 
@@ -93,7 +93,7 @@ func getMobTimerRoom(configuration config.Configuration) string {
 
 func StartBreakTimer(timerInMinutes string, configuration config.Configuration) {
 	if err := startBreakTimer(configuration.Timer, configuration); err != nil {
-		exit(1)
+		Exit(1)
 	}
 }
 
@@ -113,7 +113,7 @@ func startBreakTimer(timerInMinutes string, configuration config.Configuration)
 
 	if !startRemoteTimer && !startLocalTimer {
 		say.Error("No break timer configured, not starting break timer")
-		exit(1)
+		Exit(1)
 	}
 
 	if startRemoteTimer {
@@ -123,7 +123,7 @@ func startBreakTimer(timerInMinutes string, configuration config.Configuration)
 		if err != nil {
 			say.Error("remote break timer couldn't be started")
 			say.Error(err.Error())
-			exit(1)
+			Exit(1)
 		}
 	}
 
@@ -133,7 +133,7 @@ func startBreakTimer(timerInMinutes string, configuration config.Configuration)
 		if err != nil {
 			say.Error(fmt.Sprintf("break timer couldn't be started on your system (%s)", runtime.GOOS))
 			say.Error(err.Error())
-			exit(1)
+			Exit(1)
 		}
 	}
 


Test patch:
diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go
index 7843ad2..e11e559 100644
--- a/configuration/configuration_test.go
+++ b/configuration/configuration_test.go
@@ -200,8 +200,7 @@ func TestParseRequireCommitMessageEnvVariables(t *testing.T) {
 	test.Equals(t, true, configuration.RequireCommitMessage)
 }
 
-func TestReadConfigurationFromFileOverrideEverything(t *testing.T) {
-	say.TurnOnDebugging()
+func TestReadUserConfigurationFromFileOverrideEverything(t *testing.T) {
 	tempDir = t.TempDir()
 	test.SetWorkingDir(tempDir)
 
@@ -210,6 +209,7 @@ func TestReadConfigurationFromFileOverrideEverything(t *testing.T) {
 		MOB_REMOTE_NAME="gitlab"
 		MOB_WIP_COMMIT_MESSAGE="team next"
 		MOB_START_COMMIT_MESSAGE="mob: start"
+		MOB_SKIP_CI_PUSH_OPTION_ENABLED=false
 		MOB_REQUIRE_COMMIT_MESSAGE=true
 		MOB_VOICE_COMMAND="whisper \"%s\""
 		MOB_VOICE_MESSAGE="team next"
@@ -235,6 +235,7 @@ func TestReadConfigurationFromFileOverrideEverything(t *testing.T) {
 	test.Equals(t, "gitlab", actualConfiguration.RemoteName)
 	test.Equals(t, "team next", actualConfiguration.WipCommitMessage)
 	test.Equals(t, "mob: start", actualConfiguration.StartCommitMessage)
+	test.Equals(t, false, actualConfiguration.SkipCiPushOptionEnabled)
 	test.Equals(t, true, actualConfiguration.RequireCommitMessage)
 	test.Equals(t, "whisper \"%s\"", actualConfiguration.VoiceCommand)
 	test.Equals(t, "team next", actualConfiguration.VoiceMessage)
@@ -260,6 +261,73 @@ func TestReadConfigurationFromFileOverrideEverything(t *testing.T) {
 	test.Equals(t, "Room\"\"_42", actualConfiguration1.TimerRoom)
 }
 
+func TestReadProjectConfigurationFromFileOverrideEverything(t *testing.T) {
+	output := test.CaptureOutput(t)
+	tempDir = t.TempDir()
+	test.SetWorkingDir(tempDir)
+
+	test.CreateFile(t, ".mob", `
+		MOB_CLI_NAME="team"
+		MOB_REMOTE_NAME="gitlab"
+		MOB_WIP_COMMIT_MESSAGE="team next"
+		MOB_START_COMMIT_MESSAGE="mob: start"
+		MOB_SKIP_CI_PUSH_OPTION_ENABLED=false
+		MOB_REQUIRE_COMMIT_MESSAGE=true
+		MOB_VOICE_COMMAND="whisper \"%s\""
+		MOB_VOICE_MESSAGE="team next"
+		MOB_NOTIFY_COMMAND="/usr/bin/osascript -e 'display notification \"%s!!!\"'"
+		MOB_NOTIFY_MESSAGE="team next"
+		MOB_NEXT_STAY=false
+		MOB_START_CREATE=true
+		MOB_WIP_BRANCH_QUALIFIER="green"
+		MOB_WIP_BRANCH_QUALIFIER_SEPARATOR="---"
+		MOB_WIP_BRANCH_PREFIX="ensemble/"
+		MOB_DONE_SQUASH=no-squash
+		MOB_OPEN_COMMAND="idea %s"
+		MOB_TIMER="123"
+		MOB_TIMER_ROOM="Room_42"
+		MOB_TIMER_ROOM_USE_WIP_BRANCH_QUALIFIER=true
+		MOB_TIMER_LOCAL=false
+		MOB_TIMER_USER="Mona"
+		MOB_TIMER_URL="https://timer.innoq.io/"
+		MOB_STASH_NAME="team-stash-name"
+	`)
+	actualConfiguration := parseProjectConfiguration(GetDefaultConfiguration(), tempDir+"/.mob")
+	test.Equals(t, "team", actualConfiguration.CliName)
+	test.Equals(t, "gitlab", actualConfiguration.RemoteName)
+	test.Equals(t, "team next", actualConfiguration.WipCommitMessage)
+	test.Equals(t, "mob: start", actualConfiguration.StartCommitMessage)
+	test.Equals(t, false, actualConfiguration.SkipCiPushOptionEnabled)
+	test.Equals(t, true, actualConfiguration.RequireCommitMessage)
+	test.NotEquals(t, "whisper \"%s\"", actualConfiguration.VoiceCommand)
+	test.NotEquals(t, "team next", actualConfiguration.VoiceMessage)
+	test.NotEquals(t, "/usr/bin/osascript -e 'display notification \"%s!!!\"'", actualConfiguration.NotifyCommand)
+	test.NotEquals(t, "team next", actualConfiguration.NotifyMessage)
+	test.Equals(t, false, actualConfiguration.NextStay)
+	test.Equals(t, true, actualConfiguration.StartCreate)
+	test.Equals(t, "green", actualConfiguration.WipBranchQualifier)
+	test.Equals(t, "---", actualConfiguration.WipBranchQualifierSeparator)
+	test.Equals(t, "ensemble/", actualConfiguration.WipBranchPrefix)
+	test.Equals(t, NoSquash, actualConfiguration.DoneSquash)
+	test.NotEquals(t, "idea %s", actualConfiguration.OpenCommand)
+	test.Equals(t, "123", actualConfiguration.Timer)
+	test.Equals(t, "Room_42", actualConfiguration.TimerRoom)
+	test.Equals(t, true, actualConfiguration.TimerRoomUseWipBranchQualifier)
+	test.Equals(t, false, actualConfiguration.TimerLocal)
+	test.Equals(t, "Mona", actualConfiguration.TimerUser)
+	test.Equals(t, "https://timer.innoq.io/", actualConfiguration.TimerUrl)
+	test.Equals(t, "team-stash-name", actualConfiguration.StashName)
+
+	test.CreateFile(t, ".mob", "\nMOB_TIMER_ROOM=\"Room\\\"\\\"_42\"\n")
+	actualConfiguration1 := parseUserConfiguration(GetDefaultConfiguration(), tempDir+"/.mob")
+	test.Equals(t, "Room\"\"_42", actualConfiguration1.TimerRoom)
+	test.AssertOutputContains(t, output, "Skipped overwriting key MOB_VOICE_COMMAND from project/.mob file out of security reasons!")
+	test.AssertOutputContains(t, output, "Skipped overwriting key MOB_VOICE_MESSAGE from project/.mob file out of security reasons!")
+	test.AssertOutputContains(t, output, "Skipped overwriting key MOB_NOTIFY_COMMAND from project/.mob file out of security reasons!")
+	test.AssertOutputContains(t, output, "Skipped overwriting key MOB_NOTIFY_MESSAGE from project/.mob file out of security reasons!")
+	test.AssertOutputContains(t, output, "Skipped overwriting key MOB_OPEN_COMMAND from project/.mob file out of security reasons!")
+}
+
 func TestReadConfigurationFromFileWithNonBooleanQuotedDoneSquashValue(t *testing.T) {
 	say.TurnOnDebugging()
 	tempDir = t.TempDir()
diff --git a/mob_test.go b/mob_test.go
index 31ab58a..9505755 100644
--- a/mob_test.go
+++ b/mob_test.go
@@ -16,7 +16,8 @@ import (
 )
 
 var (
-	tempDir string
+	tempDir              string
+	originalExitFunction func(int)
 )
 
 type GitStatus = map[string]string
@@ -221,7 +222,7 @@ func TestStartWithCISkip(t *testing.T) {
 
 	assertOnBranch(t, "mob-session")
 	assertMobSessionBranches(t, configuration, "mob-session")
-	assertCommitLogContainsMessage(t, "mob-session", configuration.StartCommitMessage)
+	assertCommitLogNotContainsMessage(t, "mob-session", configuration.StartCommitMessage)
 }
 
 func TestStartWithMultipleExistingBranches(t *testing.T) {
@@ -944,6 +945,99 @@ func TestStartDoneWithMobDoneSquash(t *testing.T) {
 	assertNoMobSessionBranches(t, configuration, "mob-session")
 }
 
+func TestStartDoneWithMobDoneSquashWithOldEmptyMobStartCommit(t *testing.T) {
+	_, configuration := setup(t)
+	configuration.DoneSquash = config.Squash
+
+	start(configuration)
+	assertOnBranch(t, "mob-session")
+	assertCommitsOnBranch(t, 1, "mob-session")
+
+	git("commit", gitHooksOption(configuration), "--allow-empty", "-m", configuration.StartCommitMessage)
+	git("push")
+	assertCommitsOnBranch(t, 2, "mob-session")
+
+	createFile(t, "test1.txt", "contentIrrelevant")
+	next(configuration)
+	assertCommitsOnBranch(t, 3, "mob-session")
+
+	start(configuration)
+	createFile(t, "test2.txt", "contentIrrelevant")
+
+	done(configuration)
+
+	assertOnBranch(t, "master")
+	assertGitStatus(t, GitStatus{
+		"test1.txt": "A",
+		"test2.txt": "A",
+	})
+	assertCommitsOnBranch(t, 1, "master")
+	assertNoMobSessionBranches(t, configuration, "mob-session")
+}
+
+func TestStartDoneWithMobDoneNoSquashWithOldEmptyMobStartCommit(t *testing.T) {
+	_, configuration := setup(t)
+	configuration.DoneSquash = config.NoSquash
+
+	start(configuration)
+	assertOnBranch(t, "mob-session")
+	assertCommitsOnBranch(t, 1, "mob-session")
+
+	git("commit", gitHooksOption(configuration), "--allow-empty", "-m", configuration.StartCommitMessage)
+	git("push")
+	assertCommitsOnBranch(t, 2, "mob-session")
+
+	createFile(t, "test1.txt", "contentIrrelevant")
+	next(configuration)
+	assertCommitsOnBranch(t, 3, "mob-session")
+
+	start(configuration)
+	createFile(t, "test2.txt", "contentIrrelevant")
+
+	done(configuration)
+
+	assertOnBranch(t, "master")
+	assertGitStatus(t, GitStatus{
+		"test2.txt": "A",
+	})
+	assertCommitsOnBranch(t, 3, "master")
+	assertNoMobSessionBranches(t, configuration, "mob-session")
+}
+
+func TestStartDoneWithMobDoneSquashWipWithOldEmptyMobStartCommit(t *testing.T) {
+	_, configuration := setup(t)
+	configuration.DoneSquash = config.SquashWip
+
+	start(configuration)
+	assertOnBranch(t, "mob-session")
+	assertCommitsOnBranch(t, 1, "mob-session")
+
+	git("commit", gitHooksOption(configuration), "--allow-empty", "-m", configuration.StartCommitMessage)
+	git("push")
+	assertCommitsOnBranch(t, 2, "mob-session")
+
+	manualCommit(t, configuration, "test1.txt", "test1")
+	assertCommitsOnBranch(t, 3, "mob-session")
+
+	start(configuration)
+	createFile(t, "test2.txt", "contentIrrelevant")
+	next(configuration)
+	assertCommitsOnBranch(t, 4, "mob-session")
+
+	start(configuration)
+	createFile(t, "test3.txt", "contentIrrelevant")
+
+	done(configuration)
+
+	assertOnBranch(t, "master")
+	assertGitStatus(t, GitStatus{
+		"test2.txt": "A",
+		"test3.txt": "A",
+	})
+	assertCommitsOnBranch(t, 2, "master")
+	assertNoMobSessionBranches(t, configuration, "mob-session")
+}
+
 func TestStartDoneWithMobDoneBugMergeTwice(t *testing.T) {
 	output, configuration := setup(t)
 
@@ -1020,17 +1114,14 @@ func TestStartDonePublishingOneManualCommit(t *testing.T) {
 
 	start(configuration)
 	assertOnBranch(t, "mob-session")
-	// should be 2 commits on mob-session so far
-	// 1 commit from setup() and 1 commit from ci-skip commit when mob start
-
 	createFileAndCommitIt(t, "example.txt", "contentIrrelevant", "[manual-commit-1] publish this commit to master")
-	assertCommits(t, 3)
+	assertCommits(t, 2)
 
 	done(configuration) // without squash (configuration)
 
 	assertOnBranch(t, "master")
 	assertCleanGitStatus(t)
-	assertCommitsOnBranch(t, 3, "master")
+	assertCommitsOnBranch(t, 2, "master")
 	assertCommitLogContainsMessage(t, "master", "[manual-commit-1] publish this commit to master")
 	assertCommitsOnBranch(t, 1, "origin/master")
 	assertNoMobSessionBranches(t, configuration, "mob-session")
@@ -1042,11 +1133,8 @@ func TestStartDoneSquashTheOneManualCommit(t *testing.T) {
 
 	start(configuration)
 	assertOnBranch(t, "mob-session")
-	// should be 2 commits on mob-session so far
-	// 1 commit from setup() and 1 commit from ci-skip commit when mob start
-
 	createFileAndCommitIt(t, "example.txt", "contentIrrelevant", "[manual-commit-1] publish this commit to master")
-	assertCommits(t, 3)
+	assertCommits(t, 2)
 
 	done(configuration)
 
@@ -1081,8 +1169,6 @@ func TestStartDoneNoSquashWithUncommittedChanges(t *testing.T) {
 	configuration.DoneSquash = config.NoSquash
 
 	start(configuration)
-	// should be 2 commits on mob-session so far
-	// 1 commit from setup() and 1 commit from ci-skip commit when mob start
 	createFile(t, "example.txt", "content")
 
 	done(configuration) // without squash (configuration)
@@ -1091,7 +1177,7 @@ func TestStartDoneNoSquashWithUncommittedChanges(t *testing.T) {
 	assertGitStatus(t, GitStatus{
 		"example.txt": "A",
 	})
-	assertCommitsOnBranch(t, 2, "master")
+	assertCommitsOnBranch(t, 1, "master")
 	assertCommitsOnBranch(t, 1, "origin/master")
 	assertNoMobSessionBranches(t, configuration, "mob-session")
 }
@@ -1120,7 +1206,7 @@ func TestStartDoneSquashWipPublishingOneManualCommit(t *testing.T) {
 func TestStartDoneSquashWipWithUncommittedChanges(t *testing.T) {
 	_, configuration := setup(t)
 
-	start(configuration) // should be 1 commit on mob-session so far
+	start(configuration)
 	createFile(t, "example.txt", "contentIrrelevant")
 
 	configuration.DoneSquash = config.SquashWip
@@ -1366,13 +1452,11 @@ func TestNothingToCommitCreatesNoCommits(t *testing.T) {
 
 	setWorkingDir(tempDir + "/local")
 	start(configuration)
-	// should be 2 commits on mob-session so far
-	// 1 commit from setup() and 1 commit from ci-skip commit when mob start
-	assertCommits(t, 2)
+	assertCommits(t, 1)
 
 	setWorkingDir(tempDir + "/localother")
 	start(configuration)
-	assertCommits(t, 2)
+	assertCommits(t, 1)
 
 	setWorkingDir(tempDir + "/local")
 	next(configuration)
@@ -1382,11 +1466,11 @@ func TestNothingToCommitCreatesNoCommits(t *testing.T) {
 
 	setWorkingDir(tempDir + "/local")
 	start(configuration)
-	assertCommits(t, 2)
+	assertCommits(t, 1)
 
 	setWorkingDir(tempDir + "/localother")
 	start(configuration)
-	assertCommits(t, 2)
+	assertCommits(t, 1)
 }
 
 func TestStartNextPushManualCommits(t *testing.T) {
@@ -1405,6 +1489,7 @@ func TestStartNextPushManualCommits(t *testing.T) {
 
 func TestStartBranchWithUncommitedChangesFixWithBranch(t *testing.T) {
 	output, _ := setup(t)
+	mockExit()
 
 	setWorkingDir(tempDir + "/local")
 
@@ -1412,10 +1497,12 @@ func TestStartBranchWithUncommitedChangesFixWithBranch(t *testing.T) {
 	runMob(t, tempDir+"/local", "start", "-b", "green")
 
 	assertOutputContains(t, output, "mob start --branch green --include-uncommitted-changes")
+	resetExit()
 }
 
 func TestStartBranchEnvWithUncommitedChangesFixWithoutBranch(t *testing.T) {
 	output, _ := setup(t)
+	mockExit()
 
 	setWorkingDir(tempDir + "/local")
 	t.Setenv("MOB_WIP_BRANCH_QUALIFIER", "red")
@@ -1423,10 +1510,12 @@ func TestStartBranchEnvWithUncommitedChangesFixWithoutBranch(t *testing.T) {
 	runMob(t, tempDir+"/local", "start")
 
 	assertOutputContains(t, output, "mob start --include-uncommitted-changes")
+	resetExit()
 }
 
 func TestStartCreateBranchWithUncommitedChangesFixWithBranch(t *testing.T) {
 	output, _ := setup(t)
+	mockExit()
 
 	setWorkingDir(tempDir + "/local")
 
@@ -1435,10 +1524,12 @@ func TestStartCreateBranchWithUncommitedChangesFixWithBranch(t *testing.T) {
 	runMob(t, tempDir+"/local", "start", "--create", "-b", "green")
 
 	assertOutputContains(t, output, "mob start --create --branch green --include-uncommitted-changes")
+	resetExit()
 }
 
 func TestStartCreateBranchEnvWithUncommitedChangesFixWithoutBranch(t *testing.T) {
 	output, _ := setup(t)
+	mockExit()
 
 	setWorkingDir(tempDir + "/local")
 	os.Setenv("MOB_WIP_BRANCH_QUALIFIER", "red")
@@ -1448,6 +1539,7 @@ func TestStartCreateBranchEnvWithUncommitedChangesFixWithoutBranch(t *testing.T)
 
 	os.Unsetenv("MOB_WIP_BRANCH_QUALIFIER")
 	assertOutputContains(t, output, "mob start --create --include-uncommitted-changes")
+	resetExit()
 }
 
 func TestStartNextPushManualCommitsFeatureBranch(t *testing.T) {
@@ -1574,7 +1666,7 @@ func TestDoneSquashWipStartCommit(t *testing.T) {
 	start(configuration)
 	createFile(t, "file1.txt", "contentIrrelevant")
 	next(configuration)
-	assertCommitsOnBranch(t, 3, "mob-session")
+	assertCommitsOnBranch(t, 2, "mob-session")
 	done(configuration)
 	assertCommitsOnBranch(t, 1, "master")
 }
@@ -1587,9 +1679,9 @@ func TestDoneNoSquashStartCommit(t *testing.T) {
 	start(configuration)
 	createFile(t, "file1.txt", "contentIrrelevant")
 	next(configuration)
-	assertCommitsOnBranch(t, 3, "mob-session")
+	assertCommitsOnBranch(t, 2, "mob-session")
 	done(configuration)
-	assertCommitsOnBranch(t, 3, "master")
+	assertCommitsOnBranch(t, 2, "master")
 }
 
 func TestStartAndNextInSubdir(t *testing.T) {
@@ -1835,6 +1927,23 @@ func mockOpenInBrowser() {
 	}
 }
 
+func mockExit() {
+	originalExitFunction = Exit
+	Exit = func(code int) {
+		defer func() {
+			if r := recover(); r != nil {
+				fmt.Printf("exit(%d)\n", code)
+			}
+		}()
+
+		panic(code)
+	}
+}
+
+func resetExit() {
+	Exit = originalExitFunction
+}
+
 func createTestbed(t *testing.T, configuration config.Configuration) {
 	workingDir = ""
 
@@ -1938,6 +2047,13 @@ func assertCommitLogContainsMessage(t *testing.T, branchName string, commitMessa
 	}
 }
 
+func assertCommitLogNotContainsMessage(t *testing.T, branchName string, commitMessage string) {
+	logMessages := silentgit("log", branchName, "--oneline")
+	if strings.Contains(logMessages, commitMessage) {
+		failWithFailure(t, "git log does not contain '"+commitMessage+"'", logMessages)
+	}
+}
+
 func assertFileExist(t *testing.T, filename string) {
 	path := workingDir + "/" + filename
 	if strings.Index(filename, "/") == 0 {
diff --git a/squash_wip_test.go b/squash_wip_test.go
index 9823fb2..d9eda2c 100644
--- a/squash_wip_test.go
+++ b/squash_wip_test.go
@@ -157,7 +157,6 @@ func TestSquashWipCommits_acceptanceWithDroppingStartCommit(t *testing.T) {
 		"second manual commit",
 		"first manual commit",
 		configuration.WipCommitMessage,
-		configuration.StartCommitMessage,
 	}, commitsOnCurrentBranch(configuration))
 
 	squashWip(configuration)
@@ -186,7 +185,6 @@ func TestCommitsOnCurrentBranch(t *testing.T) {
 	equals(t, []string{
 		configuration.WipCommitMessage,
 		"on branch",
-		configuration.StartCommitMessage,
 	}, commits)
 }
 
diff --git a/status_test.go b/status_test.go
index db7ae33..9f76f1a 100644
--- a/status_test.go
+++ b/status_test.go
@@ -34,8 +34,7 @@ func TestStatusWithMoreThan5LinesOfLog(t *testing.T) {
 	}
 
 	status(configuration)
-	// 6 wip commits + 1 start commit
-	assertOutputContains(t, output, "wip branch 'mob-session' contains 7 commits.")
+	assertOutputContains(t, output, "wip branch 'mob-session' contains 6 commits.")
 }
 
 func TestStatusDetectsWipBranches(t *testing.T) {
diff --git a/test/test.go b/test/test.go
index 7fd681d..0e4f036 100644
--- a/test/test.go
+++ b/test/test.go
@@ -29,6 +29,13 @@ func Equals(t *testing.T, exp, act interface{}) {
 	}
 }
 
+func NotEquals(t *testing.T, exp, act interface{}) {
+	if reflect.DeepEqual(exp, act) {
+		t.Log(string(debug.Stack()))
+		failWithFailure(t, exp, act)
+	}
+}
+
 func failWithFailure(t *testing.T, exp interface{}, act interface{}) {
 	_, file, line, _ := runtime.Caller(1)
 	fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act)