Split oversized runtime, sandbox, and interactive add files into focused helpers

This commit is contained in:
Joe Tretter
2026-05-18 21:49:43 -05:00
parent ed0b81e587
commit bea901acfe
13 changed files with 1743 additions and 1622 deletions

View File

@@ -0,0 +1,183 @@
package solutions.tretter.githugandroid
import android.content.Context
import android.system.Os
import java.io.File
import java.nio.file.Files
internal class GitProcessRunner(
private val context: Context?,
private val sandboxesRoot: File,
) {
private companion object {
val RequiredGitCommandAliases = listOf(
"add",
"branch",
"checkout",
"commit",
"commit-tree",
"config",
"diff",
"fetch",
"index-pack",
"merge",
"merge-base",
"pack-objects",
"pull",
"push",
"read-tree",
"rebase",
"receive-pack",
"rev-list",
"rev-parse",
"show-ref",
"stash",
"status",
"symbolic-ref",
"unpack-objects",
"update-ref",
"upload-pack",
)
}
fun shellExecutable(): String {
return when {
context != null -> "/system/bin/sh"
File("/bin/sh").exists() -> "/bin/sh"
else -> "sh"
}
}
fun runGit(
binary: File,
workingDir: File,
arguments: List<String>,
environment: Map<String, String> = emptyMap(),
): ProcessExecutionResult {
return runProcess(binary, workingDir, arguments, environment)
}
fun runShellProcess(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()["LC_ALL"] = "C"
}
.start()
val output = process.inputStream.bufferedReader().readLines()
val exit = process.waitFor()
ProcessExecutionResult(exitCode = exit, outputLines = output)
} catch (error: Exception) {
ProcessExecutionResult(exitCode = -1, outputLines = listOf("Shell execution failed: ${error.message ?: error::class.java.simpleName}"))
}
}
private fun runProcess(
binary: File,
workingDir: File,
arguments: List<String>,
extraEnvironment: Map<String, String> = emptyMap(),
): ProcessExecutionResult {
return try {
val gitExecPath = gitExecDirectory(binary)
val process = ProcessBuilder(listOf(binary.absolutePath) + arguments)
.directory(workingDir)
.redirectErrorStream(true)
.apply {
environment()["HOME"] = workingDir.absolutePath
environment()["GIT_EXEC_PATH"] = gitExecPath.absolutePath
environment()["PATH"] = listOfNotNull(
gitExecPath.absolutePath,
System.getenv("PATH")?.takeIf { it.isNotBlank() },
).joinToString(File.pathSeparator)
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"
environment().putAll(extraEnvironment)
}
.start()
val output = process.inputStream.bufferedReader().readLines()
val exit = process.waitFor()
ProcessExecutionResult(exitCode = exit, outputLines = output)
} catch (error: Exception) {
ProcessExecutionResult(exitCode = -1, outputLines = listOf("Native Git execution failed: ${error.message ?: error::class.java.simpleName}"))
}
}
private fun gitExecDirectory(binary: File): File {
val directory = if (context != null) {
File(context.filesDir, "git-exec")
} else {
File(binary.parentFile ?: binary.absoluteFile.parentFile ?: sandboxesRoot, "git-exec")
}
refreshGitExecDirectoryIfNeeded(directory, binary)
directory.mkdirs()
File(directory, "git").also { alias ->
if (!alias.exists()) {
createGitAlias(binary, alias)
}
}
RequiredGitCommandAliases.forEach { command ->
listOf("git-$command", command).forEach { aliasName ->
val alias = File(directory, aliasName)
if (!alias.exists()) {
createGitAlias(binary, alias)
}
}
}
return directory
}
private fun refreshGitExecDirectoryIfNeeded(directory: File, binary: File) {
val fingerprint = buildString {
append(binary.absolutePath)
append('\n')
append(binary.length())
append('\n')
append(binary.lastModified())
}
val stamp = File(directory, ".binary-fingerprint")
val previousFingerprint = stamp.takeIf { it.isFile }?.readText()
if (previousFingerprint != fingerprint) {
directory.deleteRecursively()
}
directory.mkdirs()
stamp.writeText(fingerprint)
}
private fun createGitAlias(binary: File, alias: File) {
if (alias.exists() || Files.isSymbolicLink(alias.toPath())) {
alias.delete()
}
if (context != null) {
try {
Os.symlink(binary.absolutePath, alias.absolutePath)
alias.setExecutable(true, false)
return
} catch (_: Exception) {
// Fall through to the portable options below.
}
}
try {
Files.createSymbolicLink(alias.toPath(), binary.toPath())
} catch (_: Exception) {
binary.copyTo(alias, overwrite = true)
}
alias.setExecutable(true, false)
}
}
internal data class ProcessExecutionResult(
val exitCode: Int,
val outputLines: List<String>,
)