diff --git a/build.gradle.kts b/build.gradle.kts index e1d98d1245ea..00d4c8b9faa9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -164,6 +164,9 @@ dependencies { // Calculator includeImplementation(libs.keval) + // Repo mgmt + includeImplementation(libs.jgit) + detektPlugins(libs.detektrules.neu) detektPlugins(project(":detekt")) detektPlugins(libs.detektrules.ktlint) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f013b45cd8a3..026aecb9b130 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ gson = "2.13.1" guava = "33.2.1-jre" changelog-builder = "1.1.3" methanol = "1.8.3" +jgit = "7.6.0.202603022253-r" shadow = "9.3.1" loom = "1.15-SNAPSHOT" @@ -42,6 +43,7 @@ kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = " changelog-builder = { module = "com.github.SkyHanniStudios:SkyHanniChangelogBuilder", version.ref = "changelog-builder" } methanol = { module = "com.github.mizosoft.methanol:methanol", version.ref = "methanol" } guava = { module = "com.google.guava:guava", version.ref = "guava" } +jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" } devauth = { module = "me.djtheredstoner:DevAuth-fabric", version.ref = "devauth" } junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } diff --git a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt index 42bb955b8102..0c4c4d593119 100644 --- a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt +++ b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt @@ -138,9 +138,6 @@ object SkyHanniMod : CompatCoroutineManager by SkyHanniCoroutineManager( lateinit var configManager: ConfigManager val logger: Logger = LogManager.getLogger("SkyHanni") - fun getLogger(name: String): Logger { - return LogManager.getLogger("SkyHanni.$name") - } val modules: MutableList = ArrayList() diff --git a/src/main/java/at/hannibal2/skyhanni/data/git/commit/Commit.kt b/src/main/java/at/hannibal2/skyhanni/data/git/commit/Commit.kt new file mode 100644 index 000000000000..728ae487a2cf --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/git/commit/Commit.kt @@ -0,0 +1,14 @@ +package at.hannibal2.skyhanni.data.git.commit + +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +data class Commit( + @Expose val author: ShortCommitAuthor, + @Expose val committer: ShortCommitAuthor, + @Expose val message: String, + @Expose val tree: CommitTree, + @Expose val url: String, + @Expose @field:SerializedName("comment_count") val commentCount: Int, + @Expose val verification: CommitVerification, +) diff --git a/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitAuthor.kt b/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitAuthor.kt new file mode 100644 index 000000000000..d7e6657a481b --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitAuthor.kt @@ -0,0 +1,26 @@ +package at.hannibal2.skyhanni.data.git.commit + +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +data class CommitAuthor( + @Expose val login: String, + @Expose val id: Int, + @Expose @field:SerializedName("node_id") val nodeId: String, + @Expose @field:SerializedName("avatar_url") val avatarUrl: String, + @Expose @field:SerializedName("gravatar_id") val gravatarId: String, + @Expose val url: String, + @Expose @field:SerializedName("html_url") val htmlUrl: String, + @Expose @field:SerializedName("followers_url") val followersUrl: String, + @Expose @field:SerializedName("following_url") val followingUrl: String, + @Expose @field:SerializedName("gists_url") val gistsUrl: String, + @Expose @field:SerializedName("starred_url") val starredUrl: String, + @Expose @field:SerializedName("subscriptions_url") val subscriptionsUrl: String, + @Expose @field:SerializedName("organizations_url") val organizationsUrl: String, + @Expose @field:SerializedName("repos_url") val reposUrl: String, + @Expose @field:SerializedName("events_url") val eventsUrl: String, + @Expose @field:SerializedName("received_events_url") val receivedEventsUrl: String, + @Expose val type: String, + @Expose @field:SerializedName("user_view_type") val userViewType: String, + @Expose @field:SerializedName("site_admin") val siteAdmin: Boolean, +) diff --git a/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitFile.kt b/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitFile.kt new file mode 100644 index 000000000000..9b63dee511d0 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitFile.kt @@ -0,0 +1,17 @@ +package at.hannibal2.skyhanni.data.git.commit + +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +data class CommitFile( + @Expose val sha: String, + @Expose val filename: String, + @Expose val status: String, + @Expose val additions: Int, + @Expose val deletions: Int, + @Expose val changes: Int, + @Expose @field:SerializedName("blob_url") val blobUrl: String, + @Expose @field:SerializedName("raw_url") val rawUrl: String, + @Expose @field:SerializedName("contents_url") val contentsUrl: String, + @Expose @field:SerializedName("patch") val patch: String, +) diff --git a/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitStats.kt b/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitStats.kt new file mode 100644 index 000000000000..de7c77ab9f03 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitStats.kt @@ -0,0 +1,9 @@ +package at.hannibal2.skyhanni.data.git.commit + +import com.google.gson.annotations.Expose + +data class CommitStats( + @Expose val total: Long, + @Expose val additions: Long, + @Expose val deletions: Long, +) diff --git a/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitTree.kt b/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitTree.kt new file mode 100644 index 000000000000..555e93492b07 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitTree.kt @@ -0,0 +1,10 @@ +package at.hannibal2.skyhanni.data.git.commit + +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +data class CommitTree( + @Expose val sha: String, + @Expose val url: String, + @Expose @field:SerializedName("html_url") val htmlUrl: String? = null, +) diff --git a/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitVerification.kt b/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitVerification.kt new file mode 100644 index 000000000000..acef93e7ad71 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitVerification.kt @@ -0,0 +1,19 @@ +package at.hannibal2.skyhanni.data.git.commit + +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.SimpleTimeMark.Companion.asTimeMark +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName +import java.time.Instant + +data class CommitVerification( + @Expose val verified: Boolean, + @Expose val reason: String, + @Expose val signature: String? = null, + @Expose val payload: String? = null, + @Expose @field:SerializedName("verified_at") private val verifiedAtString: String? = null, +) { + val verifiedAt: SimpleTimeMark? get() = verifiedAtString?.let { + Instant.parse(it).toEpochMilli().asTimeMark() + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitsApiResponse.kt b/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitsApiResponse.kt new file mode 100644 index 000000000000..31f912a5c066 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/git/commit/CommitsApiResponse.kt @@ -0,0 +1,20 @@ +package at.hannibal2.skyhanni.data.git.commit + +import at.hannibal2.skyhanni.utils.KSerializable +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +@KSerializable +data class CommitsApiResponse( + @Expose val sha: String, + @Expose @field:SerializedName("node_id") val nodeId: String, + @Expose val commit: Commit, + @Expose val url: String, + @Expose @field:SerializedName("html_url") val htmlUrl: String, + @Expose @field:SerializedName("comments_url") val commentsUrl: String, + @Expose val author: CommitAuthor, + @Expose val committer: CommitAuthor, + @Expose val parents: List, + @Expose val stats: CommitStats, + @Expose val files: List, +) diff --git a/src/main/java/at/hannibal2/skyhanni/data/git/commit/ShortCommitAuthor.kt b/src/main/java/at/hannibal2/skyhanni/data/git/commit/ShortCommitAuthor.kt new file mode 100644 index 000000000000..e5e8e950a692 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/git/commit/ShortCommitAuthor.kt @@ -0,0 +1,15 @@ +package at.hannibal2.skyhanni.data.git.commit + +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.SimpleTimeMark.Companion.asTimeMark +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName +import java.time.Instant + +data class ShortCommitAuthor( + @Expose val name: String, + @Expose val email: String, + @Expose @field:SerializedName("date") private val dateString: String, +) { + val date: SimpleTimeMark get() = Instant.parse(dateString).toEpochMilli().asTimeMark() +} diff --git a/src/main/java/at/hannibal2/skyhanni/data/repo/AbstractRepoManager.kt b/src/main/java/at/hannibal2/skyhanni/data/repo/AbstractRepoManager.kt index 0678d0802318..9233dc475da3 100644 --- a/src/main/java/at/hannibal2/skyhanni/data/repo/AbstractRepoManager.kt +++ b/src/main/java/at/hannibal2/skyhanni/data/repo/AbstractRepoManager.kt @@ -10,7 +10,6 @@ import at.hannibal2.skyhanni.data.repo.filesystem.DiskRepoFileSystem import at.hannibal2.skyhanni.data.repo.filesystem.MemoryRepoFileSystem import at.hannibal2.skyhanni.data.repo.filesystem.RepoFileSystem import at.hannibal2.skyhanni.utils.ChatUtils -import at.hannibal2.skyhanni.utils.GitHubUtils import at.hannibal2.skyhanni.utils.SimpleTimeMark import at.hannibal2.skyhanni.utils.chat.TextHelper import at.hannibal2.skyhanni.utils.chat.TextHelper.asComponent @@ -57,7 +56,7 @@ abstract class AbstractRepoManager { abstract val configDirectory: File @PublishedApi - internal val logger by lazy { RepoLogger("[Repo - $commonName]") } + internal val logger by lazy { RepoLogger(this) } val repoDirectory by lazy { // ~/.minecraft/config/[...]/repo File(configDirectory, "repo") @@ -82,8 +81,9 @@ abstract class AbstractRepoManager { private val commonShortName by lazy { commonShortNameCased.lowercase() } private val successfulConstants = mutableSetOf() private val unsuccessfulConstants = mutableSetOf() - private val githubRepoLocation: GitHubUtils.RepoLocation - get() = GitHubUtils.RepoLocation(config.location, SkyHanniMod.feature.dev.debug.logRepoErrors) + private val gitRepo: GitRepo by lazy { + GitRepo(config.location, SkyHanniMod.feature.dev.debug.logRepoErrors) + } private val repoMutex = Mutex() val repoLocked get() = repoMutex.isLocked private val repoIOCoroutineConfig = repoCoroutineConfig("IO") @@ -118,7 +118,7 @@ abstract class AbstractRepoManager { logger.throwErrorWithCause("Could not load constant '$constant'", e) } fun getFailedConstants() = unsuccessfulConstants.toList() - fun getGitHubRepoPath(): String = githubRepoLocation.location + fun getGitHubRepoPath(): String = gitRepo.location private fun repoCoroutineConfig(repoAction: String, repoMutex: Mutex? = null) = CoroutineSettings( name = "$commonName Repo $repoAction Coroutine", @@ -174,7 +174,7 @@ abstract class AbstractRepoManager { else -> repoDirectory.list()?.size?.let { "$it top-level entries in repo directory" } ?: "repo directory exists but could not be listed" } - logger.logNonDestructiveError("Repo file not found: $path ($repoDiagnostic)") + logger.error("Repo file not found: $path ($repoDiagnostic)") return null } @@ -201,31 +201,32 @@ abstract class AbstractRepoManager { progress.update("Remove and re-download, forceReset=$forceReset") shouldManuallyReload = true if (!config.location.valid) { - logger.errorToChat("Invalid $commonName repo settings detected, resetting default settings.") + logger.chatError("Invalid $commonName repo settings detected, resetting default settings.") resetRepositoryLocation() } repoUpdateCoroutineConfig.launch { if (!fetchAndUnpackRepo(progress, command = true, forceReset = forceReset).canContinue) { logger.warn("Failed to fetch & unpack repo - aborting repository reload.") + dumpDiagnosticsToLog("operation" to "fetchAndUnpack", "forceReset" to forceReset) return@launch } reloadRepository(progress, "$commonName repo updated successfully.") if (unsuccessfulConstants.isEmpty() && !isUsingBackup) return@launch - val informed = logger.logErrorStateWithData( + val informed = logger.errorStateWithData( "Error updating reading $commonName repo", "no success", "usingBackupRepo" to isUsingBackup, "unsuccessfulConstants" to unsuccessfulConstants, ) if (informed) return@launch - logger.logToChat("§cFailed to load the $commonShortNameCased repo! See above for more infos.") + logger.chat("§cFailed to load the $commonShortNameCased repo! See above for more infos.") } } private fun resetRepositoryLocation(manual: Boolean = false) = with(config.location) { if (hasDefaultSettings()) { - if (manual) logger.logToChat("$commonShortNameCased repo settings are already on default!") + if (manual) logger.chat("$commonShortNameCased repo settings are already on default!") return } @@ -259,7 +260,10 @@ abstract class AbstractRepoManager { // i.e. before any internal return path could call progress.end() // In all normal return paths above, progress is ended explicitly // We only need to guard here against the coroutine being torn down prematurely - if (cause != null) progress.end("init ended abnormally: ${cause.message}") + if (cause != null) { + progress.end("init ended abnormally: ${cause.message}") + dumpDiagnosticsToLog("exceptionType" to cause::class.simpleName, "exception" to cause.message) + } } } @@ -303,7 +307,8 @@ abstract class AbstractRepoManager { logger.debug("Successfully switched to backup repo") return FetchUnpackResult.SWITCHED_TO_BACKUP }.onFailure { e -> - logger.logNonDestructiveError("Failed to switch to backup repo: ${e.message}") + logger.error("Failed to switch to backup repo: ${e.message}") + dumpDiagnosticsToLog("operation" to "switchToBackupRepo", "exceptionType" to e::class.simpleName, "exception" to e.message) progress.update("reason: ${e.message ?: "no reason"}") progress.end("Failed to switch to backup repo") }.getOrDefault(FetchUnpackResult.FAILED) @@ -318,18 +323,18 @@ abstract class AbstractRepoManager { val comparison = getCommitComparison(silentError = false) val isOutdated = comparison?.let { !it.hashesMatch } ?: run { - logger.logNonDestructiveError("Failed to fetch latest commit for repo status check.") + logger.error("Failed to fetch latest commit for repo status check.") false } if (isOutdated) { - logger.logToChat("Repo Issue caught, however the repo is outdated.\n§aTrying to update it now...") + logger.chat("Repo Issue caught, however the repo is outdated.\n§aTrying to update it now...") val result = fetchAndUnpackRepo(progress, command = false) if (result == FetchUnpackResult.SUCCESS) { - logger.logToChat("§a$commonName repo updated successfully!") + logger.chat("§a$commonName repo updated successfully!") progress.update("repo update successfully!") return true } else { - logger.logToChat("§cFailed to update the $commonName repo.") + logger.chat("§cFailed to update the $commonName repo.") progress.update("Failed to update the $commonName repo.") } } @@ -342,19 +347,20 @@ abstract class AbstractRepoManager { val (currentDownloadedCommit, _) = commitStorage.readFromFile() ?: RepoCommit() if (unsuccessfulConstants.isEmpty() && successfulConstants.isNotEmpty()) { - logger.logToChat("$commonName repo working fine! Commit hash: §b$currentDownloadedCommit§r") + logger.chat("$commonName repo working fine! Commit hash: §b$currentDownloadedCommit§r") reportExtraStatusInfo() return } if (!command && isRepeatErrorOrFixed(progress)) return - logger.errorToChat("$commonName repo has errors! Commit hash: §b$currentDownloadedCommit§r") + logger.chatError("$commonName repo has errors! Commit hash: §b$currentDownloadedCommit§r") + dumpDiagnosticsToLog() - if (successfulConstants.isNotEmpty()) logger.logToChat("Successful Constants §7(${successfulConstants.size}):") - for (constant in successfulConstants) logger.logToChat(" - §7$constant") + if (successfulConstants.isNotEmpty()) logger.chat("Successful Constants §7(${successfulConstants.size}):") + for (constant in successfulConstants) logger.chat(" - §7$constant") - logger.logToChat("Unsuccessful Constants §7(${unsuccessfulConstants.size}):", color = "§e") - for (constant in unsuccessfulConstants) logger.logToChat(" - §7$constant", color = "§e") + logger.chat("Unsuccessful Constants §7(${unsuccessfulConstants.size}):", color = "§e") + for (constant in unsuccessfulConstants) logger.chat(" - §7$constant", color = "§e") progress.update("reportExtraStatusInfo") reportExtraStatusInfo() @@ -377,6 +383,7 @@ abstract class AbstractRepoManager { } }.map { it.asComponent() } TextHelper.multiline(text).send() + dumpDiagnosticsToLog() } private enum class FetchUnpackResult(val canContinue: Boolean = true) { @@ -394,7 +401,7 @@ abstract class AbstractRepoManager { */ private suspend fun getCommitComparison(silentError: Boolean): RepoComparison? { localRepoCommit = commitStorage.readFromFile() ?: RepoCommit() - val latestRepoCommit = githubRepoLocation.getLatestCommit(silentError) ?: return null + val latestRepoCommit = gitRepo.getLatestCommit(silentError) ?: return null return RepoComparison(commonName, localRepoCommit, latestRepoCommit) } @@ -427,6 +434,17 @@ abstract class AbstractRepoManager { forceReset: Boolean = false, switchToBackupOnFail: Boolean = true, ): FetchUnpackResult { + progress.update("try loading repo from jgit") + with(gitRepo) { + if (repoFileSystem.loadFromJGit()) { + progress.update("loaded from jgit") + return FetchUnpackResult.SUCCESS + } else { + progress.update("failed to load repo from jgit") + dumpDiagnosticsToLog("operation" to "jgit load") + } + } + progress.update("fetchAndUnpackRepo") val comparison = getCommitComparison(silentError) ?: run { return if (switchToBackupOnFail) switchToBackupRepo(progress) @@ -449,9 +467,10 @@ abstract class AbstractRepoManager { prepCleanRepoFileSystem(progress) progress.update("downloadCommitZipToFile") - if (!githubRepoLocation.downloadCommitZipToFile(repoZipFile)) { + if (!gitRepo.downloadCommitZipToFile(repoZipFile)) { progress.update("Failed to download the repo zip file from GitHub.") - logger.logNonDestructiveError("Failed to download the repo zip file from GitHub.") + logger.error("Failed to download the repo zip file from GitHub.") + dumpDiagnosticsToLog("operation" to "download zip", "destination" to repoZipFile.name) return if (switchToBackupOnFail) switchToBackupRepo(progress) else { progress.update("FetchUnpackResult.FAILED") @@ -461,22 +480,23 @@ abstract class AbstractRepoManager { progress.update("loadFromZip") // Actually unpack the repo zip file into our local 'file system' - if (!repoFileSystem.loadFromZip(progress, repoZipFile)) { + return if (!repoFileSystem.loadFromZip(progress, repoZipFile)) { progress.update("Failed to unpack the downloaded zip file.") - logger.logNonDestructiveError("Failed to unpack the downloaded zip file.") - return if (switchToBackupOnFail) switchToBackupRepo(progress) + logger.error("Failed to unpack the downloaded zip file.") + dumpDiagnosticsToLog("operation" to "unpack zip", "zipFile" to repoZipFile.name, "zipSize" to repoZipFile.length()) + if (switchToBackupOnFail) switchToBackupRepo(progress) else FetchUnpackResult.FAILED + } else { + progress.update("writeToFile: fetchAndUnpackRepo") + commitStorage.writeToFile(comparison.latest) + isUsingBackup = false + FetchUnpackResult.SUCCESS } - - progress.update("writeToFile: fetchAndUnpackRepo") - commitStorage.writeToFile(comparison.latest) - isUsingBackup = false - return FetchUnpackResult.SUCCESS } private fun prepCleanRepoFileSystem(progress: ChatProgressUpdates) { progress.update("deleteRecursively") - repoDirectory.deleteRecursively() + repoDirectory.listFiles()?.forEach { if (it != logger.logsDir) it.deleteRecursively() } progress.update("createAndClean") repoFileSystem = repoDirectory.let { root -> @@ -514,7 +534,7 @@ abstract class AbstractRepoManager { event.post { error -> if (loadingError) return@post progress.update("Error while posting repo reload event: ${error.message}") - logger.logErrorWithData(error, "Error while posting repo reload event") + logger.errorWithData(error, "Error while posting repo reload event") loadingError = true } progress.update("post done") @@ -526,7 +546,7 @@ abstract class AbstractRepoManager { progress.update("transitionAfterReload done") if (answerMessage.isNotEmpty() && !loadingError) { progress.end("answerMessage: $answerMessage") - logger.logToChat("§a$answerMessage") + logger.chat("§a$answerMessage") } else if (loadingError) { progress.end("Error with the $commonShortName repo detected") ChatUtils.clickableChat( @@ -540,4 +560,25 @@ abstract class AbstractRepoManager { progress.end("done reloading $commonShortName repo") } } + + internal fun dumpDiagnosticsToLog(vararg extraData: Pair) = with(logger) { + val loc = config.location + val fileCount = repoDirectory.walkTopDown().count { it.isFile } + debug("Diagnostic dump for $commonName:") + debug(" config: autoUpdate=${config.repoAutoUpdate}, unzipToMemory=${config.unzipToMemory}") + debug(" location: ${loc.user}/${loc.repoName}@${loc.branch} (default=${loc.hasDefaultSettings()})") + debug(" localCommit: sha=${localRepoCommit.sha ?: "none"}, time=${localRepoCommit.time ?: "none"}") + debug(" usingBackup: $isUsingBackup") + debug(" repoDir: exists=${repoDirectory.exists()}, files=$fileCount, path=${repoDirectory.absolutePath}") + debug(" gitPresent: ${repoDirectory.resolve(".git").exists()}") + debug(" fileSystem: ${repoFileSystem::class.simpleName}") + debug(" successful: ${successfulConstants.size}, failed: ${unsuccessfulConstants.size}") + if (unsuccessfulConstants.isNotEmpty()) { + debug(" failedConstants: ${unsuccessfulConstants.joinToString()}") + } + if (extraData.isNotEmpty()) { + debug(" extra:") + for ((key, value) in extraData) debug(" $key: $value") + } + } } diff --git a/src/main/java/at/hannibal2/skyhanni/data/repo/GitRepo.kt b/src/main/java/at/hannibal2/skyhanni/data/repo/GitRepo.kt new file mode 100644 index 000000000000..1ceca08b426d --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/repo/GitRepo.kt @@ -0,0 +1,179 @@ +package at.hannibal2.skyhanni.data.repo + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.config.ConfigManager +import at.hannibal2.skyhanni.data.repo.filesystem.RepoFileSystem +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.api.ApiUtils +import at.hannibal2.skyhanni.data.git.commit.CommitsApiResponse +import at.hannibal2.skyhanni.utils.json.fromJsonOrNull +import org.eclipse.jgit.api.CreateBranchCommand +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.PullResult +import org.eclipse.jgit.api.ResetCommand.ResetType +import org.eclipse.jgit.api.TransportConfigCallback +import org.eclipse.jgit.lib.Constants +import org.eclipse.jgit.merge.ContentMergeStrategy +import org.eclipse.jgit.merge.MergeStrategy +import org.eclipse.jgit.transport.RefSpec +import org.eclipse.jgit.transport.SshSessionFactory +import org.eclipse.jgit.transport.SshTransport +import org.eclipse.jgit.util.FS +import java.io.File + +/** + * Represents the location of a Git repository. + * @param config the [AbstractRepoLocationConfig] containing the information for this repository. + * @param shouldError If true, will throw an error if the latest commit SHA cannot be fetched, or if the download fails. + */ +data class GitRepo( + val config: AbstractRepoLocationConfig, + private val shouldError: Boolean = false, +) { + private val user get() = config.user + private val repo get() = config.repoName + private val branch get() = config.branch + + val location get() = "$user/$repo/$branch" + + private val commitApiUrl: String get() = "https://api.github.com/repos/$user/$repo/commits/$branch" + private val shallowRefSpec get() = "+refs/heads/$branch:refs/remotes/origin/$branch" + private val sshConfigurer = TransportConfigCallback { transport -> + if (transport is SshTransport) transport.sshSessionFactory = SshSessionFactory.getInstance() + } + + private fun String.isSshUri() = startsWith("git@") || startsWith("ssh://") + + private fun RepoFileSystem.getAvailableSources(): List { + val sources = mutableListOf("https://github.com/$user/$repo.git") + + val userHome = FS.DETECTED.userHome() ?: return sources.also { + logger.debug("Skipping SSH fallback: Unable to determine user home directory.") + } + val keyPresent = File(userHome, ".ssh").listFiles()?.any { file -> + file.isFile && !file.name.endsWith(".pub") && file.name.startsWith("id_") + } ?: false + + if (keyPresent) sources.add("git@github.com:$user/$repo.git") + else logger.debug("Skipping SSH fallback: No private keys found in ~/.ssh/") + + return sources + } + + suspend fun getLatestCommit(silentError: Boolean = true): RepoCommit? { + val (_, jsonResponse) = ApiUtils.getJsonResponse(commitApiUrl, location, silentError).assertSuccessWithData() ?: run { + SkyHanniMod.logger.error("Failed to fetch latest commits.") + return null + } + val apiResponse = ConfigManager.gson.fromJsonOrNull(jsonResponse) ?: run { + SkyHanniMod.logger.error("Failed to parse latest commit response: $jsonResponse") + return null + } + return RepoCommit(sha = apiResponse.sha, time = apiResponse.commit.committer.date) + } + + fun RepoFileSystem.loadFromJGit(): Boolean { + val gitFile = File(root, ".git") + return if (gitFile.exists() && tryPullRepo() != null) true + else if (root.listFiles()?.isNotEmpty() == true) tryGitConvertLocalRepo() + else tryCloneRepo() + } + + private fun RepoFileSystem.tryGitConvertLocalRepo(): Boolean = runCatching { + Git.init().setDirectory(root).call().use { git -> + git.repository.config.apply { + setString("remote", "origin", "url", "https://github.com/$user/$repo.git") + setString("remote", "origin", "fetch", shallowRefSpec) + }.save() + + git.fetch().apply { + setRemote("origin") + setDepth(1) + setRefSpecs(RefSpec(shallowRefSpec)) + setProgressMonitor(RepoJGitMonitor(this@tryGitConvertLocalRepo)) + }.call() + + git.reset().apply { + setMode(ResetType.HARD) + setRef("origin/$branch") + setProgressMonitor(RepoJGitMonitor(this@tryGitConvertLocalRepo)) + }.call() + + git.branchCreate().apply { + setName(branch) + setStartPoint("origin/$branch") + setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK) + setForce(true) + }.call() + + logger.debug("Successfully linked and matched existing files for $location") + } + true + }.getOrElse { e -> + logger.error("Failed to link existing directory to Git: $e") + false + } + + private fun RepoFileSystem.tryPullRepo(): PullResult? = runCatching { + Git.open(root).use { localRepo -> + val remoteUrl = localRepo.repository.config.getString("remote", "origin", "url").orEmpty() + localRepo.pull().apply { + setRemote("origin") + setRemoteBranchName(branch) + setStrategy(MergeStrategy.THEIRS) + setContentMergeStrategy(ContentMergeStrategy.THEIRS) + if (remoteUrl.isSshUri()) setTransportConfigCallback(sshConfigurer) + setProgressMonitor(RepoJGitMonitor(this@tryPullRepo)) + }.call()?.takeIf { it.isSuccessful }?.also { + val latestHash = localRepo.repository.resolve(Constants.HEAD)?.name ?: "" + logger.debug("Pulled latest changes ($latestHash) for $location") + } + } + }.getOrElse { e -> + logger.error("Failed to pull latest changes for $location\n$e") + null + } + + private fun RepoFileSystem.tryCloneRepo(): Boolean = getAvailableSources().firstNotNullOfOrNull { source -> + val success = runCatching { + Git.cloneRepository().apply { + setURI(source) + setBranch(branch) + setDirectory(root) + setDepth(1) + setCloneAllBranches(false) + setNoCheckout(false) + if (source.isSshUri()) setTransportConfigCallback(sshConfigurer) + setProgressMonitor(RepoJGitMonitor(this@tryCloneRepo)) + }.call().use { cloned -> + logger.debug("Cloned ${cloned.repository.directory.absolutePath} for $location via $source") + } + true + }.getOrElse { e -> + logger.error("Failed to clone $location from $source\n$e") + root.deleteRecursively() + root.mkdirs() + false + } + if (success) true else null + } ?: false + + suspend fun downloadCommitZipToFile(destinationZip: File, shaOverride: String? = null): Boolean { + val shaToUse = shaOverride ?: getLatestCommit(!shouldError)?.sha ?: run { + if (shouldError) ErrorManager.skyHanniError("Cannot get full archive URL without a valid SHA") + return false + } + val fullArchiveUrl = "https://github.com/$user/$repo/archive/$shaToUse.zip" + return try { + if (shouldError) { + SkyHanniMod.logger.info("Downloading $shaToUse for $location\nUrl: $fullArchiveUrl") + } + ApiUtils.getZipResponse(destinationZip, fullArchiveUrl, location, !shouldError) + true + } catch (e: Exception) { + ErrorManager.logErrorWithData(e, "Failed to download archive from $fullArchiveUrl") + SkyHanniMod.logger.error("Failed to download archive from $fullArchiveUrl", e) + false + } + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/data/repo/RepoJGitMonitor.kt b/src/main/java/at/hannibal2/skyhanni/data/repo/RepoJGitMonitor.kt new file mode 100644 index 000000000000..3840a472fbc2 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/repo/RepoJGitMonitor.kt @@ -0,0 +1,30 @@ +package at.hannibal2.skyhanni.data.repo + +import at.hannibal2.skyhanni.data.repo.filesystem.RepoFileSystem +import org.eclipse.jgit.lib.ProgressMonitor + +class RepoJGitMonitor(private val repoFs: RepoFileSystem) : ProgressMonitor { + private var totalTasks = 0 + private var completedTasks = 0 + + override fun start(totalTasks: Int) { + this.totalTasks = totalTasks + } + + override fun beginTask(title: String, totalWork: Int) { + repoFs.logger.debug("Starting task: $title ($totalWork units)") + } + + override fun update(completed: Int) { + // This is called frequently (like every percent/file) + // Usually, you don't want to log every single update to avoid spam + } + + override fun endTask() { + completedTasks++ + repoFs.logger.debug("Task completed ($completedTasks/$totalTasks)") + } + + override fun isCancelled(): Boolean = false + override fun showDuration(enabled: Boolean) = Unit +} diff --git a/src/main/java/at/hannibal2/skyhanni/data/repo/RepoLogger.kt b/src/main/java/at/hannibal2/skyhanni/data/repo/RepoLogger.kt index c951803193c9..1454f3220093 100644 --- a/src/main/java/at/hannibal2/skyhanni/data/repo/RepoLogger.kt +++ b/src/main/java/at/hannibal2/skyhanni/data/repo/RepoLogger.kt @@ -1,24 +1,26 @@ package at.hannibal2.skyhanni.data.repo -import at.hannibal2.skyhanni.SkyHanniMod import at.hannibal2.skyhanni.test.command.ErrorManager import at.hannibal2.skyhanni.utils.ChatUtils +import at.hannibal2.skyhanni.utils.SkyHanniLogger +import java.io.File -// todo this class is a mess, it should get cleaned up and standardized with ChatUtils and ErrorManager -// should be some genericized way to create loggers that can utilize either ChatUtils or ErrorManager or SkyHanniMod.logger -// depending on the use case -class RepoLogger(private val loggingPrefix: String) { - fun debug(message: String) = SkyHanniMod.logger.debug("$loggingPrefix $message") - fun preDebug(message: String) = println("$loggingPrefix $message") - fun warn(message: String) = SkyHanniMod.logger.warn("$loggingPrefix $message") - fun logToChat(message: String, color: String = "§a") = ChatUtils.chat("$color$loggingPrefix $message", prefix = false) - fun errorToChat(error: String) = ChatUtils.userError("§c$loggingPrefix $error") - - fun logNonDestructiveError(error: String) = SkyHanniMod.logger.error("$loggingPrefix $error") - fun logError(error: String): Nothing = ErrorManager.skyHanniError("$loggingPrefix $error") - fun logErrorWithData(cause: Throwable, error: String): Boolean = +class RepoLogger(manager: AbstractRepoManager<*>) : SkyHanniLogger(manager.commonName) { + + private val loggingPrefix = "[Repo - ${manager.commonName}]" + override val logsDir = File(manager.repoDirectory, "logs") + + fun debug(message: String) = log("[DEBUG] $loggingPrefix $message") + fun warn(message: String) = log("[WARN] $loggingPrefix $message") + fun error(message: String) = log("[ERROR] $loggingPrefix $message") + + fun chat(message: String, color: String = "§a") = ChatUtils.chat("$color$loggingPrefix $message", prefix = false) + fun chatError(error: String) = ChatUtils.userError("§c$loggingPrefix $error") + + fun errorWithData(cause: Throwable, error: String): Boolean = ErrorManager.logErrorWithData(cause, "$loggingPrefix $error") - fun logErrorStateWithData( + + fun errorStateWithData( userMessage: String, internalMessage: String, vararg extraData: Pair, diff --git a/src/main/java/at/hannibal2/skyhanni/data/repo/filesystem/DiskRepoFileSystem.kt b/src/main/java/at/hannibal2/skyhanni/data/repo/filesystem/DiskRepoFileSystem.kt index ed27ac115f6b..40e476dac80b 100644 --- a/src/main/java/at/hannibal2/skyhanni/data/repo/filesystem/DiskRepoFileSystem.kt +++ b/src/main/java/at/hannibal2/skyhanni/data/repo/filesystem/DiskRepoFileSystem.kt @@ -16,7 +16,8 @@ class DiskRepoFileSystem( } override fun deleteRecursively(path: String) { - File(root, path).deleteRecursively() + if (path.isEmpty()) root.listFiles()?.forEach { if (it != logger.logsDir) it.deleteRecursively() } + else File(root, path).deleteRecursively() } override fun list(path: String) = root.resolve(path).listFiles { file -> diff --git a/src/main/java/at/hannibal2/skyhanni/data/repo/filesystem/MemoryRepoFileSystem.kt b/src/main/java/at/hannibal2/skyhanni/data/repo/filesystem/MemoryRepoFileSystem.kt index f8159c19288a..1686d9a3cd22 100644 --- a/src/main/java/at/hannibal2/skyhanni/data/repo/filesystem/MemoryRepoFileSystem.kt +++ b/src/main/java/at/hannibal2/skyhanni/data/repo/filesystem/MemoryRepoFileSystem.kt @@ -56,22 +56,28 @@ class MemoryRepoFileSystem( * [transitionAfterReload] to wait for the flush and switch to [DiskRepoFileSystem]. */ override suspend fun loadFromZip(progress: ChatProgressUpdates, zipFile: File): Boolean { - progress.update("repo file system loadFromZip") + progress.update("repo memory file system loadFromZip") val success = super.loadFromZip(progress, zipFile) - check(flushResult == null) { "loadFromZip called twice on the same MemoryRepoFileSystem instance" } + check(flushResult == null) { + "loadFromZip called twice on the same MemoryRepoFileSystem instance" + } + flushResult = flushToDisk(progress.category, root) + progress.update("loadFromZip end") + return success + } - // Snapshot the category reference now — storage may be cleared before the flush job reads it. - val progressCategory = progress.category + // Launched into the module-level scope so it outlives this call site and can be + // awaited later in transitionAfterReload. + // We use CompletableDeferred to propagate success or failure, because launchUnScoped routes + // through runWithErrorHandling which would otherwise swallow exceptions silently. + private fun flushToDisk( + category: ChatProgressUpdates.ChatProgressCategory, + root: File, + ): CompletableDeferred { val deferred = CompletableDeferred() - flushResult = deferred - - // Launched into the module-level scope so it outlives this call site and can be - // awaited later in transitionAfterReload. - // We use CompletableDeferred to propagate success or failure, because launchUnScoped routes - // through runWithErrorHandling which would otherwise swallow exceptions silently. coroutineSettings.withIOContext().launchUnScoped { try { - saveToDisk(progressCategory, root) + saveToDisk(category, root) deferred.complete(Unit) } catch (e: CancellationException) { deferred.completeExceptionally(e) @@ -81,9 +87,7 @@ class MemoryRepoFileSystem( deferred.completeExceptionally(e) } } - - progress.update("loadFromZip end") - return success + return deferred } override fun dispose() = storage.clear() @@ -92,8 +96,8 @@ class MemoryRepoFileSystem( * Waits for the background disk flush to complete, then disposes in-memory storage and * returns a [DiskRepoFileSystem] backed by [root]. * - * If the flush failed, the error is logged and the transition still proceeds — callers - * should treat unsuccessful repo constants as the signal that something went wrong on disk. + * If the flush failed, the error is logged and the transition still proceeds. + * Callers should treat unsuccessful repo constants as the signal that something went wrong on disk. */ override suspend fun transitionAfterReload(progress: ChatProgressUpdates): RepoFileSystem { val deferred = flushResult @@ -104,7 +108,7 @@ class MemoryRepoFileSystem( runCatching { it.await() }.onFailure { e -> // Disk state may be incomplete. We still transition so that memory is freed, // but callers will observe failures via unsuccessfulConstants. - progress.update("disk flush failed — repo on disk may be incomplete: ${e.message}") + progress.update("disk flush failed! repo on disk may be incomplete: ${e.message}") } } diff --git a/src/main/java/at/hannibal2/skyhanni/data/repo/filesystem/RepoFileSystem.kt b/src/main/java/at/hannibal2/skyhanni/data/repo/filesystem/RepoFileSystem.kt index c3e5c50945d0..c0ed187356ab 100644 --- a/src/main/java/at/hannibal2/skyhanni/data/repo/filesystem/RepoFileSystem.kt +++ b/src/main/java/at/hannibal2/skyhanni/data/repo/filesystem/RepoFileSystem.kt @@ -44,14 +44,13 @@ sealed interface RepoFileSystem { fun readJson(path: String): JsonElement { val bytes = readAllBytes(path) check(bytes.isNotEmpty()) { - "Repo file '$path' is empty (0 bytes) — ${pathDiagnostics(path)}" + "Repo file '$path' is empty (0 bytes)\n${pathDiagnostics(path)}" } val content = String(bytes, Charsets.UTF_8) - return ConfigManager.gson.fromJson(content, JsonElement::class.java) - ?: throw IllegalStateException( - "Repo file '$path' parsed as JSON null — file may contain only the literal 'null' or be malformed " + - "(${content.length} chars) — ${pathDiagnostics(path)}", - ) + return ConfigManager.gson.fromJson(content, JsonElement::class.java) ?: throw IllegalStateException( + "Repo file '$path' parsed as JSON null. File may contain only the literal 'null' or be malformed " + + "(${content.length} chars)\n${pathDiagnostics(path)}", + ) } /** @@ -61,8 +60,8 @@ sealed interface RepoFileSystem { * as this strongly suggests the zip is corrupt and continuing would silently produce * an unusable repo on disk. * - * This is a plain suspend function — callers are responsible for ensuring they are already - * running in an appropriate dispatcher (e.g. IO). No extra coroutine is launched here. + * This is a plain suspend function. + * Callers are responsible for ensuring they are already running in an appropriate dispatcher (e.g. IO). */ suspend fun loadFromZip(progress: ChatProgressUpdates, zipFile: File): Boolean = runCatching { progress.update("loadFromZip") @@ -80,7 +79,7 @@ sealed interface RepoFileSystem { true }.getOrElse { e -> progress.update("Failed to load repo from zip '${zipFile.name}': ${e.message}") - logger.logNonDestructiveError("Failed to load repo from zip '${zipFile.name}': ${e.message}") + logger.error("Failed to load repo from zip '${zipFile.name}': ${e.message}") false } @@ -103,9 +102,9 @@ sealed interface RepoFileSystem { val data = zip.getInputStream(entry).use { it.readBytes() } if (data.isEmpty()) { val incrementedCount = emptyDataCount + 1 - logger.logNonDestructiveError("Empty zip entry: $relativePath ($incrementedCount/$MAX_EMPTY_ZIP_ENTRIES)") + logger.error("Empty zip entry: $relativePath ($incrementedCount/$MAX_EMPTY_ZIP_ENTRIES)") check(incrementedCount <= MAX_EMPTY_ZIP_ENTRIES) { - "Aborting: $incrementedCount empty zip entries in '${zipFile.name}' — zip is likely corrupt" + "Aborting: $incrementedCount empty zip entries in '${zipFile.name}'. Zip is likely corrupt" } incrementedCount } else { diff --git a/src/main/java/at/hannibal2/skyhanni/utils/GitHubUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/GitHubUtils.kt deleted file mode 100644 index e8071e715a1f..000000000000 --- a/src/main/java/at/hannibal2/skyhanni/utils/GitHubUtils.kt +++ /dev/null @@ -1,167 +0,0 @@ -package at.hannibal2.skyhanni.utils - -import at.hannibal2.skyhanni.SkyHanniMod -import at.hannibal2.skyhanni.config.ConfigManager -import at.hannibal2.skyhanni.data.repo.AbstractRepoLocationConfig -import at.hannibal2.skyhanni.data.repo.RepoCommit -import at.hannibal2.skyhanni.test.command.ErrorManager -import at.hannibal2.skyhanni.utils.SimpleTimeMark.Companion.asTimeMark -import at.hannibal2.skyhanni.utils.api.ApiUtils -import com.google.gson.annotations.Expose -import com.google.gson.annotations.SerializedName -import java.io.File -import java.time.Instant - -object GitHubUtils { - - /** - * Represents the location of a GitHub repository. - * @param user The GitHub username or organization. - * @param repo The repository name. - * @param branch The branch name, defaults to "main". - * @param shouldError If true, will throw an error if the latest commit SHA cannot be fetched, or if the download fails. - */ - data class RepoLocation( - val user: String, - val repo: String, - val branch: String = "main", - private val shouldError: Boolean = false, - ) { - constructor(config: AbstractRepoLocationConfig, shouldError: Boolean = false) : this( - config.user, - config.repoName, - config.branch, - shouldError, - ) - - val location = "$user/$repo/$branch" - private val apiName = "GitHub - $location" - private val commitApiUrl: String = "https://api.github.com/repos/$user/$repo/commits/$branch" - - suspend fun getLatestCommit(silentError: Boolean = true): RepoCommit? { - val (_, jsonResponse) = ApiUtils.getJsonResponse(commitApiUrl, apiName, silentError).assertSuccessWithData() ?: run { - SkyHanniMod.logger.error("Failed to fetch latest commits.") - return null - } - val apiResponse = runCatching { - ConfigManager.gson.fromJson(jsonResponse, CommitsApiResponse::class.java) - }.getOrNull() ?: run { - SkyHanniMod.logger.error("Failed to parse latest commit response: $jsonResponse") - return null - } - return RepoCommit(sha = apiResponse.sha, time = apiResponse.commit.committer.date) - } - - suspend fun downloadCommitZipToFile(destinationZip: File, shaOverride: String? = null): Boolean { - val shaToUse = shaOverride ?: getLatestCommit(!shouldError)?.sha ?: run { - if (shouldError) ErrorManager.skyHanniError("Cannot get full archive URL without a valid SHA") - return false - } - val fullArchiveUrl = "https://github.com/$user/$repo/archive/$shaToUse.zip" - return try { - if (shouldError) { - SkyHanniMod.logger.info("Downloading $shaToUse for $user/$repo/$branch\nUrl: $fullArchiveUrl") - } - ApiUtils.getZipResponse(destinationZip, fullArchiveUrl, apiName, !shouldError) - true - } catch (e: Exception) { - ErrorManager.logErrorWithData(e, "Failed to download archive from $fullArchiveUrl") - SkyHanniMod.logger.error("Failed to download archive from $fullArchiveUrl", e) - false - } - } - } - - data class CommitsApiResponse( - @Expose val sha: String, - @Expose @field:SerializedName("node_id") val nodeId: String, - @Expose val commit: Commit, - @Expose val url: String, - @Expose @field:SerializedName("html_url") val htmlUrl: String, - @Expose @field:SerializedName("comments_url") val commentsUrl: String, - @Expose val author: CommitAuthor, - @Expose val committer: CommitAuthor, - @Expose val parents: List, - @Expose val stats: CommitStats, - @Expose val files: List, - ) - - data class Commit( - @Expose val author: ShortCommitAuthor, - @Expose val committer: ShortCommitAuthor, - @Expose val message: String, - @Expose val tree: CommitTree, - @Expose val url: String, - @Expose @field:SerializedName("comment_count") val commentCount: Int, - @Expose val verification: CommitVerification, - ) - - data class ShortCommitAuthor( - @Expose val name: String, - @Expose val email: String, - @Expose @field:SerializedName("date") private val dateString: String, - ) { - val date: SimpleTimeMark get() = Instant.parse(dateString).toEpochMilli().asTimeMark() - } - - data class CommitAuthor( - @Expose val login: String, - @Expose val id: Int, - @Expose @field:SerializedName("node_id") val nodeId: String, - @Expose @field:SerializedName("avatar_url") val avatarUrl: String, - @Expose @field:SerializedName("gravatar_id") val gravatarId: String, - @Expose val url: String, - @Expose @field:SerializedName("html_url") val htmlUrl: String, - @Expose @field:SerializedName("followers_url") val followersUrl: String, - @Expose @field:SerializedName("following_url") val followingUrl: String, - @Expose @field:SerializedName("gists_url") val gistsUrl: String, - @Expose @field:SerializedName("starred_url") val starredUrl: String, - @Expose @field:SerializedName("subscriptions_url") val subscriptionsUrl: String, - @Expose @field:SerializedName("organizations_url") val organizationsUrl: String, - @Expose @field:SerializedName("repos_url") val reposUrl: String, - @Expose @field:SerializedName("events_url") val eventsUrl: String, - @Expose @field:SerializedName("received_events_url") val receivedEventsUrl: String, - @Expose val type: String, - @Expose @field:SerializedName("user_view_type") val userViewType: String, - @Expose @field:SerializedName("site_admin") val siteAdmin: Boolean, - ) - - data class CommitTree( - @Expose val sha: String, - @Expose val url: String, - @Expose @field:SerializedName("html_url") val htmlUrl: String? = null, - ) - - data class CommitVerification( - @Expose val verified: Boolean, - @Expose val reason: String, - @Expose val signature: String? = null, - @Expose val payload: String? = null, - @Expose @field:SerializedName("verified_at") private val verifiedAtString: String? = null, - ) { - val verifiedAt: SimpleTimeMark? - get() = verifiedAtString?.let { - Instant.parse(it).toEpochMilli().asTimeMark() - } - } - - data class CommitStats( - @Expose val total: Long, - @Expose val additions: Long, - @Expose val deletions: Long, - ) - - data class CommitFile( - @Expose val sha: String, - @Expose val filename: String, - @Expose val status: String, - @Expose val additions: Int, - @Expose val deletions: Int, - @Expose val changes: Int, - @Expose @field:SerializedName("blob_url") val blobUrl: String, - @Expose @field:SerializedName("raw_url") val rawUrl: String, - @Expose @field:SerializedName("contents_url") val contentsUrl: String, - @Expose @field:SerializedName("patch") val patch: String, - ) - -} diff --git a/src/main/java/at/hannibal2/skyhanni/utils/SkyHanniLogger.kt b/src/main/java/at/hannibal2/skyhanni/utils/SkyHanniLogger.kt index eb1db3268cd7..9337900bf370 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/SkyHanniLogger.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/SkyHanniLogger.kt @@ -3,7 +3,6 @@ package at.hannibal2.skyhanni.utils import at.hannibal2.skyhanni.SkyHanniMod import at.hannibal2.skyhanni.utils.TimeUtils.formatCurrentTime import java.io.File -import java.io.IOException import java.text.SimpleDateFormat import java.util.logging.FileHandler import java.util.logging.Formatter @@ -11,74 +10,42 @@ import java.util.logging.LogRecord import java.util.logging.Logger import kotlin.time.Duration.Companion.days -class SkyHanniLogger(filePath: String) { +open class SkyHanniLogger(filePath: String) { private val format = SimpleDateFormat("HH:mm:ss") - private val fileName = "$PREFIX_PATH$filePath.log" - - companion object { - - private val LOG_DIRECTORY = File("config/skyhanni/logs") - // I'm ab to change this in another PR I CBA - daveed - @Suppress("PropertyName") - private var PREFIX_PATH: String - var hasDone = false - - init { - val format = SimpleDateFormat("yyyy_MM_dd/HH_mm_ss").formatCurrentTime() - PREFIX_PATH = "config/skyhanni/logs/$format/" - } + private val fullFormat by lazy { + SimpleDateFormat("yyyy_MM_dd/HH_mm_ss").formatCurrentTime() } + internal open val logsDir = File("config/skyhanni/logs") + internal open val timedFormattedDir by lazy { "$logsDir/$fullFormat" } + private val logFileName by lazy { "$timedFormattedDir/$filePath.log" } - private lateinit var logger: Logger - - private fun getLogger(): Logger { - if (::logger.isInitialized) { - return logger - } - - val initLogger = initLogger() - this.logger = initLogger - return initLogger + companion object { + private var deletedExpired = false } @Suppress("PrintStackTrace") - private fun initLogger(): Logger { - val logger = Logger.getLogger("Lorenz-Logger-" + System.nanoTime()) - try { - createParent(File(fileName)) - val handler = FileHandler(fileName) - handler.encoding = "utf-8" - logger.addHandler(handler) - logger.useParentHandlers = false - handler.formatter = object : Formatter() { - override fun format(logRecord: LogRecord): String { - val message = logRecord.message - return format.formatCurrentTime() + " $message\n" - } + private val logger: Logger by lazy { + Logger.getLogger("SkyHanni-Logger-" + System.nanoTime()).apply { + try { + File(logFileName).parentFile?.takeIf { !it.isDirectory }?.mkdirs() + FileHandler(logFileName).apply { + encoding = Charsets.UTF_8.name() + formatter = object : Formatter() { + override fun format(logRecord: LogRecord) = "${format.formatCurrentTime()} ${logRecord.message}\n" + } + }.let(::addHandler) + useParentHandlers = false + } catch (e: Exception) { + e.printStackTrace() } - } catch (e: SecurityException) { - e.printStackTrace() - } catch (e: IOException) { - e.printStackTrace() - } - - if (!hasDone && SkyBlockUtils.onHypixel) { - hasDone = true - OSUtils.deleteExpiredFiles(LOG_DIRECTORY, SkyHanniMod.feature.dev.logExpiryTime.days) - } - return logger - } - - private fun createParent(file: File) { - val parent = file.parentFile - if (parent != null && !parent.isDirectory) { - parent.mkdirs() + if (!deletedExpired && SkyBlockUtils.onHypixel) { + deletedExpired = true + OSUtils.deleteExpiredFiles(logsDir, SkyHanniMod.feature.dev.logExpiryTime.days) + } } } - fun log(text: String?) { - getLogger().info(text) - } + fun log(text: String?) = logger.info(text) }