Improve upstream parity for level fixtures
- Add native per-level fixtures for complex histories, tags, branch graphs, rebases, resets, restores, stashes, and local remotes. - Strengthen repository inspection so branch commit counts, remote-tracking branches, and detached tag checkouts reflect real Git state. - Align Fetch, Pull, Push Branch, Branch At, and Rebase validators/tests with upstream-style repository state. - Keep the prompt callout and native filesystem tab-completion fixes from the previous batch.
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 = 120
|
versionCode = 121
|
||||||
versionName = "0.1.119"
|
versionName = "0.1.120"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -39,15 +40,18 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
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.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
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.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.TextRange
|
||||||
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
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -91,6 +95,7 @@ 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 promptBounds by remember { mutableStateOf<Rect?>(null) }
|
||||||
val currentLevel = levels[currentLevelIndex]
|
val currentLevel = levels[currentLevelIndex]
|
||||||
|
|
||||||
LaunchedEffect(solvedCelebrationTitle) {
|
LaunchedEffect(solvedCelebrationTitle) {
|
||||||
@@ -293,7 +298,7 @@ fun GitHugApp() {
|
|||||||
val isCdCompletion = leadingCommand == "cd"
|
val isCdCompletion = leadingCommand == "cd"
|
||||||
if (token.isBlank() && !isCdCompletion) return
|
if (token.isBlank() && !isCdCompletion) return
|
||||||
|
|
||||||
val candidates = if (isCdCompletion) directoryCompletionCandidates(repo) else fileCompletionCandidates(repo)
|
val candidates = runtime.completionCandidates(currentLevel, repo, directoriesOnly = isCdCompletion)
|
||||||
val matches = candidates.sorted().filter { it.startsWith(token) }
|
val matches = candidates.sorted().filter { it.startsWith(token) }
|
||||||
if (matches.isEmpty()) return
|
if (matches.isEmpty()) return
|
||||||
|
|
||||||
@@ -572,6 +577,7 @@ fun GitHugApp() {
|
|||||||
onCursorRight = { moveCursor(1) },
|
onCursorRight = { moveCursor(1) },
|
||||||
onHistoryUp = { historyUp() },
|
onHistoryUp = { historyUp() },
|
||||||
onHistoryDown = { historyDown() },
|
onHistoryDown = { historyDown() },
|
||||||
|
onPromptBoundsChanged = { promptBounds = it },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -585,6 +591,7 @@ fun GitHugApp() {
|
|||||||
scope.launch { gameProgressStore.saveShowHelpOnStart(showHelpOnStart) }
|
scope.launch { gameProgressStore.saveShowHelpOnStart(showHelpOnStart) }
|
||||||
},
|
},
|
||||||
onClose = { showHelpOverlay = false },
|
onClose = { showHelpOverlay = false },
|
||||||
|
promptBounds = promptBounds,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -663,7 +670,9 @@ private fun HelpCalloutOverlay(
|
|||||||
onShowHelpOnStartChange: (Boolean) -> Unit,
|
onShowHelpOnStartChange: (Boolean) -> Unit,
|
||||||
onOk: () -> Unit,
|
onOk: () -> Unit,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
|
promptBounds: Rect?,
|
||||||
) {
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -674,9 +683,12 @@ private fun HelpCalloutOverlay(
|
|||||||
modifier = Modifier.align(Alignment.TopStart),
|
modifier = Modifier.align(Alignment.TopStart),
|
||||||
)
|
)
|
||||||
HelpBubbleWithControls(
|
HelpBubbleWithControls(
|
||||||
modifier = Modifier
|
modifier = promptBounds?.let { bounds ->
|
||||||
|
val calloutTop = with(density) { (bounds.top.toDp() - 148.dp).coerceAtLeast(12.dp) }
|
||||||
|
Modifier.offset { IntOffset(0, with(density) { calloutTop.roundToPx() }) }
|
||||||
|
} ?: Modifier
|
||||||
.align(Alignment.BottomStart)
|
.align(Alignment.BottomStart)
|
||||||
.padding(bottom = 54.dp),
|
.padding(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,
|
||||||
|
|||||||
@@ -103,6 +103,34 @@ class GitRepositoryRuntime private constructor(
|
|||||||
return augmentObservedRepoFacts(currentRepo, inspectedRepo, expandedTokens) to result.second
|
return augmentObservedRepoFacts(currentRepo, inspectedRepo, expandedTokens) to result.second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun completionCandidates(level: Level, currentRepo: RepoState, directoriesOnly: Boolean): List<String> {
|
||||||
|
val nativeGit = nativeGitBinary()
|
||||||
|
val sandbox = sandboxDir(level)
|
||||||
|
if (nativeGit == null || !sandbox.exists()) {
|
||||||
|
return if (directoriesOnly) {
|
||||||
|
directoryCompletionCandidates(currentRepo)
|
||||||
|
} else {
|
||||||
|
fileCompletionCandidates(currentRepo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sandboxRoot = sandbox.canonicalFile
|
||||||
|
val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
|
||||||
|
.takeIf { it.path == sandboxRoot.path || it.path.startsWith(sandboxRoot.path + File.separator) }
|
||||||
|
?: sandboxRoot
|
||||||
|
if (!workingDir.exists() || !workingDir.isDirectory) return emptyList()
|
||||||
|
|
||||||
|
return workingDir.walkTopDown()
|
||||||
|
.drop(1)
|
||||||
|
.filter { file -> !directoriesOnly || file.isDirectory }
|
||||||
|
.map { file ->
|
||||||
|
val relativePath = file.relativeTo(workingDir).path
|
||||||
|
if (file.isDirectory) "$relativePath/" else relativePath
|
||||||
|
}
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
fun commandReferenceLines(): List<String> {
|
fun commandReferenceLines(): List<String> {
|
||||||
return buildList {
|
return buildList {
|
||||||
addAll(GitSandboxEngine.commandReferenceLines())
|
addAll(GitSandboxEngine.commandReferenceLines())
|
||||||
@@ -390,6 +418,12 @@ class GitRepositoryRuntime private constructor(
|
|||||||
materializeNativePushLevel(nativeGit, sandbox)
|
materializeNativePushLevel(nativeGit, sandbox)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (materializeNativeLevelFixture(levelId, sandbox) { directory, arguments ->
|
||||||
|
runGit(nativeGit, directory, arguments).exitCode
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
desired.config.forEach { (key, value) ->
|
desired.config.forEach { (key, value) ->
|
||||||
runGit(nativeGit, sandbox, listOf("config", key, value))
|
runGit(nativeGit, sandbox, listOf("config", key, value))
|
||||||
@@ -466,8 +500,17 @@ class GitRepositoryRuntime private constructor(
|
|||||||
|
|
||||||
val remoteWorkTree = File(sandbox.parentFile ?: sandbox, "${sandbox.name}-origin")
|
val remoteWorkTree = File(sandbox.parentFile ?: sandbox, "${sandbox.name}-origin")
|
||||||
remoteWorkTree.deleteRecursively()
|
remoteWorkTree.deleteRecursively()
|
||||||
runGit(nativeGit, sandbox.parentFile ?: sandbox, listOf("clone", sandbox.absolutePath, remoteWorkTree.absolutePath))
|
remoteWorkTree.mkdirs()
|
||||||
|
val initResult = runGit(nativeGit, remoteWorkTree, listOf("init", "-b", "master"))
|
||||||
|
if (initResult.exitCode != 0) {
|
||||||
|
runGit(nativeGit, remoteWorkTree, listOf("init"))
|
||||||
|
runGit(nativeGit, remoteWorkTree, listOf("checkout", "-B", "master"))
|
||||||
|
}
|
||||||
runGit(nativeGit, remoteWorkTree, listOf("config", "receive.denyCurrentBranch", "ignore"))
|
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("remote", "remove", "push-setup-origin"))
|
||||||
|
runGit(nativeGit, remoteWorkTree, listOf("checkout", "-f", "master"))
|
||||||
File(remoteWorkTree, "file4").writeText("file4\n")
|
File(remoteWorkTree, "file4").writeText("file4\n")
|
||||||
commitIn(remoteWorkTree, "Fourth commit", "file4")
|
commitIn(remoteWorkTree, "Fourth commit", "file4")
|
||||||
|
|
||||||
@@ -580,9 +623,11 @@ class GitRepositoryRuntime private constructor(
|
|||||||
|
|
||||||
val logResult = runGit(nativeGit, sandbox, listOf("log", "--pretty=format:%h\t%s"))
|
val logResult = runGit(nativeGit, sandbox, listOf("log", "--pretty=format:%h\t%s"))
|
||||||
val branchResult = runGit(nativeGit, sandbox, listOf("branch", "--list"))
|
val branchResult = runGit(nativeGit, sandbox, listOf("branch", "--list"))
|
||||||
|
val remoteBranchResult = runGit(nativeGit, sandbox, listOf("branch", "-r", "--list"))
|
||||||
val tagResult = runGit(nativeGit, sandbox, listOf("tag", "--list"))
|
val tagResult = runGit(nativeGit, sandbox, listOf("tag", "--list"))
|
||||||
val remoteResult = runGit(nativeGit, sandbox, listOf("remote", "-v"))
|
val remoteResult = runGit(nativeGit, sandbox, listOf("remote", "-v"))
|
||||||
val headResult = runGit(nativeGit, sandbox, listOf("branch", "--show-current"))
|
val headResult = runGit(nativeGit, sandbox, listOf("branch", "--show-current"))
|
||||||
|
val exactTagResult = runGit(nativeGit, sandbox, listOf("describe", "--tags", "--exact-match"))
|
||||||
val userNameResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.name"))
|
val userNameResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.name"))
|
||||||
val userEmailResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.email"))
|
val userEmailResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.email"))
|
||||||
val config = buildMap {
|
val config = buildMap {
|
||||||
@@ -606,6 +651,23 @@ class GitRepositoryRuntime private constructor(
|
|||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val branches = branchResult.outputLines
|
||||||
|
.map { it.removePrefix("*").trim() }
|
||||||
|
.filter { it.isNotBlank() && !it.startsWith("(") }
|
||||||
|
.associateWith { branch ->
|
||||||
|
runGit(nativeGit, sandbox, listOf("rev-list", "--count", branch))
|
||||||
|
.outputLines
|
||||||
|
.firstOrNull()
|
||||||
|
?.toIntOrNull()
|
||||||
|
?: 0
|
||||||
|
}
|
||||||
|
val headBranch = headResult.outputLines.firstOrNull()
|
||||||
|
?.ifBlank { null }
|
||||||
|
?: exactTagResult.outputLines.firstOrNull()
|
||||||
|
?.takeIf { exactTagResult.exitCode == 0 && it.isNotBlank() }
|
||||||
|
?.let { "tags/$it" }
|
||||||
|
?: "DETACHED"
|
||||||
|
|
||||||
return RepoState(
|
return RepoState(
|
||||||
initialized = true,
|
initialized = true,
|
||||||
files = filesOnDisk.map { file ->
|
files = filesOnDisk.map { file ->
|
||||||
@@ -627,16 +689,20 @@ class GitRepositoryRuntime private constructor(
|
|||||||
tracked = tracked,
|
tracked = tracked,
|
||||||
deleted = true,
|
deleted = true,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
commits = commits,
|
commits = commits,
|
||||||
headBranch = headResult.outputLines.firstOrNull()?.ifBlank { null } ?: "master",
|
headBranch = headBranch,
|
||||||
branches = branchResult.outputLines.map { it.removePrefix("*").trim() }.filter { it.isNotBlank() }.associateWith { 0 },
|
branches = branches,
|
||||||
tags = tagResult.outputLines.filter { it.isNotBlank() },
|
tags = tagResult.outputLines.filter { it.isNotBlank() },
|
||||||
remotes = remoteResult.outputLines.mapNotNull { line ->
|
remotes = remoteResult.outputLines.mapNotNull { line ->
|
||||||
val parts = line.trim().split(Regex("\\s+"))
|
val parts = line.trim().split(Regex("\\s+"))
|
||||||
if (parts.size >= 2) parts[0] to parts[1] else null
|
if (parts.size >= 2) parts[0] to parts[1] else null
|
||||||
}.toMap(),
|
}.toMap(),
|
||||||
config = config,
|
config = config,
|
||||||
|
fetchedBranches = remoteBranchResult.outputLines
|
||||||
|
.map { it.removePrefix("*").trim() }
|
||||||
|
.filter { it.isNotBlank() && " -> " !in it }
|
||||||
|
.toSet(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,389 @@
|
|||||||
|
package solutions.tretter.githugandroid
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal fun materializeNativeLevelFixture(
|
||||||
|
levelId: String,
|
||||||
|
sandbox: File,
|
||||||
|
runGit: (File, List<String>) -> Int,
|
||||||
|
): Boolean {
|
||||||
|
val fixture = NativeLevelFixture(sandbox, runGit)
|
||||||
|
return when (levelId) {
|
||||||
|
"branch_at" -> fixture.branchAt()
|
||||||
|
"checkout_tag" -> fixture.checkoutTag()
|
||||||
|
"checkout_tag_over_branch" -> fixture.checkoutTagOverBranch()
|
||||||
|
"fetch" -> fixture.fetch()
|
||||||
|
"pull" -> fixture.pull()
|
||||||
|
"push_branch" -> fixture.pushBranch()
|
||||||
|
"push_tags" -> fixture.pushTags()
|
||||||
|
"merge" -> fixture.merge()
|
||||||
|
"rebase" -> fixture.rebase()
|
||||||
|
"rebase_onto" -> fixture.rebaseOnto()
|
||||||
|
"merge_squash" -> fixture.mergeSquash()
|
||||||
|
"reset" -> fixture.reset()
|
||||||
|
"reset_soft" -> fixture.resetSoft()
|
||||||
|
"restore" -> fixture.restore()
|
||||||
|
"squash" -> fixture.squash()
|
||||||
|
"reorder" -> fixture.reorder()
|
||||||
|
"rename_commit" -> fixture.renameCommit()
|
||||||
|
"revert" -> fixture.revert()
|
||||||
|
"stash" -> fixture.stash()
|
||||||
|
"checkout_file" -> fixture.checkoutFile()
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NativeLevelFixture(
|
||||||
|
private val sandbox: File,
|
||||||
|
private val runGit: (File, List<String>) -> Int,
|
||||||
|
) {
|
||||||
|
private fun resetFiles() {
|
||||||
|
sandbox.listFiles()
|
||||||
|
?.filterNot { it.name == ".git" }
|
||||||
|
?.forEach { it.deleteRecursively() }
|
||||||
|
git("checkout", "-B", "master")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun git(vararg arguments: String): Int = runGit(sandbox, arguments.toList())
|
||||||
|
|
||||||
|
private fun git(directory: File, vararg arguments: String): Int = runGit(directory, arguments.toList())
|
||||||
|
|
||||||
|
private fun initRepo(directory: File) {
|
||||||
|
directory.mkdirs()
|
||||||
|
val initResult = git(directory, "init", "-b", "master")
|
||||||
|
if (initResult != 0) {
|
||||||
|
git(directory, "init")
|
||||||
|
git(directory, "checkout", "-B", "master")
|
||||||
|
}
|
||||||
|
git(directory, "config", "receive.denyCurrentBranch", "ignore")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun write(path: String, content: String = "") {
|
||||||
|
File(sandbox, path).apply {
|
||||||
|
parentFile?.mkdirs()
|
||||||
|
writeText(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun append(path: String, content: String) {
|
||||||
|
File(sandbox, path).appendText(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun add(vararg paths: String) {
|
||||||
|
git("add", *paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun commit(message: String) {
|
||||||
|
git("commit", "-m", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addCommit(message: String, vararg paths: String) {
|
||||||
|
add(*paths)
|
||||||
|
commit(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeIn(directory: File, path: String, content: String = "") {
|
||||||
|
File(directory, path).apply {
|
||||||
|
parentFile?.mkdirs()
|
||||||
|
writeText(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addCommitIn(directory: File, message: String, vararg paths: String) {
|
||||||
|
git(directory, "add", *paths)
|
||||||
|
git(directory, "commit", "-m", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun siblingRepo(name: String): File {
|
||||||
|
val directory = File(sandbox.parentFile ?: sandbox, "${sandbox.name}-$name")
|
||||||
|
directory.deleteRecursively()
|
||||||
|
initRepo(directory)
|
||||||
|
return directory
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkoutNew(branch: String) {
|
||||||
|
git("checkout", "-b", branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkout(branch: String) {
|
||||||
|
git("checkout", branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tag(name: String) {
|
||||||
|
git("tag", "-f", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun branchAt(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("file1")
|
||||||
|
addCommit("Adding file1", "file1")
|
||||||
|
write("file1", "content")
|
||||||
|
addCommit("Updating file1", "file1")
|
||||||
|
append("file1", "\nAdding some more text")
|
||||||
|
addCommit("Updating file1 again", "file1")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkoutFile(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("config.rb", "This is the initial config file")
|
||||||
|
addCommit("Added initial config file", "config.rb")
|
||||||
|
append("config.rb", "\nThese are changes you don't want to keep!")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkoutTag(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("app.rb")
|
||||||
|
addCommit("Initial commit", "app.rb")
|
||||||
|
append("app.rb", "some changes\n")
|
||||||
|
addCommit("Some changes", "app.rb")
|
||||||
|
tag("v1.0")
|
||||||
|
append("app.rb", "some more changes\n")
|
||||||
|
addCommit("Some more changes", "app.rb")
|
||||||
|
tag("v1.2")
|
||||||
|
append("app.rb", "yet more changes\n")
|
||||||
|
addCommit("Yet more changes", "app.rb")
|
||||||
|
append("app.rb", "changes galore\n")
|
||||||
|
addCommit("Changes galore", "app.rb")
|
||||||
|
tag("v1.5")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkoutTagOverBranch(): Boolean {
|
||||||
|
checkoutTag()
|
||||||
|
checkoutNew("v1.2")
|
||||||
|
write("file3", "some feature\n")
|
||||||
|
addCommit("Developing new features", "file3")
|
||||||
|
checkout("master")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetch(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("master_file")
|
||||||
|
addCommit("Commits master_file", "master_file")
|
||||||
|
val remote = siblingRepo("origin")
|
||||||
|
git("remote", "add", "fetch-setup-origin", File(remote, ".git").absolutePath)
|
||||||
|
git("push", "fetch-setup-origin", "master")
|
||||||
|
git("remote", "remove", "fetch-setup-origin")
|
||||||
|
git(remote, "checkout", "-f", "master")
|
||||||
|
git(remote, "checkout", "-b", "new_branch")
|
||||||
|
writeIn(remote, "file1")
|
||||||
|
addCommitIn(remote, "Commits file 1", "file1")
|
||||||
|
git("remote", "add", "origin", File(remote, ".git").absolutePath)
|
||||||
|
git("branch", "--set-upstream-to=origin/master", "master")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pull(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("local_file")
|
||||||
|
addCommit("Initial local commit", "local_file")
|
||||||
|
val remote = siblingRepo("origin")
|
||||||
|
git("remote", "add", "pull-setup-origin", File(remote, ".git").absolutePath)
|
||||||
|
git("push", "pull-setup-origin", "master")
|
||||||
|
git("remote", "remove", "pull-setup-origin")
|
||||||
|
git(remote, "checkout", "-f", "master")
|
||||||
|
writeIn(remote, "remote_file", "pulled from origin\n")
|
||||||
|
addCommitIn(remote, "Pulled commit", "remote_file")
|
||||||
|
git("remote", "add", "origin", File(remote, ".git").absolutePath)
|
||||||
|
git("config", "branch.master.remote", "origin")
|
||||||
|
git("config", "branch.master.merge", "refs/heads/master")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pushBranch(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("file1")
|
||||||
|
addCommit("committed changes on master", "file1")
|
||||||
|
val remote = siblingRepo("origin")
|
||||||
|
git("remote", "add", "push-branch-setup-origin", File(remote, ".git").absolutePath)
|
||||||
|
git("push", "push-branch-setup-origin", "master")
|
||||||
|
git("remote", "remove", "push-branch-setup-origin")
|
||||||
|
git(remote, "checkout", "-f", "master")
|
||||||
|
write("file2")
|
||||||
|
addCommit("If this commit gets pushed to repo, then you have lost the level :( ", "file2")
|
||||||
|
checkoutNew("other_branch")
|
||||||
|
write("file3")
|
||||||
|
addCommit("If this commit gets pushed to repo, then you have lost the level :( ", "file3")
|
||||||
|
checkoutNew("test_branch")
|
||||||
|
write("file4")
|
||||||
|
addCommit("committed change on test_branch", "file4")
|
||||||
|
git("remote", "add", "origin", File(remote, ".git").absolutePath)
|
||||||
|
git("branch", "--set-upstream-to=origin/master", "master")
|
||||||
|
checkout("master")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pushTags(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("file1")
|
||||||
|
addCommit("First commit", "file1")
|
||||||
|
tag("tag_to_be_pushed")
|
||||||
|
write("file2")
|
||||||
|
addCommit("Second commit", "file2")
|
||||||
|
val remote = siblingRepo("origin")
|
||||||
|
git("remote", "add", "push-tags-setup-origin", File(remote, ".git").absolutePath)
|
||||||
|
git("push", "push-tags-setup-origin", "master")
|
||||||
|
git("remote", "remove", "push-tags-setup-origin")
|
||||||
|
git(remote, "checkout", "-f", "master")
|
||||||
|
git("remote", "add", "origin", File(remote, ".git").absolutePath)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun merge(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("file1")
|
||||||
|
addCommit("added file1", "file1")
|
||||||
|
checkoutNew("feature")
|
||||||
|
write("file2")
|
||||||
|
addCommit("added file2", "file2")
|
||||||
|
checkout("master")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rebase(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("README", "readme\n")
|
||||||
|
addCommit("init commit", "README")
|
||||||
|
checkoutNew("feature")
|
||||||
|
write("feature", "feature\n")
|
||||||
|
addCommit("add feature", "feature")
|
||||||
|
checkout("master")
|
||||||
|
append("README", "content\n")
|
||||||
|
addCommit("add content", "README")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rebaseOnto(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("authors.md", "https://github.com/janis-vitols\n")
|
||||||
|
addCommit("Create authors file", "authors.md")
|
||||||
|
checkoutNew("wrong_branch")
|
||||||
|
write("authors.md", "None\n")
|
||||||
|
addCommit("Wrong changes", "authors.md")
|
||||||
|
checkoutNew("readme-update")
|
||||||
|
write("README.md", "# SuperApp\n")
|
||||||
|
addCommit("Add app name in readme", "README.md")
|
||||||
|
append("README.md", "## About\n")
|
||||||
|
addCommit("Add `About` header in readme", "README.md")
|
||||||
|
append("README.md", "## Install\n")
|
||||||
|
addCommit("Add `Install` header in readme", "README.md")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mergeSquash(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("file1")
|
||||||
|
addCommit("First commit", "file1")
|
||||||
|
checkoutNew("long-feature-branch")
|
||||||
|
write("file3", "some feature\n")
|
||||||
|
addCommit("Developing new features", "file3")
|
||||||
|
append("file3", "getting awesomer\n")
|
||||||
|
addCommit("Takes", "file3")
|
||||||
|
append("file3", "and awesomer!\n")
|
||||||
|
addCommit("Time", "file3")
|
||||||
|
checkout("master")
|
||||||
|
write("file2")
|
||||||
|
addCommit("Second commit", "file2")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("README")
|
||||||
|
addCommit("Initial commit", "README")
|
||||||
|
write("to_commit_first.rb")
|
||||||
|
write("to_commit_second.rb")
|
||||||
|
add("to_commit_first.rb", "to_commit_second.rb")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetSoft(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("README")
|
||||||
|
addCommit("Initial commit", "README")
|
||||||
|
write("newfile.rb")
|
||||||
|
addCommit("Premature commit", "newfile.rb")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restore(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("file1")
|
||||||
|
addCommit("Initial commit", "file1")
|
||||||
|
write("file2")
|
||||||
|
addCommit("First commit", "file2")
|
||||||
|
write("file3")
|
||||||
|
addCommit("Restore this commit", "file3")
|
||||||
|
git("reset", "--hard", "HEAD^")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun squash(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write(".hidden")
|
||||||
|
addCommit("Initial Commit", ".hidden")
|
||||||
|
write("README")
|
||||||
|
addCommit("Adding README", "README")
|
||||||
|
write("README", "hey there")
|
||||||
|
addCommit("Updating README (squash this commit into Adding README)", "README")
|
||||||
|
append("README", "\nAdding some more text")
|
||||||
|
addCommit("Updating README (squash this commit into Adding README)", "README")
|
||||||
|
append("README", "\neven more text")
|
||||||
|
addCommit("Updating README (squash this commit into Adding README)", "README")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reorder(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("README")
|
||||||
|
addCommit("Initial Setup", "README")
|
||||||
|
write("file1")
|
||||||
|
addCommit("First commit", "file1")
|
||||||
|
write("file3")
|
||||||
|
addCommit("Third commit", "file3")
|
||||||
|
write("file2")
|
||||||
|
addCommit("Second commit", "file2")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renameCommit(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("README")
|
||||||
|
addCommit("Initial commit", "README")
|
||||||
|
write("file1")
|
||||||
|
addCommit("First coommit", "file1")
|
||||||
|
write("file2")
|
||||||
|
addCommit("Second commit", "file2")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun revert(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write("file1")
|
||||||
|
addCommit("First commit", "file1")
|
||||||
|
write("file3")
|
||||||
|
addCommit("Bad commit", "file3")
|
||||||
|
write("file2")
|
||||||
|
addCommit("Second commit", "file2")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stash(): Boolean {
|
||||||
|
resetFiles()
|
||||||
|
write(
|
||||||
|
"lyrics.txt",
|
||||||
|
"""
|
||||||
|
Down in Louisiana in that sunny clime,
|
||||||
|
They play a class of music that is super fine,
|
||||||
|
And it makes no difference if its rain or shine,
|
||||||
|
You can hear that that jazz band music playing all the time.
|
||||||
|
""".trimIndent() + "\n",
|
||||||
|
)
|
||||||
|
addCommit("Add some lyrics", "lyrics.txt")
|
||||||
|
append("lyrics.txt", "\nHey!\n")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,9 +37,12 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
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.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.layout.boundsInRoot
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
@@ -69,6 +72,7 @@ fun TerminalPane(
|
|||||||
onCursorRight: () -> Unit,
|
onCursorRight: () -> Unit,
|
||||||
onHistoryUp: () -> Unit,
|
onHistoryUp: () -> Unit,
|
||||||
onHistoryDown: () -> Unit,
|
onHistoryDown: () -> Unit,
|
||||||
|
onPromptBoundsChanged: (Rect) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
@@ -161,6 +165,9 @@ fun TerminalPane(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.widthIn(min = terminalMinWidth)
|
.widthIn(min = terminalMinWidth)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
onPromptBoundsChanged(coordinates.boundsInRoot())
|
||||||
|
}
|
||||||
.background(PanelPrimary, RoundedCornerShape(8.dp))
|
.background(PanelPrimary, RoundedCornerShape(8.dp))
|
||||||
.padding(horizontal = 8.dp, vertical = 3.dp),
|
.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ internal fun branchAtLevel(): Level = level(
|
|||||||
hints = listOf("Just like creating a branch, but you have to pass an extra argument."),
|
hints = listOf("Just like creating a branch, but you have to pass an extra argument."),
|
||||||
commandSuggestions = listOf("git branch test_branch HEAD~1"),
|
commandSuggestions = listOf("git branch test_branch HEAD~1"),
|
||||||
setup = { RepoState(initialized = true, commits = listOf(CommitNode("1", "Adding file1"), CommitNode("2", "Updating file1"), CommitNode("3", "Updating file1 again")), branches = mapOf("master" to 3)) },
|
setup = { RepoState(initialized = true, commits = listOf(CommitNode("1", "Adding file1"), CommitNode("2", "Updating file1"), CommitNode("3", "Updating file1 again")), branches = mapOf("master" to 3)) },
|
||||||
validator = repoPredicate { repo -> "test_branch" in repo.branches },
|
validator = repoPredicate { repo -> repo.branches["test_branch"] == 2 },
|
||||||
testCases = listOf(
|
testCases = listOf(
|
||||||
levelTestCase("branch at previous commit", "git branch test_branch HEAD~1"),
|
levelTestCase("branch at previous commit", "git branch test_branch HEAD~1"),
|
||||||
levelTestCase("branch at caret", "git branch test_branch HEAD^"),
|
levelTestCase("branch at caret", "git branch test_branch HEAD^"),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ internal fun fetchLevel(): Level = level(
|
|||||||
hints = listOf("Look up the 'git fetch' command"),
|
hints = listOf("Look up the 'git fetch' command"),
|
||||||
commandSuggestions = listOf("git fetch origin"),
|
commandSuggestions = listOf("git fetch origin"),
|
||||||
setup = { RepoState(initialized = true, branches = mapOf("master" to 1), remotes = mapOf("origin" to "remote")) },
|
setup = { RepoState(initialized = true, branches = mapOf("master" to 1), remotes = mapOf("origin" to "remote")) },
|
||||||
validator = repoPredicate { repo -> "origin/feature_branch" in repo.fetchedBranches && repo.headBranch == "master" },
|
validator = repoPredicate { repo -> "origin/new_branch" in repo.fetchedBranches && repo.headBranch == "master" },
|
||||||
testCases = listOf(
|
testCases = listOf(
|
||||||
levelTestCase("fetch origin", "git fetch origin"),
|
levelTestCase("fetch origin", "git fetch origin"),
|
||||||
levelTestCase("fetch default", "git fetch"),
|
levelTestCase("fetch default", "git fetch"),
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ internal fun pullLevel(): Level = level(
|
|||||||
hints = listOf("Check out the remote repositories and research `git pull`."),
|
hints = listOf("Check out the remote repositories and research `git pull`."),
|
||||||
commandSuggestions = listOf("git pull origin master"),
|
commandSuggestions = listOf("git pull origin master"),
|
||||||
setup = { RepoState(initialized = true, remotes = mapOf("origin" to "remote"), branches = mapOf("master" to 1)) },
|
setup = { RepoState(initialized = true, remotes = mapOf("origin" to "remote"), branches = mapOf("master" to 1)) },
|
||||||
validator = repoPredicate { repo -> "origin/master" in repo.fetchedBranches },
|
validator = repoPredicate { repo ->
|
||||||
|
"origin/master" in repo.fetchedBranches &&
|
||||||
|
repo.files.any { it.name == "remote_file" && it.tracked } &&
|
||||||
|
repo.commits.size >= 2
|
||||||
|
},
|
||||||
testCases = listOf(
|
testCases = listOf(
|
||||||
levelTestCase("pull explicit remote branch", "git pull origin master"),
|
levelTestCase("pull explicit remote branch", "git pull origin master"),
|
||||||
levelTestCase("pull default origin", "git pull"),
|
levelTestCase("pull default origin", "git pull"),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ internal fun pushBranchLevel(): Level = level(
|
|||||||
description = "You've made some changes to a local branch and want to share it, but aren't yet ready to merge it with the 'master' branch. Push only 'test_branch' to the remote repository",
|
description = "You've made some changes to a local branch and want to share it, but aren't yet ready to merge it with the 'master' branch. Push only 'test_branch' to the remote repository",
|
||||||
hints = listOf("Investigate the options in `git push` using `git push --help`"),
|
hints = listOf("Investigate the options in `git push` using `git push --help`"),
|
||||||
commandSuggestions = listOf("git push origin test_branch"),
|
commandSuggestions = listOf("git push origin test_branch"),
|
||||||
setup = { RepoState(initialized = true, branches = mapOf("master" to 2, "other_branch" to 3, "test_branch" to 4), remotes = mapOf("origin" to "remote"), headBranch = "test_branch") },
|
setup = { RepoState(initialized = true, branches = mapOf("master" to 2, "other_branch" to 3, "test_branch" to 4), remotes = mapOf("origin" to "remote"), headBranch = "master") },
|
||||||
validator = repoPredicate { repo -> "origin/test_branch" in repo.pushedBranches && "origin/master" !in repo.pushedBranches && "origin/other_branch" !in repo.pushedBranches },
|
validator = repoPredicate { repo -> "origin/test_branch" in repo.pushedBranches && "origin/master" !in repo.pushedBranches && "origin/other_branch" !in repo.pushedBranches },
|
||||||
testCases = listOf(
|
testCases = listOf(
|
||||||
levelTestCase("push named branch", "git push origin test_branch"),
|
levelTestCase("push named branch", "git push origin test_branch"),
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ internal fun rebaseLevel(): Level = level(
|
|||||||
description = "We are using a git rebase workflow and the feature branch is ready to go into master. Let's rebase the feature branch onto our master branch.",
|
description = "We are using a git rebase workflow and the feature branch is ready to go into master. Let's rebase the feature branch onto our master branch.",
|
||||||
hints = listOf("You want to research the `git rebase` command"),
|
hints = listOf("You want to research the `git rebase` command"),
|
||||||
commandSuggestions = listOf("git checkout feature", "git rebase master"),
|
commandSuggestions = listOf("git checkout feature", "git rebase master"),
|
||||||
setup = { RepoState(initialized = true, headBranch = "feature", branches = mapOf("master" to 2, "feature" to 3)) },
|
setup = { RepoState(initialized = true, headBranch = "master", branches = mapOf("master" to 2, "feature" to 2)) },
|
||||||
validator = repoPredicate { repo -> repo.headBranch == "feature" && (repo.branches["feature"] ?: 0) >= (repo.branches["master"] ?: 0) },
|
validator = repoPredicate { repo ->
|
||||||
|
repo.headBranch == "feature" &&
|
||||||
|
repo.commits.take(3).map { it.message } == listOf("add feature", "add content", "init commit")
|
||||||
|
},
|
||||||
testCases = listOf(
|
testCases = listOf(
|
||||||
levelTestCase("rebase feature on master", "git rebase master"),
|
levelTestCase("checkout feature then rebase", "git checkout feature", "git rebase master"),
|
||||||
levelTestCase("checkout then rebase", "git checkout feature", "git rebase master"),
|
levelTestCase("rebase named branch", "git rebase master feature"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package solutions.tretter.githugandroid
|
|||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Assume.assumeTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
class GitSandboxEngineTest {
|
class GitSandboxEngineTest {
|
||||||
@Test
|
@Test
|
||||||
@@ -147,6 +150,31 @@ class GitSandboxEngineTest {
|
|||||||
assertEquals(listOf("model/"), directoryCompletionCandidates(repo))
|
assertEquals(listOf("model/"), directoryCompletionCandidates(repo))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nativeCompletionUsesCurrentDirectoryIncludingGitMetadata() {
|
||||||
|
val git = File("/usr/bin/git")
|
||||||
|
assumeTrue(git.exists() && git.canExecute())
|
||||||
|
val root = Files.createTempDirectory("githug-completion").toFile()
|
||||||
|
try {
|
||||||
|
val runtime = GitRepositoryRuntime(root, git)
|
||||||
|
val level = level(
|
||||||
|
id = "completion-test",
|
||||||
|
title = "Completion Test",
|
||||||
|
description = "",
|
||||||
|
hints = emptyList(),
|
||||||
|
commandSuggestions = emptyList(),
|
||||||
|
setup = { RepoState(initialized = true) },
|
||||||
|
validator = { _, _ -> false },
|
||||||
|
)
|
||||||
|
val repo = runtime.prepareLevel(level)
|
||||||
|
val (gitDirRepo, _) = runtime.execute(level, repo, "cd .git")
|
||||||
|
|
||||||
|
assertTrue("HEAD" in runtime.completionCandidates(level, gitDirRepo, directoriesOnly = false))
|
||||||
|
} 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