Split oversized runtime, sandbox, and interactive add files into focused helpers
This commit is contained in:
@@ -2,46 +2,16 @@ package solutions.tretter.githugandroid
|
||||
|
||||
import android.os.Build
|
||||
import android.content.Context
|
||||
import android.system.Os
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
class GitRepositoryRuntime private constructor(
|
||||
private val context: Context?,
|
||||
private val nativeGitOverride: File?,
|
||||
private val sandboxesRoot: File,
|
||||
) {
|
||||
private companion object {
|
||||
const val SyntheticRemoteUrl = "remote"
|
||||
val RequiredGitCommandAliases = listOf(
|
||||
"add",
|
||||
"branch",
|
||||
"checkout",
|
||||
"commit",
|
||||
"commit-tree",
|
||||
"config",
|
||||
"diff",
|
||||
"fetch",
|
||||
"index-pack",
|
||||
"merge",
|
||||
"merge-base",
|
||||
"pack-objects",
|
||||
"pull",
|
||||
"push",
|
||||
"read-tree",
|
||||
"rebase",
|
||||
"receive-pack",
|
||||
"rev-list",
|
||||
"rev-parse",
|
||||
"show-ref",
|
||||
"stash",
|
||||
"status",
|
||||
"symbolic-ref",
|
||||
"unpack-objects",
|
||||
"update-ref",
|
||||
"upload-pack",
|
||||
)
|
||||
}
|
||||
private val processRunner = GitProcessRunner(context, sandboxesRoot)
|
||||
private val helperCommands = GitHelperCommands(processRunner)
|
||||
private val levelMaterializer = GitLevelMaterializer(::runGit)
|
||||
|
||||
constructor(context: Context) : this(
|
||||
context = context.applicationContext,
|
||||
@@ -107,7 +77,7 @@ class GitRepositoryRuntime private constructor(
|
||||
runGit(nativeGit, sandbox, listOf("checkout", "-B", desired.headBranch))
|
||||
}
|
||||
|
||||
materializeNativeGitState(nativeGit, sandbox, desired, level)
|
||||
levelMaterializer.materialize(nativeGit, sandbox, desired, level)
|
||||
}
|
||||
|
||||
return inspectSandbox(level)
|
||||
@@ -157,25 +127,14 @@ class GitRepositoryRuntime private constructor(
|
||||
val result = when (expandedTokens.first()) {
|
||||
"git" -> currentRepo to runGit(nativeGit, workingDir, expandedTokens.drop(1), invocation.environment).outputLines
|
||||
"help", "?" -> currentRepo to commandReferenceLines()
|
||||
else -> executeExecutableShortcut(sandboxRoot, workingDir, currentRepo, expandedTokens)
|
||||
?: executeHelperCommand(sandboxRoot, workingDir, currentRepo, expandedTokens)
|
||||
else -> helperCommands.executeExecutableShortcut(sandboxRoot, workingDir, currentRepo, expandedTokens)
|
||||
?: helperCommands.execute(sandboxRoot, workingDir, currentRepo, expandedTokens)
|
||||
}
|
||||
|
||||
val inspectedRepo = inspectSandbox(level).copy(currentDir = result.first.currentDir)
|
||||
return augmentObservedRepoFacts(currentRepo, inspectedRepo, expandedTokens, result.second) to result.second
|
||||
}
|
||||
|
||||
private fun executeExecutableShortcut(
|
||||
sandboxRoot: File,
|
||||
workingDir: File,
|
||||
currentRepo: RepoState,
|
||||
tokens: List<String>,
|
||||
): Pair<RepoState, List<String>>? {
|
||||
val executable = tokens.firstOrNull() ?: return null
|
||||
if (!executable.startsWith("./") || executable.length <= 2) return null
|
||||
return executeShellScript(sandboxRoot, workingDir, currentRepo, listOf("sh", executable) + tokens.drop(1))
|
||||
}
|
||||
|
||||
fun completionCandidates(level: Level, currentRepo: RepoState, directoriesOnly: Boolean): List<String> {
|
||||
requireNativeGit()
|
||||
val sandbox = sandboxDir(level)
|
||||
@@ -213,7 +172,7 @@ class GitRepositoryRuntime private constructor(
|
||||
}
|
||||
|
||||
fun gitManPage(level: Level, currentRepo: RepoState, topic: String): String {
|
||||
bundledManPage(topic)?.let { return it }
|
||||
bundledGitManPage(context, topic)?.let { return it }
|
||||
|
||||
val nativeGit = requireNativeGit()
|
||||
|
||||
@@ -224,7 +183,7 @@ class GitRepositoryRuntime private constructor(
|
||||
return result.outputLines
|
||||
.filterNot { it.contains("no man viewer", ignoreCase = true) }
|
||||
.joinToString("\n")
|
||||
.ifBlank { placeholderManPage(topic) }
|
||||
.ifBlank { placeholderGitManPage(context, topic) }
|
||||
}
|
||||
|
||||
fun readEditorFile(level: Level, currentRepo: RepoState, path: String): Pair<String, List<String>> {
|
||||
@@ -382,301 +341,6 @@ class GitRepositoryRuntime private constructor(
|
||||
return inspectSandbox(level).copy(currentDir = currentRepo.currentDir) to result.outputLines
|
||||
}
|
||||
|
||||
private fun executeHelperCommand(sandboxRoot: File, workingDir: File, currentRepo: RepoState, tokens: List<String>): Pair<RepoState, List<String>> {
|
||||
return when (tokens.first()) {
|
||||
"ls", "dir" -> currentRepo to workingDir.listFiles()
|
||||
?.sortedBy { it.name }
|
||||
?.map { it.name }
|
||||
.orEmpty()
|
||||
"pwd" -> currentRepo to listOf(
|
||||
"/sandbox" + if (workingDir == sandboxRoot) "" else "/${workingDir.relativeTo(sandboxRoot).path}"
|
||||
)
|
||||
"cat" -> {
|
||||
val target = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: cat <file>")
|
||||
val file = File(workingDir, target)
|
||||
if (!file.exists() || file.isDirectory) currentRepo to listOf("cat: $target: No such file")
|
||||
else currentRepo to file.readLines().ifEmpty { listOf("") }
|
||||
}
|
||||
"sh" -> executeShellScript(sandboxRoot, workingDir, currentRepo, tokens)
|
||||
"touch" -> {
|
||||
val target = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: touch <file>")
|
||||
val file = File(workingDir, target)
|
||||
if (file.exists()) {
|
||||
currentRepo to listOf("$target already exists")
|
||||
} else {
|
||||
file.parentFile?.mkdirs()
|
||||
file.writeText("")
|
||||
currentRepo to emptyList()
|
||||
}
|
||||
}
|
||||
"mkdir", "md" -> {
|
||||
val target = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: mkdir <dir>")
|
||||
val dir = File(workingDir, target)
|
||||
if (dir.exists()) currentRepo to listOf("mkdir: $target: File exists") else {
|
||||
dir.mkdirs()
|
||||
currentRepo to emptyList()
|
||||
}
|
||||
}
|
||||
"cd", "cd.." -> {
|
||||
val target = if (tokens.first() == "cd..") ".." else tokens.getOrNull(1)
|
||||
?: return currentRepo to listOf("usage: cd <dir>")
|
||||
val dir = File(workingDir, target).canonicalFile
|
||||
when {
|
||||
!dir.exists() -> currentRepo to listOf("cd: $target: does not exist")
|
||||
!dir.isDirectory -> currentRepo to listOf("cd: $target: Not a directory")
|
||||
!dir.path.startsWith(sandboxRoot.path) -> currentRepo to listOf("cd: $target: Permission denied")
|
||||
else -> {
|
||||
val relativeDir = sandboxRoot.toPath().relativize(dir.toPath()).toString().ifEmpty { "." }
|
||||
currentRepo.copy(currentDir = relativeDir) to emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
"rm", "del" -> {
|
||||
val targets = tokens.drop(1)
|
||||
if (targets.isEmpty()) return currentRepo to listOf("usage: rm <path>")
|
||||
val errors = targets.mapNotNull { target ->
|
||||
val file = File(workingDir, target)
|
||||
if (!file.exists()) {
|
||||
"${tokens.first()}: $target: No such file or directory"
|
||||
} else {
|
||||
file.deleteRecursively()
|
||||
null
|
||||
}
|
||||
}
|
||||
currentRepo to errors
|
||||
}
|
||||
"echo" -> executeEcho(workingDir, currentRepo, tokens)
|
||||
else -> currentRepo to listOf("Command not supported in prototype runtime. Try a git command or helper command.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeShellScript(
|
||||
sandboxRoot: File,
|
||||
workingDir: File,
|
||||
currentRepo: RepoState,
|
||||
tokens: List<String>,
|
||||
): Pair<RepoState, List<String>> {
|
||||
val script = tokens.getOrNull(1) ?: return currentRepo to listOf("usage: sh <script>")
|
||||
val scriptFile = File(workingDir, script).canonicalFile
|
||||
if (!scriptFile.path.startsWith(sandboxRoot.path)) {
|
||||
return currentRepo to listOf("sh: $script: Permission denied")
|
||||
}
|
||||
if (!scriptFile.isFile) {
|
||||
return currentRepo to listOf("sh: $script: No such file")
|
||||
}
|
||||
val result = runShellProcess(
|
||||
File(shellExecutable()),
|
||||
workingDir,
|
||||
tokens.drop(1),
|
||||
)
|
||||
return currentRepo to result.outputLines
|
||||
}
|
||||
|
||||
private fun placeholderManPage(topic: String): String {
|
||||
bundledManPage(topic)?.let { return it }
|
||||
val body = when (topic) {
|
||||
"tag" -> """
|
||||
NAME
|
||||
git-tag - Create, list, delete or verify a tag object
|
||||
|
||||
SYNOPSIS
|
||||
git tag <tagname>
|
||||
git tag -d <tagname>
|
||||
git tag
|
||||
|
||||
DESCRIPTION
|
||||
Tags name specific points in history. In GitHug Android,
|
||||
tag levels usually expect you to create or reference a tag
|
||||
by name.
|
||||
|
||||
EXAMPLES
|
||||
git tag new_tag
|
||||
git tag v1.2
|
||||
""".trimIndent()
|
||||
"branch" -> """
|
||||
NAME
|
||||
git-branch - List, create, or delete branches
|
||||
|
||||
SYNOPSIS
|
||||
git branch <branchname>
|
||||
git branch -d <branchname>
|
||||
|
||||
EXAMPLES
|
||||
git branch test_code
|
||||
git branch -d delete_me
|
||||
""".trimIndent()
|
||||
"checkout" -> """
|
||||
NAME
|
||||
git-checkout - Switch branches or restore files
|
||||
|
||||
SYNOPSIS
|
||||
git checkout <branch>
|
||||
git checkout -b <branch>
|
||||
git checkout -- <file>
|
||||
|
||||
EXAMPLES
|
||||
git checkout -b my_branch
|
||||
git checkout -- config.rb
|
||||
""".trimIndent()
|
||||
"add" -> """
|
||||
NAME
|
||||
git-add - Add file contents to the index
|
||||
|
||||
SYNOPSIS
|
||||
git add <path>
|
||||
git add .
|
||||
git add -p <path>
|
||||
""".trimIndent()
|
||||
"commit" -> """
|
||||
NAME
|
||||
git-commit - Record changes to the repository
|
||||
|
||||
SYNOPSIS
|
||||
git commit -m <message>
|
||||
git commit --amend
|
||||
""".trimIndent()
|
||||
"ignore" -> """
|
||||
NAME
|
||||
gitignore - Specifies intentionally untracked files to ignore
|
||||
|
||||
SYNOPSIS
|
||||
$'GIT_DIR/info/exclude', .gitignore
|
||||
|
||||
DESCRIPTION
|
||||
A gitignore file specifies intentionally untracked files
|
||||
that Git should ignore. Each line contains a pattern.
|
||||
|
||||
Blank lines are ignored. Lines beginning with # are comments.
|
||||
An optional ! prefix negates a pattern and re-includes a path.
|
||||
A pattern ending with / matches directories.
|
||||
|
||||
EXAMPLES
|
||||
*.swp
|
||||
*.a
|
||||
!lib.a
|
||||
""".trimIndent()
|
||||
else -> """
|
||||
NAME
|
||||
git-$topic
|
||||
|
||||
DESCRIPTION
|
||||
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, environment ->
|
||||
runGit(nativeGit, directory, arguments, environment).exitCode
|
||||
}
|
||||
if (nativeSetup.setup()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
desired.config.forEach { (key, value) ->
|
||||
runGit(nativeGit, sandbox, listOf("config", key, value))
|
||||
}
|
||||
|
||||
val trackedFiles = desired.files.filter { it.tracked }
|
||||
val desiredCommitCount = maxOf(
|
||||
desired.commits.size,
|
||||
desired.branches.values.maxOrNull() ?: 0,
|
||||
if (desired.tags.isNotEmpty()) 1 else 0,
|
||||
)
|
||||
if (desiredCommitCount > 0) {
|
||||
repeat(desiredCommitCount) { index ->
|
||||
filesForSetupCommit(trackedFiles, index, desiredCommitCount).forEach { file ->
|
||||
runGit(nativeGit, sandbox, listOf("add", file.name))
|
||||
}
|
||||
val message = desired.commits.getOrNull(index)?.message ?: "Setup commit ${index + 1}"
|
||||
runGit(nativeGit, sandbox, listOf("commit", "--allow-empty", "-m", message))
|
||||
}
|
||||
|
||||
desired.branches
|
||||
.keys
|
||||
.filterNot { it == desired.headBranch }
|
||||
.forEach { branch -> runGit(nativeGit, sandbox, listOf("branch", branch)) }
|
||||
if (desired.branches.containsKey(desired.headBranch)) {
|
||||
runGit(nativeGit, sandbox, listOf("checkout", desired.headBranch))
|
||||
}
|
||||
}
|
||||
|
||||
desired.tags.forEach { tag -> runGit(nativeGit, sandbox, listOf("tag", tag)) }
|
||||
desired.remotes.forEach { (name, url) ->
|
||||
val materializedUrl = if (url == SyntheticRemoteUrl) {
|
||||
materializeSyntheticRemote(nativeGit, sandbox, name)
|
||||
} else {
|
||||
url
|
||||
}
|
||||
runGit(nativeGit, sandbox, listOf("remote", "add", name, materializedUrl))
|
||||
if (url == SyntheticRemoteUrl) {
|
||||
runGit(nativeGit, sandbox, listOf("fetch", name))
|
||||
runGit(nativeGit, sandbox, listOf("branch", "--set-upstream-to=$name/master", desired.headBranch))
|
||||
}
|
||||
}
|
||||
|
||||
val stagedTargets = desired.files.filter { it.staged }.map { it.name }
|
||||
if (stagedTargets.isNotEmpty()) {
|
||||
runGit(nativeGit, sandbox, listOf("add") + stagedTargets)
|
||||
}
|
||||
desired.files.filter { it.deleted }.forEach { file ->
|
||||
File(sandbox, file.name).delete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun filesForSetupCommit(files: List<GitFile>, index: Int, commitCount: Int): List<GitFile> {
|
||||
if (files.isEmpty()) return emptyList()
|
||||
if (commitCount <= 1) return files
|
||||
if (index == 0) return files.take(1)
|
||||
if (index == commitCount - 1) return files.drop(index)
|
||||
return files.getOrNull(index)?.let { listOf(it) }.orEmpty()
|
||||
}
|
||||
|
||||
private fun materializeSyntheticRemote(nativeGit: File, sandbox: File, remoteName: String): String {
|
||||
val remoteDir = File(sandbox.parentFile, "${sandbox.name}-$remoteName.git")
|
||||
remoteDir.deleteRecursively()
|
||||
val cloneResult = runGit(nativeGit, sandbox.parentFile ?: sandbox, listOf("clone", "--bare", sandbox.absolutePath, remoteDir.absolutePath))
|
||||
if (cloneResult.exitCode != 0) {
|
||||
remoteDir.mkdirs()
|
||||
runGit(nativeGit, remoteDir, listOf("init", "--bare"))
|
||||
}
|
||||
return remoteDir.absolutePath
|
||||
}
|
||||
|
||||
private fun executeSyntheticGitCommand(
|
||||
currentRepo: RepoState,
|
||||
command: String,
|
||||
@@ -730,27 +394,6 @@ class GitRepositoryRuntime private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return currentRepo to listOf(tokens.drop(1).joinToString(" "))
|
||||
}
|
||||
|
||||
val file = File(workingDir, tokens[redirectIndex + 1])
|
||||
file.parentFile?.mkdirs()
|
||||
val content = tokens.subList(1, redirectIndex).joinToString(" ")
|
||||
if (tokens[redirectIndex] == ">>") {
|
||||
if (file.exists() && file.length() > 0L) {
|
||||
file.appendText("\n$content")
|
||||
} else {
|
||||
file.writeText(content)
|
||||
}
|
||||
} else {
|
||||
file.writeText(content)
|
||||
}
|
||||
return currentRepo to emptyList()
|
||||
}
|
||||
|
||||
private fun inspectSandbox(level: Level): RepoState {
|
||||
val sandbox = sandboxDir(level)
|
||||
val nativeGit = requireNativeGit()
|
||||
@@ -1031,144 +674,13 @@ class GitRepositoryRuntime private constructor(
|
||||
return "'" + replace("'", "'\"'\"'") + "'"
|
||||
}
|
||||
|
||||
private fun shellExecutable(): String {
|
||||
return when {
|
||||
context != null -> "/system/bin/sh"
|
||||
File("/bin/sh").exists() -> "/bin/sh"
|
||||
else -> "sh"
|
||||
}
|
||||
}
|
||||
private fun shellExecutable(): String = processRunner.shellExecutable()
|
||||
|
||||
private fun runGit(
|
||||
binary: File,
|
||||
workingDir: File,
|
||||
arguments: List<String>,
|
||||
environment: Map<String, String> = emptyMap(),
|
||||
): ProcessExecutionResult {
|
||||
return runProcess(binary, workingDir, arguments, environment)
|
||||
}
|
||||
): ProcessExecutionResult = processRunner.runGit(binary, workingDir, arguments, environment)
|
||||
|
||||
private fun runProcess(
|
||||
binary: File,
|
||||
workingDir: File,
|
||||
arguments: List<String>,
|
||||
extraEnvironment: Map<String, String> = emptyMap(),
|
||||
): ProcessExecutionResult {
|
||||
return try {
|
||||
val gitExecPath = gitExecDirectory(binary)
|
||||
val process = ProcessBuilder(listOf(binary.absolutePath) + arguments)
|
||||
.directory(workingDir)
|
||||
.redirectErrorStream(true)
|
||||
.apply {
|
||||
environment()["HOME"] = workingDir.absolutePath
|
||||
environment()["GIT_EXEC_PATH"] = gitExecPath.absolutePath
|
||||
environment()["PATH"] = listOfNotNull(
|
||||
gitExecPath.absolutePath,
|
||||
System.getenv("PATH")?.takeIf { it.isNotBlank() },
|
||||
).joinToString(File.pathSeparator)
|
||||
environment()["GIT_CONFIG_NOSYSTEM"] = "1"
|
||||
environment()["GIT_AUTHOR_NAME"] = "GitHug"
|
||||
environment()["GIT_AUTHOR_EMAIL"] = "githug@example.com"
|
||||
environment()["GIT_COMMITTER_NAME"] = "GitHug"
|
||||
environment()["GIT_COMMITTER_EMAIL"] = "githug@example.com"
|
||||
environment()["LC_ALL"] = "C"
|
||||
environment().putAll(extraEnvironment)
|
||||
}
|
||||
.start()
|
||||
|
||||
val output = process.inputStream.bufferedReader().readLines()
|
||||
val exit = process.waitFor()
|
||||
ProcessExecutionResult(exitCode = exit, outputLines = output)
|
||||
} catch (error: Exception) {
|
||||
ProcessExecutionResult(exitCode = -1, outputLines = listOf("Native Git execution failed: ${error.message ?: error::class.java.simpleName}"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun runShellProcess(binary: File, workingDir: File, arguments: List<String>): ProcessExecutionResult {
|
||||
return try {
|
||||
val process = ProcessBuilder(listOf(binary.absolutePath) + arguments)
|
||||
.directory(workingDir)
|
||||
.redirectErrorStream(true)
|
||||
.apply {
|
||||
environment()["HOME"] = workingDir.absolutePath
|
||||
environment()["LC_ALL"] = "C"
|
||||
}
|
||||
.start()
|
||||
|
||||
val output = process.inputStream.bufferedReader().readLines()
|
||||
val exit = process.waitFor()
|
||||
ProcessExecutionResult(exitCode = exit, outputLines = output)
|
||||
} catch (error: Exception) {
|
||||
ProcessExecutionResult(exitCode = -1, outputLines = listOf("Shell execution failed: ${error.message ?: error::class.java.simpleName}"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun gitExecDirectory(binary: File): File {
|
||||
val directory = if (context != null) {
|
||||
File(context.filesDir, "git-exec")
|
||||
} else {
|
||||
File(binary.parentFile ?: binary.absoluteFile.parentFile ?: sandboxesRoot, "git-exec")
|
||||
}
|
||||
refreshGitExecDirectoryIfNeeded(directory, binary)
|
||||
directory.mkdirs()
|
||||
|
||||
File(directory, "git").also { alias ->
|
||||
if (!alias.exists()) {
|
||||
createGitAlias(binary, alias)
|
||||
}
|
||||
}
|
||||
RequiredGitCommandAliases.forEach { command ->
|
||||
listOf("git-$command", command).forEach { aliasName ->
|
||||
val alias = File(directory, aliasName)
|
||||
if (!alias.exists()) {
|
||||
createGitAlias(binary, alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
return directory
|
||||
}
|
||||
|
||||
private fun refreshGitExecDirectoryIfNeeded(directory: File, binary: File) {
|
||||
val fingerprint = buildString {
|
||||
append(binary.absolutePath)
|
||||
append('\n')
|
||||
append(binary.length())
|
||||
append('\n')
|
||||
append(binary.lastModified())
|
||||
}
|
||||
val stamp = File(directory, ".binary-fingerprint")
|
||||
val previousFingerprint = stamp.takeIf { it.isFile }?.readText()
|
||||
if (previousFingerprint != fingerprint) {
|
||||
directory.deleteRecursively()
|
||||
}
|
||||
directory.mkdirs()
|
||||
stamp.writeText(fingerprint)
|
||||
}
|
||||
|
||||
private fun createGitAlias(binary: File, alias: File) {
|
||||
if (alias.exists() || Files.isSymbolicLink(alias.toPath())) {
|
||||
alias.delete()
|
||||
}
|
||||
if (context != null) {
|
||||
try {
|
||||
Os.symlink(binary.absolutePath, alias.absolutePath)
|
||||
alias.setExecutable(true, false)
|
||||
return
|
||||
} catch (_: Exception) {
|
||||
// Fall through to the portable options below.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Files.createSymbolicLink(alias.toPath(), binary.toPath())
|
||||
} catch (_: Exception) {
|
||||
binary.copyTo(alias, overwrite = true)
|
||||
}
|
||||
alias.setExecutable(true, false)
|
||||
}
|
||||
}
|
||||
|
||||
private data class ProcessExecutionResult(
|
||||
val exitCode: Int,
|
||||
val outputLines: List<String>,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user