package solutions.tretter.githugandroid import android.os.Build import android.content.Context import java.io.File class GitRepositoryRuntime private constructor( private val context: Context?, private val nativeGitOverride: File?, private val sandboxesRoot: File, ) { private val processRunner = GitProcessRunner(context, sandboxesRoot) private val helperCommands = GitHelperCommands(processRunner) private val levelMaterializer = GitLevelMaterializer(::runGit) constructor(context: Context) : this( context = context.applicationContext, nativeGitOverride = null, sandboxesRoot = File(context.applicationContext.filesDir, "githug-sandboxes"), ) internal constructor(sandboxesRoot: File, nativeGitBinary: File) : this( context = null, nativeGitOverride = nativeGitBinary, sandboxesRoot = sandboxesRoot, ) fun isNativeGitAvailable(): Boolean = nativeGitBinary() != null fun unavailableMessage(): String { val selectedAbi = Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown" val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir ?: "unknown" return buildString { append("GitHug Android cannot start because this build does not include a native Git binary for this device ABI.") append("\n\nCurrent device ABI: ") append(selectedAbi) append("\nSupported device ABIs: ") append(Build.SUPPORTED_ABIS.joinToString(", ").ifBlank { "unknown" }) append("\nSupported 64-bit ABIs: ") append(Build.SUPPORTED_64_BIT_ABIS.joinToString(", ").ifBlank { "none" }) append("\nSupported 32-bit ABIs: ") append(Build.SUPPORTED_32_BIT_ABIS.joinToString(", ").ifBlank { "none" }) append("\nPlatform: Android ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})") append("\nDevice: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE}; ${Build.HARDWARE})") append("\nExpected binary: $nativeLibraryDir/libgit.so") append("\n\nPlease report this information to the developer so support can be added for this device/platform.") append("\nInstall a build that bundles the cross-compiled Git binary for this device.") } } fun startupBanner(): String { requireNativeGit() return "Welcome to GitHug Android. Native Git ready." } fun prepareLevel(level: Level): RepoState { AppLog.d("GitRuntime", "Preparing level id=${level.id} title=${level.title}") val nativeGit = requireNativeGit() 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)) } levelMaterializer.materialize(nativeGit, sandbox, desired, level) } return inspectSandbox(level) } fun execute(level: Level, currentRepo: RepoState, command: String): Pair> { AppLog.d("GitRuntime", "Executing command for level=${level.id}: $command") val nativeGit = requireNativeGit() val sandbox = sandboxDir(level) if (!sandbox.exists()) { prepareLevel(level) } val sandboxRoot = sandbox.canonicalFile val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile .takeIf { it.path.startsWith(sandboxRoot.path) } ?: sandboxRoot val shellTokens = GitSandboxEngine.tokenizeShellCommand(command) val invocation = parseEnvironmentPrefixedCommand(shellTokens) val tokens = invocation.command.map { it.value } if (tokens.isEmpty()) return inspectSandbox(level).copy(currentDir = currentRepo.currentDir) to emptyList() if (currentRepo.interactiveAddSession != null) { val (updatedRepo, output) = GitSandboxEngine.execute(currentRepo, command) val newlyStagedPaths = updatedRepo.files.filter { updatedFile -> updatedFile.staged && currentRepo.files.firstOrNull { it.name == updatedFile.name }?.staged != true }.map { it.name } if (newlyStagedPaths.isNotEmpty()) { runGit(nativeGit, workingDir, listOf("add") + newlyStagedPaths) } val inspectedRepo = inspectSandbox(level).copy( currentDir = updatedRepo.currentDir, interactiveAddSession = updatedRepo.interactiveAddSession, ) return augmentObservedRepoFacts(updatedRepo, inspectedRepo, tokens) to output } val expandedTokens = if (tokens.firstOrNull() == "echo") { tokens } else { expandShellPathspecs(currentRepo, invocation.command) } .normalizeGitStageAlias() .normalizeGitBisectRunScriptShortcut() executeSyntheticGitCommand(currentRepo, command, expandedTokens, invocation.environment)?.let { return it } val result = when (expandedTokens.first()) { "git" -> currentRepo to runGit(nativeGit, workingDir, expandedTokens.drop(1), invocation.environment).outputLines "help", "?" -> currentRepo to commandReferenceLines() else -> helperCommands.executeExecutableShortcut(sandboxRoot, workingDir, currentRepo, expandedTokens) ?: helperCommands.execute(sandboxRoot, workingDir, currentRepo, expandedTokens) } val inspectedRepo = inspectSandbox(level).copy(currentDir = result.first.currentDir) return augmentObservedRepoFacts(currentRepo, inspectedRepo, expandedTokens, result.second) to result.second } fun completionCandidates(level: Level, currentRepo: RepoState, directoriesOnly: Boolean): List { requireNativeGit() val sandbox = sandboxDir(level) if (!sandbox.exists()) return emptyList() val sandboxRoot = sandbox.canonicalFile val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile .takeIf { it.path == sandboxRoot.path || it.path.startsWith(sandboxRoot.path + File.separator) } ?: sandboxRoot if (!workingDir.exists() || !workingDir.isDirectory) return emptyList() val relativeCandidates = workingDir.walkTopDown() .drop(1) .filter { file -> !directoriesOnly || file.isDirectory } .map { file -> val relativePath = file.relativeTo(workingDir).path if (file.isDirectory) "$relativePath/" else relativePath } .filter { it.isNotBlank() } .toList() return relativeCandidates + relativeCandidates.map { "./$it" } } fun commandReferenceLines(): List { return buildList { addAll(GitSandboxEngine.commandReferenceLines()) add("Native Git runtime:") add(" binary path: nativeLibraryDir/libgit.so") add(" selected ABI: ${Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"}") add(" helper commands: ls/dir, pwd, cat, sh