Coverage / src/handoff.ts 91.9%
68 of 74 lines run, 5 of 5 functions, 23 of 30 branches · run #26 on main · run missed
| 1 | 0× | // "Resolve in VS Code" (OxygenHub's conflict resolver hands a conflict to |
| 2 | 0× | // the extension): vscode://oxygenhub.oxygen-vscode/resolve?url=…&bookmark=…&path=… |
| 3 | 0× | // Parsing and matching live here, apart from VS Code, so they are tested. |
| 4 | 1× | |
| 5 | export interface ResolveRequest { | |
| 6 | /** The repository's clone URL on the hub. */ | |
| 7 | url: string; | |
| 8 | bookmark: string; | |
| 9 | /** Relative to the repository root. */ | |
| 10 | path: string; | |
| 11 | } | |
| 12 | 1× | |
| 13 | 0× | /** Read a resolve request's query string, or say what is wrong with it. */ |
| 14 | 1× | export function parseResolveQuery(query: string): ResolveRequest | string { |
| 15 | 1× | const q = new URLSearchParams(query); |
| 16 | 1× | const url = (q.get("url") ?? "").trim(); |
| 17 | 1× | const bookmark = (q.get("bookmark") ?? "").trim(); |
| 18 | 1× | const path = (q.get("path") ?? "").trim().replace(/^\/+/, ""); |
| 19 | 1× | if (!/^https?:\/\/[^\s]+$/.test(url)) { |
| 20 | 1× | return "the link names no repository URL"; |
| 21 | 1× | } |
| 22 | 1× | if (!bookmark || /[\s~^:?*[\\]/.test(bookmark) || bookmark.includes("..")) { |
| 23 | 2× | return "the link names no valid bookmark"; |
| 24 | 2× | } |
| 25 | 1× | if (!path || path.split("/").some((c) => c === "" || c === "." || c === ".." || c === ".oxy")) { |
| 26 | 2× | return "the link names no valid path"; |
| 27 | 2× | } |
| 28 | 1× | return { url, bookmark, path }; |
| 29 | 1× | } |
| 30 | 1× | |
| 31 | 1× | /** Whether two remote URLs name the same repository (case of the host and a trailing slash aside). */ |
| 32 | 1× | export function sameRemote(a: string, b: string): boolean { |
| 33 | 3× | const norm = (u: string) => { |
| 34 | 6× | try { |
| 35 | 6× | const x = new URL(u.trim()); |
| 36 | 6× | return `${x.protocol}//${x.host.toLowerCase()}${x.pathname.replace(/\/+$/, "").replace(/\.oxy$/, "")}`; |
| 37 | 6× | } catch { |
| 38 | 0× | return u.trim().replace(/\/+$/, ""); |
| 39 | 0× | } |
| 40 | 3× | }; |
| 41 | 3× | return norm(a) === norm(b); |
| 42 | 5× | } |
| 43 | 5× | |
| 44 | export interface PlanRequest { | |
| 45 | url: string; | |
| 46 | bookmark: string; | |
| 47 | /** `squash` (change into target) or `rebase` (change onto target). */ | |
| 48 | kind: "squash" | "rebase"; | |
| 49 | change: string; | |
| 50 | target: string; | |
| 51 | } | |
| 52 | 5× | |
| 53 | 5× | /** |
| 54 | 5× | * A rewrite planned on OxygenHub's swimlanes: |
| 55 | 5× | * vscode://oxygenhub.oxygen-vscode/plan?url=…&bookmark=…&kind=…&change=…&target=… |
| 56 | 5× | */ |
| 57 | 5× | export function parsePlanQuery(query: string): PlanRequest | string { |
| 58 | 5× | const q = new URLSearchParams(query); |
| 59 | 5× | const base = parseResolveQuery(new URLSearchParams({ url: q.get("url") ?? "", bookmark: q.get("bookmark") ?? "", path: "x" }).toString()); |
| 60 | 5× | if (typeof base === "string") { |
| 61 | 1× | return base; |
| 62 | 1× | } |
| 63 | 4× | const kind = q.get("kind"); |
| 64 | 5× | if (kind !== "squash" && kind !== "rebase") { |
| 65 | 1× | return "the link names no squash or rebase"; |
| 66 | 1× | } |
| 67 | 3× | const hex = /^[0-9a-f]{64}$/; |
| 68 | 5× | const change = q.get("change") ?? ""; |
| 69 | 5× | const target = q.get("target") ?? ""; |
| 70 | 5× | if (!hex.test(change) || !hex.test(target) || change === target) { |
| 71 | 2× | return "the link names no two changes"; |
| 72 | 2× | } |
| 73 | 5× | return { url: base.url, bookmark: base.bookmark, kind, change, target }; |
| 74 | 5× | } |