diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7039198..e724f03 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "solutions.tretter.githugandroid" minSdk = 26 targetSdk = 35 - versionCode = 182 - versionName = "0.1.181" + versionCode = 183 + versionName = "0.1.182" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/java/solutions/tretter/githugandroid/CompletionCandidates.kt b/app/src/main/java/solutions/tretter/githugandroid/CompletionCandidates.kt index 347ce6f..a2fa1a0 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/CompletionCandidates.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/CompletionCandidates.kt @@ -24,6 +24,27 @@ internal fun directoryCompletionCandidates(repo: RepoState): List { .distinct() } +internal fun contextualCompletionCandidates( + candidates: List, + commandBeforeToken: String, + token: String, + directoriesOnly: Boolean, +): List { + val matches = candidates.sorted().filter { it.startsWith(token) } + if (directoriesOnly) return matches + + val commandTokens = GitSandboxEngine.tokenizeCommand(commandBeforeToken.trim()) + if (commandTokens == listOf("git", "bisect", "run")) { + val scriptMatches = matches.filter { candidate -> + val normalized = candidate.removePrefix("./") + normalized.endsWith(".sh") && '/' !in normalized && !normalized.startsWith(".") + } + if (scriptMatches.isNotEmpty()) return scriptMatches + } + + return matches +} + private fun RepoState.currentDirPrefix(): String { return if (currentDir == ".") "" else currentDir.trimEnd('/') + "/" } diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt b/app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt index a7e7d39..4a3afb6 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitHugApp.kt @@ -369,7 +369,12 @@ fun GitHugApp() { if (token.isBlank() && !isCdCompletion) return val candidates = runtime.completionCandidates(currentLevel, repo, directoriesOnly = isCdCompletion) - val matches = candidates.sorted().filter { it.startsWith(token) } + val matches = contextualCompletionCandidates( + candidates = candidates, + commandBeforeToken = beforeCursor.substring(0, tokenStart), + token = token, + directoriesOnly = isCdCompletion, + ) if (matches.isEmpty()) return val replacement = if (matches.size == 1) matches.first() else commonPrefix(matches) diff --git a/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt b/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt index f035aa6..99d13d8 100644 --- a/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt +++ b/app/src/main/java/solutions/tretter/githugandroid/GitRuntime.kt @@ -144,7 +144,12 @@ class GitRepositoryRuntime private constructor( val result = when (expandedTokens.first()) { "git" -> { - val gitResult = runGit(nativeGit, workingDir, expandedTokens.drop(1), invocation.environment) + val gitResult = runGit( + nativeGit, + workingDir, + normalizeGitArgumentsForAndroid(expandedTokens.drop(1)), + invocation.environment, + ) AppLog.d( "GitRuntime", "Git command result level=${level.id} exit=${gitResult.exitCode} output=${gitResult.outputLines}", @@ -167,6 +172,19 @@ class GitRepositoryRuntime private constructor( return refreshedRepo to result.second } + private fun normalizeGitArgumentsForAndroid(arguments: List): List { + if ( + arguments.size >= 3 && + arguments[0] == "bisect" && + arguments[1] == "run" && + arguments[2].startsWith("./") && + arguments[2].endsWith(".sh") + ) { + return arguments.take(2) + listOf("sh", arguments[2]) + arguments.drop(3) + } + return arguments + } + fun completionCandidates(level: Level, currentRepo: RepoState, directoriesOnly: Boolean): List { requireNativeGit() val sandbox = sandboxDir(level) diff --git a/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt b/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt index 1fb3069..098703b 100644 --- a/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt +++ b/app/src/test/java/solutions/tretter/githugandroid/GitSandboxEngineTest.kt @@ -504,6 +504,37 @@ class GitSandboxEngineTest { } } + @Test + fun nativeBisectRunScriptShortcutRunsThroughShell() { + val git = testGitBinary() + assumeTrue(git.exists() && git.canExecute()) + val root = Files.createTempDirectory("githug-bisect-run-script").toFile() + try { + val runtime = GitRepositoryRuntime(root, git) + val level = bisectLevel() + var repo = runtime.prepareLevel(level) + + listOf( + "git bisect start", + "git bisect bad HEAD", + "git bisect good known-good", + ).forEach { command -> + val (nextRepo, _) = runtime.execute(level, repo, command) + repo = nextRepo + } + + val (_, output) = runtime.execute(level, repo, "git bisect run ./test-balance.sh") + val text = output.joinToString("\n") + + assertFalse(text, text.contains("Permission denied", ignoreCase = true)) + assertFalse(text, text.contains("can't execute", ignoreCase = true)) + assertFalse(text, text.contains("bogus exit code", ignoreCase = true)) + assertTrue(text, text.contains("first bad commit", ignoreCase = true)) + } finally { + root.deleteRecursively() + } + } + private fun testGitBinary(): File { System.getenv("GITHUG_TEST_GIT_BINARY") ?.takeIf { it.isNotBlank() }