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:
23
README.md
23
README.md
@@ -9,7 +9,7 @@ This repository contains a starter Android app with:
|
|||||||
- Kotlin + Jetpack Compose project scaffolding
|
- Kotlin + Jetpack Compose project scaffolding
|
||||||
- CLI-first gameplay shell
|
- CLI-first gameplay shell
|
||||||
- Optional hideable visualization panel
|
- Optional hideable visualization panel
|
||||||
- In-memory Git sandbox engine for early commands
|
- A native-Git runtime scaffold with in-memory fallback while no bundled Git binary is present
|
||||||
- First playable GitHug-inspired levels: `init`, `add`, and `commit`
|
- First playable GitHug-inspired levels: `init`, `add`, and `commit`
|
||||||
|
|
||||||
## Product direction
|
## Product direction
|
||||||
@@ -75,9 +75,28 @@ It will also automatically bump the Android app version in `app/build.gradle.kts
|
|||||||
- incrementing `versionCode` by 1
|
- incrementing `versionCode` by 1
|
||||||
- incrementing the patch component of `versionName` (for example `0.1.0` → `0.1.1`)
|
- incrementing the patch component of `versionName` (for example `0.1.0` → `0.1.1`)
|
||||||
|
|
||||||
|
## Native Git prototype status
|
||||||
|
|
||||||
|
The app now includes a **filesystem-backed runtime scaffold** for moving from the custom Kotlin Git emulator toward a real native Git backend.
|
||||||
|
|
||||||
|
Current prototype behavior:
|
||||||
|
|
||||||
|
- the app prepares per-level sandbox directories in app-private storage
|
||||||
|
- terminal commands are routed through a runtime abstraction instead of directly calling the old in-memory engine
|
||||||
|
- if a bundled native Git binary is available at runtime, Git commands are executed against a real repository sandbox
|
||||||
|
- if no bundled native Git binary is present yet, the app automatically falls back to the existing in-memory sandbox so development can continue
|
||||||
|
|
||||||
|
Planned native Git packaging path:
|
||||||
|
|
||||||
|
- cross-compile Git for Android ABIs with the NDK
|
||||||
|
- bundle one binary payload per ABI
|
||||||
|
- extract the correct executable into app-private storage on first launch
|
||||||
|
- keep helper shell-like commands (`ls`, `pwd`, `cat`, `touch`, `mkdir`, `rm`, `echo`) implemented in Kotlin
|
||||||
|
|
||||||
## Next steps toward full GitHug parity
|
## Next steps toward full GitHug parity
|
||||||
|
|
||||||
- Expand the Git sandbox to cover merge, rebase, stash, tags, remotes, reset, revert, cherry-pick, bisect, and more
|
- Replace the fallback in-memory Git engine with a packaged native Git binary
|
||||||
|
- Expand real repository-backed level validation beyond `init`, `add`, and `commit`
|
||||||
- Port all original levels and hints into structured content files
|
- Port all original levels and hints into structured content files
|
||||||
- Add richer validation rules and per-level explanations
|
- Add richer validation rules and per-level explanations
|
||||||
- Add onboarding, accessibility polish, icons, tests, and Play Store assets
|
- Add onboarding, accessibility polish, icons, tests, and Play Store assets
|
||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "com.kawomi.githugandroid"
|
applicationId = "com.kawomi.githugandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 7
|
versionCode = 8
|
||||||
versionName = "0.1.6"
|
versionName = "0.1.7"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -164,6 +164,51 @@ object GitSandboxEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun tokenizeCommand(command: String): List<String> {
|
||||||
|
val result = mutableListOf<String>()
|
||||||
|
val current = StringBuilder()
|
||||||
|
var quoteChar: Char? = null
|
||||||
|
var escaping = false
|
||||||
|
|
||||||
|
command.forEach { char ->
|
||||||
|
when {
|
||||||
|
escaping -> {
|
||||||
|
current.append(char)
|
||||||
|
escaping = false
|
||||||
|
}
|
||||||
|
char == '\\' && quoteChar != '\'' -> {
|
||||||
|
escaping = true
|
||||||
|
}
|
||||||
|
quoteChar != null -> {
|
||||||
|
if (char == quoteChar) {
|
||||||
|
quoteChar = null
|
||||||
|
} else {
|
||||||
|
current.append(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char == '"' || char == '\'' -> {
|
||||||
|
quoteChar = char
|
||||||
|
}
|
||||||
|
char.isWhitespace() -> {
|
||||||
|
if (current.isNotEmpty()) {
|
||||||
|
result += current.toString()
|
||||||
|
current.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> current.append(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (escaping) {
|
||||||
|
current.append('\\')
|
||||||
|
}
|
||||||
|
if (current.isNotEmpty()) {
|
||||||
|
result += current.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
private fun commit(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
|
private fun commit(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
|
||||||
val parsed = parseCommitArguments(arguments)
|
val parsed = parseCommitArguments(arguments)
|
||||||
if (parsed.error != null) {
|
if (parsed.error != null) {
|
||||||
@@ -269,51 +314,6 @@ object GitSandboxEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tokenizeCommand(command: String): List<String> {
|
|
||||||
val result = mutableListOf<String>()
|
|
||||||
val current = StringBuilder()
|
|
||||||
var quoteChar: Char? = null
|
|
||||||
var escaping = false
|
|
||||||
|
|
||||||
command.forEach { char ->
|
|
||||||
when {
|
|
||||||
escaping -> {
|
|
||||||
current.append(char)
|
|
||||||
escaping = false
|
|
||||||
}
|
|
||||||
char == '\\' && quoteChar != '\'' -> {
|
|
||||||
escaping = true
|
|
||||||
}
|
|
||||||
quoteChar != null -> {
|
|
||||||
if (char == quoteChar) {
|
|
||||||
quoteChar = null
|
|
||||||
} else {
|
|
||||||
current.append(char)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
char == '"' || char == '\'' -> {
|
|
||||||
quoteChar = char
|
|
||||||
}
|
|
||||||
char.isWhitespace() -> {
|
|
||||||
if (current.isNotEmpty()) {
|
|
||||||
result += current.toString()
|
|
||||||
current.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> current.append(char)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (escaping) {
|
|
||||||
current.append('\\')
|
|
||||||
}
|
|
||||||
if (current.isNotEmpty()) {
|
|
||||||
result += current.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class ParsedCommitArguments(
|
private data class ParsedCommitArguments(
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val stageAllTracked: Boolean = false,
|
val stageAllTracked: Boolean = false,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.TextRange
|
||||||
@@ -73,15 +74,17 @@ private val GitHugColorScheme = darkColorScheme(
|
|||||||
@Composable
|
@Composable
|
||||||
fun GitHugApp() {
|
fun GitHugApp() {
|
||||||
MaterialTheme(colorScheme = GitHugColorScheme) {
|
MaterialTheme(colorScheme = GitHugColorScheme) {
|
||||||
|
val context = LocalContext.current
|
||||||
val levels = remember { sampleLevels() }
|
val levels = remember { sampleLevels() }
|
||||||
|
val runtime = remember(context) { GitRepositoryRuntime(context.applicationContext) }
|
||||||
val screenScrollState = rememberScrollState()
|
val screenScrollState = rememberScrollState()
|
||||||
var currentLevelIndex by remember { mutableStateOf(0) }
|
var currentLevelIndex by remember { mutableStateOf(0) }
|
||||||
var mode by remember { mutableStateOf(PlayMode.CLI_ONLY) }
|
var mode by remember { mutableStateOf(PlayMode.CLI_ONLY) }
|
||||||
var repo by remember { mutableStateOf(levels.first().setup()) }
|
var repo by remember { mutableStateOf(runtime.prepareLevel(levels.first())) }
|
||||||
var commandInput by remember { mutableStateOf(TextFieldValue("")) }
|
var commandInput by remember { mutableStateOf(TextFieldValue("")) }
|
||||||
var inputFieldVersion by remember { mutableStateOf(0) }
|
var inputFieldVersion by remember { mutableStateOf(0) }
|
||||||
var suppressedImeEcho by remember { mutableStateOf<String?>(null) }
|
var suppressedImeEcho by remember { mutableStateOf<String?>(null) }
|
||||||
var output by remember { mutableStateOf(listOf("Welcome to GitHug Android.")) }
|
var output by remember { mutableStateOf(listOf(runtime.startupBanner())) }
|
||||||
var hintIndex by remember { mutableStateOf(0) }
|
var hintIndex by remember { mutableStateOf(0) }
|
||||||
var completedLevels by remember { mutableStateOf(setOf<String>()) }
|
var completedLevels by remember { mutableStateOf(setOf<String>()) }
|
||||||
var commandHistory by remember { mutableStateOf(listOf<String>()) }
|
var commandHistory by remember { mutableStateOf(listOf<String>()) }
|
||||||
@@ -101,7 +104,7 @@ fun GitHugApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun resetCurrentLevel(message: String = "Level reset.") {
|
fun resetCurrentLevel(message: String = "Level reset.") {
|
||||||
repo = currentLevel.setup()
|
repo = runtime.prepareLevel(currentLevel)
|
||||||
clearCommandInput()
|
clearCommandInput()
|
||||||
suppressedImeEcho = null
|
suppressedImeEcho = null
|
||||||
output = listOf(message)
|
output = listOf(message)
|
||||||
@@ -176,7 +179,7 @@ fun GitHugApp() {
|
|||||||
historyIndex = -1
|
historyIndex = -1
|
||||||
historyDraft = ""
|
historyDraft = ""
|
||||||
|
|
||||||
val (newRepo, lines) = GitSandboxEngine.execute(repo, raw)
|
val (newRepo, lines) = runtime.execute(currentLevel, repo, raw)
|
||||||
val solvedAfterCommand = currentLevel.validator(newRepo)
|
val solvedAfterCommand = currentLevel.validator(newRepo)
|
||||||
val wasAlreadyCompleted = currentLevel.id in completedLevels
|
val wasAlreadyCompleted = currentLevel.id in completedLevels
|
||||||
val newOutput = buildList {
|
val newOutput = buildList {
|
||||||
@@ -194,7 +197,7 @@ fun GitHugApp() {
|
|||||||
val nextLevelIndex = currentLevelIndex + 1
|
val nextLevelIndex = currentLevelIndex + 1
|
||||||
val nextLevel = levels[nextLevelIndex]
|
val nextLevel = levels[nextLevelIndex]
|
||||||
currentLevelIndex = nextLevelIndex
|
currentLevelIndex = nextLevelIndex
|
||||||
repo = nextLevel.setup()
|
repo = runtime.prepareLevel(nextLevel)
|
||||||
output = listOf("Loaded level: ${nextLevel.title}")
|
output = listOf("Loaded level: ${nextLevel.title}")
|
||||||
clearCommandInput()
|
clearCommandInput()
|
||||||
suppressedImeEcho = null
|
suppressedImeEcho = null
|
||||||
@@ -212,7 +215,7 @@ fun GitHugApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun showCommandHelp() {
|
fun showCommandHelp() {
|
||||||
output = output + GitSandboxEngine.commandReferenceLines()
|
output = output + runtime.commandReferenceLines()
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold { padding ->
|
Scaffold { padding ->
|
||||||
@@ -231,7 +234,7 @@ fun GitHugApp() {
|
|||||||
) {
|
) {
|
||||||
Header(levels, currentLevelIndex, completedLevels) { index ->
|
Header(levels, currentLevelIndex, completedLevels) { index ->
|
||||||
currentLevelIndex = index
|
currentLevelIndex = index
|
||||||
repo = levels[index].setup()
|
repo = runtime.prepareLevel(levels[index])
|
||||||
output = listOf("Loaded level: ${levels[index].title}")
|
output = listOf("Loaded level: ${levels[index].title}")
|
||||||
clearCommandInput()
|
clearCommandInput()
|
||||||
suppressedImeEcho = null
|
suppressedImeEcho = null
|
||||||
|
|||||||
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