Make bisect playable and validate commit_in_future using Git author timestamps
This commit is contained in:
@@ -148,18 +148,32 @@ class GitRepositoryRuntime private constructor(
|
||||
tokens
|
||||
} else {
|
||||
expandShellPathspecs(currentRepo, invocation.command)
|
||||
}.normalizeGitStageAlias()
|
||||
}
|
||||
.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 -> executeHelperCommand(sandboxRoot, workingDir, currentRepo, expandedTokens)
|
||||
else -> executeExecutableShortcut(sandboxRoot, workingDir, currentRepo, expandedTokens)
|
||||
?: executeHelperCommand(sandboxRoot, workingDir, currentRepo, expandedTokens)
|
||||
}
|
||||
|
||||
val inspectedRepo = inspectSandbox(level).copy(currentDir = result.first.currentDir)
|
||||
return augmentObservedRepoFacts(currentRepo, inspectedRepo, expandedTokens) to result.second
|
||||
return augmentObservedRepoFacts(currentRepo, inspectedRepo, expandedTokens, result.second) to result.second
|
||||
}
|
||||
|
||||
private fun executeExecutableShortcut(
|
||||
sandboxRoot: File,
|
||||
workingDir: File,
|
||||
currentRepo: RepoState,
|
||||
tokens: List<String>,
|
||||
): Pair<RepoState, List<String>>? {
|
||||
val executable = tokens.firstOrNull() ?: return null
|
||||
if (!executable.startsWith("./") || executable.length <= 2) return null
|
||||
return executeShellScript(sandboxRoot, workingDir, currentRepo, listOf("sh", executable) + tokens.drop(1))
|
||||
}
|
||||
|
||||
fun completionCandidates(level: Level, currentRepo: RepoState, directoriesOnly: Boolean): List<String> {
|
||||
@@ -190,7 +204,7 @@ class GitRepositoryRuntime private constructor(
|
||||
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")
|
||||
add(" helper commands: ls/dir, pwd, cat, sh <script>, ./<script>, touch, mkdir/md, cd.., rm/del, echo")
|
||||
add(" visual editors: vi, vim, nano, emacs, ed, ex, edit, notepad")
|
||||
add(" git help <command>")
|
||||
}
|
||||
@@ -366,6 +380,7 @@ class GitRepositoryRuntime private constructor(
|
||||
if (!file.exists() || file.isDirectory) currentRepo to listOf("cat: $target: No such file")
|
||||
else currentRepo to file.readLines().ifEmpty { listOf("") }
|
||||
}
|
||||
"sh" -> executeShellScript(sandboxRoot, workingDir, currentRepo, tokens)
|
||||
"touch" -> {
|
||||
val target = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: touch <file>")
|
||||
val file = File(workingDir, target)
|
||||
@@ -418,6 +433,28 @@ class GitRepositoryRuntime private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeShellScript(
|
||||
sandboxRoot: File,
|
||||
workingDir: File,
|
||||
currentRepo: RepoState,
|
||||
tokens: List<String>,
|
||||
): Pair<RepoState, List<String>> {
|
||||
val script = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: sh <script>")
|
||||
val scriptFile = File(workingDir, script).canonicalFile
|
||||
if (!scriptFile.path.startsWith(sandboxRoot.path)) {
|
||||
return currentRepo to listOf("sh: $script: Permission denied")
|
||||
}
|
||||
if (!scriptFile.isFile) {
|
||||
return currentRepo to listOf("sh: $script: No such file")
|
||||
}
|
||||
val result = runShellProcess(
|
||||
File(shellExecutable()),
|
||||
workingDir,
|
||||
tokens.drop(1),
|
||||
)
|
||||
return currentRepo to result.outputLines
|
||||
}
|
||||
|
||||
private fun placeholderManPage(topic: String): String {
|
||||
bundledManPage(topic)?.let { return it }
|
||||
val body = when (topic) {
|
||||
@@ -645,7 +682,7 @@ class GitRepositoryRuntime private constructor(
|
||||
"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=") }
|
||||
"commit" -> "merge-squash" in currentRepo.maintenanceActions
|
||||
else -> false
|
||||
}
|
||||
if (!shouldUseSandboxSemantics) return null
|
||||
@@ -662,6 +699,20 @@ class GitRepositoryRuntime private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<String>.normalizeGitBisectRunScriptShortcut(): List<String> {
|
||||
return if (
|
||||
size >= 4 &&
|
||||
this[0] == "git" &&
|
||||
this[1] == "bisect" &&
|
||||
this[2] == "run" &&
|
||||
this[3].startsWith("./")
|
||||
) {
|
||||
take(3) + listOf("sh") + drop(3)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -714,7 +765,7 @@ class GitRepositoryRuntime private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val logResult = runGit(nativeGit, sandbox, listOf("log", "--pretty=format:%h\t%s"))
|
||||
val logResult = runGit(nativeGit, sandbox, listOf("log", "--pretty=format:%h\t%at\t%s"))
|
||||
val branchResult = runGit(nativeGit, sandbox, listOf("branch", "--list"))
|
||||
val remoteBranchResult = runGit(nativeGit, sandbox, listOf("branch", "-r", "--list"))
|
||||
val tagResult = runGit(nativeGit, sandbox, listOf("tag", "--list"))
|
||||
@@ -742,8 +793,16 @@ class GitRepositoryRuntime private constructor(
|
||||
)
|
||||
val commits = if (logResult.exitCode == 0) {
|
||||
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) { "" })
|
||||
val parts = line.split('\t', limit = 3)
|
||||
if (parts.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
CommitNode(
|
||||
id = parts[0],
|
||||
authorTimestampSeconds = parts.getOrNull(1)?.toLongOrNull(),
|
||||
message = parts.getOrElse(2) { "" },
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
@@ -828,10 +887,22 @@ class GitRepositoryRuntime private constructor(
|
||||
return listOf(tokens.first().value) + GitSandboxEngine.expandPathspecTokens(repo, tokens.drop(1))
|
||||
}
|
||||
|
||||
private fun augmentObservedRepoFacts(previousRepo: RepoState, inspectedRepo: RepoState, tokens: List<String>): RepoState {
|
||||
private fun augmentObservedRepoFacts(
|
||||
previousRepo: RepoState,
|
||||
inspectedRepo: RepoState,
|
||||
tokens: List<String>,
|
||||
outputLines: List<String> = emptyList(),
|
||||
): RepoState {
|
||||
if (tokens.firstOrNull() != "git") return inspectedRepo
|
||||
|
||||
return when (tokens.getOrNull(1)) {
|
||||
"bisect" -> {
|
||||
if (tokens.getOrNull(2) == "run" && outputLines.any { it.contains("is the first bad commit") }) {
|
||||
inspectedRepo.copy(maintenanceActions = previousRepo.maintenanceActions + inspectedRepo.maintenanceActions + "bisect-found")
|
||||
} else {
|
||||
inspectedRepo.copy(maintenanceActions = previousRepo.maintenanceActions + inspectedRepo.maintenanceActions)
|
||||
}
|
||||
}
|
||||
"stash" -> inspectedRepo.copy(
|
||||
stashes = mergeDistinct(previousRepo.stashes, inspectedRepo.stashes + "stash@{${previousRepo.stashes.size}}"),
|
||||
)
|
||||
@@ -996,6 +1067,25 @@ class GitRepositoryRuntime private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private 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 gitExecDirectory(binary: File): File {
|
||||
val directory = if (context != null) {
|
||||
File(context.filesDir, "git-exec")
|
||||
|
||||
Reference in New Issue
Block a user