diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 03360dc..5475c85 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,8 +19,8 @@ android { applicationId = "solutions.tretter.githugandroid" minSdk = 26 targetSdk = 35 - versionCode = 95 - versionName = "0.1.94" + versionCode = 96 + versionName = "0.1.95" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt b/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt index 741a454..e4ec455 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt @@ -43,10 +43,7 @@ class GitRepositoryRuntime(private val context: Context) { runGit(nativeGit, sandbox, listOf("checkout", "-B", desired.headBranch)) } - val stageTargets = desired.files.filter { it.staged || it.tracked }.map { it.name } - if (stageTargets.isNotEmpty()) { - runGit(nativeGit, sandbox, listOf("add") + stageTargets) - } + materializeNativeGitState(nativeGit, sandbox, desired) } return inspectSandbox(level) @@ -146,17 +143,85 @@ class GitRepositoryRuntime(private val context: Context) { currentRepo to emptyList() } } - "echo" -> currentRepo to listOf(tokens.drop(1).joinToString(" ")) + "echo" -> executeEcho(workingDir, currentRepo, tokens) else -> currentRepo to listOf("Command not supported in prototype runtime. Try a git command or helper command.") } } + private fun materializeNativeGitState(nativeGit: File, sandbox: File, desired: RepoState) { + desired.config.forEach { (key, value) -> + runGit(nativeGit, sandbox, listOf("config", key, value)) + } + + val trackedFiles = desired.files.filter { it.tracked } + val desiredCommitCount = maxOf( + desired.commits.size, + desired.branches.values.maxOrNull() ?: 0, + if (desired.tags.isNotEmpty()) 1 else 0, + ) + if (desiredCommitCount > 0) { + repeat(desiredCommitCount) { index -> + filesForSetupCommit(trackedFiles, index, desiredCommitCount).forEach { file -> + runGit(nativeGit, sandbox, listOf("add", file.name)) + } + val message = desired.commits.getOrNull(index)?.message ?: "Setup commit ${index + 1}" + runGit(nativeGit, sandbox, listOf("commit", "--allow-empty", "-m", message)) + } + + desired.branches + .keys + .filterNot { it == desired.headBranch } + .forEach { branch -> runGit(nativeGit, sandbox, listOf("branch", branch)) } + if (desired.branches.containsKey(desired.headBranch)) { + runGit(nativeGit, sandbox, listOf("checkout", desired.headBranch)) + } + } + + desired.tags.forEach { tag -> runGit(nativeGit, sandbox, listOf("tag", tag)) } + desired.remotes.forEach { (name, url) -> runGit(nativeGit, sandbox, listOf("remote", "add", name, url)) } + + val stagedTargets = desired.files.filter { it.staged }.map { it.name } + if (stagedTargets.isNotEmpty()) { + runGit(nativeGit, sandbox, listOf("add") + stagedTargets) + } + } + + private fun filesForSetupCommit(files: List, index: Int, commitCount: Int): List { + if (files.isEmpty()) return emptyList() + if (commitCount <= 1) return files + if (index == 0) return files.take(1) + if (index == commitCount - 1) return files.drop(index) + return files.getOrNull(index)?.let { listOf(it) }.orEmpty() + } + + private fun executeEcho(workingDir: File, currentRepo: RepoState, tokens: List): Pair> { + val redirectIndex = tokens.indexOfFirst { it == ">" || it == ">>" } + if (redirectIndex == -1 || redirectIndex == tokens.lastIndex) { + return currentRepo to listOf(tokens.drop(1).joinToString(" ")) + } + + val file = File(workingDir, tokens[redirectIndex + 1]) + file.parentFile?.mkdirs() + val content = tokens.subList(1, redirectIndex).joinToString(" ") + if (tokens[redirectIndex] == ">>") { + if (file.exists() && file.length() > 0L) { + file.appendText("\n$content") + } else { + file.writeText(content) + } + } else { + file.writeText(content) + } + return currentRepo to emptyList() + } + private fun inspectSandbox(level: Level): RepoState { val sandbox = sandboxDir(level) val nativeGit = nativeGitBinary() - val filesOnDisk = sandbox.listFiles() - ?.filter { it.isFile && it.name != ".git" } + val filesOnDisk = sandbox.walkTopDown() + .filter { it.isFile && !it.relativeTo(sandbox).path.startsWith(".git/") } .orEmpty() + .toList() if (nativeGit == null || !File(sandbox, ".git").exists()) { return RepoState( @@ -208,9 +273,10 @@ class GitRepositoryRuntime(private val context: Context) { return RepoState( initialized = true, files = filesOnDisk.map { file -> - val (staged, tracked) = statusMap[file.name] ?: (false to true) + val relativePath = file.relativeTo(sandbox).path + val (staged, tracked) = statusMap[relativePath] ?: (false to true) GitFile( - name = file.name, + name = relativePath, content = file.readText(), staged = staged, tracked = tracked, @@ -272,4 +338,4 @@ class GitRepositoryRuntime(private val context: Context) { private data class ProcessExecutionResult( val exitCode: Int, val outputLines: List, -) \ No newline at end of file +) diff --git a/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt b/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt index d27a51b..d2c77ab 100644 --- a/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt +++ b/app/src/test/java/solutions/tretter/githugandroid/LevelSolutionsTest.kt @@ -25,7 +25,8 @@ class LevelSolutionsTest { fun knownSolutionsCompleteEveryLevel() { val evidence = StringBuilder() evidence.appendLine("GitHug Android level solution evidence") - evidence.appendLine("Engine: GitSandboxEngine") + evidence.appendLine("Engine: GitSandboxEngine fallback") + evidence.appendLine("Scope: validates fallback command handling and level validators; native runtime setup is covered by app runtime tests/builds.") evidence.appendLine("Levels: ${allGithugLevels().size}") evidence.appendLine()