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:
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@@ -39,15 +40,18 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -91,6 +95,7 @@ fun GitHugApp() {
|
||||
var showHelpOverlay by remember { mutableStateOf(false) }
|
||||
var showHelpOnStart by remember { mutableStateOf(true) }
|
||||
var helpPreferenceInitialized by remember { mutableStateOf(false) }
|
||||
var promptBounds by remember { mutableStateOf<Rect?>(null) }
|
||||
val currentLevel = levels[currentLevelIndex]
|
||||
|
||||
LaunchedEffect(solvedCelebrationTitle) {
|
||||
@@ -293,7 +298,7 @@ fun GitHugApp() {
|
||||
val isCdCompletion = leadingCommand == "cd"
|
||||
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) }
|
||||
if (matches.isEmpty()) return
|
||||
|
||||
@@ -572,6 +577,7 @@ fun GitHugApp() {
|
||||
onCursorRight = { moveCursor(1) },
|
||||
onHistoryUp = { historyUp() },
|
||||
onHistoryDown = { historyDown() },
|
||||
onPromptBoundsChanged = { promptBounds = it },
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -585,6 +591,7 @@ fun GitHugApp() {
|
||||
scope.launch { gameProgressStore.saveShowHelpOnStart(showHelpOnStart) }
|
||||
},
|
||||
onClose = { showHelpOverlay = false },
|
||||
promptBounds = promptBounds,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -663,7 +670,9 @@ private fun HelpCalloutOverlay(
|
||||
onShowHelpOnStartChange: (Boolean) -> Unit,
|
||||
onOk: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
promptBounds: Rect?,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -674,9 +683,12 @@ private fun HelpCalloutOverlay(
|
||||
modifier = Modifier.align(Alignment.TopStart),
|
||||
)
|
||||
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)
|
||||
.padding(bottom = 54.dp),
|
||||
.padding(bottom = 96.dp),
|
||||
text = "Tap the prompt to enter a command.",
|
||||
showHelpOnStart = showHelpOnStart,
|
||||
onShowHelpOnStartChange = onShowHelpOnStartChange,
|
||||
|
||||
@@ -103,6 +103,34 @@ class GitRepositoryRuntime private constructor(
|
||||
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> {
|
||||
return buildList {
|
||||
addAll(GitSandboxEngine.commandReferenceLines())
|
||||
@@ -390,6 +418,12 @@ class GitRepositoryRuntime private constructor(
|
||||
materializeNativePushLevel(nativeGit, sandbox)
|
||||
return
|
||||
}
|
||||
if (materializeNativeLevelFixture(levelId, sandbox) { directory, arguments ->
|
||||
runGit(nativeGit, directory, arguments).exitCode
|
||||
}
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
desired.config.forEach { (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")
|
||||
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, 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")
|
||||
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 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 remoteResult = runGit(nativeGit, sandbox, listOf("remote", "-v"))
|
||||
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 userEmailResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.email"))
|
||||
val config = buildMap {
|
||||
@@ -606,6 +651,23 @@ class GitRepositoryRuntime private constructor(
|
||||
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(
|
||||
initialized = true,
|
||||
files = filesOnDisk.map { file ->
|
||||
@@ -627,16 +689,20 @@ class GitRepositoryRuntime private constructor(
|
||||
tracked = tracked,
|
||||
deleted = true,
|
||||
)
|
||||
},
|
||||
},
|
||||
commits = commits,
|
||||
headBranch = headResult.outputLines.firstOrNull()?.ifBlank { null } ?: "master",
|
||||
branches = branchResult.outputLines.map { it.removePrefix("*").trim() }.filter { it.isNotBlank() }.associateWith { 0 },
|
||||
headBranch = headBranch,
|
||||
branches = branches,
|
||||
tags = tagResult.outputLines.filter { it.isNotBlank() },
|
||||
remotes = remoteResult.outputLines.mapNotNull { line ->
|
||||
val parts = line.trim().split(Regex("\\s+"))
|
||||
if (parts.size >= 2) parts[0] to parts[1] else null
|
||||
}.toMap(),
|
||||
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.onFocusChanged
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
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.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
@@ -69,6 +72,7 @@ fun TerminalPane(
|
||||
onCursorRight: () -> Unit,
|
||||
onHistoryUp: () -> Unit,
|
||||
onHistoryDown: () -> Unit,
|
||||
onPromptBoundsChanged: (Rect) -> Unit = {},
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@@ -161,6 +165,9 @@ fun TerminalPane(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.widthIn(min = terminalMinWidth)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
onPromptBoundsChanged(coordinates.boundsInRoot())
|
||||
}
|
||||
.background(PanelPrimary, RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||
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."),
|
||||
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)) },
|
||||
validator = repoPredicate { repo -> "test_branch" in repo.branches },
|
||||
validator = repoPredicate { repo -> repo.branches["test_branch"] == 2 },
|
||||
testCases = listOf(
|
||||
levelTestCase("branch at previous commit", "git branch test_branch HEAD~1"),
|
||||
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"),
|
||||
commandSuggestions = listOf("git fetch origin"),
|
||||
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(
|
||||
levelTestCase("fetch origin", "git fetch origin"),
|
||||
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`."),
|
||||
commandSuggestions = listOf("git pull origin master"),
|
||||
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(
|
||||
levelTestCase("pull explicit remote branch", "git pull origin master"),
|
||||
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",
|
||||
hints = listOf("Investigate the options in `git push` using `git push --help`"),
|
||||
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 },
|
||||
testCases = listOf(
|
||||
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.",
|
||||
hints = listOf("You want to research the `git rebase` command"),
|
||||
commandSuggestions = listOf("git checkout feature", "git rebase master"),
|
||||
setup = { RepoState(initialized = true, headBranch = "feature", branches = mapOf("master" to 2, "feature" to 3)) },
|
||||
validator = repoPredicate { repo -> repo.headBranch == "feature" && (repo.branches["feature"] ?: 0) >= (repo.branches["master"] ?: 0) },
|
||||
setup = { RepoState(initialized = true, headBranch = "master", branches = mapOf("master" to 2, "feature" to 2)) },
|
||||
validator = repoPredicate { repo ->
|
||||
repo.headBranch == "feature" &&
|
||||
repo.commits.take(3).map { it.message } == listOf("add feature", "add content", "init commit")
|
||||
},
|
||||
testCases = listOf(
|
||||
levelTestCase("rebase feature on master", "git rebase master"),
|
||||
levelTestCase("checkout then rebase", "git checkout feature", "git rebase master"),
|
||||
levelTestCase("checkout feature 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.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
class GitSandboxEngineTest {
|
||||
@Test
|
||||
@@ -147,6 +150,31 @@ class GitSandboxEngineTest {
|
||||
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
|
||||
fun cdDotDotShortcutMovesToParentDirectory() {
|
||||
val repo = RepoState(initialized = true, currentDir = "src/main")
|
||||
|
||||
Reference in New Issue
Block a user