Require native Git runtime and bundle full manpages
- Remove the app fallback path when native Git is unavailable and show a startup blocker instead - Fix native level setup for staged file counting and cherry-pick parity - Improve manpage loading/search and bundle full Git documentation assets - Integrate Git cross-compilation into AndroidProjectTooling.sh and remove debug AAB support
This commit is contained in:
@@ -55,20 +55,26 @@ class GitRepositoryRuntime private constructor(
|
||||
sandboxesRoot = sandboxesRoot,
|
||||
)
|
||||
|
||||
fun startupBanner(): String {
|
||||
return if (nativeGitBinary() != null) {
|
||||
"Welcome to GitHug Android. Native Git prototype ready."
|
||||
} else {
|
||||
"Welcome to GitHug Android. Native Git binary not bundled for this ABI yet; using in-memory fallback."
|
||||
fun isNativeGitAvailable(): Boolean = nativeGitBinary() != null
|
||||
|
||||
fun unavailableMessage(): String {
|
||||
return buildString {
|
||||
append("GitHug Android cannot start because this build does not include a native Git binary for this device ABI.")
|
||||
append("\n\nSupported device ABIs: ")
|
||||
append(Build.SUPPORTED_ABIS.joinToString(", ").ifBlank { "unknown" })
|
||||
append("\nExpected binary: nativeLibraryDir/libgit.so")
|
||||
append("\n\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 = nativeGitBinary()
|
||||
if (nativeGit == null) {
|
||||
return level.setup()
|
||||
}
|
||||
val nativeGit = requireNativeGit()
|
||||
|
||||
val sandbox = sandboxDir(level)
|
||||
sandbox.deleteRecursively()
|
||||
@@ -98,10 +104,7 @@ class GitRepositoryRuntime private constructor(
|
||||
|
||||
fun execute(level: Level, currentRepo: RepoState, command: String): Pair<RepoState, List<String>> {
|
||||
AppLog.d("GitRuntime", "Executing command for level=${level.id}: $command")
|
||||
val nativeGit = nativeGitBinary()
|
||||
if (nativeGit == null) {
|
||||
return GitSandboxEngine.execute(currentRepo, command)
|
||||
}
|
||||
val nativeGit = requireNativeGit()
|
||||
|
||||
val sandbox = sandboxDir(level)
|
||||
if (!sandbox.exists()) {
|
||||
@@ -119,7 +122,9 @@ class GitRepositoryRuntime private constructor(
|
||||
tokens
|
||||
} else {
|
||||
expandShellPathspecs(currentRepo, shellTokens)
|
||||
}
|
||||
}.normalizeGitStageAlias()
|
||||
|
||||
rejectUnsupportedInteractiveGitCommand(currentRepo, expandedTokens)?.let { return it }
|
||||
|
||||
executeSyntheticGitCommand(currentRepo, command, expandedTokens)?.let { return it }
|
||||
|
||||
@@ -134,15 +139,9 @@ class GitRepositoryRuntime private constructor(
|
||||
}
|
||||
|
||||
fun completionCandidates(level: Level, currentRepo: RepoState, directoriesOnly: Boolean): List<String> {
|
||||
val nativeGit = nativeGitBinary()
|
||||
requireNativeGit()
|
||||
val sandbox = sandboxDir(level)
|
||||
if (nativeGit == null || !sandbox.exists()) {
|
||||
return if (directoriesOnly) {
|
||||
directoryCompletionCandidates(currentRepo)
|
||||
} else {
|
||||
fileCompletionCandidates(currentRepo)
|
||||
}
|
||||
}
|
||||
if (!sandbox.exists()) return emptyList()
|
||||
|
||||
val sandboxRoot = sandbox.canonicalFile
|
||||
val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
|
||||
@@ -164,7 +163,7 @@ class GitRepositoryRuntime private constructor(
|
||||
fun commandReferenceLines(): List<String> {
|
||||
return buildList {
|
||||
addAll(GitSandboxEngine.commandReferenceLines())
|
||||
add("Native Git prototype:")
|
||||
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, touch, mkdir/md, cd.., rm/del, echo")
|
||||
@@ -174,14 +173,9 @@ class GitRepositoryRuntime private constructor(
|
||||
}
|
||||
|
||||
fun gitManPage(level: Level, currentRepo: RepoState, topic: String): String {
|
||||
if (topic == "ignore") {
|
||||
return fallbackManPage(topic)
|
||||
}
|
||||
bundledManPage(topic)?.let { return it }
|
||||
|
||||
val nativeGit = nativeGitBinary()
|
||||
if (nativeGit == null) {
|
||||
return fallbackManPage(topic)
|
||||
}
|
||||
val nativeGit = requireNativeGit()
|
||||
|
||||
val sandboxRoot = sandboxDir(level).canonicalFile
|
||||
val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
|
||||
@@ -190,14 +184,11 @@ class GitRepositoryRuntime private constructor(
|
||||
return result.outputLines
|
||||
.filterNot { it.contains("no man viewer", ignoreCase = true) }
|
||||
.joinToString("\n")
|
||||
.ifBlank { fallbackManPage(topic) }
|
||||
.ifBlank { placeholderManPage(topic) }
|
||||
}
|
||||
|
||||
fun readEditorFile(level: Level, currentRepo: RepoState, path: String): Pair<String, List<String>> {
|
||||
val nativeGit = nativeGitBinary()
|
||||
if (nativeGit == null) {
|
||||
return currentRepo.files.find { it.name == path }?.content.orEmpty() to emptyList()
|
||||
}
|
||||
requireNativeGit()
|
||||
|
||||
val sandboxRoot = sandboxDir(level).canonicalFile
|
||||
val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
|
||||
@@ -214,17 +205,7 @@ class GitRepositoryRuntime private constructor(
|
||||
}
|
||||
|
||||
fun writeEditorFile(level: Level, currentRepo: RepoState, path: String, content: String): Pair<RepoState, List<String>> {
|
||||
val nativeGit = nativeGitBinary()
|
||||
if (nativeGit == null) {
|
||||
val updatedFiles = currentRepo.files.toMutableList()
|
||||
val index = updatedFiles.indexOfFirst { it.name == path }
|
||||
if (index == -1) {
|
||||
updatedFiles += GitFile(name = path, content = content)
|
||||
} else {
|
||||
updatedFiles[index] = updatedFiles[index].copy(content = content)
|
||||
}
|
||||
return currentRepo.copy(files = updatedFiles) to listOf("Saved $path")
|
||||
}
|
||||
requireNativeGit()
|
||||
|
||||
val sandboxRoot = sandboxDir(level).canonicalFile
|
||||
val workingDir = File(sandboxRoot, currentRepo.currentDir).canonicalFile
|
||||
@@ -248,10 +229,7 @@ class GitRepositoryRuntime private constructor(
|
||||
invocation: GitEditorInvocation,
|
||||
message: String,
|
||||
): Pair<RepoState, List<String>> {
|
||||
val nativeGit = nativeGitBinary()
|
||||
if (nativeGit == null) {
|
||||
return GitSandboxEngine.execute(currentRepo, invocation.command.withFallbackMessage(invocation.kind, message))
|
||||
}
|
||||
val nativeGit = requireNativeGit()
|
||||
|
||||
val sandboxRoot = sandboxDir(level).canonicalFile
|
||||
if (!sandboxRoot.exists()) {
|
||||
@@ -341,15 +319,8 @@ class GitRepositoryRuntime private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.withFallbackMessage(kind: GitEditorCommandKind, message: String): String {
|
||||
val escaped = message.replace("\\", "\\\\").replace("\"", "\\\"").lineSequence().firstOrNull().orEmpty()
|
||||
return when (kind) {
|
||||
GitEditorCommandKind.COMMIT_MESSAGE -> "$this -m \"$escaped\""
|
||||
GitEditorCommandKind.TAG_MESSAGE -> "$this -m \"$escaped\""
|
||||
}
|
||||
}
|
||||
|
||||
private fun fallbackManPage(topic: String): String {
|
||||
private fun placeholderManPage(topic: String): String {
|
||||
bundledManPage(topic)?.let { return it }
|
||||
val body = when (topic) {
|
||||
"tag" -> """
|
||||
NAME
|
||||
@@ -436,13 +407,43 @@ class GitRepositoryRuntime private constructor(
|
||||
git-$topic
|
||||
|
||||
DESCRIPTION
|
||||
No bundled fallback manpage is available for git $topic.
|
||||
No bundled manpage is available for git $topic.
|
||||
Try the command suggestions or use git help for another command.
|
||||
""".trimIndent()
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
private fun bundledManPage(topic: String): String? {
|
||||
val assetNames = listOf(
|
||||
"manpages/git-$topic.txt",
|
||||
"manpages/git${topic.removePrefix("-")}.txt",
|
||||
"manpages/$topic.txt",
|
||||
).distinct()
|
||||
|
||||
if (context != null) {
|
||||
assetNames.forEach { assetName ->
|
||||
try {
|
||||
context.assets.open(assetName).bufferedReader().use { reader ->
|
||||
return reader.readText().takeIf { it.isNotBlank() }
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Try the next bundled filename.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val repoAssetRoot = File("src/main/assets")
|
||||
assetNames.forEach { assetName ->
|
||||
val file = File(repoAssetRoot, assetName)
|
||||
if (file.exists()) {
|
||||
return file.readText().takeIf { it.isNotBlank() }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun materializeNativeGitState(nativeGit: File, sandbox: File, desired: RepoState, level: Level) {
|
||||
level.nativeSetup?.let { setup ->
|
||||
val nativeSetup = NativeLevelSetup(sandbox) { directory, arguments ->
|
||||
@@ -540,7 +541,7 @@ class GitRepositoryRuntime private constructor(
|
||||
"add" -> tokens.any { it == "-p" || it == "--patch" }
|
||||
"rebase" -> "-i" in tokens || "--onto" in tokens
|
||||
"merge" -> "--squash" in tokens || tokens.lastOrNull() == "mybranch" || tokens.lastOrNull() == "feature"
|
||||
"cherry-pick", "revert", "stash" -> true
|
||||
"revert", "stash" -> true
|
||||
"checkout" -> tokens.any { it == "file3" } || tokens.takeLast(2) == listOf("--", "config.rb")
|
||||
"submodule" -> tokens.getOrNull(2) == "add"
|
||||
"commit" -> "merge-squash" in currentRepo.maintenanceActions || tokens.any { it == "--date" || it.startsWith("--date=") }
|
||||
@@ -552,6 +553,27 @@ class GitRepositoryRuntime private constructor(
|
||||
return updatedRepo to output
|
||||
}
|
||||
|
||||
private fun List<String>.normalizeGitStageAlias(): List<String> {
|
||||
return if (size >= 2 && this[0] == "git" && this[1] == "stage") {
|
||||
toMutableList().also { it[1] = "add" }
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private fun rejectUnsupportedInteractiveGitCommand(
|
||||
currentRepo: RepoState,
|
||||
tokens: List<String>,
|
||||
): Pair<RepoState, List<String>>? {
|
||||
if (tokens.firstOrNull() != "git") return null
|
||||
val subcommand = tokens.getOrNull(1) ?: return null
|
||||
val interactive = tokens.drop(2).any { it == "-i" || it == "--interactive" }
|
||||
if (subcommand == "add" && interactive) {
|
||||
return currentRepo to listOf("Interactive staging is not supported in the mobile terminal. Use git add <path> or git add -p <path>.")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun executeEcho(workingDir: File, currentRepo: RepoState, tokens: List<String>): Pair<RepoState, List<String>> {
|
||||
val redirectIndex = tokens.indexOfFirst { it == ">" || it == ">>" }
|
||||
if (redirectIndex == -1 || redirectIndex == tokens.lastIndex) {
|
||||
@@ -575,13 +597,13 @@ class GitRepositoryRuntime private constructor(
|
||||
|
||||
private fun inspectSandbox(level: Level): RepoState {
|
||||
val sandbox = sandboxDir(level)
|
||||
val nativeGit = nativeGitBinary()
|
||||
val nativeGit = requireNativeGit()
|
||||
val filesOnDisk = sandbox.walkTopDown()
|
||||
.filter { it.isFile && !it.relativeTo(sandbox).path.startsWith(".git/") }
|
||||
.orEmpty()
|
||||
.toList()
|
||||
|
||||
if (nativeGit == null || !File(sandbox, ".git").exists()) {
|
||||
if (!File(sandbox, ".git").exists()) {
|
||||
return RepoState(
|
||||
initialized = File(sandbox, ".git").exists(),
|
||||
files = filesOnDisk.map { GitFile(name = it.name, content = it.readText()) },
|
||||
@@ -690,7 +712,13 @@ class GitRepositoryRuntime private constructor(
|
||||
}
|
||||
|
||||
private fun nativeGitBinary(): File? {
|
||||
return nativeGitOverride ?: packagedNativeGitBinary()
|
||||
return nativeGitOverride
|
||||
?.takeIf { it.exists() && it.canExecute() }
|
||||
?: packagedNativeGitBinary()
|
||||
}
|
||||
|
||||
private fun requireNativeGit(): File {
|
||||
return nativeGitBinary() ?: error(unavailableMessage())
|
||||
}
|
||||
|
||||
private fun packagedNativeGitBinary(): File? {
|
||||
|
||||
Reference in New Issue
Block a user