Auto-commit after successful build: update app gameplay/UI, improve build setup
Changed files:\napp/build.gradle.kts app/src/main/java/com/kawomi/githugandroid/GameModels.kt app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "com.kawomi.githugandroid"
|
applicationId = "com.kawomi.githugandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 6
|
versionCode = 7
|
||||||
versionName = "0.1.5"
|
versionName = "0.1.6"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ data class GitFile(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val content: String = "",
|
val content: String = "",
|
||||||
val staged: Boolean = false,
|
val staged: Boolean = false,
|
||||||
|
val tracked: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class CommitNode(
|
data class CommitNode(
|
||||||
@@ -71,7 +72,7 @@ val RepoStateSaver = listSaver<RepoState, Any>(
|
|||||||
listOf(
|
listOf(
|
||||||
state.initialized,
|
state.initialized,
|
||||||
state.headBranch,
|
state.headBranch,
|
||||||
state.files.flatMap { listOf(it.name, it.content, it.staged.toString()) },
|
state.files.flatMap { listOf(it.name, it.content, it.staged.toString(), it.tracked.toString()) },
|
||||||
state.commits.flatMap { listOf(it.id, it.message) },
|
state.commits.flatMap { listOf(it.id, it.message) },
|
||||||
state.branches.flatMap { listOf(it.key, it.value.toString()) },
|
state.branches.flatMap { listOf(it.key, it.value.toString()) },
|
||||||
)
|
)
|
||||||
@@ -85,8 +86,13 @@ val RepoStateSaver = listSaver<RepoState, Any>(
|
|||||||
RepoState(
|
RepoState(
|
||||||
initialized = initialized,
|
initialized = initialized,
|
||||||
headBranch = headBranch,
|
headBranch = headBranch,
|
||||||
files = fileParts.chunked(3).map {
|
files = fileParts.chunked(4).map {
|
||||||
GitFile(it[0] as String, it[1] as String, (it[2] as String).toBoolean())
|
GitFile(
|
||||||
|
name = it[0] as String,
|
||||||
|
content = it[1] as String,
|
||||||
|
staged = (it[2] as String).toBoolean(),
|
||||||
|
tracked = (it[3] as String).toBoolean(),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
commits = commitParts.chunked(2).map {
|
commits = commitParts.chunked(2).map {
|
||||||
CommitNode(it[0] as String, it[1] as String)
|
CommitNode(it[0] as String, it[1] as String)
|
||||||
@@ -97,10 +103,28 @@ val RepoStateSaver = listSaver<RepoState, Any>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
object GitSandboxEngine {
|
object GitSandboxEngine {
|
||||||
|
fun commandReferenceLines(): List<String> = listOf(
|
||||||
|
"Available sandbox commands:",
|
||||||
|
" git init",
|
||||||
|
" git status",
|
||||||
|
" git add <file>",
|
||||||
|
" git add .",
|
||||||
|
" git commit -m \"message\"",
|
||||||
|
" git commit -am \"message\"",
|
||||||
|
" git log",
|
||||||
|
" git branch <name>",
|
||||||
|
" git checkout <name>",
|
||||||
|
" ls",
|
||||||
|
" touch <file>",
|
||||||
|
" help",
|
||||||
|
" git help",
|
||||||
|
)
|
||||||
|
|
||||||
fun execute(repo: RepoState, command: String): Pair<RepoState, List<String>> {
|
fun execute(repo: RepoState, command: String): Pair<RepoState, List<String>> {
|
||||||
val parts = command.split(" ").filter { it.isNotBlank() }
|
val parts = tokenizeCommand(command)
|
||||||
if (parts.isEmpty()) return repo to emptyList()
|
if (parts.isEmpty()) return repo to emptyList()
|
||||||
return when {
|
return when {
|
||||||
|
parts[0] == "help" || parts[0] == "?" -> repo to commandReferenceLines()
|
||||||
parts[0] == "touch" && parts.size >= 2 -> {
|
parts[0] == "touch" && parts.size >= 2 -> {
|
||||||
val name = parts[1]
|
val name = parts[1]
|
||||||
if (repo.files.any { it.name == name }) repo to listOf("$name already exists")
|
if (repo.files.any { it.name == name }) repo to listOf("$name already exists")
|
||||||
@@ -110,6 +134,7 @@ object GitSandboxEngine {
|
|||||||
parts[0] != "git" -> repo to listOf("Command not supported in sandbox. Try a git command or 'touch'.")
|
parts[0] != "git" -> repo to listOf("Command not supported in sandbox. Try a git command or 'touch'.")
|
||||||
parts.size >= 2 && parts[1] == "init" -> repo.copy(initialized = true, branches = mapOf("master" to repo.commits.size)) to listOf("Initialized empty Git repository")
|
parts.size >= 2 && parts[1] == "init" -> repo.copy(initialized = true, branches = mapOf("master" to repo.commits.size)) to listOf("Initialized empty Git repository")
|
||||||
!repo.initialized -> repo to listOf("fatal: not a git repository")
|
!repo.initialized -> repo to listOf("fatal: not a git repository")
|
||||||
|
parts.size >= 2 && parts[1] == "help" -> repo to commandReferenceLines()
|
||||||
parts.size >= 2 && parts[1] == "status" -> repo to statusLines(repo)
|
parts.size >= 2 && parts[1] == "status" -> repo to statusLines(repo)
|
||||||
parts.size >= 3 && parts[1] == "add" -> {
|
parts.size >= 3 && parts[1] == "add" -> {
|
||||||
val target = parts[2]
|
val target = parts[2]
|
||||||
@@ -120,7 +145,7 @@ object GitSandboxEngine {
|
|||||||
repo.copy(files = updated) to listOf("staged ${if (target == ".") "all files" else target}")
|
repo.copy(files = updated) to listOf("staged ${if (target == ".") "all files" else target}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parts.size >= 2 && parts[1] == "commit" -> commit(repo, parts)
|
parts.size >= 2 && parts[1] == "commit" -> commit(repo, parts.drop(2))
|
||||||
parts.size >= 2 && parts[1] == "log" -> {
|
parts.size >= 2 && parts[1] == "log" -> {
|
||||||
repo to if (repo.commits.isEmpty()) listOf("fatal: your current branch '${repo.headBranch}' does not have any commits yet")
|
repo to if (repo.commits.isEmpty()) listOf("fatal: your current branch '${repo.headBranch}' does not have any commits yet")
|
||||||
else repo.commits.reversed().flatMap { listOf("commit ${it.id}", " ${it.message}") }
|
else repo.commits.reversed().flatMap { listOf("commit ${it.id}", " ${it.message}") }
|
||||||
@@ -139,26 +164,94 @@ object GitSandboxEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun commit(repo: RepoState, parts: List<String>): Pair<RepoState, List<String>> {
|
private fun commit(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
|
||||||
val messageIndex = parts.indexOf("-m")
|
val parsed = parseCommitArguments(arguments)
|
||||||
if (messageIndex == -1 || messageIndex == parts.lastIndex) {
|
if (parsed.error != null) {
|
||||||
return repo to listOf("error: commit message required. Use git commit -m \"message\"")
|
return repo to listOf(parsed.error)
|
||||||
}
|
}
|
||||||
val message = parts.drop(messageIndex + 1).joinToString(" ").trim('"')
|
|
||||||
val staged = repo.files.filter { it.staged }
|
val repoForCommit = if (parsed.stageAllTracked) {
|
||||||
|
repo.copy(files = repo.files.map { file -> if (file.tracked) file.copy(staged = true) else file })
|
||||||
|
} else {
|
||||||
|
repo
|
||||||
|
}
|
||||||
|
|
||||||
|
val message = parsed.message.orEmpty()
|
||||||
|
val staged = repoForCommit.files.filter { it.staged }
|
||||||
if (staged.isEmpty()) return repo to listOf("nothing to commit")
|
if (staged.isEmpty()) return repo to listOf("nothing to commit")
|
||||||
|
|
||||||
val nextId = "${repo.commits.size + 1}".padStart(7, '0')
|
val nextId = "${repo.commits.size + 1}".padStart(7, '0')
|
||||||
val cleanedFiles = repo.files.map { it.copy(staged = false) }
|
val cleanedFiles = repoForCommit.files.map { file ->
|
||||||
return repo.copy(
|
if (file.staged) file.copy(staged = false, tracked = true) else file
|
||||||
|
}
|
||||||
|
|
||||||
|
return repoForCommit.copy(
|
||||||
files = cleanedFiles,
|
files = cleanedFiles,
|
||||||
commits = repo.commits + CommitNode(nextId, message),
|
commits = repo.commits + CommitNode(nextId, message),
|
||||||
branches = repo.branches + (repo.headBranch to (repo.commits.size + 1)),
|
branches = repo.branches + (repo.headBranch to (repo.commits.size + 1)),
|
||||||
) to listOf("[$nextId] $message")
|
) to listOf("[$nextId] $message")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseCommitArguments(arguments: List<String>): ParsedCommitArguments {
|
||||||
|
var message: String? = null
|
||||||
|
var stageAllTracked = false
|
||||||
|
var index = 0
|
||||||
|
|
||||||
|
while (index < arguments.size) {
|
||||||
|
val argument = arguments[index]
|
||||||
|
when {
|
||||||
|
argument == "-a" || argument == "--all" -> {
|
||||||
|
stageAllTracked = true
|
||||||
|
}
|
||||||
|
argument == "-m" || argument == "--message" -> {
|
||||||
|
val next = arguments.getOrNull(index + 1)
|
||||||
|
?: return ParsedCommitArguments(error = "error: commit message required. Use git commit -m \"message\"")
|
||||||
|
message = next
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
argument.startsWith("--message=") -> {
|
||||||
|
message = argument.substringAfter('=')
|
||||||
|
}
|
||||||
|
argument.startsWith("-") && !argument.startsWith("--") && argument.length > 2 -> {
|
||||||
|
val shortFlags = argument.drop(1)
|
||||||
|
var shortIndex = 0
|
||||||
|
while (shortIndex < shortFlags.length) {
|
||||||
|
when (val flag = shortFlags[shortIndex]) {
|
||||||
|
'a' -> stageAllTracked = true
|
||||||
|
'm' -> {
|
||||||
|
val attachedValue = shortFlags.substring(shortIndex + 1)
|
||||||
|
if (attachedValue.isNotEmpty()) {
|
||||||
|
message = attachedValue
|
||||||
|
} else {
|
||||||
|
val next = arguments.getOrNull(index + 1)
|
||||||
|
?: return ParsedCommitArguments(error = "error: commit message required. Use git commit -m \"message\"")
|
||||||
|
message = next
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
else -> return ParsedCommitArguments(error = "error: unsupported commit option '-$flag'")
|
||||||
|
}
|
||||||
|
shortIndex += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> return ParsedCommitArguments(error = "error: unsupported commit argument '$argument'")
|
||||||
|
}
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.isNullOrBlank()) {
|
||||||
|
return ParsedCommitArguments(error = "error: commit message required. Use git commit -m \"message\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParsedCommitArguments(message = message, stageAllTracked = stageAllTracked)
|
||||||
|
}
|
||||||
|
|
||||||
private fun statusLines(repo: RepoState): List<String> {
|
private fun statusLines(repo: RepoState): List<String> {
|
||||||
val staged = repo.files.filter { it.staged }.map { "new file: ${it.name}" }
|
val staged = repo.files.filter { it.staged }.map {
|
||||||
val unstaged = repo.files.filterNot { it.staged }.map { "untracked: ${it.name}" }
|
if (it.tracked) "modified: ${it.name}" else "new file: ${it.name}"
|
||||||
|
}
|
||||||
|
val unstaged = repo.files.filterNot { it.staged || it.tracked }.map { "untracked: ${it.name}" }
|
||||||
return buildList {
|
return buildList {
|
||||||
add("On branch ${repo.headBranch}")
|
add("On branch ${repo.headBranch}")
|
||||||
if (staged.isEmpty() && unstaged.isEmpty()) {
|
if (staged.isEmpty() && unstaged.isEmpty()) {
|
||||||
@@ -175,4 +268,55 @@ object GitSandboxEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun tokenizeCommand(command: String): List<String> {
|
||||||
|
val result = mutableListOf<String>()
|
||||||
|
val current = StringBuilder()
|
||||||
|
var quoteChar: Char? = null
|
||||||
|
var escaping = false
|
||||||
|
|
||||||
|
command.forEach { char ->
|
||||||
|
when {
|
||||||
|
escaping -> {
|
||||||
|
current.append(char)
|
||||||
|
escaping = false
|
||||||
|
}
|
||||||
|
char == '\\' && quoteChar != '\'' -> {
|
||||||
|
escaping = true
|
||||||
|
}
|
||||||
|
quoteChar != null -> {
|
||||||
|
if (char == quoteChar) {
|
||||||
|
quoteChar = null
|
||||||
|
} else {
|
||||||
|
current.append(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char == '"' || char == '\'' -> {
|
||||||
|
quoteChar = char
|
||||||
|
}
|
||||||
|
char.isWhitespace() -> {
|
||||||
|
if (current.isNotEmpty()) {
|
||||||
|
result += current.toString()
|
||||||
|
current.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> current.append(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (escaping) {
|
||||||
|
current.append('\\')
|
||||||
|
}
|
||||||
|
if (current.isNotEmpty()) {
|
||||||
|
result += current.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ParsedCommitArguments(
|
||||||
|
val message: String? = null,
|
||||||
|
val stageAllTracked: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -211,6 +211,10 @@ fun GitHugApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showCommandHelp() {
|
||||||
|
output = output + GitSandboxEngine.commandReferenceLines()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold { padding ->
|
Scaffold { padding ->
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -265,6 +269,7 @@ fun GitHugApp() {
|
|||||||
},
|
},
|
||||||
onRun = { runCommand() },
|
onRun = { runCommand() },
|
||||||
onTab = { tabComplete() },
|
onTab = { tabComplete() },
|
||||||
|
onHelp = { showCommandHelp() },
|
||||||
onCursorLeft = { moveCursor(-1) },
|
onCursorLeft = { moveCursor(-1) },
|
||||||
onCursorRight = { moveCursor(1) },
|
onCursorRight = { moveCursor(1) },
|
||||||
onHistoryUp = { historyUp() },
|
onHistoryUp = { historyUp() },
|
||||||
@@ -383,6 +388,7 @@ private fun TerminalPanel(
|
|||||||
onValueChange: (TextFieldValue) -> Unit,
|
onValueChange: (TextFieldValue) -> Unit,
|
||||||
onRun: () -> Unit,
|
onRun: () -> Unit,
|
||||||
onTab: () -> Unit,
|
onTab: () -> Unit,
|
||||||
|
onHelp: () -> Unit,
|
||||||
onCursorLeft: () -> Unit,
|
onCursorLeft: () -> Unit,
|
||||||
onCursorRight: () -> Unit,
|
onCursorRight: () -> Unit,
|
||||||
onHistoryUp: () -> Unit,
|
onHistoryUp: () -> Unit,
|
||||||
@@ -419,6 +425,7 @@ private fun TerminalPanel(
|
|||||||
}
|
}
|
||||||
SpecialKeyBar(
|
SpecialKeyBar(
|
||||||
onTab = onTab,
|
onTab = onTab,
|
||||||
|
onHelp = onHelp,
|
||||||
onCursorLeft = onCursorLeft,
|
onCursorLeft = onCursorLeft,
|
||||||
onCursorRight = onCursorRight,
|
onCursorRight = onCursorRight,
|
||||||
onHistoryUp = onHistoryUp,
|
onHistoryUp = onHistoryUp,
|
||||||
@@ -464,6 +471,7 @@ private fun TerminalPanel(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SpecialKeyBar(
|
private fun SpecialKeyBar(
|
||||||
onTab: () -> Unit,
|
onTab: () -> Unit,
|
||||||
|
onHelp: () -> Unit,
|
||||||
onCursorLeft: () -> Unit,
|
onCursorLeft: () -> Unit,
|
||||||
onCursorRight: () -> Unit,
|
onCursorRight: () -> Unit,
|
||||||
onHistoryUp: () -> Unit,
|
onHistoryUp: () -> Unit,
|
||||||
@@ -471,13 +479,14 @@ private fun SpecialKeyBar(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
TerminalKeyButton(label = "TAB", onClick = onTab, modifier = Modifier.width(68.dp))
|
TerminalKeyButton(label = "↹", onClick = onTab, modifier = Modifier.width(56.dp), fontFamily = FontFamily.Default)
|
||||||
TerminalKeyButton(label = "←", onClick = onCursorLeft, modifier = Modifier.width(48.dp))
|
TerminalKeyButton(label = "?", onClick = onHelp, modifier = Modifier.width(40.dp), fontFamily = FontFamily.Default)
|
||||||
TerminalKeyButton(label = "→", onClick = onCursorRight, modifier = Modifier.width(48.dp))
|
TerminalKeyButton(label = "←", onClick = onCursorLeft, modifier = Modifier.width(40.dp))
|
||||||
TerminalKeyButton(label = "↑", onClick = onHistoryUp, modifier = Modifier.width(48.dp))
|
TerminalKeyButton(label = "→", onClick = onCursorRight, modifier = Modifier.width(40.dp))
|
||||||
TerminalKeyButton(label = "↓", onClick = onHistoryDown, modifier = Modifier.width(48.dp))
|
TerminalKeyButton(label = "↑", onClick = onHistoryUp, modifier = Modifier.width(40.dp))
|
||||||
|
TerminalKeyButton(label = "↓", onClick = onHistoryDown, modifier = Modifier.width(40.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,6 +495,7 @@ private fun TerminalKeyButton(
|
|||||||
label: String,
|
label: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
fontFamily: FontFamily = FontFamily.Monospace,
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
@@ -498,7 +508,7 @@ private fun TerminalKeyButton(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = fontFamily,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 13.sp
|
fontSize = 13.sp
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user