Fix help callout anchoring and Push remote setup
- Anchor startup help callouts inside the scrollable pane workspace so they scroll with the exercise and terminal content they point at. - Rebuild the Push level remote as a bare sibling repo with a temporary worktree for the remote-only commit, avoiding Android non-bare remote push failures. - Leave a visible local worktree edit in Push while enabling rebase autostash so status, diff, and pull --rebase all behave as expected. - Add native Push setup coverage for diverged status output, plain diff output, and clean pull --rebase behavior.
This commit is contained in:
@@ -19,8 +19,8 @@ android {
|
|||||||
applicationId = "solutions.tretter.githugandroid"
|
applicationId = "solutions.tretter.githugandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 121
|
versionCode = 122
|
||||||
versionName = "0.1.120"
|
versionName = "0.1.121"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.layout.boundsInRoot
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -32,12 +35,15 @@ fun ExercisePane(
|
|||||||
visibleHint: String?,
|
visibleHint: String?,
|
||||||
suggestionsExpanded: Boolean,
|
suggestionsExpanded: Boolean,
|
||||||
showDescriptionHint: Boolean,
|
showDescriptionHint: Boolean,
|
||||||
|
onBoundsChanged: (Rect) -> Unit = {},
|
||||||
onToggleSuggestions: () -> Unit,
|
onToggleSuggestions: () -> Unit,
|
||||||
onHint: () -> Unit,
|
onHint: () -> Unit,
|
||||||
onReset: () -> Unit,
|
onReset: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onGloballyPositioned { onBoundsChanged(it.boundsInRoot()) },
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
if (showDescriptionHint) {
|
if (showDescriptionHint) {
|
||||||
|
|||||||
@@ -53,8 +53,11 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.layout.boundsInRoot
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GitHugApp() {
|
fun GitHugApp() {
|
||||||
@@ -95,6 +98,8 @@ fun GitHugApp() {
|
|||||||
var showHelpOverlay by remember { mutableStateOf(false) }
|
var showHelpOverlay by remember { mutableStateOf(false) }
|
||||||
var showHelpOnStart by remember { mutableStateOf(true) }
|
var showHelpOnStart by remember { mutableStateOf(true) }
|
||||||
var helpPreferenceInitialized by remember { mutableStateOf(false) }
|
var helpPreferenceInitialized by remember { mutableStateOf(false) }
|
||||||
|
var workspaceBounds by remember { mutableStateOf<Rect?>(null) }
|
||||||
|
var exerciseBounds by remember { mutableStateOf<Rect?>(null) }
|
||||||
var promptBounds by remember { mutableStateOf<Rect?>(null) }
|
var promptBounds by remember { mutableStateOf<Rect?>(null) }
|
||||||
val currentLevel = levels[currentLevelIndex]
|
val currentLevel = levels[currentLevelIndex]
|
||||||
|
|
||||||
@@ -495,104 +500,112 @@ fun GitHugApp() {
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
FixedHeader()
|
FixedHeader()
|
||||||
PaneWorkspace(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
paneLayout = paneLayout,
|
.onGloballyPositioned { workspaceBounds = it.boundsInRoot() },
|
||||||
onMovePane = { paneId, delta ->
|
) {
|
||||||
updatePaneLayout(transform = { layout -> movePane(layout, paneId, delta) })
|
PaneWorkspace(
|
||||||
},
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onTogglePane = { paneId ->
|
paneLayout = paneLayout,
|
||||||
updatePaneLayout(transform = { layout ->
|
onMovePane = { paneId, delta ->
|
||||||
val collapsed = layout.collapsed.toMutableSet()
|
updatePaneLayout(transform = { layout -> movePane(layout, paneId, delta) })
|
||||||
if (!collapsed.add(paneId)) collapsed.remove(paneId)
|
},
|
||||||
layout.copy(collapsed = collapsed)
|
onTogglePane = { paneId ->
|
||||||
})
|
updatePaneLayout(transform = { layout ->
|
||||||
},
|
val collapsed = layout.collapsed.toMutableSet()
|
||||||
levelsContent = {
|
if (!collapsed.add(paneId)) collapsed.remove(paneId)
|
||||||
LevelsPane(levels, currentLevelIndex, completedLevels) { index -> loadLevel(index) }
|
layout.copy(collapsed = collapsed)
|
||||||
},
|
})
|
||||||
visualContent = { VisualPane(repo) },
|
},
|
||||||
exerciseContent = {
|
levelsContent = {
|
||||||
ExercisePane(
|
LevelsPane(levels, currentLevelIndex, completedLevels) { index -> loadLevel(index) }
|
||||||
level = currentLevel,
|
},
|
||||||
visibleHint = if (activeExerciseDetail == ExerciseDetailPanel.HINT) visibleHint else null,
|
visualContent = { VisualPane(repo) },
|
||||||
suggestionsExpanded = activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS,
|
exerciseContent = {
|
||||||
showDescriptionHint = false,
|
ExercisePane(
|
||||||
onToggleSuggestions = {
|
level = currentLevel,
|
||||||
showExerciseDescriptionHint = false
|
visibleHint = if (activeExerciseDetail == ExerciseDetailPanel.HINT) visibleHint else null,
|
||||||
showHelpOverlay = false
|
suggestionsExpanded = activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS,
|
||||||
activeExerciseDetail = if (activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS) null else ExerciseDetailPanel.SUGGESTIONS
|
showDescriptionHint = false,
|
||||||
if (activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS) {
|
onBoundsChanged = { exerciseBounds = it },
|
||||||
visibleHint = null
|
onToggleSuggestions = {
|
||||||
}
|
|
||||||
applyRecommendedPaneWeights(persist = false)
|
|
||||||
},
|
|
||||||
onHint = {
|
|
||||||
showExerciseDescriptionHint = false
|
|
||||||
showHelpOverlay = false
|
|
||||||
val hint = currentLevel.hints.getOrNull(hintIndex) ?: "No more hints for this level."
|
|
||||||
visibleHint = hint
|
|
||||||
activeExerciseDetail = ExerciseDetailPanel.HINT
|
|
||||||
hintIndex = (hintIndex + 1).coerceAtMost(currentLevel.hints.size)
|
|
||||||
applyRecommendedPaneWeights(persist = false)
|
|
||||||
},
|
|
||||||
onReset = {
|
|
||||||
showExerciseDescriptionHint = false
|
|
||||||
showHelpOverlay = false
|
|
||||||
resetCurrentLevel()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
terminalContent = {
|
|
||||||
TerminalPane(
|
|
||||||
output = output,
|
|
||||||
inputFieldVersion = inputFieldVersion,
|
|
||||||
commandInput = commandInput,
|
|
||||||
showInputHint = false,
|
|
||||||
onInputHintDismiss = {
|
|
||||||
showTerminalInputHint = false
|
|
||||||
showHelpOverlay = false
|
|
||||||
},
|
|
||||||
onValueChange = {
|
|
||||||
val blockedEcho = suppressedImeEcho
|
|
||||||
if (blockedEcho != null && commandInput.text.isEmpty()) {
|
|
||||||
val blockedTrimmed = blockedEcho.trim()
|
|
||||||
if (it.text == blockedEcho || (blockedTrimmed.isNotEmpty() && it.text == blockedTrimmed)) {
|
|
||||||
return@TerminalPane
|
|
||||||
}
|
|
||||||
}
|
|
||||||
suppressedImeEcho = null
|
|
||||||
if (it.text.isNotEmpty()) {
|
|
||||||
showTerminalInputHint = false
|
|
||||||
showExerciseDescriptionHint = false
|
showExerciseDescriptionHint = false
|
||||||
showHelpOverlay = false
|
showHelpOverlay = false
|
||||||
}
|
activeExerciseDetail = if (activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS) null else ExerciseDetailPanel.SUGGESTIONS
|
||||||
commandInput = it
|
if (activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS) {
|
||||||
|
visibleHint = null
|
||||||
|
}
|
||||||
|
applyRecommendedPaneWeights(persist = false)
|
||||||
|
},
|
||||||
|
onHint = {
|
||||||
|
showExerciseDescriptionHint = false
|
||||||
|
showHelpOverlay = false
|
||||||
|
val hint = currentLevel.hints.getOrNull(hintIndex) ?: "No more hints for this level."
|
||||||
|
visibleHint = hint
|
||||||
|
activeExerciseDetail = ExerciseDetailPanel.HINT
|
||||||
|
hintIndex = (hintIndex + 1).coerceAtMost(currentLevel.hints.size)
|
||||||
|
applyRecommendedPaneWeights(persist = false)
|
||||||
|
},
|
||||||
|
onReset = {
|
||||||
|
showExerciseDescriptionHint = false
|
||||||
|
showHelpOverlay = false
|
||||||
|
resetCurrentLevel()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
terminalContent = {
|
||||||
|
TerminalPane(
|
||||||
|
output = output,
|
||||||
|
inputFieldVersion = inputFieldVersion,
|
||||||
|
commandInput = commandInput,
|
||||||
|
showInputHint = false,
|
||||||
|
onInputHintDismiss = {
|
||||||
|
showTerminalInputHint = false
|
||||||
|
showHelpOverlay = false
|
||||||
|
},
|
||||||
|
onValueChange = {
|
||||||
|
val blockedEcho = suppressedImeEcho
|
||||||
|
if (blockedEcho != null && commandInput.text.isEmpty()) {
|
||||||
|
val blockedTrimmed = blockedEcho.trim()
|
||||||
|
if (it.text == blockedEcho || (blockedTrimmed.isNotEmpty() && it.text == blockedTrimmed)) {
|
||||||
|
return@TerminalPane
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suppressedImeEcho = null
|
||||||
|
if (it.text.isNotEmpty()) {
|
||||||
|
showTerminalInputHint = false
|
||||||
|
showExerciseDescriptionHint = false
|
||||||
|
showHelpOverlay = false
|
||||||
|
}
|
||||||
|
commandInput = it
|
||||||
|
},
|
||||||
|
onRun = { runCommand() },
|
||||||
|
onTab = { tabComplete() },
|
||||||
|
onHelp = { showCommandHelp() },
|
||||||
|
onCursorLeft = { moveCursor(-1) },
|
||||||
|
onCursorRight = { moveCursor(1) },
|
||||||
|
onHistoryUp = { historyUp() },
|
||||||
|
onHistoryDown = { historyDown() },
|
||||||
|
onPromptBoundsChanged = { promptBounds = it },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (showHelpOverlay) {
|
||||||
|
HelpCalloutOverlay(
|
||||||
|
showHelpOnStart = showHelpOnStart,
|
||||||
|
onShowHelpOnStartChange = { showHelpOnStart = it },
|
||||||
|
onOk = {
|
||||||
|
showHelpOverlay = false
|
||||||
|
scope.launch { gameProgressStore.saveShowHelpOnStart(showHelpOnStart) }
|
||||||
},
|
},
|
||||||
onRun = { runCommand() },
|
onClose = { showHelpOverlay = false },
|
||||||
onTab = { tabComplete() },
|
workspaceBounds = workspaceBounds,
|
||||||
onHelp = { showCommandHelp() },
|
exerciseBounds = exerciseBounds,
|
||||||
onCursorLeft = { moveCursor(-1) },
|
promptBounds = promptBounds,
|
||||||
onCursorRight = { moveCursor(1) },
|
|
||||||
onHistoryUp = { historyUp() },
|
|
||||||
onHistoryDown = { historyDown() },
|
|
||||||
onPromptBoundsChanged = { promptBounds = it },
|
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
}
|
|
||||||
if (showHelpOverlay) {
|
|
||||||
HelpCalloutOverlay(
|
|
||||||
showHelpOnStart = showHelpOnStart,
|
|
||||||
onShowHelpOnStartChange = { showHelpOnStart = it },
|
|
||||||
onOk = {
|
|
||||||
showHelpOverlay = false
|
|
||||||
scope.launch { gameProgressStore.saveShowHelpOnStart(showHelpOnStart) }
|
|
||||||
},
|
|
||||||
onClose = { showHelpOverlay = false },
|
|
||||||
promptBounds = promptBounds,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -670,25 +683,41 @@ private fun HelpCalloutOverlay(
|
|||||||
onShowHelpOnStartChange: (Boolean) -> Unit,
|
onShowHelpOnStartChange: (Boolean) -> Unit,
|
||||||
onOk: () -> Unit,
|
onOk: () -> Unit,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
|
workspaceBounds: Rect?,
|
||||||
|
exerciseBounds: Rect?,
|
||||||
promptBounds: Rect?,
|
promptBounds: Rect?,
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
val workspaceTop = workspaceBounds?.top ?: 0f
|
||||||
|
val insetPx = with(density) { 14.dp.roundToPx() }
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
) {
|
) {
|
||||||
HelpBubble(
|
HelpBubble(
|
||||||
text = "Read the exercise description, then solve it by entering commands below.",
|
text = "Read the exercise description, then solve it by entering commands below.",
|
||||||
modifier = Modifier.align(Alignment.TopStart),
|
modifier = exerciseBounds?.let { bounds ->
|
||||||
|
Modifier.offset {
|
||||||
|
IntOffset(
|
||||||
|
x = insetPx,
|
||||||
|
y = (bounds.top - workspaceTop - insetPx).roundToInt().coerceAtLeast(0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||||
)
|
)
|
||||||
HelpBubbleWithControls(
|
HelpBubbleWithControls(
|
||||||
modifier = promptBounds?.let { bounds ->
|
modifier = promptBounds?.let { bounds ->
|
||||||
val calloutTop = with(density) { (bounds.top.toDp() - 148.dp).coerceAtLeast(12.dp) }
|
val bubbleHeight = with(density) { 190.dp.roundToPx() }
|
||||||
Modifier.offset { IntOffset(0, with(density) { calloutTop.roundToPx() }) }
|
Modifier.offset {
|
||||||
|
IntOffset(
|
||||||
|
x = insetPx,
|
||||||
|
y = (bounds.top - workspaceTop - bubbleHeight).roundToInt().coerceAtLeast(0),
|
||||||
|
)
|
||||||
|
}
|
||||||
} ?: Modifier
|
} ?: Modifier
|
||||||
.align(Alignment.BottomStart)
|
.align(Alignment.BottomStart)
|
||||||
.padding(bottom = 96.dp),
|
.padding(start = 14.dp, bottom = 96.dp),
|
||||||
text = "Tap the prompt to enter a command.",
|
text = "Tap the prompt to enter a command.",
|
||||||
showHelpOnStart = showHelpOnStart,
|
showHelpOnStart = showHelpOnStart,
|
||||||
onShowHelpOnStartChange = onShowHelpOnStartChange,
|
onShowHelpOnStartChange = onShowHelpOnStartChange,
|
||||||
|
|||||||
@@ -498,27 +498,38 @@ class GitRepositoryRuntime private constructor(
|
|||||||
writeFile("file2")
|
writeFile("file2")
|
||||||
commitIn(sandbox, "Second commit", "file2")
|
commitIn(sandbox, "Second commit", "file2")
|
||||||
|
|
||||||
val remoteWorkTree = File(sandbox.parentFile ?: sandbox, "${sandbox.name}-origin")
|
val parent = sandbox.parentFile ?: sandbox
|
||||||
|
val remoteDir = File(parent, "${sandbox.name}-origin.git")
|
||||||
|
val remoteWorkTree = File(parent, "${sandbox.name}-origin-work")
|
||||||
|
remoteDir.deleteRecursively()
|
||||||
remoteWorkTree.deleteRecursively()
|
remoteWorkTree.deleteRecursively()
|
||||||
|
remoteDir.mkdirs()
|
||||||
remoteWorkTree.mkdirs()
|
remoteWorkTree.mkdirs()
|
||||||
val initResult = runGit(nativeGit, remoteWorkTree, listOf("init", "-b", "master"))
|
runGit(nativeGit, remoteDir, listOf("init", "--bare", "-b", "master"))
|
||||||
if (initResult.exitCode != 0) {
|
runGit(nativeGit, sandbox, listOf("remote", "add", "push-setup-origin", remoteDir.absolutePath))
|
||||||
runGit(nativeGit, remoteWorkTree, listOf("init"))
|
|
||||||
runGit(nativeGit, remoteWorkTree, listOf("checkout", "-B", "master"))
|
|
||||||
}
|
|
||||||
runGit(nativeGit, remoteWorkTree, listOf("config", "receive.denyCurrentBranch", "ignore"))
|
|
||||||
runGit(nativeGit, sandbox, listOf("remote", "add", "push-setup-origin", File(remoteWorkTree, ".git").absolutePath))
|
|
||||||
runGit(nativeGit, sandbox, listOf("push", "push-setup-origin", "master"))
|
runGit(nativeGit, sandbox, listOf("push", "push-setup-origin", "master"))
|
||||||
runGit(nativeGit, sandbox, listOf("remote", "remove", "push-setup-origin"))
|
runGit(nativeGit, sandbox, listOf("remote", "remove", "push-setup-origin"))
|
||||||
runGit(nativeGit, remoteWorkTree, listOf("checkout", "-f", "master"))
|
|
||||||
|
fun runRemoteGit(arguments: List<String>): ProcessExecutionResult {
|
||||||
|
return runGit(
|
||||||
|
nativeGit,
|
||||||
|
sandbox,
|
||||||
|
listOf("--git-dir", remoteDir.absolutePath, "--work-tree", remoteWorkTree.absolutePath) + arguments,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
runRemoteGit(listOf("checkout", "-f", "master"))
|
||||||
File(remoteWorkTree, "file4").writeText("file4\n")
|
File(remoteWorkTree, "file4").writeText("file4\n")
|
||||||
commitIn(remoteWorkTree, "Fourth commit", "file4")
|
runRemoteGit(listOf("add", "file4"))
|
||||||
|
runRemoteGit(listOf("commit", "-m", "Fourth commit"))
|
||||||
|
|
||||||
writeFile("file3")
|
writeFile("file3")
|
||||||
commitIn(sandbox, "Third commit", "file3")
|
commitIn(sandbox, "Third commit", "file3")
|
||||||
runGit(nativeGit, sandbox, listOf("remote", "add", "origin", File(remoteWorkTree, ".git").absolutePath))
|
runGit(nativeGit, sandbox, listOf("remote", "add", "origin", remoteDir.absolutePath))
|
||||||
runGit(nativeGit, sandbox, listOf("fetch", "origin"))
|
runGit(nativeGit, sandbox, listOf("fetch", "origin"))
|
||||||
runGit(nativeGit, sandbox, listOf("branch", "--set-upstream-to=origin/master", "master"))
|
runGit(nativeGit, sandbox, listOf("branch", "--set-upstream-to=origin/master", "master"))
|
||||||
|
runGit(nativeGit, sandbox, listOf("config", "rebase.autoStash", "true"))
|
||||||
|
File(sandbox, "file3").appendText("local worktree change\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filesForSetupCommit(files: List<GitFile>, index: Int, commitCount: Int): List<GitFile> {
|
private fun filesForSetupCommit(files: List<GitFile>, index: Int, commitCount: Int): List<GitFile> {
|
||||||
|
|||||||
@@ -175,6 +175,30 @@ class GitSandboxEngineTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nativePushLevelHasDivergedStatusAndRebasesCleanly() {
|
||||||
|
val git = File("/usr/bin/git")
|
||||||
|
assumeTrue(git.exists() && git.canExecute())
|
||||||
|
val root = Files.createTempDirectory("githug-push-level").toFile()
|
||||||
|
try {
|
||||||
|
val runtime = GitRepositoryRuntime(root, git)
|
||||||
|
val level = pushLevel()
|
||||||
|
val repo = runtime.prepareLevel(level)
|
||||||
|
|
||||||
|
val (_, statusOutput) = runtime.execute(level, repo, "git status")
|
||||||
|
assertTrue(statusOutput.any { it.contains("diverged", ignoreCase = true) })
|
||||||
|
|
||||||
|
val (_, diffOutput) = runtime.execute(level, repo, "git diff")
|
||||||
|
assertTrue(diffOutput.isNotEmpty())
|
||||||
|
|
||||||
|
val (rebasedRepo, pullOutput) = runtime.execute(level, repo, "git pull --rebase origin master")
|
||||||
|
assertFalse(pullOutput.any { it.contains("fatal:", ignoreCase = true) || it.contains("error:", ignoreCase = true) })
|
||||||
|
assertTrue(rebasedRepo.files.any { it.name == "file4" && it.tracked })
|
||||||
|
} finally {
|
||||||
|
root.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun cdDotDotShortcutMovesToParentDirectory() {
|
fun cdDotDotShortcutMovesToParentDirectory() {
|
||||||
val repo = RepoState(initialized = true, currentDir = "src/main")
|
val repo = RepoState(initialized = true, currentDir = "src/main")
|
||||||
|
|||||||
Reference in New Issue
Block a user