Auto-commit after successful build: update app gameplay/UI, improve build setup, update docs/config
Changed files:\nREADME.md app/build.gradle.kts app/src/main/java/com/kawomi/githugandroid/GameModels.kt app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt app/src/main/java/com/kawomi/githugandroid/GitRuntime.kt
This commit is contained in:
222
app/src/main/java/com/kawomi/githugandroid/GitRuntime.kt
Normal file
222
app/src/main/java/com/kawomi/githugandroid/GitRuntime.kt
Normal file
@@ -0,0 +1,222 @@
|
||||
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<RepoState, List<String>> {
|
||||
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<String> {
|
||||
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<String>): List<String> {
|
||||
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 <file>")
|
||||
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 <file>")
|
||||
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 <dir>")
|
||||
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 <path>")
|
||||
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<String, Pair<Boolean, Boolean>>()
|
||||
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<String>): ProcessExecutionResult {
|
||||
return runProcess(binary, workingDir, arguments)
|
||||
}
|
||||
|
||||
private fun runProcess(binary: File, workingDir: File, arguments: List<String>): 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<String>,
|
||||
)
|
||||
Reference in New Issue
Block a user