|
|
|
|
@@ -68,6 +68,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
writeText(file.content)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
File(sandbox, desired.currentDir).mkdirs()
|
|
|
|
|
|
|
|
|
|
val needsGit = desired.initialized || desired.files.any { it.staged || it.tracked } || desired.commits.isNotEmpty()
|
|
|
|
|
if (needsGit) {
|
|
|
|
|
@@ -80,7 +81,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
levelMaterializer.materialize(nativeGit, sandbox, desired, level)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return inspectSandbox(level)
|
|
|
|
|
return inspectSandbox(level, desired.currentDir).copy(currentDir = desired.currentDir)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun execute(level: Level, currentRepo: RepoState, command: String): Pair<RepoState, List<String>> {
|
|
|
|
|
@@ -99,7 +100,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
val shellTokens = GitSandboxEngine.tokenizeShellCommand(command)
|
|
|
|
|
val invocation = parseEnvironmentPrefixedCommand(shellTokens)
|
|
|
|
|
val tokens = invocation.command.map { it.value }
|
|
|
|
|
if (tokens.isEmpty()) return inspectSandbox(level).copy(currentDir = currentRepo.currentDir) to emptyList()
|
|
|
|
|
if (tokens.isEmpty()) return inspectSandbox(level, currentRepo.currentDir).copy(currentDir = currentRepo.currentDir) to emptyList()
|
|
|
|
|
if (currentRepo.interactiveAddSession != null) {
|
|
|
|
|
val (updatedRepo, output) = GitSandboxEngine.execute(currentRepo, command)
|
|
|
|
|
val newlyStagedPaths = updatedRepo.files.filter { updatedFile ->
|
|
|
|
|
@@ -108,7 +109,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
if (newlyStagedPaths.isNotEmpty()) {
|
|
|
|
|
runGit(nativeGit, workingDir, listOf("add") + newlyStagedPaths)
|
|
|
|
|
}
|
|
|
|
|
val inspectedRepo = inspectSandbox(level).copy(
|
|
|
|
|
val inspectedRepo = inspectSandbox(level, updatedRepo.currentDir).copy(
|
|
|
|
|
currentDir = updatedRepo.currentDir,
|
|
|
|
|
interactiveAddSession = updatedRepo.interactiveAddSession,
|
|
|
|
|
)
|
|
|
|
|
@@ -131,7 +132,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
?: helperCommands.execute(sandboxRoot, workingDir, currentRepo, expandedTokens)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val inspectedRepo = inspectSandbox(level).copy(currentDir = result.first.currentDir)
|
|
|
|
|
val inspectedRepo = inspectSandbox(level, result.first.currentDir).copy(currentDir = result.first.currentDir)
|
|
|
|
|
return augmentObservedRepoFacts(currentRepo, inspectedRepo, expandedTokens, result.second) to result.second
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -219,7 +220,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
|
|
|
|
|
file.parentFile?.mkdirs()
|
|
|
|
|
file.writeText(content)
|
|
|
|
|
return inspectSandbox(level).copy(currentDir = currentRepo.currentDir) to listOf("Saved $path")
|
|
|
|
|
return inspectSandbox(level, currentRepo.currentDir).copy(currentDir = currentRepo.currentDir) to listOf("Saved $path")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun gitEditorInitialContent(level: Level, currentRepo: RepoState, invocation: GitEditorInvocation): String {
|
|
|
|
|
@@ -245,7 +246,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
appendLine("#")
|
|
|
|
|
appendLine("# Commands:")
|
|
|
|
|
appendLine("# p, pick <commit> = use commit")
|
|
|
|
|
appendLine("# r, reword <commit> = use commit, but edit the commit message")
|
|
|
|
|
appendLine("# r, reword <commit> = use commit and replace its message with the text on this line")
|
|
|
|
|
appendLine("# e, edit <commit> = use commit, but stop for amending")
|
|
|
|
|
appendLine("# s, squash <commit> = use commit, but meld into previous commit")
|
|
|
|
|
appendLine("# f, fixup [-C | -c] <commit> = like squash but keep only the previous commit's log message")
|
|
|
|
|
@@ -282,7 +283,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
if (newlyStagedPaths.isNotEmpty()) {
|
|
|
|
|
runGit(nativeGit, workingDir, listOf("add") + newlyStagedPaths)
|
|
|
|
|
}
|
|
|
|
|
val inspectedRepo = inspectSandbox(level).copy(
|
|
|
|
|
val inspectedRepo = inspectSandbox(level, updatedRepo.currentDir).copy(
|
|
|
|
|
currentDir = updatedRepo.currentDir,
|
|
|
|
|
interactiveAddSession = updatedRepo.interactiveAddSession,
|
|
|
|
|
)
|
|
|
|
|
@@ -300,7 +301,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
val result = runGit(nativeGit, workingDir, arguments)
|
|
|
|
|
messageFile.delete()
|
|
|
|
|
|
|
|
|
|
return inspectSandbox(level).copy(currentDir = currentRepo.currentDir) to result.outputLines
|
|
|
|
|
return inspectSandbox(level, currentRepo.currentDir).copy(currentDir = currentRepo.currentDir) to result.outputLines
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun executeInteractiveRebaseEditorCommand(
|
|
|
|
|
@@ -316,6 +317,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
val todoFile = File(gitDir, "GITHUG_ANDROID_REBASE_TODO").apply {
|
|
|
|
|
writeText(todo)
|
|
|
|
|
}
|
|
|
|
|
val rewordMessages = todo.lineSequence().mapNotNull(::rewordMessageFromTodoLine).toList()
|
|
|
|
|
val editorScript = File(gitDir, "githug-android-sequence-editor.sh").apply {
|
|
|
|
|
writeText(
|
|
|
|
|
"""
|
|
|
|
|
@@ -325,6 +327,31 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
)
|
|
|
|
|
setReadable(true, true)
|
|
|
|
|
}
|
|
|
|
|
val rewordMessageFiles = rewordMessages.mapIndexed { index, message ->
|
|
|
|
|
File(gitDir, "GITHUG_ANDROID_REWORD_${index + 1}").apply {
|
|
|
|
|
writeText(message.trimEnd() + "\n")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
val rewordCounterFile = File(gitDir, "GITHUG_ANDROID_REWORD_COUNTER")
|
|
|
|
|
val messageEditorScript = File(gitDir, "githug-android-message-editor.sh").apply {
|
|
|
|
|
writeText(
|
|
|
|
|
"""
|
|
|
|
|
|#!/bin/sh
|
|
|
|
|
|counter_file=${rewordCounterFile.absolutePath.toShellSingleQuoted()}
|
|
|
|
|
|index=0
|
|
|
|
|
|if [ -f "${'$'}counter_file" ]; then
|
|
|
|
|
| index=$(cat "${'$'}counter_file")
|
|
|
|
|
|fi
|
|
|
|
|
|index=$((index + 1))
|
|
|
|
|
|printf '%s\n' "${'$'}index" > "${'$'}counter_file"
|
|
|
|
|
|message_file=${File(gitDir, "GITHUG_ANDROID_REWORD_").absolutePath.toShellSingleQuoted()}"${'$'}index"
|
|
|
|
|
|if [ -f "${'$'}message_file" ]; then
|
|
|
|
|
| cat "${'$'}message_file" > "$1"
|
|
|
|
|
|fi
|
|
|
|
|
|""".trimMargin(),
|
|
|
|
|
)
|
|
|
|
|
setReadable(true, true)
|
|
|
|
|
}
|
|
|
|
|
val arguments = GitSandboxEngine.tokenizeCommand(invocation.command).drop(1)
|
|
|
|
|
val result = runGit(
|
|
|
|
|
nativeGit,
|
|
|
|
|
@@ -332,13 +359,28 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
arguments,
|
|
|
|
|
mapOf(
|
|
|
|
|
"GIT_SEQUENCE_EDITOR" to "${shellExecutable().toShellSingleQuoted()} ${editorScript.absolutePath.toShellSingleQuoted()}",
|
|
|
|
|
"GIT_EDITOR" to "true",
|
|
|
|
|
"GIT_EDITOR" to if (rewordMessages.isEmpty()) {
|
|
|
|
|
"true"
|
|
|
|
|
} else {
|
|
|
|
|
"${shellExecutable().toShellSingleQuoted()} ${messageEditorScript.absolutePath.toShellSingleQuoted()}"
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
todoFile.delete()
|
|
|
|
|
editorScript.delete()
|
|
|
|
|
rewordMessageFiles.forEach { it.delete() }
|
|
|
|
|
rewordCounterFile.delete()
|
|
|
|
|
messageEditorScript.delete()
|
|
|
|
|
|
|
|
|
|
return inspectSandbox(level).copy(currentDir = currentRepo.currentDir) to result.outputLines
|
|
|
|
|
return inspectSandbox(level, currentRepo.currentDir).copy(currentDir = currentRepo.currentDir) to result.outputLines
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun rewordMessageFromTodoLine(line: String): String? {
|
|
|
|
|
val trimmed = line.trim()
|
|
|
|
|
if (trimmed.isEmpty() || trimmed.startsWith("#")) return null
|
|
|
|
|
val parts = trimmed.split(Regex("\\s+"), limit = 3)
|
|
|
|
|
if (parts.size < 3 || parts[0] !in setOf("r", "reword")) return null
|
|
|
|
|
return parts[2].takeIf { it.isNotBlank() }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun executeSyntheticGitCommand(
|
|
|
|
|
@@ -394,22 +436,33 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun inspectSandbox(level: Level): RepoState {
|
|
|
|
|
private fun inspectSandbox(level: Level, currentDir: String = "."): RepoState {
|
|
|
|
|
val sandbox = sandboxDir(level)
|
|
|
|
|
val nativeGit = requireNativeGit()
|
|
|
|
|
val filesOnDisk = sandbox.walkTopDown()
|
|
|
|
|
.filter { it.isFile && !it.relativeTo(sandbox).path.startsWith(".git/") }
|
|
|
|
|
val workingDir = File(sandbox, currentDir).canonicalFile
|
|
|
|
|
.takeIf { it.path == sandbox.canonicalPath || it.path.startsWith(sandbox.canonicalPath + File.separator) }
|
|
|
|
|
?: sandbox
|
|
|
|
|
val repositoryRoot = generateSequence(workingDir) { directory ->
|
|
|
|
|
directory.parentFile?.takeIf {
|
|
|
|
|
it.path == sandbox.canonicalPath || it.path.startsWith(sandbox.canonicalPath + File.separator)
|
|
|
|
|
}
|
|
|
|
|
}.firstOrNull { File(it, ".git").exists() }
|
|
|
|
|
val inspectionRoot = repositoryRoot ?: sandbox
|
|
|
|
|
val filesOnDisk = inspectionRoot.walkTopDown()
|
|
|
|
|
.filter { it.isFile && !it.relativeTo(inspectionRoot).path.startsWith(".git/") }
|
|
|
|
|
.orEmpty()
|
|
|
|
|
.toList()
|
|
|
|
|
|
|
|
|
|
if (!File(sandbox, ".git").exists()) {
|
|
|
|
|
if (repositoryRoot == null) {
|
|
|
|
|
return RepoState(
|
|
|
|
|
initialized = File(sandbox, ".git").exists(),
|
|
|
|
|
files = filesOnDisk.map { GitFile(name = it.name, content = it.readText()) },
|
|
|
|
|
initialized = false,
|
|
|
|
|
files = filesOnDisk.map {
|
|
|
|
|
GitFile(name = it.relativeTo(inspectionRoot).path, content = it.readText())
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val statusResult = runGit(nativeGit, sandbox, listOf("status", "--porcelain"))
|
|
|
|
|
val statusResult = runGit(nativeGit, inspectionRoot, listOf("status", "--porcelain"))
|
|
|
|
|
val statusMap = mutableMapOf<String, Pair<Boolean, Boolean>>()
|
|
|
|
|
val deletedStatusPaths = mutableSetOf<String>()
|
|
|
|
|
statusResult.outputLines.forEach { line ->
|
|
|
|
|
@@ -425,16 +478,16 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val logResult = runGit(nativeGit, sandbox, listOf("log", "--pretty=format:%h\t%at\t%P\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"))
|
|
|
|
|
val remoteResult = runGit(nativeGit, sandbox, listOf("remote", "-v"))
|
|
|
|
|
val headResult = runGit(nativeGit, sandbox, listOf("branch", "--show-current"))
|
|
|
|
|
val exactTagResult = runGit(nativeGit, sandbox, listOf("describe", "--tags", "--exact-match"))
|
|
|
|
|
val userNameResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.name"))
|
|
|
|
|
val userEmailResult = runGit(nativeGit, sandbox, listOf("config", "--get", "user.email"))
|
|
|
|
|
val fetchHeadCount = File(sandbox, ".git/FETCH_HEAD")
|
|
|
|
|
val logResult = runGit(nativeGit, inspectionRoot, listOf("log", "--pretty=format:%h\t%at\t%P\t%s"))
|
|
|
|
|
val branchResult = runGit(nativeGit, inspectionRoot, listOf("branch", "--list"))
|
|
|
|
|
val remoteBranchResult = runGit(nativeGit, inspectionRoot, listOf("branch", "-r", "--list"))
|
|
|
|
|
val tagResult = runGit(nativeGit, inspectionRoot, listOf("tag", "--list"))
|
|
|
|
|
val remoteResult = runGit(nativeGit, inspectionRoot, listOf("remote", "-v"))
|
|
|
|
|
val headResult = runGit(nativeGit, inspectionRoot, listOf("branch", "--show-current"))
|
|
|
|
|
val exactTagResult = runGit(nativeGit, inspectionRoot, listOf("describe", "--tags", "--exact-match"))
|
|
|
|
|
val userNameResult = runGit(nativeGit, inspectionRoot, listOf("config", "--get", "user.name"))
|
|
|
|
|
val userEmailResult = runGit(nativeGit, inspectionRoot, listOf("config", "--get", "user.email"))
|
|
|
|
|
val fetchHeadCount = File(inspectionRoot, ".git/FETCH_HEAD")
|
|
|
|
|
.takeIf { it.isFile }
|
|
|
|
|
?.readLines()
|
|
|
|
|
?.count { it.isNotBlank() }
|
|
|
|
|
@@ -474,7 +527,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
.map { it.removePrefix("*").trim() }
|
|
|
|
|
.filter { it.isNotBlank() && !it.startsWith("(") }
|
|
|
|
|
.associateWith { branch ->
|
|
|
|
|
runGit(nativeGit, sandbox, listOf("rev-list", "--count", branch))
|
|
|
|
|
runGit(nativeGit, inspectionRoot, listOf("rev-list", "--count", branch))
|
|
|
|
|
.outputLines
|
|
|
|
|
.firstOrNull()
|
|
|
|
|
?.toIntOrNull()
|
|
|
|
|
@@ -490,7 +543,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
return RepoState(
|
|
|
|
|
initialized = true,
|
|
|
|
|
files = filesOnDisk.map { file ->
|
|
|
|
|
val relativePath = file.relativeTo(sandbox).path
|
|
|
|
|
val relativePath = file.relativeTo(inspectionRoot).path
|
|
|
|
|
val (staged, tracked) = statusMap[relativePath] ?: (false to true)
|
|
|
|
|
GitFile(
|
|
|
|
|
name = relativePath,
|
|
|
|
|
@@ -499,7 +552,7 @@ class GitRepositoryRuntime private constructor(
|
|
|
|
|
tracked = tracked,
|
|
|
|
|
)
|
|
|
|
|
} + deletedStatusPaths
|
|
|
|
|
.filterNot { deletedPath -> filesOnDisk.any { it.relativeTo(sandbox).path == deletedPath } }
|
|
|
|
|
.filterNot { deletedPath -> filesOnDisk.any { it.relativeTo(inspectionRoot).path == deletedPath } }
|
|
|
|
|
.map { deletedPath ->
|
|
|
|
|
val (staged, tracked) = statusMap[deletedPath] ?: (false to true)
|
|
|
|
|
GitFile(
|
|
|
|
|
|