Blame · Line-by-line history
GluecronUtil.kt
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 0377886 | 1 | package com.gluecron |
| 2 | ||
| 3 | import com.gluecron.settings.GluecronSettingsState | |
| 4 | import com.intellij.notification.NotificationGroupManager | |
| 5 | import com.intellij.notification.NotificationType | |
| 6 | import com.intellij.openapi.ide.CopyPasteManager | |
| 7 | import com.intellij.openapi.project.Project | |
| 8 | import git4idea.repo.GitRepositoryManager | |
| 9 | import java.awt.Desktop | |
| 10 | import java.net.URI | |
| 11 | import java.net.http.HttpClient | |
| 12 | import java.net.http.HttpRequest | |
| 13 | import java.net.http.HttpResponse | |
| 14 | import java.time.Duration | |
| 15 | ||
| 16 | /** | |
| 17 | * Shared utilities for Gluecron actions. | |
| 18 | * | |
| 19 | * - [detectOwnerRepo] — infers `owner/repo` from the project's git remote URL | |
| 20 | * - [openBrowser] — opens a URL in the system's default browser | |
| 21 | * - [apiPost] — synchronous POST helper used by MergePrAction | |
| 22 | * - [notify] — show a balloon notification | |
| 23 | */ | |
| 24 | object GluecronUtil { | |
| 25 | ||
| 26 | /** | |
| 27 | * Detects the owner and repository name from the first git remote URL | |
| 28 | * in the project that contains the configured Gluecron host, or falls | |
| 29 | * back to any remote named "origin". | |
| 30 | * | |
| 31 | * Handles both HTTPS and SSH remote formats: | |
| 32 | * https://gluecron.com/owner/repo.git | |
| 33 | * git@gluecron.com:owner/repo.git | |
| 34 | * | |
| 35 | * Returns null if no usable git remote is found. | |
| 36 | */ | |
| 37 | fun detectOwnerRepo(project: Project): Pair<String, String>? { | |
| 38 | val settings = GluecronSettingsState.getInstance() | |
| 39 | val manager = GitRepositoryManager.getInstance(project) | |
| 40 | val repos = manager.repositories | |
| 41 | if (repos.isEmpty()) return null | |
| 42 | ||
| 43 | // Prefer remotes whose URL contains the configured host | |
| 44 | val hostDomain = settings.host | |
| 45 | .removePrefix("https://") | |
| 46 | .removePrefix("http://") | |
| 47 | .trimEnd('/') | |
| 48 | ||
| 49 | fun parseRemoteUrl(url: String): Pair<String, String>? { | |
| 50 | // Normalise: strip protocol + host prefix or git@ prefix | |
| 51 | val cleaned = url | |
| 52 | .replace(Regex("^https?://[^/]+/"), "") | |
| 53 | .replace(Regex("^git@[^:]+:"), "") | |
| 54 | .removeSuffix(".git") | |
| 55 | val parts = cleaned.split("/").filter { it.isNotBlank() } | |
| 56 | if (parts.size < 2) return null | |
| 57 | return Pair(parts[0], parts[1]) | |
| 58 | } | |
| 59 | ||
| 60 | for (repo in repos) { | |
| 61 | val remotes = repo.remotes | |
| 62 | // Try host-matching remote first | |
| 63 | for (remote in remotes) { | |
| 64 | val url = remote.firstUrl ?: continue | |
| 65 | if (url.contains(hostDomain)) { | |
| 66 | return parseRemoteUrl(url) | |
| 67 | } | |
| 68 | } | |
| 69 | // Fall back to any remote named "origin" | |
| 70 | val origin = remotes.firstOrNull { it.name == "origin" } | |
| 71 | if (origin != null) { | |
| 72 | val url = origin.firstUrl ?: continue | |
| 73 | return parseRemoteUrl(url) | |
| 74 | } | |
| 75 | } | |
| 76 | return null | |
| 77 | } | |
| 78 | ||
| 79 | /** | |
| 80 | * Returns the name of the currently checked-out branch in the first | |
| 81 | * git repository found in the project, or null if none. | |
| 82 | */ | |
| 83 | fun currentBranch(project: Project): String? { | |
| 84 | val manager = GitRepositoryManager.getInstance(project) | |
| 85 | return manager.repositories.firstOrNull() | |
| 86 | ?.currentBranchName | |
| 87 | } | |
| 88 | ||
| 89 | /** | |
| 90 | * Opens [url] in the system's default browser. | |
| 91 | * Falls back gracefully if Desktop is not supported. | |
| 92 | */ | |
| 93 | fun openBrowser(url: String) { | |
| 94 | if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { | |
| 95 | Desktop.getDesktop().browse(URI(url)) | |
| 96 | } | |
| 97 | } | |
| 98 | ||
| 99 | /** | |
| 100 | * Performs a synchronous HTTP POST to [path] (relative to the configured host) | |
| 101 | * with a JSON [body]. Returns the response body as a string, or throws on error. | |
| 102 | * | |
| 103 | * This is intentionally minimal — used only by MergePrAction which needs a | |
| 104 | * blocking call so we can surface the result in the same action invocation. | |
| 105 | */ | |
| 106 | fun apiPost(path: String, body: String): String { | |
| 107 | val settings = GluecronSettingsState.getInstance() | |
| 108 | val url = "${settings.host}$path" | |
| 109 | val token = settings.token | |
| 110 | ||
| 111 | val client = HttpClient.newBuilder() | |
| 112 | .connectTimeout(Duration.ofSeconds(10)) | |
| 113 | .build() | |
| 114 | ||
| 115 | val requestBuilder = HttpRequest.newBuilder() | |
| 116 | .uri(URI(url)) | |
| 117 | .timeout(Duration.ofSeconds(30)) | |
| 118 | .header("Content-Type", "application/json") | |
| 119 | .header("Accept", "application/json") | |
| 120 | .POST(HttpRequest.BodyPublishers.ofString(body)) | |
| 121 | ||
| 122 | if (token.isNotBlank()) { | |
| 123 | requestBuilder.header("Authorization", "Bearer $token") | |
| 124 | } | |
| 125 | ||
| 126 | val response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()) | |
| 127 | if (response.statusCode() !in 200..299) { | |
| 128 | throw RuntimeException("HTTP ${response.statusCode()}: ${response.body().take(200)}") | |
| 129 | } | |
| 130 | return response.body() | |
| 131 | } | |
| 132 | ||
| 133 | /** | |
| 134 | * Shows a balloon notification attached to [project]. | |
| 135 | */ | |
| 136 | fun notify(project: Project, message: String, type: NotificationType = NotificationType.INFORMATION) { | |
| 137 | NotificationGroupManager.getInstance() | |
| 138 | .getNotificationGroup("Gluecron Notifications") | |
| 139 | .createNotification("Gluecron", message, type) | |
| 140 | .notify(project) | |
| 141 | } | |
| 142 | } |