Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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.

GluecronUtil.ktBlame142 lines · 1 contributor
0377886Claude1package com.gluecron
2
3import com.gluecron.settings.GluecronSettingsState
4import com.intellij.notification.NotificationGroupManager
5import com.intellij.notification.NotificationType
6import com.intellij.openapi.ide.CopyPasteManager
7import com.intellij.openapi.project.Project
8import git4idea.repo.GitRepositoryManager
9import java.awt.Desktop
10import java.net.URI
11import java.net.http.HttpClient
12import java.net.http.HttpRequest
13import java.net.http.HttpResponse
14import 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 */
24object 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}