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:
@@ -303,8 +303,12 @@ object GitSandboxEngine {
|
||||
val value = parts.drop(3).joinToString(" ")
|
||||
repo.copy(config = repo.config + (key to value)) to emptyList()
|
||||
}
|
||||
parts.size >= 3 && parts[1] == "add" -> {
|
||||
val target = parts.drop(2).last { !it.startsWith("-") }
|
||||
parts.size >= 2 && (parts[1] == "stage" || parts[1] == "add") -> {
|
||||
if (parts.drop(2).any { it == "-i" || it == "--interactive" }) {
|
||||
return repo to listOf("Interactive staging is not supported in the mobile terminal. Use git add <path> or git add -p <path>.")
|
||||
}
|
||||
val target = parts.drop(2).lastOrNull { !it.startsWith("-") }
|
||||
?: return repo to listOf("usage: git add <path>")
|
||||
if (target != "." && repo.files.none { it.name == target && !it.deleted }) {
|
||||
repo to listOf("fatal: pathspec '$target' did not match any files")
|
||||
} else {
|
||||
@@ -352,12 +356,12 @@ object GitSandboxEngine {
|
||||
parts.size >= 3 && parts[1] == "merge" -> merge(repo, parts.drop(2))
|
||||
parts.size >= 2 && parts[1] == "rebase" -> rebase(repo, parts.drop(2))
|
||||
parts.size >= 2 && parts[1] == "cherry-pick" -> {
|
||||
val files = if (repo.files.none { it.name == "README" }) {
|
||||
repo.files + GitFile("README", tracked = true)
|
||||
val files = if (repo.files.none { it.name == "README.md" }) {
|
||||
repo.files + GitFile("README.md", "Proper input instructions\n", tracked = true)
|
||||
} else {
|
||||
repo.files.map { if (it.name == "README") it.copy(tracked = true) else it }
|
||||
repo.files.map { if (it.name == "README.md") it.copy(tracked = true) else it }
|
||||
}
|
||||
repo.copy(files = files, commits = repo.commits + CommitNode("${repo.commits.size + 1}", "Cherry-picked feature")) to emptyList()
|
||||
repo.copy(files = files, commits = listOf(CommitNode("${repo.commits.size + 1}", "Filled in README.md with proper input")) + repo.commits) to emptyList()
|
||||
}
|
||||
parts.size >= 2 && parts[1] == "revert" -> {
|
||||
repo.copy(commits = repo.commits + CommitNode("${repo.commits.size + 1}", "Revert \"Bad commit\"")) to emptyList()
|
||||
|
||||
@@ -7,11 +7,22 @@ data class GitHelpInvocation(
|
||||
|
||||
fun parseGitHelpInvocation(command: String): GitHelpInvocation? {
|
||||
val tokens = GitSandboxEngine.tokenizeCommand(command)
|
||||
if (tokens.size >= 2 && tokens[0] == "man") {
|
||||
val topic = when {
|
||||
tokens[1] == "git" -> tokens.getOrNull(2) ?: "git"
|
||||
tokens[1].startsWith("git-") -> tokens[1].removePrefix("git-")
|
||||
tokens[1].startsWith("git") && tokens[1].length > 3 -> tokens[1].removePrefix("git")
|
||||
else -> tokens[1]
|
||||
}
|
||||
.takeIf { it.matches(Regex("[A-Za-z0-9_-]+")) }
|
||||
?: return null
|
||||
return GitHelpInvocation(topic = topic, command = command)
|
||||
}
|
||||
if (tokens.size < 3 || tokens[0] != "git") return null
|
||||
val topic = when {
|
||||
tokens[1] == "help" -> tokens[2]
|
||||
tokens[1] == "-h" || tokens[1] == "--help" -> tokens[2]
|
||||
tokens.drop(2).any { it == "-h" || it == "--help" } -> tokens[1]
|
||||
tokens[1] == "--help" -> tokens[2]
|
||||
tokens.drop(2).any { it == "--help" } -> tokens[1]
|
||||
else -> return null
|
||||
}.takeIf { it.matches(Regex("[A-Za-z0-9_-]+")) } ?: return null
|
||||
return GitHelpInvocation(topic = topic, command = command)
|
||||
|
||||
@@ -64,8 +64,13 @@ fun GitHugApp() {
|
||||
MaterialTheme(colorScheme = GitHugColorScheme) {
|
||||
val context = LocalContext.current
|
||||
val configuration = LocalConfiguration.current
|
||||
val levels = remember { sampleLevels() }
|
||||
val runtime = remember(context) { GitRepositoryRuntime(context.applicationContext) }
|
||||
if (!runtime.isNativeGitAvailable()) {
|
||||
MissingNativeGitScreen(message = runtime.unavailableMessage())
|
||||
return@MaterialTheme
|
||||
}
|
||||
|
||||
val levels = remember { sampleLevels() }
|
||||
val paneLayoutStore = remember(context) { PaneLayoutStore(context.applicationContext) }
|
||||
val gameProgressStore = remember(context) { GameProgressStore(context.applicationContext) }
|
||||
val persistedPaneLayout by paneLayoutStore.layoutFlow.collectAsState(initial = defaultPaneLayout())
|
||||
@@ -718,7 +723,7 @@ private fun HelpCalloutOverlay(
|
||||
} ?: Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(start = 14.dp, bottom = 96.dp),
|
||||
text = "Tap the prompt to enter a command.",
|
||||
text = "Tap the prompt to enter a command or answer.",
|
||||
showHelpOnStart = showHelpOnStart,
|
||||
onShowHelpOnStartChange = onShowHelpOnStartChange,
|
||||
onOk = onOk,
|
||||
@@ -836,6 +841,36 @@ private fun HelpBubble(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MissingNativeGitScreen(message: String) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppBackground)
|
||||
.padding(24.dp),
|
||||
color = AppBackground,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Text(
|
||||
text = "Native Git unavailable",
|
||||
color = TextPrimary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp,
|
||||
)
|
||||
Text(
|
||||
text = message,
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
color = TextSecondary,
|
||||
fontSize = 15.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SolvedCelebrationOverlay(title: String) {
|
||||
val transition = rememberInfiniteTransition(label = "solved-celebration")
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -27,6 +27,9 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -38,6 +41,13 @@ data class ManPageState(
|
||||
val content: String,
|
||||
)
|
||||
|
||||
private data class ManPageMatch(
|
||||
val start: Int,
|
||||
val end: Int,
|
||||
val lineIndex: Int,
|
||||
val columnIndex: Int,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ManPageDialog(
|
||||
state: ManPageState,
|
||||
@@ -52,21 +62,52 @@ fun ManPageDialog(
|
||||
if (searchQuery.isBlank()) {
|
||||
emptyList()
|
||||
} else {
|
||||
contentLines.mapIndexedNotNull { index, line ->
|
||||
index.takeIf { line.contains(searchQuery, ignoreCase = true) }
|
||||
val result = mutableListOf<ManPageMatch>()
|
||||
var absoluteLineStart = 0
|
||||
contentLines.forEachIndexed { lineIndex, line ->
|
||||
var searchFrom = 0
|
||||
while (searchFrom <= line.length) {
|
||||
val columnIndex = line.indexOf(searchQuery, searchFrom, ignoreCase = true)
|
||||
if (columnIndex == -1) break
|
||||
val start = absoluteLineStart + columnIndex
|
||||
result += ManPageMatch(
|
||||
start = start,
|
||||
end = start + searchQuery.length,
|
||||
lineIndex = lineIndex,
|
||||
columnIndex = columnIndex,
|
||||
)
|
||||
searchFrom = columnIndex + searchQuery.length.coerceAtLeast(1)
|
||||
}
|
||||
absoluteLineStart += line.length + 1
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
val matchCount = matches.size
|
||||
val currentMatchNumber = if (matchCount == 0) 0 else selectedMatch + 1
|
||||
val highlightedContent = remember(state.content, matches, selectedMatch) {
|
||||
AnnotatedString.Builder(state.content).apply {
|
||||
matches.forEachIndexed { index, match ->
|
||||
addStyle(
|
||||
SpanStyle(
|
||||
color = if (index == selectedMatch) Color.White else Color.Black,
|
||||
background = if (index == selectedMatch) Color(0xFFD32F2F) else Color(0xFFFFEB3B),
|
||||
),
|
||||
match.start,
|
||||
match.end,
|
||||
)
|
||||
}
|
||||
}.toAnnotatedString()
|
||||
}
|
||||
|
||||
LaunchedEffect(searchQuery, matchCount) {
|
||||
selectedMatch = selectedMatch.coerceIn(0, (matchCount - 1).coerceAtLeast(0))
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedMatch, matches) {
|
||||
val lineIndex = matches.getOrNull(selectedMatch) ?: return@LaunchedEffect
|
||||
verticalScrollState.animateScrollTo((lineIndex * 18).coerceAtMost(verticalScrollState.maxValue))
|
||||
val match = matches.getOrNull(selectedMatch) ?: return@LaunchedEffect
|
||||
verticalScrollState.animateScrollTo((match.lineIndex * 18).coerceAtMost(verticalScrollState.maxValue))
|
||||
horizontalScrollState.animateScrollTo((match.columnIndex * 8).coerceAtMost(horizontalScrollState.maxValue))
|
||||
}
|
||||
|
||||
Dialog(onDismissRequest = onClose) {
|
||||
@@ -170,7 +211,7 @@ fun ManPageDialog(
|
||||
) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = state.content,
|
||||
text = highlightedContent,
|
||||
modifier = Modifier.widthIn(min = 1200.dp),
|
||||
color = TextPrimary,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
|
||||
@@ -241,7 +241,7 @@ private fun RowScope.TerminalInputField(
|
||||
) {
|
||||
if (commandInput.text.isEmpty()) {
|
||||
Text(
|
||||
text = "Enter git command",
|
||||
text = "Enter git command or answer",
|
||||
color = TextMuted.copy(alpha = 0.7f),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 15.sp,
|
||||
@@ -269,7 +269,7 @@ private fun TerminalInputHintBubble() {
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Tap here to enter a command",
|
||||
text = "Tap here to enter a command or answer.",
|
||||
color = Color(0xFF161000),
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
|
||||
@@ -12,10 +12,26 @@ internal fun cherryPickLevel(): Level = level(
|
||||
title = "Cherry Pick",
|
||||
description = "Your new feature isn't worth the time and you're going to delete it. But it has one commit that fills in `README` file, and you want this commit to be on the master as well.",
|
||||
hints = listOf("Sneak a peek at the `git help cherry-pick` command."),
|
||||
commandSuggestions = listOf("git cherry-pick feature"),
|
||||
setup = { RepoState(initialized = true, files = listOf(GitFile("nokia.js", tracked = true)), commits = listOf(CommitNode("1", "Initial"), CommitNode("2", "Master work")), branches = mapOf("master" to 2, "feature" to 3)) },
|
||||
validator = repoPredicate { repo -> repo.files.any { it.name == "README" && it.tracked } && repo.headBranch == "master" },
|
||||
commandSuggestions = listOf("git log --oneline new-feature", "git cherry-pick new-feature"),
|
||||
setup = { RepoState(initialized = true, files = listOf(GitFile("nokia.js", tracked = true)), commits = listOf(CommitNode("1", "Added fancy branded output"), CommitNode("2", "Filled in README.md with proper input")), branches = mapOf("master" to 1, "new-feature" to 2)) },
|
||||
validator = repoPredicate { repo ->
|
||||
repo.files.any { it.name == "README.md" && it.tracked } &&
|
||||
repo.headBranch == "master" &&
|
||||
repo.commits.take(2).map { it.message } == listOf("Filled in README.md with proper input", "Added fancy branded output")
|
||||
},
|
||||
nativeSetup = {
|
||||
resetFiles()
|
||||
write("nokia.js", "console.log('Nokia tune')\n")
|
||||
addCommit("Added fancy branded output", "nokia.js")
|
||||
checkoutNew("new-feature")
|
||||
write("README.md", "Proper input instructions\n")
|
||||
addCommit("Filled in README.md with proper input", "README.md")
|
||||
checkout("master")
|
||||
git("branch", "-f", "feature", "new-feature")
|
||||
true
|
||||
},
|
||||
testCases = listOf(
|
||||
levelTestCase("cherry pick feature tip", "git cherry-pick feature"),
|
||||
levelTestCase("cherry pick new feature tip", "git cherry-pick new-feature"),
|
||||
levelTestCase("cherry pick feature alias", "git cherry-pick feature"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -13,8 +13,22 @@ internal fun numberOfFilesCommittedLevel(): Level = level(
|
||||
description = "There are some files in this repository; how many of them are staged for a commit?",
|
||||
hints = listOf("You are looking for a command to identify the status of the repository (resembles a Linux command)."),
|
||||
commandSuggestions = listOf("git status"),
|
||||
setup = { RepoState(initialized = true, files = listOf(GitFile("rubyfile1.rb", staged = true), GitFile("rubyfile4.rb", staged = true, tracked = true), GitFile("rubyfile5.rb", tracked = true), GitFile("rubyfile6.rb"), GitFile("rubyfile7.rb")), branches = mapOf("master" to 1)) },
|
||||
setup = { RepoState(initialized = true, files = listOf(GitFile("rubyfile1.rb", staged = true), GitFile("rubyfile4.rb", "#Changes", staged = true, tracked = true), GitFile("rubyfile5.rb", "#Changes", tracked = true), GitFile("rubyfile6.rb"), GitFile("rubyfile7.rb")), branches = mapOf("master" to 1)) },
|
||||
validator = commandAnswer("2"),
|
||||
nativeSetup = {
|
||||
resetFiles()
|
||||
write("rubyfile4.rb")
|
||||
write("rubyfile5.rb")
|
||||
addCommit("Commit", "rubyfile4.rb", "rubyfile5.rb")
|
||||
write("rubyfile4.rb", "#Changes")
|
||||
add("rubyfile4.rb")
|
||||
write("rubyfile5.rb", "#Changes")
|
||||
write("rubyfile1.rb")
|
||||
add("rubyfile1.rb")
|
||||
write("rubyfile6.rb")
|
||||
write("rubyfile7.rb")
|
||||
true
|
||||
},
|
||||
testCases = listOf(
|
||||
levelTestCase("answer staged count", "2"),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user