import QtQuick
// Thin client for GitGud's mobile REST API (/api/mobile/v1) — the same
// surface the Android app speaks, authenticated with a Personal Access
// Token. Every call takes a callback(success, dataOrMessage).
QtObject {
id: gitgud
property string baseUrl: ""
property string accessToken: ""
readonly property bool configured: normalizedBase() !== "" && accessToken.trim() !== ""
signal error(string message)
function normalizedBase() {
var url = (baseUrl || "").trim()
while (url.length > 0 && url.charAt(url.length - 1) === "/") {
url = url.substring(0, url.length - 1)
}
return url
}
function repoPath(fullName) {
var parts = fullName.split("/")
return "/repos/" + encodeURIComponent(parts[0]) + "/" + encodeURIComponent(parts[1])
}
// QML's XMLHttpRequest ignores `timeout`/`ontimeout`, and a socket
// that was open across a suspend can hang without ever reaching DONE.
// Every request therefore gets its own watchdog Timer, and stays in
// `inFlight` until it settles so `abortAll()` can drop it on wake.
property int timeoutMs: 20000
property var inFlight: []
readonly property Component watchdog: Component {
Timer { repeat: false }
}
// Abandon every outstanding request without calling its callback —
// the caller is about to start over and doesn't want stale answers.
function abortAll() {
var pending = inFlight
inFlight = []
for (var i = 0; i < pending.length; i++) pending[i].cancel()
}
// callback(ok, dataOrMessage, network): `network` is true when the
// server was never reached (offline, DNS, timeout) rather than when it
// answered with an error, so callers can retry sooner.
function request(method, path, callback) {
if (!configured) {
var msg = "GitGud URL and access token must be configured"
error(msg)
if (callback) callback(false, msg, false)
return
}
var xhr = new XMLHttpRequest()
xhr.open(method, normalizedBase() + "/api/mobile/v1" + path, true)
xhr.setRequestHeader("Authorization", "Bearer " + accessToken.trim())
xhr.setRequestHeader("Accept", "application/json")
var finished = false
var timer = watchdog.createObject(gitgud, { interval: timeoutMs })
var entry = { cancel: function() { settle(); xhr.abort() } }
inFlight = inFlight.concat([entry])
// Marks the request done exactly once, whichever way it ends.
function settle() {
if (finished) return false
finished = true
timer.stop()
timer.destroy()
inFlight = inFlight.filter(function(e) { return e !== entry })
return true
}
function succeed(data) {
if (!settle()) return
if (callback) callback(true, data, false)
}
function fail(message, network) {
if (!settle()) return
console.log("GitGud API error:", method, path, message)
error(message)
if (callback) callback(false, message, !!network)
}
timer.triggered.connect(function() {
fail("Timed out talking to " + normalizedBase(), true)
xhr.abort()
})
timer.start()
xhr.onreadystatechange = function() {
if (xhr.readyState !== XMLHttpRequest.DONE || finished) return
if (xhr.status >= 200 && xhr.status < 300) {
var data
try {
data = JSON.parse(xhr.responseText || "{}")
} catch (e) {
fail("Could not parse response from " + path, false)
return
}
succeed(data)
return
}
// Every 4xx is {"message": "..."} with a reason worth showing.
var serverMessage = ""
try {
serverMessage = JSON.parse(xhr.responseText).message || ""
} catch (e) {}
if (xhr.status === 0) {
fail("Could not reach " + normalizedBase(), true)
} else if (xhr.status === 401) {
fail(serverMessage || "Authentication failed — check your access token")
} else if (xhr.status === 403) {
fail(serverMessage || "Token is not allowed to do that")
} else if (xhr.status === 404) {
fail(serverMessage || "Not found: " + path)
} else {
fail(serverMessage || ("Request failed: HTTP " + xhr.status))
}
}
xhr.send()
}
// ── identity ────────────────────────────────────────────────────────
// {user, scopes, mobile, instance}
function me(callback) {
request("GET", "/me", callback)
}
// Wraps /me into a human-readable verdict, including whether the
// token carries the scopes this widget needs.
function testConnection(callback) {
me(function(ok, data) {
if (!ok) {
callback(false, data)
return
}
var scopes = data.scopes || []
var missing = []
if (scopes.indexOf("repo:read") === -1) missing.push("repo:read")
if (scopes.indexOf("ci:read") === -1) missing.push("ci:read")
var who = data.user ? (data.user.display_name || data.user.handle) : "?"
if (missing.length > 0) {
callback(false, "Connected as " + who + ", but the token is missing: " + missing.join(", "))
} else {
var note = scopes.indexOf("ci:write") === -1 ? " (read-only: add ci:write to re-run and cancel)" : ""
callback(true, "Connected as " + who + note, data)
}
})
}
// ── repositories ────────────────────────────────────────────────────
// {namespaces: [{handle, kind, name}], repositories: [{full_name, owner, ci_status, ...}]}
function listRepos(callback) {
request("GET", "/repos", callback)
}
// ── CI ──────────────────────────────────────────────────────────────
function listRuns(fullName, limit, callback) {
request("GET", repoPath(fullName) + "/runs?limit=" + (limit || 10), callback)
}
// {run, jobs, artifacts, can_manage}
function getRun(fullName, number, callback) {
request("GET", repoPath(fullName) + "/runs/" + number, callback)
}
// {job, steps: [{step, lines: [{seq, body}]}], truncated}
function getJobLogs(fullName, jobId, limit, callback) {
request("GET", repoPath(fullName) + "/jobs/" + jobId + "/logs?limit=" + (limit || 2000), callback)
}
function rerunRun(fullName, number, callback) {
request("POST", repoPath(fullName) + "/runs/" + number + "/rerun", callback)
}
function rerunJob(fullName, number, jobKey, callback) {
request("POST", repoPath(fullName) + "/runs/" + number + "/jobs/" + encodeURIComponent(jobKey) + "/rerun", callback)
}
function cancelRun(fullName, number, callback) {
request("POST", repoPath(fullName) + "/runs/" + number + "/cancel", callback)
}
function pullUrl(fullName, number) {
return normalizedBase() + "/r/" + fullName + "/pulls/" + number
}
function webUrl(fullName, runNumber) {
var url = normalizedBase() + "/r/" + fullName
if (runNumber !== undefined) url += "/actions/" + runNumber
return url
}
}
neiam
/gitgud-plasmoid
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
7.8 KiB · text
History
010513e