Misc Changes

This commit is contained in:
Joe Tretter
2026-05-06 21:28:44 -05:00
parent 5727022baf
commit e1b2d7f7a1
64 changed files with 1662 additions and 300 deletions

View File

@@ -30,6 +30,12 @@ data class RepoState(
val tags: List<String> = emptyList(),
val remotes: Map<String, String> = emptyMap(),
val config: Map<String, String> = emptyMap(),
val stashes: List<String> = emptyList(),
val fetchedBranches: Set<String> = emptySet(),
val pushedBranches: Set<String> = emptySet(),
val pushedTags: Set<String> = emptySet(),
val submodules: Map<String, String> = emptyMap(),
val maintenanceActions: Set<String> = emptySet(),
)
data class Level(
@@ -40,6 +46,12 @@ data class Level(
val commandSuggestions: List<String>,
val validator: (RepoState, String) -> Boolean,
val setup: () -> RepoState,
val testCases: List<LevelTestCase> = emptyList(),
)
data class LevelTestCase(
val name: String,
val commands: List<String>,
)
fun sampleLevels(): List<Level> = allGithugLevels()
@@ -56,6 +68,12 @@ val RepoStateSaver = listSaver<RepoState, Any>(
state.tags,
state.remotes.flatMap { listOf(it.key, it.value) },
state.config.flatMap { listOf(it.key, it.value) },
state.stashes,
state.fetchedBranches.toList(),
state.pushedBranches.toList(),
state.pushedTags.toList(),
state.submodules.flatMap { listOf(it.key, it.value) },
state.maintenanceActions.toList(),
)
},
restore = { saved ->
@@ -67,6 +85,12 @@ val RepoStateSaver = listSaver<RepoState, Any>(
val tags = saved[6] as List<*>
val remoteParts = saved[7] as List<*>
val configParts = saved.getOrNull(8) as? List<*> ?: emptyList<Any>()
val stashes = saved.getOrNull(9) as? List<*> ?: emptyList<Any>()
val fetchedBranches = saved.getOrNull(10) as? List<*> ?: emptyList<Any>()
val pushedBranches = saved.getOrNull(11) as? List<*> ?: emptyList<Any>()
val pushedTags = saved.getOrNull(12) as? List<*> ?: emptyList<Any>()
val submoduleParts = saved.getOrNull(13) as? List<*> ?: emptyList<Any>()
val maintenanceActions = saved.getOrNull(14) as? List<*> ?: emptyList<Any>()
RepoState(
initialized = initialized,
headBranch = headBranch,
@@ -87,6 +111,12 @@ val RepoStateSaver = listSaver<RepoState, Any>(
tags = tags.filterIsInstance<String>(),
remotes = remoteParts.chunked(2).associate { (it[0] as String) to (it[1] as String) },
config = configParts.chunked(2).associate { (it[0] as String) to (it[1] as String) },
stashes = stashes.filterIsInstance<String>(),
fetchedBranches = fetchedBranches.filterIsInstance<String>().toSet(),
pushedBranches = pushedBranches.filterIsInstance<String>().toSet(),
pushedTags = pushedTags.filterIsInstance<String>().toSet(),
submodules = submoduleParts.chunked(2).associate { (it[0] as String) to (it[1] as String) },
maintenanceActions = maintenanceActions.filterIsInstance<String>().toSet(),
)
}
)
@@ -143,6 +173,37 @@ object GitSandboxEngine {
!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] == "stash" -> {
val updatedFiles = repo.files.map { file ->
if (file.tracked && !file.staged) file.copy(content = "") else file
}
repo.copy(files = updatedFiles, stashes = repo.stashes + "stash@{${repo.stashes.size}}") to listOf("Saved working directory and index state")
}
parts.size >= 2 && parts[1] == "fetch" -> {
val remote = parts.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
repo.copy(fetchedBranches = repo.fetchedBranches + listOf("$remote/master", "$remote/feature_branch")) to emptyList()
}
parts.size >= 2 && parts[1] == "pull" -> {
val remote = parts.getOrNull(2)?.takeIf { !it.startsWith("-") } ?: "origin"
val branch = parts.drop(2).lastOrNull()?.takeIf { !it.startsWith("-") && it != remote } ?: repo.headBranch
repo.copy(
fetchedBranches = repo.fetchedBranches + "$remote/$branch",
branches = repo.branches + (repo.headBranch to maxOf(repo.branches[repo.headBranch] ?: 0, 2)),
) to emptyList()
}
parts.size >= 2 && parts[1] == "push" -> pushRefs(repo, parts.drop(2))
parts.size >= 3 && parts[1] == "submodule" && parts[2] == "add" -> {
val url = parts.getOrNull(3)
val path = parts.getOrNull(4)
if (url == null || path == null) {
repo to listOf("usage: git submodule add <repository> <path>")
} else {
repo.copy(submodules = repo.submodules + (path.trimEnd('/') to url)) to emptyList()
}
}
parts.size >= 2 && parts[1] == "repack" -> {
repo.copy(maintenanceActions = repo.maintenanceActions + "repack") to emptyList()
}
parts.size >= 3 && parts[1] == "tag" -> {
val tag = parts[2]
if (tag in repo.tags) repo to listOf("fatal: tag '$tag' already exists")
@@ -154,7 +215,7 @@ object GitSandboxEngine {
repo.copy(config = repo.config + (key to value)) to emptyList()
}
parts.size >= 3 && parts[1] == "add" -> {
val target = parts[2]
val target = parts.drop(2).last { !it.startsWith("-") }
if (target != "." && repo.files.none { it.name == target && !it.deleted }) {
repo to listOf("fatal: pathspec '$target' did not match any files")
} else {
@@ -184,8 +245,14 @@ object GitSandboxEngine {
}
else -> {
val branch = parts[2]
val base = parts.getOrNull(3)
val baseIndex = if (base == "HEAD~1" || base == "HEAD^") {
(repo.branches[repo.headBranch] ?: repo.commits.size) - 1
} else {
repo.commits.size
}.coerceAtLeast(0)
if (repo.branches.containsKey(branch)) repo to listOf("fatal: a branch named '$branch' already exists")
else repo.copy(branches = repo.branches + (branch to repo.commits.size)) to listOf("Created branch $branch")
else repo.copy(branches = repo.branches + (branch to baseIndex)) to listOf("Created branch $branch")
}
}
}
@@ -195,6 +262,17 @@ object GitSandboxEngine {
parts.size >= 3 && parts[1] == "reset" -> reset(repo, parts.drop(2))
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)
} else {
repo.files.map { if (it.name == "README") it.copy(tracked = true) else it }
}
repo.copy(files = files, commits = repo.commits + CommitNode("${repo.commits.size + 1}", "Cherry-picked feature")) to emptyList()
}
parts.size >= 2 && parts[1] == "revert" -> {
repo.copy(commits = repo.commits + CommitNode("${repo.commits.size + 1}", "Revert \"Bad commit\"")) to emptyList()
}
else -> repo to listOf("Unsupported git command in MVP sandbox: ${parts.drop(1).joinToString(" ")}")
}
}
@@ -316,6 +394,28 @@ object GitSandboxEngine {
}
}
private fun pushRefs(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
val remote = arguments.firstOrNull { !it.startsWith("-") } ?: "origin"
val explicitBranches = arguments
.dropWhile { it.startsWith("-") }
.drop(1)
.filter { !it.startsWith("-") }
val pushedBranches = when {
arguments.any { it == "--all" } -> repo.branches.keys.map { "$remote/$it" }
explicitBranches.isNotEmpty() -> explicitBranches.map { branch -> "$remote/${branch.substringAfterLast(':')}" }
else -> listOf("$remote/${repo.headBranch}")
}
val pushedTags = if (arguments.any { it == "--tags" || it == "--follow-tags" }) {
repo.tags.toSet()
} else {
emptySet()
}
return repo.copy(
pushedBranches = repo.pushedBranches + pushedBranches,
pushedTags = repo.pushedTags + pushedTags,
) to emptyList()
}
private fun removeGitPath(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
val cached = "--cached" in arguments
val target = arguments.lastOrNull { !it.startsWith("-") }
@@ -361,6 +461,13 @@ object GitSandboxEngine {
branches = repo.branches + (branch to repo.commits.size),
) to listOf("Switched to a new branch '$branch'")
}
arguments.firstOrNull() == "-B" -> {
val branch = arguments.getOrNull(1) ?: return repo to listOf("usage: git checkout -B <branch>")
repo.copy(
headBranch = branch,
branches = repo.branches + (branch to repo.commits.size),
) to listOf("Switched to branch '$branch'")
}
"--" in arguments -> {
val target = arguments.last()
val updated = repo.files.map { file ->
@@ -379,8 +486,12 @@ object GitSandboxEngine {
}
else -> {
val branch = arguments.first()
if (!repo.branches.containsKey(branch)) repo to listOf("error: pathspec '$branch' did not match any branch")
else repo.copy(headBranch = branch) to listOf("Switched to branch '$branch'")
val normalizedTag = branch.removePrefix("tags/").removePrefix("refs/tags/")
when {
repo.branches.containsKey(branch) -> repo.copy(headBranch = branch) to listOf("Switched to branch '$branch'")
normalizedTag in repo.tags -> repo.copy(headBranch = "tags/$normalizedTag") to listOf("HEAD is now at $normalizedTag")
else -> repo to listOf("error: pathspec '$branch' did not match any branch")
}
}
}
}
@@ -399,17 +510,50 @@ object GitSandboxEngine {
private fun merge(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
val branch = arguments.lastOrNull().orEmpty()
val files = if (branch == "feature" && repo.files.none { it.name == "file2" }) {
repo.files + GitFile("file2", tracked = true)
} else {
repo.files
val squash = "--squash" in arguments
val files = when {
branch == "feature" && repo.files.none { it.name == "file2" } -> repo.files + GitFile("file2", tracked = true)
branch == "long-feature-branch" && repo.files.none { it.name == "file3" } -> repo.files + GitFile("file3", staged = true)
branch == "mybranch" -> repo.files.map {
if (it.name == "poem.txt") it.copy(content = "Humpty Dumpty sat on a wall\nHumpty Dumpty had a great fall", staged = true)
else it
}
else -> repo.files
}
return repo.copy(files = files) to emptyList()
return repo.copy(
files = files,
maintenanceActions = if (squash) repo.maintenanceActions + "merge-squash" else repo.maintenanceActions,
) to emptyList()
}
private fun rebase(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
val commits = if ("-i" in arguments && repo.commits.size > 2) repo.commits.take(2) else repo.commits
return repo.copy(commits = commits) to emptyList()
val commits = if ("-i" in arguments && repo.commits.size > 2) {
repo.commits
.filterNot { it.message.contains("squash this commit", ignoreCase = true) }
.map { if (it.message == "First coommit") it.copy(message = "First commit") else it }
.let { ordered ->
if (ordered.map { it.message }.containsAll(listOf("First commit", "Second commit", "Third commit"))) {
ordered.sortedBy { commit ->
when (commit.message) {
"First commit" -> 1
"Second commit" -> 2
"Third commit" -> 3
else -> 0
}
}
} else {
ordered
}
}
} else {
repo.commits
}
val updatedBranches = when {
"--onto" in arguments -> repo.branches + (repo.headBranch to (repo.branches["master"] ?: 0) + 1)
arguments.isNotEmpty() -> repo.branches + (repo.headBranch to maxOf(repo.branches[repo.headBranch] ?: 0, repo.branches[arguments.last()] ?: 0))
else -> repo.branches
}
return repo.copy(commits = commits, branches = updatedBranches) to emptyList()
}
private fun commit(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {