package com.kawomi.githugandroid import android.content.Context import java.io.File class GitRepositoryRuntime(private val context: Context) { private val sandboxesRoot = File(context.filesDir, "githug-sandboxes") fun startupBanner(): String { return if (nativeGitBinary() != null) { "Welcome to GitHug Android. Native Git prototype ready." } else { "Welcome to GitHug Android. Native Git prototype scaffolded; using in-memory fallback until a bundled Git binary is available." } } fun prepareLevel(level: Level): RepoState { val nativeGit = nativeGitBinary() if (nativeGit == null) { return level.setup() } val sandbox = sandboxDir(level) sandbox.deleteRecursively() sandbox.mkdirs() val desired = level.setup() desired.files.forEach { file -> File(sandbox, file.name).apply { parentFile?.mkdirs() writeText(file.content) } } val needsGit = desired.initialized || desired.files.any { it.staged || it.tracked } || desired.commits.isNotEmpty() if (needsGit) { val initResult = runGit(nativeGit, sandbox, listOf("init", "-b", desired.headBranch)) if (initResult.exitCode != 0) { runGit(nativeGit, sandbox, listOf("init")) 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) } } return inspectSandbox(level) } fun execute(level: Level, currentRepo: RepoState, command: String): Pair> { val nativeGit = nativeGitBinary() if (nativeGit == null) { return GitSandboxEngine.execute(currentRepo, command) } val sandbox = sandboxDir(level) if (!sandbox.exists()) { prepareLevel(level) } val tokens = GitSandboxEngine.tokenizeCommand(command) if (tokens.isEmpty()) return inspectSandbox(level) to emptyList() val output = when (tokens.first()) { "git" -> runGit(nativeGit, sandbox, tokens.drop(1)).outputLines "help", "?" -> commandReferenceLines() else -> executeHelperCommand(sandbox, tokens) } return inspectSandbox(level) to output } fun commandReferenceLines(): List { return buildList { addAll(GitSandboxEngine.commandReferenceLines()) add("Native Git prototype:") add(" bundled binary path: files/native-git/bin/git") add(" helper commands: ls, pwd, cat, touch, mkdir, rm, echo") } } private fun executeHelperCommand(workingDir: File, tokens: List): List { return when (tokens.first()) { "ls" -> workingDir.listFiles() ?.filterNot { it.name == ".git" } ?.sortedBy { it.name } ?.map { it.name } .orEmpty() "pwd" -> listOf("/sandbox/${workingDir.name}") "cat" -> { val target = tokens.getOrNull(1) ?: return listOf("usage: cat ") val file = File(workingDir, target) if (!file.exists() || file.isDirectory) listOf("cat: $target: No such file") else file.readLines().ifEmpty { listOf("") } } "touch" -> { val target = tokens.getOrNull(1) ?: return listOf("usage: touch ") val file = File(workingDir, target) if (file.exists()) { listOf("$target already exists") } else { file.parentFile?.mkdirs() file.writeText("") emptyList() } } "mkdir" -> { val target = tokens.getOrNull(1) ?: return listOf("usage: mkdir ") val dir = File(workingDir, target) if (dir.exists()) listOf("mkdir: $target: File exists") else { dir.mkdirs() emptyList() } } "rm" -> { val target = tokens.getOrNull(1) ?: return listOf("usage: rm ") val file = File(workingDir, target) if (!file.exists()) listOf("rm: $target: No such file or directory") else { file.deleteRecursively() emptyList() } } "echo" -> listOf(tokens.drop(1).joinToString(" ")) else -> listOf("Command not supported in prototype runtime. Try a git command or helper command.") } } private fun inspectSandbox(level: Level): RepoState { val sandbox = sandboxDir(level) val nativeGit = nativeGitBinary() val filesOnDisk = sandbox.listFiles() ?.filter { it.isFile && it.name != ".git" } .orEmpty() if (nativeGit == null || !File(sandbox, ".git").exists()) { return RepoState( initialized = File(sandbox, ".git").exists(), files = filesOnDisk.map { GitFile(name = it.name, content = it.readText()) }, ) } val statusResult = runGit(nativeGit, sandbox, listOf("status", "--porcelain")) val statusMap = mutableMapOf>() statusResult.outputLines.forEach { line -> if (line.length < 4) return@forEach val x = line[0] val y = line[1] val path = line.substring(3).trim() val staged = x != ' ' && x != '?' val tracked = x != '?' || y != '?' statusMap[path] = staged to tracked } val logResult = runGit(nativeGit, sandbox, listOf("log", "--pretty=format:%h\t%s")) val branchResult = runGit(nativeGit, sandbox, listOf("branch", "--list")) val headResult = runGit(nativeGit, sandbox, listOf("branch", "--show-current")) return RepoState( initialized = true, files = filesOnDisk.map { file -> val (staged, tracked) = statusMap[file.name] ?: (false to true) GitFile( name = file.name, content = file.readText(), staged = staged, tracked = tracked, ) }, commits = logResult.outputLines.filter { it.isNotBlank() }.mapNotNull { line -> val parts = line.split('\t', limit = 2) if (parts.isEmpty()) null else CommitNode(parts[0], parts.getOrElse(1) { "" }) }, headBranch = headResult.outputLines.firstOrNull()?.ifBlank { null } ?: "master", branches = branchResult.outputLines.map { it.removePrefix("*").trim() }.filter { it.isNotBlank() }.associateWith { 0 }, ) } private fun nativeGitBinary(): File? { val candidates = listOf( File(context.filesDir, "native-git/bin/git"), File(context.filesDir, "git/bin/git"), ) return candidates.firstOrNull { it.exists() && it.canExecute() } } private fun sandboxDir(level: Level): File = File(sandboxesRoot, level.id) private fun runGit(binary: File, workingDir: File, arguments: List): ProcessExecutionResult { return runProcess(binary, workingDir, arguments) } private fun runProcess(binary: File, workingDir: File, arguments: List): ProcessExecutionResult { return try { val process = ProcessBuilder(listOf(binary.absolutePath) + arguments) .directory(workingDir) .redirectErrorStream(true) .apply { environment()["HOME"] = workingDir.absolutePath environment()["GIT_CONFIG_NOSYSTEM"] = "1" environment()["GIT_AUTHOR_NAME"] = "GitHug" environment()["GIT_AUTHOR_EMAIL"] = "githug@example.com" environment()["GIT_COMMITTER_NAME"] = "GitHug" environment()["GIT_COMMITTER_EMAIL"] = "githug@example.com" environment()["LC_ALL"] = "C" } .start() val output = process.inputStream.bufferedReader().readLines() val exit = process.waitFor() ProcessExecutionResult(exitCode = exit, outputLines = output.ifEmpty { if (exit == 0) emptyList() else listOf("Command failed") }) } catch (error: Exception) { ProcessExecutionResult(exitCode = -1, outputLines = listOf("Native Git execution failed: ${error.message ?: error::class.java.simpleName}")) } } } private data class ProcessExecutionResult( val exitCode: Int, val outputLines: List, )