Repository: remotemobprogramming/mob
Pull request #237 (2022-02-19 14:40:55)

Squash wip improvement
Adds new `mob done --squash-wip` option, which does a `mob done` but also runs `squashWip` beforehand.
As a result, when you use `mob done --squash-wip`, you can keep the manual commits you made on the wip branch while getting rid of all the wip commits.

`squashWip` had to be tweaked for this to fixup subsequent wip commits that had no following manual commit. And it also undoes a final wip commit, so that the user is forced to commit those changes manually.

The environment var MOB_DONE_SQUASH is made backward compatible, while the `.mob` config file does not support activating --squash-wip yet.

Questions:

- Should we now already discard the `mob squash-wip` command after all?
- Can you find any corner cases I missed?



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95ccdca..4444ceb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 2.6.0
+- **NEW** Allow keeping manual commits while squashing all wip commits by finishing a mob-session with `mob done --squash-wip`.
+- Removed experimental command `mob squash-wip` in favor of new `mob done --squash-wip`.  
+
 # 2.5.0
 - Enable git hooks with `MOB_GIT_HOOKS_ENABLED=true`. By default, this option is false and no git hooks such as `pre-commit` or `pre-push` are triggered via mob itself.
 
diff --git a/README.md b/README.md
index 5622bbf..54e5cdc 100644
--- a/README.md
+++ b/README.md
@@ -168,15 +168,10 @@ Basic Commands(Options):
   done
     [--no-squash]                        Do not squash commits from wip branch
     [--squash]                           Squash commits from wip branch
+    [--squash-wip]                       Squash wip commits from wip branch, maintaining manual commits
   reset
     [--branch|-b <branch-postfix>]       Set wip branch to 'mob/<base-branch>-<branch-postfix>'
 
-Experimental Commands:
-  squash-wip                             Combines wip commits in wip branch with subsequent manual commits to leave only manual commits.
-                                         ! Works only if all wip commits have the same wip commit message !
-    [--git-editor]                       Not intended for manual use. Used as a non-interactive editor (GIT_EDITOR) for git.
-    [--git-sequence-editor]              Not intended for manual use. Used as a non-interactive sequence editor (GIT_SEQUENCE_EDITOR) for git.
-
 Timer Commands:
   timer <minutes>    start a <minutes> timer
   start <minutes>    start mob session in wip branch and a <minutes> timer
diff --git a/mob.go b/mob.go
index 54c0fc2..178269c 100644
--- a/mob.go
+++ b/mob.go
@@ -27,29 +27,47 @@ var (
 	Debug      = false // override with --debug parameter
 )
 
