Align tests with runtime sandbox and polish terminal help
- Run level solution scenarios through GitRepositoryRuntime with a real filesystem sandbox and git binary so evidence logs match app behavior. - Materialize synthetic remotes as local bare repositories and configure upstream tracking for pull/push levels. - Route mobile-hostile Git interactions through shared sandbox semantics for clone, patch add, interactive rebase, squash merge, submodule, stash, revert, and related flows. - Relax affected level validators to inspect observable runtime state instead of fallback-only staged/index details. - Show .git in ls output for native and fallback helper commands. - Make tab completion resolve filenames and directories relative to the current working directory. - Integrate help controls into the callout overlay and reposition the prompt callout above the terminal prompt. - Add regression coverage for .git listing and subfolder completion.
This commit is contained in:
@@ -4,9 +4,26 @@ import android.os.Build
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
|
||||
class GitRepositoryRuntime(private val context: Context) {
|
||||
private val sandboxesRoot = File(context.filesDir, "githug-sandboxes")
|
||||
private val extractedGitDir = File(context.filesDir, "native-git/bin")
|
||||
class GitRepositoryRuntime private constructor(
|
||||
private val context: Context?,
|
||||
private val nativeGitOverride: File?,
|
||||
private val sandboxesRoot: File,
|
||||
) {
|
||||
private companion object {
|
||||
const val SyntheticRemoteUrl = "remote"
|
||||
}
|
||||
|
||||
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 startupBanner(): String {
|
||||
return if (nativeGitBinary() != null) {
|
||||
@@ -74,6 +91,8 @@ class GitRepositoryRuntime(private val context: Context) {
|
||||
expandShellPathspecs(currentRepo, shellTokens)
|
||||
}
|
||||
|
||||
executeSyntheticGitCommand(currentRepo, command, expandedTokens)?.let { return it }
|
||||
|
||||
val result = when (expandedTokens.first()) {
|
||||
"git" -> currentRepo to runGit(nativeGit, workingDir, expandedTokens.drop(1)).outputLines
|
||||
"help", "?" -> currentRepo to commandReferenceLines()
|
||||
@@ -200,7 +219,6 @@ class GitRepositoryRuntime(private val context: Context) {
|
||||
private fun executeHelperCommand(sandboxRoot: File, workingDir: File, currentRepo: RepoState, tokens: List<String>): Pair<RepoState, List<String>> {
|
||||
return when (tokens.first()) {
|
||||
"ls", "dir" -> currentRepo to workingDir.listFiles()
|
||||
?.filterNot { it.name == ".git" }
|
||||
?.sortedBy { it.name }
|
||||
?.map { it.name }
|
||||
.orEmpty()
|
||||
@@ -397,7 +415,18 @@ class GitRepositoryRuntime(private val context: Context) {
|
||||
}
|
||||
|
||||
desired.tags.forEach { tag -> runGit(nativeGit, sandbox, listOf("tag", tag)) }
|
||||
desired.remotes.forEach { (name, url) -> runGit(nativeGit, sandbox, listOf("remote", "add", name, url)) }
|
||||
desired.remotes.forEach { (name, url) ->
|
||||
val materializedUrl = if (url == SyntheticRemoteUrl) {
|
||||
materializeSyntheticRemote(nativeGit, sandbox, name)
|
||||
} else {
|
||||
url
|
||||
}
|
||||
runGit(nativeGit, sandbox, listOf("remote", "add", name, materializedUrl))
|
||||
if (url == SyntheticRemoteUrl) {
|
||||
runGit(nativeGit, sandbox, listOf("fetch", name))
|
||||
runGit(nativeGit, sandbox, listOf("branch", "--set-upstream-to=$name/master", desired.headBranch))
|
||||
}
|
||||
}
|
||||
|
||||
val stagedTargets = desired.files.filter { it.staged }.map { it.name }
|
||||
if (stagedTargets.isNotEmpty()) {
|
||||
@@ -416,6 +445,46 @@ class GitRepositoryRuntime(private val context: Context) {
|
||||
return files.getOrNull(index)?.let { listOf(it) }.orEmpty()
|
||||
}
|
||||
|
||||
private fun materializeSyntheticRemote(nativeGit: File, sandbox: File, remoteName: String): String {
|
||||
val remoteDir = File(sandbox.parentFile, "${sandbox.name}-$remoteName.git")
|
||||
remoteDir.deleteRecursively()
|
||||
val cloneResult = runGit(nativeGit, sandbox.parentFile ?: sandbox, listOf("clone", "--bare", sandbox.absolutePath, remoteDir.absolutePath))
|
||||
if (cloneResult.exitCode != 0) {
|
||||
remoteDir.mkdirs()
|
||||
runGit(nativeGit, remoteDir, listOf("init", "--bare"))
|
||||
}
|
||||
return remoteDir.absolutePath
|
||||
}
|
||||
|
||||
private fun executeSyntheticGitCommand(
|
||||
currentRepo: RepoState,
|
||||
command: String,
|
||||
tokens: List<String>,
|
||||
): Pair<RepoState, List<String>>? {
|
||||
if (tokens.firstOrNull() != "git") return null
|
||||
val gitCommand = tokens.getOrNull(1) ?: return null
|
||||
if (gitCommand == "clone" && tokens.getOrNull(2)?.startsWith("https://github.com/Gazler/cloneme") == true) {
|
||||
val target = tokens.getOrNull(3) ?: "cloneme"
|
||||
return currentRepo.copy(
|
||||
files = currentRepo.files + GitFile("$target/README", tracked = true),
|
||||
) to listOf("Cloned ${tokens[2]} into $target")
|
||||
}
|
||||
val shouldUseSandboxSemantics = when (gitCommand) {
|
||||
"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
|
||||
"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=") }
|
||||
else -> false
|
||||
}
|
||||
if (!shouldUseSandboxSemantics) return null
|
||||
|
||||
val (updatedRepo, output) = GitSandboxEngine.execute(currentRepo, command)
|
||||
return updatedRepo to output
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -531,11 +600,11 @@ class GitRepositoryRuntime(private val context: Context) {
|
||||
}
|
||||
|
||||
private fun nativeGitBinary(): File? {
|
||||
return packagedNativeGitBinary()
|
||||
return nativeGitOverride ?: packagedNativeGitBinary()
|
||||
}
|
||||
|
||||
private fun packagedNativeGitBinary(): File? {
|
||||
val nativeLibraryDir = context.applicationInfo.nativeLibraryDir ?: return null
|
||||
val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir ?: return null
|
||||
val candidate = File(nativeLibraryDir, "libgit.so")
|
||||
return candidate.takeIf { it.exists() && it.canExecute() }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user