+type DoneSquash string
+
+const (
+	Squash    DoneSquash = "squash"
+	NoSquash             = "no-squash"
+	SquashWip            = "squash-wip"
+)
+
+func doneSquash(value string) DoneSquash {
+	switch value {
+	case "false", NoSquash:
+		return NoSquash
+	case SquashWip:
+		return SquashWip
+	}
+	return Squash
+}
+
 type Configuration struct {
-	CliName                        string // override with MOB_CLI_NAME
-	RemoteName                     string // override with MOB_REMOTE_NAME
-	WipCommitMessage               string // override with MOB_WIP_COMMIT_MESSAGE
-	GitHooksEnabled                bool   // override with MOB_GIT_HOOKS_ENABLED
-	RequireCommitMessage           bool   // override with MOB_REQUIRE_COMMIT_MESSAGE
-	VoiceCommand                   string // override with MOB_VOICE_COMMAND
-	VoiceMessage                   string // override with MOB_VOICE_MESSAGE
-	NotifyCommand                  string // override with MOB_NOTIFY_COMMAND
-	NotifyMessage                  string // override with MOB_NOTIFY_MESSAGE
-	NextStay                       bool   // override with MOB_NEXT_STAY
-	StartIncludeUncommittedChanges bool   // override with MOB_START_INCLUDE_UNCOMMITTED_CHANGES variable
-	StashName                      string // override with MOB_STASH_NAME
-	WipBranchQualifier             string // override with MOB_WIP_BRANCH_QUALIFIER
-	WipBranchQualifierSeparator    string // override with MOB_WIP_BRANCH_QUALIFIER_SEPARATOR
-	WipBranchPrefix                string // override with MOB_WIP_BRANCH_PREFIX
-	DoneSquash                     bool   // override with MOB_DONE_SQUASH
-	Timer                          string // override with MOB_TIMER
-	TimerRoom                      string // override with MOB_TIMER_ROOM
-	TimerLocal                     bool   // override with MOB_TIMER_LOCAL
-	TimerRoomUseWipBranchQualifier bool   // override with MOB_TIMER_ROOM_USE_WIP_BRANCH_QUALIFIER
-	TimerUser                      string // override with MOB_TIMER_USER
-	TimerUrl                       string // override with MOB_TIMER_URL
+	CliName                        string     // override with MOB_CLI_NAME
+	RemoteName                     string     // override with MOB_REMOTE_NAME
+	WipCommitMessage               string     // override with MOB_WIP_COMMIT_MESSAGE
+	GitHooksEnabled                bool       // override with MOB_GIT_HOOKS_ENABLED
+	RequireCommitMessage           bool       // override with MOB_REQUIRE_COMMIT_MESSAGE
+	VoiceCommand                   string     // override with MOB_VOICE_COMMAND
+	VoiceMessage                   string     // override with MOB_VOICE_MESSAGE
+	NotifyCommand                  string     // override with MOB_NOTIFY_COMMAND
+	NotifyMessage                  string     // override with MOB_NOTIFY_MESSAGE
+	NextStay                       bool       // override with MOB_NEXT_STAY
+	StartIncludeUncommittedChanges bool       // override with MOB_START_INCLUDE_UNCOMMITTED_CHANGES variable
+	StashName                      string     // override with MOB_STASH_NAME
+	WipBranchQualifier             string     // override with MOB_WIP_BRANCH_QUALIFIER
+	WipBranchQualifierSeparator    string     // override with MOB_WIP_BRANCH_QUALIFIER_SEPARATOR
+	WipBranchPrefix                string     // override with MOB_WIP_BRANCH_PREFIX
+	DoneSquash                     DoneSquash // override with MOB_DONE_SQUASH
+	Timer                          string     // override with MOB_TIMER
+	TimerRoom                      string     // override with MOB_TIMER_ROOM
+	TimerLocal                     bool       // override with MOB_TIMER_LOCAL
+	TimerRoomUseWipBranchQualifier bool       // override with MOB_TIMER_ROOM_USE_WIP_BRANCH_QUALIFIER
+	TimerUser                      string     // override with MOB_TIMER_USER
+	TimerUrl                       string     // override with MOB_TIMER_URL
 }
 
 func (c Configuration) wipBranchQualifierSuffix() string {
@@ -270,7 +288,7 @@ func getDefaultConfiguration() Configuration {
 		StartIncludeUncommittedChanges: false,
 		WipBranchQualifier:             "",
 		WipBranchQualifierSeparator:    "-",
-		DoneSquash:                     true,
+		DoneSquash:                     Squash,
 		Timer:                          "",
 		TimerLocal:                     true,
 		TimerRoom:                      "",
@@ -346,7 +364,7 @@ func parseUserConfiguration(configuration Configuration, path string) Configurat
 		case "MOB_WIP_BRANCH_QUALIFIER_SEPARATOR":
 			setUnquotedString(&configuration.WipBranchQualifierSeparator, key, value)
 		case "MOB_DONE_SQUASH":
-			setBoolean(&configuration.DoneSquash, key, value)
+			setMobDoneSquash(&configuration, key, value)
 		case "MOB_TIMER":
 			setUnquotedString(&configuration.Timer, key, value)
 		case "MOB_TIMER_ROOM":
@@ -419,7 +437,7 @@ func parseProjectConfiguration(configuration Configuration, path string) Configu
 		case "MOB_WIP_BRANCH_QUALIFIER_SEPARATOR":
 			setUnquotedString(&configuration.WipBranchQualifierSeparator, key, value)
 		case "MOB_DONE_SQUASH":
-			setBoolean(&configuration.DoneSquash, key, value)
+			setMobDoneSquash(&configuration, key, value)
 		case "MOB_TIMER":
 			setUnquotedString(&configuration.Timer, key, value)
 		case "MOB_TIMER_ROOM":
@@ -467,6 +485,20 @@ func setBoolean(s *bool, key string, value string) {
 	debugInfo("Overwriting " + key + " =" + strconv.FormatBool(boolValue))
 }
 
+func setMobDoneSquash(configuration *Configuration, key string, value string) {
+	boolValue, err := strconv.ParseBool(value)
+	if err != nil {
+		sayWarning("Could not set key from configuration file because value is not parseable (" + key + "=" + value + ")")
+		return
+	}
+	if boolValue {
+		configuration.DoneSquash = Squash
+	} else {
+		configuration.DoneSquash = NoSquash
+	}
+	debugInfo("Overwriting " + key + " =" + strconv.FormatBool(boolValue))
+}
+
 func parseEnvironmentVariables(configuration Configuration) Configuration {
 	setStringFromEnvVariable(&configuration.CliName, "MOB_CLI_NAME")
 	if configuration.CliName != getDefaultConfiguration().CliName {
@@ -497,7 +529,7 @@ func parseEnvironmentVariables(configuration Configuration) Configuration {
 
 	setBoolFromEnvVariable(&configuration.StartIncludeUncommittedChanges, "MOB_START_INCLUDE_UNCOMMITTED_CHANGES")
 
-	setBoolFromEnvVariable(&configuration.DoneSquash, "MOB_DONE_SQUASH")
+	setDoneSquashFromEnvVariable(&configuration, "MOB_DONE_SQUASH")
 
 	setStringFromEnvVariable(&configuration.Timer, "MOB_TIMER")
 	setStringFromEnvVariable(&configuration.TimerRoom, "MOB_TIMER_ROOM")
@@ -545,6 +577,22 @@ func setBoolFromEnvVariable(s *bool, key string) {
 	}
 }
 
+func setDoneSquashFromEnvVariable(configuration *Configuration, key string) {
+	value, set := os.LookupEnv(key)
+	if !set {
+		return
+	}
+
+	configuration.DoneSquash = doneSquash(value)
+
+	if value == "" {
+		debugInfo("ignoring " + key + "=" + value + " (empty string)")
+		return
+	}
+
+	debugInfo("overriding " + key + "=" + string(configuration.DoneSquash))
+}
+
 func removed(key string, message string) {
 	if _, set := os.LookupEnv(key); set {
 		say("Configuration option '" + key + "' is no longer used.")
@@ -581,7 +629,7 @@ func config(c Configuration) {
 	say("MOB_WIP_BRANCH_QUALIFIER" + "=" + quote(c.WipBranchQualifier))
 	say("MOB_WIP_BRANCH_QUALIFIER_SEPARATOR" + "=" + quote(c.WipBranchQualifierSeparator))
 	say("MOB_WIP_BRANCH_PREFIX" + "=" + quote(c.WipBranchPrefix))
-	say("MOB_DONE_SQUASH" + "=" + strconv.FormatBool(c.DoneSquash))
+	say("MOB_DONE_SQUASH" + "=" + string(c.DoneSquash))
 	say("MOB_TIMER" + "=" + quote(c.Timer))
 	say("MOB_TIMER_ROOM" + "=" + quote(c.TimerRoom))
 	say("MOB_TIMER_ROOM_USE_WIP_BRANCH_QUALIFIER" + "=" + strconv.FormatBool(c.TimerRoomUseWipBranchQualifier))
@@ -619,9 +667,11 @@ func parseArgs(args []string, configuration Configuration) (command string, para
 			}
 			i++ // skip consumed parameter
 		case "--squash":
-			newConfiguration.DoneSquash = true
+			newConfiguration.DoneSquash = Squash
 		case "--no-squash":
-			newConfiguration.DoneSquash = false
+			newConfiguration.DoneSquash = NoSquash
+		case "--squash-wip":
+			newConfiguration.DoneSquash = SquashWip
 		default:
 			if i == 1 {
 				command = arg
@@ -686,8 +736,6 @@ func execute(command string, parameter []string, configuration Configuration) {
 			squashWipGitEditor(parameter[1], configuration)
 		} else if len(parameter) > 1 && parameter[0] == "--git-sequence-editor" {
 			squashWipGitSequenceEditor(parameter[1], configuration)
-		} else {
-			squashWip(configuration)
 		}
 	case "version", "--version", "-v":
 		version()
@@ -819,8 +867,8 @@ func getMobTimerRoom(configuration Configuration) string {
 		currentBaseBranch, _ := determineBranches(currentBranch, gitBranches(), configuration)
 
 		if currentBranch.IsWipBranch(configuration) {
-			wipBranchWithouthWipPrefix := currentBranch.removeWipPrefix(configuration).Name
-			currentWipBranchQualifier = removePrefix(removePrefix(wipBranchWithouthWipPrefix, currentBaseBranch.Name), configuration.WipBranchQualifierSeparator)
+			wipBranchWithoutWipPrefix := currentBranch.removeWipPrefix(configuration).Name
+			currentWipBranchQualifier = removePrefix(removePrefix(wipBranchWithoutWipPrefix, currentBaseBranch.Name), configuration.WipBranchQualifierSeparator)
 		}
 	}
 
@@ -1176,6 +1224,10 @@ func done(configuration Configuration) {
 		return
 	}
 
+	if configuration.DoneSquash == SquashWip {
+		squashWip(configuration)
+	}
+
 	git("fetch", configuration.RemoteName, "--prune")
 
 	baseBranch, wipBranch := determineBranches(gitCurrentBranch(), gitBranches(), configuration)
@@ -1198,7 +1250,7 @@ func done(configuration Configuration) {
 
 		git("branch", "-D", wipBranch.Name)
 
-		if uncommittedChanges && !configuration.DoneSquash { // give the user the chance to name their final commit
+		if uncommittedChanges && configuration.DoneSquash != Squash { // give the user the chance to name their final commit
 			git("reset", "--soft", "HEAD^")
 		}
 
@@ -1216,7 +1268,7 @@ func done(configuration Configuration) {
 
 		if hasUncommittedChanges() {
 			sayTodo("To finish, use", "git commit")
-		} else if configuration.DoneSquash {
+		} else if configuration.DoneSquash == Squash {
 			sayInfo("nothing was done, so nothing to commit")
 		}
 
@@ -1236,7 +1288,7 @@ func gitRootDir() string {
 }
 
 func squashOrNoCommit(configuration Configuration) string {
-	if configuration.DoneSquash {
+	if configuration.DoneSquash == Squash {
 		return "--squash"
 	} else {
 		return "--no-commit"
@@ -1375,15 +1427,10 @@ Basic Commands(Options):
   done
     [--no-squash]                        Do not squash commits from wip branch
     [--squash]                           Squash commits from wip branch
+    [--squash-wip]                       Squash wip commits from wip branch, maintaining manual commits
   reset
     [--branch|-b <branch-postfix>]       Set wip branch to 'mob/<base-branch>/<branch-postfix>'
 
-Experimental Commands:
-  squash-wip                             Combines wip commits in wip branch with subsequent manual commits to leave only manual commits.
-                                         ! Works only if all wip commits have the same wip commit message !
-    [--git-editor]                       Not intended for manual use. Used as a non-interactive editor (GIT_EDITOR) for git.
-    [--git-sequence-editor]              Not intended for manual use. Used as a non-interactive sequence editor (GIT_SEQUENCE_EDITOR) for git.
-
 Timer Commands:
   timer <minutes>    start a <minutes> timer
   start <minutes>    start mob session in wip branch and a <minutes> timer
diff --git a/squash_wip.go b/squash_wip.go
index d6c2c3b..3b045e7 100644
--- a/squash_wip.go
+++ b/squash_wip.go
@@ -16,14 +16,6 @@ func squashWip(configuration Configuration) {
 		return
 	}
 
-	if endsWithWipCommit(configuration) {
-		sayError(`failed to squash wip commits
-last commit must be a manual commit`)
-		sayEmptyLine()
-		sayTodo("create a manual commit with a commit message to fix this:", "git commit --allow-empty -m \"your message goes here\"")
-		return
-	}
-
 	currentBaseBranch, currentWipBranch := determineBranches(gitCurrentBranch(), gitBranches(), configuration)
 	mergeBase := silentgit("merge-base", currentWipBranch.String(), currentBaseBranch.String())
 
@@ -32,13 +24,25 @@ last commit must be a manual commit`)
 		mobExecutable()+" squash-wip --git-editor",
 		mobExecutable()+" squash-wip --git-sequence-editor",
 	)
-	silentgit("rebase", "-i", "--keep-empty", mergeBase)
+	sayInfo("rewriting history of '" + currentWipBranch.String() + "': squashing wip commits while keeping manual commits.")
+	git("rebase", "--interactive", "--keep-empty", mergeBase)
 	setEnvGitEditor(originalGitEditor, originalGitSequenceEditor)
-	sayInfo("the history of your '" + currentWipBranch.String() + "' branch has been rewritten to combine all wip commits with their following manual commits:")
-	sayEmptyLine()
+	sayInfo("resulting history is:")
 	sayLastCommitsWithMessage(currentBaseBranch.String(), currentWipBranch.String())
-	sayEmptyLine()
-	sayTodo("to finally put the changes into the base branch preserving the resulting commits, call:", configuration.mob("done --no-squash"))
+	if lastCommitIsWipCommit(configuration) { // last commit is wip commit
+		sayInfo("undoing the final wip commit and staging its changes:")
+		git("reset", "--soft", "HEAD^")
+	}
+
+	git("push", "--force", configuration.gitHooksOption())
+}
+
+func lastCommitIsWipCommit(configuration Configuration) bool {
+	return lastCommitMessage() == configuration.WipCommitMessage
+}
+
+func lastCommitMessage() string {
+	return silentgit("log", "-1", "--pretty=format:%s")
 }
 
 func sayLastCommitsWithMessage(currentBaseBranch string, currentWipBranch string) {
@@ -120,40 +124,69 @@ func commentWipCommits(input string, configuration Configuration) string {
 	return strings.Join(result, "\n")
 }
 
-func endsWithWipCommit(configuration Configuration) bool {
-	return configuration.isWipCommitMessage(commitsOnCurrentBranch(configuration)[0])
-}
+func markPostWipCommitsForSquashing(input string, configuration Configuration) string {
+	var result []string
 
-func commitsOnCurrentBranch(configuration Configuration) []string {
-	currentBaseBranch, currentWipBranch := determineBranches(gitCurrentBranch(), gitBranches(), configuration)
-	commitsBaseWipBranch := currentBaseBranch.String() + ".." + currentWipBranch.String()
-	log := silentgit("--no-pager", "log", commitsBaseWipBranch, "--pretty=format:%s")
-	lines := strings.Split(log, "\n")
-	return lines
+	inputLines := strings.Split(input, "\n")
+	for index := range inputLines {
+		markedLine := markLine(inputLines, index, configuration)
+		result = append(result, markedLine)
+	}
+
+	return strings.Join(result, "\n")
 }
 
-func markPostWipCommitsForSquashing(input string, configuration Configuration) string {
-	var result []string
+func markLine(inputLines []string, i int, configuration Configuration) string {
+	var resultLine = inputLines[i]
+	previousLine := previousLine(inputLines, i)
+	if isWipCommitLine(previousLine, configuration) {
+		forthComingLines := inputLines[i:]
 
-	var squashNext = false
-	for _, line := range strings.Split(input, "\n") {
-		if squashNext && isPick(line) {
-			result = append(result, markSquash(line))
+		if hasOnlyWipCommits(forthComingLines, configuration) {
+			resultLine = markFixup(inputLines[i])
 		} else {
-			result = append(result, line)
+			resultLine = markSquash(inputLines[i])
 		}
-		squashNext = isRebaseWipCommitLine(line, configuration)
 	}
+	return resultLine
+}
 
-	return strings.Join(result, "\n")
+func previousLine(inputLines []string, currentIndex int) string {
+	var previousLine = ""
+	if currentIndex > 0 {
+		previousLine = inputLines[currentIndex-1]
+	}
+	return previousLine
+}
+
+func hasOnlyWipCommits(forthComingLines []string, configuration Configuration) bool {
+	var onlyWipCommits = true
+	for _, forthComingLine := range forthComingLines {
+		if isPick(forthComingLine) && isManualCommit(forthComingLine, configuration) {
+			onlyWipCommits = false
+		}
+	}
+	return onlyWipCommits
 }
 
 func markSquash(line string) string {
 	return strings.Replace(line, "pick ", "squash ", 1)
 }
 
-func isRebaseWipCommitLine(line string, configuration Configuration) bool {
-	return isPick(line) && strings.HasSuffix(line, configuration.WipCommitMessage)
+func markFixup(line string) string {
+	return strings.Replace(line, "pick ", "fixup ", 1)
+}
+
+func isWipCommitLine(line string, configuration Configuration) bool {
+	return isPick(line) && isWipCommit(line, configuration)
+}
+
+func isManualCommit(line string, configuration Configuration) bool {
+	return !isWipCommit(line, configuration)
+}
+
+func isWipCommit(line string, configuration Configuration) bool {
+	return strings.HasSuffix(line, configuration.WipCommitMessage)
 }
 
 func isPick(line string) bool {


Test patch:
diff --git a/coauthors_test.go b/coauthors_test.go
index cbd30a5..fdeff93 100644
--- a/coauthors_test.go
+++ b/coauthors_test.go
@@ -9,27 +9,27 @@ func TestStartDoneCoAuthors(t *testing.T) {
 
 	setWorkingDir(tempDir + "/alice")
 	start(configuration)
-	createFile(t, "file3.txt", "owqe")
+	createFile(t, "file3.txt", "contentIrrelevant")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/local")
 	start(configuration)
-	createFile(t, "file1.txt", "asdf")
+	createFile(t, "file1.txt", "contentIrrelevant")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/localother")
 	start(configuration)
-	createFile(t, "file2.txt", "asdf")
+	createFile(t, "file2.txt", "contentIrrelevant")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/alice")
 	start(configuration)
-	createFile(t, "file4.txt", "zcvx")
+	createFile(t, "file4.txt", "contentIrrelevant")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/bob")
 	start(configuration)
-	createFile(t, "file5.txt", "oiuo")
+	createFile(t, "file5.txt", "contentIrrelevant")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/local")
diff --git a/mob_test.go b/mob_test.go
index 0bbf2f6..5b321e1 100644
--- a/mob_test.go
+++ b/mob_test.go
@@ -45,24 +45,24 @@ func TestParseArgs(t *testing.T) {
 
 func TestParseArgsDoneNoSquash(t *testing.T) {
 	configuration := getDefaultConfiguration()
-	equals(t, true, configuration.DoneSquash)
+	equals(t, Squash, configuration.DoneSquash)
 
 	command, parameters, configuration := parseArgs([]string{"mob", "done", "--no-squash"}, configuration)
 
 	equals(t, "done", command)
 	equals(t, "", strings.Join(parameters, ""))
-	equals(t, false, configuration.DoneSquash)
+	equals(t, NoSquash, string(configuration.DoneSquash))
 }
 
 func TestParseArgsDoneSquash(t *testing.T) {
 	configuration := getDefaultConfiguration()
-	configuration.DoneSquash = false
+	configuration.DoneSquash = NoSquash
 
 	command, parameters, configuration := parseArgs([]string{"mob", "done", "--squash"}, configuration)
 
 	equals(t, "done", command)
 	equals(t, "", strings.Join(parameters, ""))
-	equals(t, true, configuration.DoneSquash)
+	equals(t, Squash, configuration.DoneSquash)
 }
 
 func TestParseArgsMessage(t *testing.T) {
@@ -173,8 +173,22 @@ func TestMobRemoteNameEnvironmentVariableEmptyString(t *testing.T) {
 	equals(t, "origin", configuration.RemoteName)
 }
 
+func TestMobDoneSquashEnvironmentVariable(t *testing.T) {
+	assertMobDoneSquashValue(t, "", Squash)
+	assertMobDoneSquashValue(t, "true", Squash)
+	assertMobDoneSquashValue(t, "false", NoSquash)
+	assertMobDoneSquashValue(t, "garbage", Squash)
+	assertMobDoneSquashValue(t, "squash", Squash)
+	assertMobDoneSquashValue(t, "no-squash", NoSquash)
+	assertMobDoneSquashValue(t, "squash-wip", SquashWip)
+}
+
+func assertMobDoneSquashValue(t *testing.T, value string, expected DoneSquash) {
+	configuration := setEnvVarAndParse("MOB_DONE_SQUASH", value)
+	equals(t, string(expected), string(configuration.DoneSquash))
+}
+
 func TestBooleanEnvironmentVariables(t *testing.T) {
-	assertBoolEnvVarParsed(t, "MOB_DONE_SQUASH", true, Configuration.GetMobDoneSquash)
 	assertBoolEnvVarParsed(t, "MOB_START_INCLUDE_UNCOMMITTED_CHANGES", false, Configuration.GetMobStartIncludeUncommittedChanges)
 	assertBoolEnvVarParsed(t, "MOB_NEXT_STAY", true, Configuration.GetMobNextStay)
 	assertBoolEnvVarParsed(t, "MOB_REQUIRE_COMMIT_MESSAGE", false, Configuration.GetRequireCommitMessage)
@@ -209,7 +223,7 @@ func boolToInterface(actual func(Configuration) bool) func(c Configuration) inte
 	}
 }
 
-func (c Configuration) GetMobDoneSquash() bool {
+func (c Configuration) GetMobDoneSquash() DoneSquash {
 	return c.DoneSquash
 }
 
@@ -265,7 +279,7 @@ func TestRequireCommitMessage(t *testing.T) {
 	// https://github.com/remotemobprogramming/mob/pull/107#issuecomment-761298861
 	assertOutputContains(t, output, "nothing to commit")
 
-	createFile(t, "example.txt", "content")
+	createFile(t, "example.txt", "contentIrrelevant")
 	next(configuration)
 	// failure message should make sense regardless of whether we
 	// provided commit message via `-m` or MOB_WIP_COMMIT_MESSAGE
@@ -296,7 +310,7 @@ func TestStatusWithMoreThan5LinesOfLog(t *testing.T) {
 	start(configuration)
 
 	for i := 0; i < 6; i++ {
-		createFile(t, "test"+strconv.Itoa(i)+".txt", "test")
+		createFile(t, "test"+strconv.Itoa(i)+".txt", "contentIrrelevant")
 		next(configuration)
 	}
 
@@ -352,7 +366,7 @@ func TestReadConfigurationFromFileOverrideEverything(t *testing.T) {
 	equals(t, true, actualConfiguration.StartIncludeUncommittedChanges)
 	equals(t, "green", actualConfiguration.WipBranchQualifier)
 	equals(t, "---", actualConfiguration.WipBranchQualifierSeparator)
-	equals(t, false, actualConfiguration.DoneSquash)
+	equals(t, NoSquash, string(actualConfiguration.DoneSquash))
 	equals(t, "123", actualConfiguration.Timer)
 	equals(t, "Room_42", actualConfiguration.TimerRoom)
 	equals(t, true, actualConfiguration.TimerRoomUseWipBranchQualifier)
@@ -551,7 +565,7 @@ func TestReset(t *testing.T) {
 func TestResetCommit(t *testing.T) {
 	_, configuration := setup(t)
 	start(configuration)
-	createFile(t, "example.txt", "content")
+	createFile(t, "example.txt", "contentIrrelevant")
 	next(configuration)
 	assertMobSessionBranches(t, configuration, "mob-session")
 
@@ -564,7 +578,7 @@ func TestResetCommit(t *testing.T) {
 func TestStartUnstagedChanges(t *testing.T) {
 	output, configuration := setup(t)
 	configuration.StartIncludeUncommittedChanges = false
-	createFile(t, "test.txt", "content")
+	createFile(t, "test.txt", "contentIrrelevant")
 
 	start(configuration)
 
@@ -576,7 +590,7 @@ func TestStartUnstagedChanges(t *testing.T) {
 func TestStartIncludeUnstagedChanges(t *testing.T) {
 	_, configuration := setup(t)
 	configuration.StartIncludeUncommittedChanges = true
-	createFile(t, "test.txt", "content")
+	createFile(t, "test.txt", "contentIrrelevant")
 
 	start(configuration)
 
@@ -590,7 +604,7 @@ func TestStartIncludeUnstagedChangesInNewWorkingDirectory(t *testing.T) {
 	Debug = true
 	createDirectory(t, "subdirnew")
 	setWorkingDir(tempDir + "/local/subdirnew")
-	createFile(t, "test.txt", "content")
+	createFile(t, "test.txt", "contentIrrelevant")
 	assertFileExist(t, tempDir+"/local/subdirnew/test.txt")
 
 	start(configuration)
@@ -600,7 +614,7 @@ func TestStartIncludeUnstagedChangesInNewWorkingDirectory(t *testing.T) {
 
 func TestStartHasUnpushedCommits(t *testing.T) {
 	output, configuration := setup(t)
-	createFileAndCommitIt(t, "test.txt", "content", "unpushed change")
+	createFileAndCommitIt(t, "test.txt", "contentIrrelevant", "unpushed change")
 
 	start(configuration)
 
@@ -620,7 +634,7 @@ func TestBranch(t *testing.T) {
 func TestStartIncludeUntrackedFiles(t *testing.T) {
 	_, configuration := setup(t)
 	configuration.StartIncludeUncommittedChanges = true
-	createFile(t, "example.txt", "content")
+	createFile(t, "example.txt", "contentIrrelevant")
 
 	start(configuration)
 
@@ -630,7 +644,7 @@ func TestStartIncludeUntrackedFiles(t *testing.T) {
 func TestStartUntrackedFiles(t *testing.T) {
 	_, configuration := setup(t)
 	configuration.StartIncludeUncommittedChanges = false
-	createFile(t, "example.txt", "content")
+	createFile(t, "example.txt", "contentIrrelevant")
 
 	start(configuration)
 
@@ -640,7 +654,7 @@ func TestStartUntrackedFiles(t *testing.T) {
 func TestStartNextBackToMaster(t *testing.T) {
 	_, configuration := setup(t)
 	start(configuration)
-	createFile(t, "example.txt", "content")
+	createFile(t, "example.txt", "contentIrrelevant")
 	assertOnBranch(t, "mob-session")
 
 	next(configuration)
@@ -653,7 +667,7 @@ func TestStartNextStay(t *testing.T) {
 	_, configuration := setup(t)
 	configuration.NextStay = true
 	start(configuration)
-	createFile(t, "file1.txt", "asdf")
+	createFile(t, "file1.txt", "contentIrrelevant")
 	assertOnBranch(t, "mob-session")
 
 	next(configuration)
@@ -662,9 +676,9 @@ func TestStartNextStay(t *testing.T) {
 	assertOnBranch(t, "mob-session")
 }
 
-func TestStartDoneWithMobDoneSquashTrue(t *testing.T) {
+func TestStartDoneWithMobDoneSquash(t *testing.T) {
 	_, configuration := setup(t)
-	configuration.DoneSquash = true
+	configuration.DoneSquash = Squash
 
 	start(configuration)
 	assertOnBranch(t, "mob-session")
@@ -718,9 +732,9 @@ func TestTestbed(t *testing.T) {
 	assertOutputContains(t, &output, "bob")
 }
 
-func TestStartDoneWithMobDoneSquashFalse(t *testing.T) {
+func TestStartDoneWithMobDoneNoSquash(t *testing.T) {
 	_, configuration := setup(t)
-	configuration.DoneSquash = false
+	configuration.DoneSquash = NoSquash
 
 	start(configuration)
 	assertOnBranch(t, "mob-session")
@@ -733,13 +747,13 @@ func TestStartDoneWithMobDoneSquashFalse(t *testing.T) {
 
 func TestStartDonePublishingOneManualCommit(t *testing.T) {
 	_, configuration := setup(t)
-	configuration.DoneSquash = false // default is true
+	configuration.DoneSquash = NoSquash
 
 	start(configuration)
 	assertOnBranch(t, "mob-session")
 	// should be 1 commit on mob-session so far
 
-	createFileAndCommitIt(t, "example.txt", "content", "[manual-commit-1] publish this commit to master")
+	createFileAndCommitIt(t, "example.txt", "contentIrrelevant", "[manual-commit-1] publish this commit to master")
 	assertCommits(t, 2)
 
 	done(configuration) // without squash (configuration)
@@ -754,13 +768,13 @@ func TestStartDonePublishingOneManualCommit(t *testing.T) {
 
 func TestStartDoneSquashTheOneManualCommit(t *testing.T) {
 	_, configuration := setup(t)
-	configuration.DoneSquash = true
+	configuration.DoneSquash = Squash
 
 	start(configuration)
 	assertOnBranch(t, "mob-session")
 	// should be 1 commit on mob-session so far
 
-	createFileAndCommitIt(t, "example.txt", "content", "[manual-commit-1] publish this commit to master")
+	createFileAndCommitIt(t, "example.txt", "contentIrrelevant", "[manual-commit-1] publish this commit to master")
 	assertCommits(t, 2)
 
 	done(configuration)
@@ -778,7 +792,7 @@ func TestStartDoneWithUncommittedChanges(t *testing.T) {
 	_, configuration := setup(t)
 
 	start(configuration) // should be 1 commit on mob-session so far
-	createFile(t, "example.txt", "content")
+	createFile(t, "example.txt", "contentIrrelevant")
 
 	done(configuration)
 
@@ -793,7 +807,7 @@ func TestStartDoneWithUncommittedChanges(t *testing.T) {
 
 func TestStartDoneNoSquashWithUncommittedChanges(t *testing.T) {
 	_, configuration := setup(t)
-	configuration.DoneSquash = false // default is true
+	configuration.DoneSquash = NoSquash
 
 	start(configuration) // should be 1 commit on mob-session so far
 	createFile(t, "example.txt", "content")
@@ -809,6 +823,145 @@ func TestStartDoneNoSquashWithUncommittedChanges(t *testing.T) {
 	assertNoMobSessionBranches(t, configuration, "mob-session")
 }
 
+func TestStartDoneSquashWipPublishingOneManualCommit(t *testing.T) {
+	_, configuration := setup(t)
+	configuration.DoneSquash = SquashWip
+
+	start(configuration)
+	createFile(t, "some.txt", "contentIrrelevant")
+	next(configuration) // this wip commit will be squashed
+
+	start(configuration)
+	createFileAndCommitIt(t, "example.txt", "contentIrrelevant", "[manual-commit-1] publish this commit to master")
+
+	done(configuration)
+
+	assertOnBranch(t, "master")
+	assertCleanGitStatus(t)
+	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")
+}
+
+func TestStartDoneSquashWipWithUncommittedChanges(t *testing.T) {
+	_, configuration := setup(t)
+
+	start(configuration) // should be 1 commit on mob-session so far
+	createFile(t, "example.txt", "contentIrrelevant")
+
+	configuration.DoneSquash = SquashWip
+	done(configuration)
+
+	assertOnBranch(t, "master")
+	assertGitStatus(t, GitStatus{
+		"example.txt": "A",
+	})
+	assertCommitsOnBranch(t, 1, "master")
+	assertCommitsOnBranch(t, 1, "origin/master")
+	assertNoMobSessionBranches(t, configuration, "mob-session")
+}
+
+func TestStartDoneSquashWipOneWipCommitAfterManualCommit(t *testing.T) {
+	_, configuration := setup(t)
+
+	start(configuration)
+	createFileAndCommitIt(t, "example.txt", "contentIrrelevant", "[manual-commit-1] publish this commit to master")
+	next(configuration)
+
+	start(configuration)
+	createFile(t, "file.txt", "contentIrrelevant") // the user should see these changes staged after done
+	next(configuration)
+
+	start(configuration)
+	configuration.DoneSquash = SquashWip
+	done(configuration)
+
+	assertOnBranch(t, "master")
+	assertGitStatus(t, GitStatus{
+		"file.txt": "A",
+	})
+	assertCommitsOnBranch(t, 2, "master")
+	assertCommitLogContainsMessage(t, "master", "[manual-commit-1] publish this commit to master")
+	assertNoMobSessionBranches(t, configuration, "mob-session")
+}
+
+func TestStartDoneSquashWipManyWipCommitsAfterManualCommit(t *testing.T) {
+	_, configuration := setup(t)
+
+	start(configuration)
+	createFileAndCommitIt(t, "example.txt", "contentIrrelevant", "[manual-commit-1] publish this commit to master")
+	next(configuration)
+
+	start(configuration)
+	createFile(t, "file1.txt", "contentIrrelevant") // the user should see these changes staged after done
+	next(configuration)
+
+	start(configuration)
+	createFile(t, "file2.txt", "contentIrrelevant") // the user should see these changes staged after done
+	next(configuration)
+
+	start(configuration)
+	configuration.DoneSquash = SquashWip
+	done(configuration)
+
+	assertOnBranch(t, "master")
+	assertGitStatus(t, GitStatus{
+		"file1.txt": "A",
+		"file2.txt": "A",
+	})
+	assertCommitsOnBranch(t, 2, "master")
+	assertCommitLogContainsMessage(t, "master", "[manual-commit-1] publish this commit to master")
+	assertNoMobSessionBranches(t, configuration, "mob-session")
+}
+
+func TestStartDoneSquashWipOnlyWipCommits(t *testing.T) {
+	_, configuration := setup(t)
+
+	start(configuration)
+	createFile(t, "file1.txt", "contentIrrelevant") // the user should see these changes staged after done
+	next(configuration)
+
+	start(configuration)
+	createFile(t, "file2.txt", "contentIrrelevant") // the user should see these changes staged after done
+	next(configuration)
+
+	start(configuration)
+	configuration.DoneSquash = SquashWip
+	done(configuration)
+
+	assertOnBranch(t, "master")
+	assertGitStatus(t, GitStatus{
+		"file1.txt": "A",
+		"file2.txt": "A",
+	})
+	assertCommitsOnBranch(t, 1, "master")
+	assertNoMobSessionBranches(t, configuration, "mob-session")
+}
+
+func TestStartDoneSquashWipOnlyManualCommits(t *testing.T) {
+	_, configuration := setup(t)
+
+	start(configuration)
+	createFileAndCommitIt(t, "example.txt", "contentIrrelevant", "[manual-commit-1] publish this commit to master")
+	next(configuration)
+
+	start(configuration)
+	createFileAndCommitIt(t, "example2.txt", "contentIrrelevant", "[manual-commit-2] publish this commit to master")
+	next(configuration)
+
+	start(configuration)
+	configuration.DoneSquash = SquashWip
+	done(configuration)
+
+	assertOnBranch(t, "master")
+	assertCleanGitStatus(t)
+	assertCommitsOnBranch(t, 3, "master")
+	assertCommitLogContainsMessage(t, "master", "[manual-commit-1] publish this commit to master")
+	assertCommitLogContainsMessage(t, "master", "[manual-commit-2] publish this commit to master")
+	assertNoMobSessionBranches(t, configuration, "mob-session")
+}
+
 func TestStartDoneFeatureBranch(t *testing.T) {
 	_, configuration := setup(t)
 	git("checkout", "-b", "feature1")
@@ -858,11 +1011,11 @@ func TestBothCreateNonemptyCommitWithNext(t *testing.T) {
 
 	setWorkingDir(tempDir + "/local")
 	start(configuration)
-	createFile(t, "file1.txt", "asdf")
+	createFile(t, "file1.txt", "contentIrrelevant")
 
 	setWorkingDir(tempDir + "/localother")
 	start(configuration)
-	createFile(t, "file2.txt", "asdf")
+	createFile(t, "file2.txt", "contentIrrelevant")
 
 	setWorkingDir(tempDir + "/local")
 	next(configuration)
@@ -915,7 +1068,7 @@ func TestStartNextPushManualCommits(t *testing.T) {
 	setWorkingDir(tempDir + "/local")
 
 	start(configuration)
-	createFileAndCommitIt(t, "example.txt", "content", "asdf")
+	createFileAndCommitIt(t, "example.txt", "contentIrrelevant", "asdf")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/localother")
@@ -934,7 +1087,7 @@ func TestStartNextPushManualCommitsFeatureBranch(t *testing.T) {
 	start(configuration)
 	assertOnBranch(t, "mob/feature1")
 
-	createFileAndCommitIt(t, "example.txt", "content", "asdf")
+	createFileAndCommitIt(t, "example.txt", "contentIrrelevant", "asdf")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/localother")
@@ -949,7 +1102,7 @@ func TestConflictingMobSessions(t *testing.T) {
 
 	setWorkingDir(tempDir + "/local")
 	start(configuration)
-	createFile(t, "example.txt", "content")
+	createFile(t, "example.txt", "contentIrrelevant")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/localother")
@@ -963,7 +1116,7 @@ func TestConflictingMobSessions(t *testing.T) {
 
 	setWorkingDir(tempDir + "/local")
 	start(configuration)
-	createFile(t, "example2.txt", "content")
+	createFile(t, "example2.txt", "contentIrrelevant")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/localother")
@@ -976,7 +1129,7 @@ func TestConflictingMobSessionsNextStay(t *testing.T) {
 
 	setWorkingDir(tempDir + "/local")
 	start(configuration)
-	createFile(t, "example.txt", "content")
+	createFile(t, "example.txt", "contentIrrelevant")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/localother")
@@ -1015,11 +1168,11 @@ func TestDoneMerge(t *testing.T) {
 
 	setWorkingDir(tempDir + "/local")
 	start(configuration)
-	createFile(t, "example.txt", "content")
+	createFile(t, "example.txt", "contentIrrelevant")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/localother")
-	createFileAndCommitIt(t, "example2.txt", "asdf", "asdf")
+	createFileAndCommitIt(t, "example2.txt", "contentIrrelevant", "asdf")
 	git("push")
 
 	setWorkingDir(tempDir + "/local")
@@ -1044,13 +1197,13 @@ func TestStartAndNextInSubdir(t *testing.T) {
 
 	setWorkingDir(tempDir + "/local/subdir")
 	start(configuration)
-	createFile(t, "example.txt", "content")
+	createFile(t, "example.txt", "contentIrrelevant")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/localother/subdir")
 	start(configuration)
-	createFile(t, "example2.txt", "content")
-	createFile(t, "../example3.txt", "content")
+	createFile(t, "example2.txt", "contentIrrelevant")
+	createFile(t, "../example3.txt", "contentIrrelevant")
 	next(configuration)
 
 	setWorkingDir(tempDir + "/local/subdir")
@@ -1091,7 +1244,7 @@ func TestEmptyGitStatus(t *testing.T) {
 
 func TestGitStatusWithOneFile(t *testing.T) {
 	setup(t)
-	createFile(t, "hello.txt", "")
+	createFile(t, "hello.txt", "contentIrrelevant")
 
 	status := gitStatus()
 
@@ -1102,8 +1255,8 @@ func TestGitStatusWithOneFile(t *testing.T) {
 
 func TestGitStatusWithManyFiles(t *testing.T) {
 	setup(t)
-	createFile(t, "hello.txt", "")
-	createFile(t, "added.txt", "")
+	createFile(t, "hello.txt", "contentIrrelevant")
+	createFile(t, "added.txt", "contentIrrelevant")
 	git("add", "added.txt")
 
 	status := gitStatus()
diff --git a/squash_wip_test.go b/squash_wip_test.go
index 7e98e65..28efb97 100644
--- a/squash_wip_test.go
+++ b/squash_wip_test.go
@@ -4,46 +4,96 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"strings"
 	"testing"
 )
 
 func TestSquashWipCommits_acceptance(t *testing.T) {
 	_, configuration := setup(t)
+	wipCommit(t, configuration, "file1.txt")
+	manualCommit(t, configuration, "file2.txt", "first manual commit")
 
-	// change without manual commit
+	// manual commit followed by a wip commit
 	start(configuration)
-	createFile(t, "file1.txt", "irrelevant")
+	createFileAndCommitIt(t, "file3.txt", "contentIrrelevant", "second manual commit")
+	createFile(t, "file4.txt", "contentIrrelevant")
 	next(configuration)
 
-	// change with a manual commit
+	// final manual commit
 	start(configuration)
-	createFileAndCommitIt(t, "file2.txt", "irrelevant", "first manual commit")
-	next(configuration)
+	createFileAndCommitIt(t, "file5.txt", "contentIrrelevant", "third manual commit")
+
+	squashWip(configuration)
+
+	assertOnBranch(t, "mob-session")
+	equals(t, []string{
+		"third manual commit",
+		"second manual commit",
+		"first manual commit",
+	}, commitsOnCurrentBranch(configuration))
+	equals(t, commitsOnCurrentBranch(configuration), commitsOnRemoteBranch(configuration))
+}
 
-	// change with a manual commit followed by an uncommited change
+func TestSquashWipCommits_withFinalWipCommit(t *testing.T) {
+	_, configuration := setup(t)
+	wipCommit(t, configuration, "file1.txt")
+	manualCommit(t, configuration, "file2.txt", "first manual commit")
+	wipCommit(t, configuration, "file3.txt")
 	start(configuration)
-	createFileAndCommitIt(t, "file3.txt", "irrelevant", "second manual commit")
-	createFile(t, "file4.txt", "irrelevant")
-	next(configuration)
 
-	// change with a final manual commit
+	squashWip(configuration)
+
+	assertOnBranch(t, "mob-session")
+	assertGitStatus(t, GitStatus{
+		"file3.txt": "A",
+	})
+	equals(t, []string{
+		"first manual commit",
+	}, commitsOnCurrentBranch(configuration))
+}
+
+func TestSquashWipCommits_withManyFinalWipCommits(t *testing.T) {
+	_, configuration := setup(t)
+	wipCommit(t, configuration, "file1.txt")
+	manualCommit(t, configuration, "file2.txt", "first manual commit")
+	wipCommit(t, configuration, "file3.txt")
+	wipCommit(t, configuration, "file4.txt")
 	start(configuration)
-	createFileAndCommitIt(t, "file5.txt", "irrelevant", "third manual commit")
 
 	squashWip(configuration)
 
 	assertOnBranch(t, "mob-session")
+	assertGitStatus(t, GitStatus{
+		"file3.txt": "A",
+		"file4.txt": "A",
+	})
 	equals(t, []string{
-		"third manual commit",
-		"second manual commit",
 		"first manual commit",
 	}, commitsOnCurrentBranch(configuration))
 }
 
+func TestSquashWipCommits_onlyWipCommits(t *testing.T) {
+	_, configuration := setup(t)
+	wipCommit(t, configuration, "file1.txt")
+	wipCommit(t, configuration, "file2.txt")
+	wipCommit(t, configuration, "file3.txt")
+	start(configuration)
+
+	squashWip(configuration)
+
+	assertOnBranch(t, "mob-session")
+	assertGitStatus(t, GitStatus{
+		"file1.txt": "A",
+		"file2.txt": "A",
+		"file3.txt": "A",
+	})
+	equals(t, []string{""}, commitsOnCurrentBranch(configuration))
+}
+
 func TestSquashWipCommits_resetsEnv(t *testing.T) {
 	_, configuration := setup(t)
 	start(configuration)
-	createFileAndCommitIt(t, "file1.txt", "irrelevant", "new file")
+	createFileAndCommitIt(t, "file1.txt", "contentIrrelevant", "new file")
 	originalGitEditor := "irrelevant"
 	originalGitSequenceEditor := "irrelevant, too"
 	os.Setenv("GIT_EDITOR", originalGitEditor)
@@ -55,19 +105,6 @@ func TestSquashWipCommits_resetsEnv(t *testing.T) {
 	equals(t, originalGitSequenceEditor, os.Getenv("GIT_SEQUENCE_EDITOR"))
 }
 
-func TestSquashWipCommits_failsOnFinalWipCommit(t *testing.T) {
-	output, configuration := setup(t)
-	start(configuration)
-	createFile(t, "file2.txt", "irrelevant")
-	next(configuration)
-	start(configuration)
-
-	squashWip(configuration)
-
-	assertCommitLogContainsMessage(t, gitCurrentBranch().Name, configuration.WipCommitMessage)
-	assertOutputContains(t, output, "failed to squash wip commits")
-}
-
 func TestSquashWipCommits_failsOnMainBranch(t *testing.T) {
 	output, configuration := setup(t)
 
@@ -78,11 +115,7 @@ func TestSquashWipCommits_failsOnMainBranch(t *testing.T) {
 
 func TestSquashWipCommits_worksWithEmptyCommits(t *testing.T) {
 	_, configuration := setup(t)
-
-	// change without manual commit
-	start(configuration)
-	createFile(t, "file1.txt", "irrelevant")
-	next(configuration)
+	wipCommit(t, configuration, "file1.txt")
 
 	start(configuration)
 	silentgit("commit", "--allow-empty", "-m ok")
@@ -97,11 +130,11 @@ func TestSquashWipCommits_worksWithEmptyCommits(t *testing.T) {
 
 func TestCommitsOnCurrentBranch(t *testing.T) {
 	_, configuration := setup(t)
-	createFileAndCommitIt(t, "file1.txt", "irrelevant", "not on branch")
+	createFileAndCommitIt(t, "file1.txt", "contentIrrelevant", "not on branch")
 	silentgit("push")
 	start(configuration)
-	createFileAndCommitIt(t, "file2.txt", "irrelevant", "on branch")
-	createFile(t, "file3.txt", "irrelevant")
+	createFileAndCommitIt(t, "file2.txt", "contentIrrelevant", "on branch")
+	createFile(t, "file3.txt", "contentIrrelevant")
 	next(configuration)
 	start(configuration)
 
@@ -113,46 +146,6 @@ func TestCommitsOnCurrentBranch(t *testing.T) {
 	}, commits)
 }
 
-func TestEndsWithWipCommit_finalManualCommit(t *testing.T) {
-	_, configuration := setup(t)
-	start(configuration)
-	createFileAndCommitIt(t, "file1.txt", "irrelevant", "new file")
-
-	equals(t, false, endsWithWipCommit(configuration))
-}
-
-func TestEndsWithWipCommit_finalWipCommit(t *testing.T) {
-	_, configuration := setup(t)
-	start(configuration)
-	createFile(t, "file1.txt", "irrelevant")
-	next(configuration)
-	start(configuration)
-
-	equals(t, true, endsWithWipCommit(configuration))
-}
-
-func TestEndsWithWipCommit_manualThenWipCommit(t *testing.T) {
-	_, configuration := setup(t)
-	start(configuration)
-	createFileAndCommitIt(t, "file1.txt", "irrelevant", "new file")
-	createFile(t, "file2.txt", "irrelevant")
-	next(configuration)
-	start(configuration)
-
-	equals(t, true, endsWithWipCommit(configuration))
-}
-
-func TestEndsWithWipCommit_wipThenManualCommit(t *testing.T) {
-	_, configuration := setup(t)
-	start(configuration)
-	createFile(t, "file2.txt", "irrelevant")
-	next(configuration)
-	start(configuration)
-	createFileAndCommitIt(t, "file1.txt", "irrelevant", "new file")
-
-	equals(t, false, endsWithWipCommit(configuration))
-}
-
 func TestMarkSquashWip_singleManualCommit(t *testing.T) {
 	configuration := getDefaultConfiguration()
 	input := `pick c51a56d new file
@@ -212,6 +205,62 @@ squash c51a56d manual commit
 	equals(t, expected, result)
 }
 
+func TestMarkSquashWip_manualCommitFollowedByWipCommit(t *testing.T) {
+	configuration := getDefaultConfiguration()
+	input := fmt.Sprintf(`pick c51a56d manual commit
+pick 01a9a31 %[1]s
+
+# Rebase ...`, configuration.WipCommitMessage)
+	expected := fmt.Sprintf(`pick c51a56d manual commit
+pick 01a9a31 %[1]s
+
+# Rebase ...`, configuration.WipCommitMessage)
+
+	result := markPostWipCommitsForSquashing(input, configuration)
+
+	equals(t, expected, result)
+}
+
+func TestMarkSquashWip_manualCommitFollowedByManyWipCommits(t *testing.T) {
+	configuration := getDefaultConfiguration()
+	input := fmt.Sprintf(`pick c51a56d manual commit
+pick 01a9a31 %[1]s
+pick 01a9a32 %[1]s
+pick 01a9a33 %[1]s
+
+# Rebase ...`, configuration.WipCommitMessage)
+	expected := fmt.Sprintf(`pick c51a56d manual commit
+pick 01a9a31 %[1]s
+fixup 01a9a32 %[1]s
+fixup 01a9a33 %[1]s
+
+# Rebase ...`, configuration.WipCommitMessage)
+
+	result := markPostWipCommitsForSquashing(input, configuration)
+
+	equals(t, expected, result)
+}
+
+func TestMarkSquashWip_wipThenManualCommitFollowedByManyWipCommits(t *testing.T) {
+	configuration := getDefaultConfiguration()
+	input := fmt.Sprintf(`pick 01a9a31 %[1]s
+pick c51a56d manual commit
+pick 01a9a32 %[1]s
+pick 01a9a33 %[1]s
+
+# Rebase ...`, configuration.WipCommitMessage)
+	expected := fmt.Sprintf(`pick 01a9a31 %[1]s
+squash c51a56d manual commit
+pick 01a9a32 %[1]s
+fixup 01a9a33 %[1]s
+
+# Rebase ...`, configuration.WipCommitMessage)
+
+	result := markPostWipCommitsForSquashing(input, configuration)
+
+	equals(t, expected, result)
+}
+
 func TestCommentWipCommits_oneWipAndOneManualCommit(t *testing.T) {
 	configuration := getDefaultConfiguration()
 	input := fmt.Sprintf(`# This is a combination of 2 commits.
@@ -297,3 +346,31 @@ squash c51a56d manual commit
 	result, _ := ioutil.ReadFile(input)
 	equals(t, expected, string(result))
 }
+
+func wipCommit(t *testing.T, configuration Configuration, filename string) {
+	start(configuration)
+	createFile(t, filename, "contentIrrelevant")
+	next(configuration)
+}
+
+func manualCommit(t *testing.T, configuration Configuration, filename string, message string) {
+	start(configuration)
+	createFileAndCommitIt(t, filename, "contentIrrelevant", message)
+	next(configuration)
+}
+
+func commitsOnCurrentBranch(configuration Configuration) []string {
+	currentBaseBranch, currentWipBranch := determineBranches(gitCurrentBranch(), gitBranches(), configuration)
+	commitsBaseWipBranch := currentBaseBranch.String() + ".." + currentWipBranch.String()
+	log := silentgit("--no-pager", "log", commitsBaseWipBranch, "--pretty=format:%s")
+	lines := strings.Split(log, "\n")
+	return lines
+}
+
+func commitsOnRemoteBranch(configuration Configuration) []string {
+	currentBaseBranch, currentWipBranch := determineBranches(gitCurrentBranch(), gitBranches(), configuration)
+	commitsBaseWipBranch := currentBaseBranch.String() + ".." + configuration.RemoteName + "/" + currentWipBranch.String()
+	log := silentgit("--no-pager", "log", commitsBaseWipBranch, "--pretty=format:%s")
+	lines := strings.Split(log, "\n")
+	return lines
+}