- .claude/agents/test-runner.md: New Claude Code agent definition for running cargo test suites and analyzing results, configured with haiku model for fast execution. - skills/agent-swarm-launcher/: New skill for bootstrapping coordinated multi-agent workflows with AGENTS.md reconnaissance, Agent Mail coordination, and beads task tracking. - api-review.html, phase-a-review.html: Self-contained HTML review artifacts for API audit and Phase A search pipeline review. - .beads/issues.jsonl, .beads/last-touched: Updated issue tracker state reflecting current project work items. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1655 lines
114 KiB
HTML
1655 lines
114 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Gitlore ↔ GitLab API Review</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0d1117;
|
|
--surface: #161b22;
|
|
--surface2: #1c2333;
|
|
--border: #30363d;
|
|
--text: #c9d1d9;
|
|
--text-muted: #8b949e;
|
|
--accent: #58a6ff;
|
|
--green: #3fb950;
|
|
--yellow: #d29922;
|
|
--red: #f85149;
|
|
--purple: #bc8cff;
|
|
--orange: #d18616;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
line-height: 1.6;
|
|
}
|
|
header {
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 20px 32px;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
header h1 { font-size: 20px; font-weight: 600; }
|
|
header p { color: var(--text-muted); font-size: 13px; margin-top: 4px; }
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 0;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--surface);
|
|
padding: 0 32px;
|
|
overflow-x: auto;
|
|
}
|
|
.tab {
|
|
padding: 10px 16px;
|
|
cursor: pointer;
|
|
color: var(--text-muted);
|
|
font-size: 14px;
|
|
border-bottom: 2px solid transparent;
|
|
white-space: nowrap;
|
|
transition: color 0.15s, border-color 0.15s;
|
|
user-select: none;
|
|
}
|
|
.tab:hover { color: var(--text); }
|
|
.tab.active { color: var(--text); border-bottom-color: var(--accent); }
|
|
|
|
.content { max-width: 1200px; margin: 0 auto; padding: 24px 32px; }
|
|
.panel { display: none; }
|
|
.panel.active { display: block; }
|
|
|
|
h2 { font-size: 18px; margin-bottom: 16px; color: var(--text); }
|
|
h3 { font-size: 15px; margin: 20px 0 10px; color: var(--accent); }
|
|
h4 { font-size: 13px; margin: 14px 0 6px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.card-title { font-weight: 600; font-size: 14px; margin-bottom: 8px; }
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 13px;
|
|
margin-bottom: 16px;
|
|
}
|
|
th {
|
|
text-align: left;
|
|
padding: 8px 12px;
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
color: var(--text-muted);
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.3px;
|
|
}
|
|
td {
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--border);
|
|
vertical-align: top;
|
|
}
|
|
tr:hover td { background: var(--surface2); }
|
|
|
|
code {
|
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
font-size: 12px;
|
|
background: var(--surface2);
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
color: var(--accent);
|
|
}
|
|
pre {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 12px 16px;
|
|
overflow-x: auto;
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
margin: 8px 0 16px;
|
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
color: var(--text);
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 12px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
.badge-get { background: #1a3a2a; color: var(--green); }
|
|
.badge-post { background: #2a2a1a; color: var(--yellow); }
|
|
.badge-put { background: #2a1a2a; color: var(--purple); }
|
|
.badge-delete { background: #2a1a1a; color: var(--red); }
|
|
.badge-used { background: #1a2a3a; color: var(--accent); }
|
|
.badge-unused { background: #1c1c1c; color: var(--text-muted); }
|
|
.badge-readonly { background: #1a3a2a; color: var(--green); }
|
|
|
|
.flow-diagram {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
margin: 12px 0;
|
|
}
|
|
.flow-box {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 8px 14px;
|
|
font-size: 12px;
|
|
text-align: center;
|
|
min-width: 100px;
|
|
}
|
|
.flow-box.highlight { border-color: var(--accent); }
|
|
.flow-arrow { color: var(--text-muted); font-size: 18px; }
|
|
|
|
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
@media (max-width: 768px) { .grid-2 { grid-template-columns: 1fr; } }
|
|
|
|
.coverage-bar {
|
|
height: 8px;
|
|
background: var(--surface2);
|
|
border-radius: 4px;
|
|
margin: 6px 0;
|
|
overflow: hidden;
|
|
}
|
|
.coverage-fill {
|
|
height: 100%;
|
|
border-radius: 4px;
|
|
transition: width 0.3s;
|
|
}
|
|
.stat-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 6px 0;
|
|
font-size: 13px;
|
|
}
|
|
.stat-label { color: var(--text-muted); }
|
|
|
|
.expandable-header {
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 10px 0;
|
|
border-bottom: 1px solid var(--border);
|
|
user-select: none;
|
|
}
|
|
.expandable-header:hover { color: var(--accent); }
|
|
.expandable-header .arrow { transition: transform 0.2s; }
|
|
.expandable-header.open .arrow { transform: rotate(90deg); }
|
|
.expandable-body { display: none; padding: 12px 0; }
|
|
.expandable-body.open { display: block; }
|
|
|
|
.search-box {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
margin-bottom: 16px;
|
|
outline: none;
|
|
}
|
|
.search-box:focus { border-color: var(--accent); }
|
|
.search-box::placeholder { color: var(--text-muted); }
|
|
|
|
.mapping-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 40px 1fr;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: 13px;
|
|
}
|
|
.mapping-row:last-child { border-bottom: none; }
|
|
.mapping-connector { text-align: center; color: var(--accent); }
|
|
|
|
.legend { display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); }
|
|
.legend-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
|
|
.f-stored td:first-child { border-left: 3px solid var(--green); }
|
|
.f-transient td:first-child { border-left: 3px solid var(--yellow); }
|
|
.f-ignored td:first-child { border-left: 3px solid var(--purple); }
|
|
.f-dropped td:first-child { border-left: 3px solid var(--red); }
|
|
.f-dropped td { color: var(--text-muted); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<h1>Gitlore CLI ↔ GitLab API Review</h1>
|
|
<p>Interactive mapping of the gitlore local sync engine to the GitLab REST API v4</p>
|
|
</header>
|
|
|
|
<div class="tabs">
|
|
<div class="tab active" onclick="showTab('overview')">Overview</div>
|
|
<div class="tab" onclick="showTab('cli')">CLI Architecture</div>
|
|
<div class="tab" onclick="showTab('api-used')">API Endpoints Used</div>
|
|
<div class="tab" onclick="showTab('api-full')">Full GitLab API</div>
|
|
<div class="tab" onclick="showTab('mapping')">Mapping</div>
|
|
<div class="tab" onclick="showTab('data')">Data Model</div>
|
|
<div class="tab" onclick="showTab('pipeline')">Ingestion Pipeline</div>
|
|
<div class="tab" onclick="showTab('fields')">Field Coverage</div>
|
|
<div class="tab" onclick="showTab('efficiency')">Efficiency Analysis</div>
|
|
</div>
|
|
|
|
<div class="content">
|
|
|
|
<!-- OVERVIEW -->
|
|
<div id="overview" class="panel active">
|
|
<h2>What is Gitlore?</h2>
|
|
<div class="card" style="margin-bottom:24px">
|
|
<p>Gitlore (<code>lore</code>) is a <strong>read-only local sync engine</strong> for GitLab data. It fetches issues, merge requests, and discussions from the GitLab REST API v4 and stores them in a local SQLite database for fast offline querying.</p>
|
|
<p style="margin-top:8px;color:var(--text-muted)">~11,148 lines of Rust. Noun-first CLI. Robot mode for automation. Cursor-based incremental sync.</p>
|
|
</div>
|
|
|
|
<h3>API Surface Coverage</h3>
|
|
<p style="font-size:13px;color:var(--text-muted);margin-bottom:12px">Gitlore uses a small, focused subset of the GitLab API. It is <strong>read-only</strong> — it never creates, updates, or deletes anything on GitLab.</p>
|
|
|
|
<div class="grid-2">
|
|
<div class="card">
|
|
<div class="card-title">Endpoints Used: 7</div>
|
|
<div class="stat-row"><span class="stat-label">Issues (list)</span><span class="badge badge-get">GET</span></div>
|
|
<div class="stat-row"><span class="stat-label">Merge Requests (list)</span><span class="badge badge-get">GET</span></div>
|
|
<div class="stat-row"><span class="stat-label">Issue Discussions (list)</span><span class="badge badge-get">GET</span></div>
|
|
<div class="stat-row"><span class="stat-label">MR Discussions (list)</span><span class="badge badge-get">GET</span></div>
|
|
<div class="stat-row"><span class="stat-label">Current User</span><span class="badge badge-get">GET</span></div>
|
|
<div class="stat-row"><span class="stat-label">Project Details</span><span class="badge badge-get">GET</span></div>
|
|
<div class="stat-row"><span class="stat-label">GitLab Version</span><span class="badge badge-get">GET</span></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">Coverage by Resource</div>
|
|
<div class="stat-row"><span>Issues API</span><span>2 of ~20 endpoints</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:10%;background:var(--green)"></div></div>
|
|
<div class="stat-row"><span>MR API</span><span>2 of ~30 endpoints</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:7%;background:var(--green)"></div></div>
|
|
<div class="stat-row"><span>Discussions API</span><span>2 of 30 endpoints</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:7%;background:var(--green)"></div></div>
|
|
<div class="stat-row"><span>Users API</span><span>1 of ~15 endpoints</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:7%;background:var(--green)"></div></div>
|
|
<div class="stat-row"><span>Projects API</span><span>1 of ~50 endpoints</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:2%;background:var(--green)"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Data Flow</h3>
|
|
<div class="flow-diagram">
|
|
<div class="flow-box">GitLab API v4</div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box highlight">Rate Limiter<br><small>10 req/s + jitter</small></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box">Async Streams<br><small>Paginated fetch</small></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box">Transformers<br><small>Normalize data</small></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box highlight">SQLite (WAL)<br><small>Local DB</small></div>
|
|
</div>
|
|
|
|
<h3>Key Design Decisions</h3>
|
|
<div class="grid-2">
|
|
<div class="card">
|
|
<div class="card-title">Read-only sync</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">Only GET requests. Never mutates GitLab state. Safe to run repeatedly.</p>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">Cursor-based incremental</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">Uses <code>updated_after</code> parameter to only fetch changed data. 2-second rewind overlap for safety.</p>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">Raw payload archival</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">Stores original JSON responses with SHA-256 dedup and optional gzip compression.</p>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">Discussion full-refresh</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">Discussions use DELETE+INSERT strategy per parent (no incremental). Parallel prefetch, serial write.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CLI ARCHITECTURE -->
|
|
<div id="cli" class="panel">
|
|
<h2>CLI Architecture</h2>
|
|
|
|
<h3>Command Structure (Noun-First)</h3>
|
|
<div class="card">
|
|
<pre>lore <noun> [verb/arg] # Primary pattern
|
|
lore issues # List all issues
|
|
lore issues 42 # Show issue #42
|
|
lore mrs # List all merge requests
|
|
lore mrs 17 # Show MR #17
|
|
lore ingest issues # Fetch issues from GitLab
|
|
lore ingest mrs # Fetch MRs from GitLab
|
|
lore count issues # Count local issues
|
|
lore count discussions # Count local discussions
|
|
lore status # Show sync state
|
|
lore auth # Verify GitLab auth
|
|
lore doctor # Health check
|
|
lore init # Initialize config + DB
|
|
lore migrate # Run DB migrations
|
|
lore version # Show version</pre>
|
|
</div>
|
|
|
|
<h3>Global Flags</h3>
|
|
<table>
|
|
<tr><th>Flag</th><th>Description</th></tr>
|
|
<tr><td><code>-c, --config</code></td><td>Path to config file</td></tr>
|
|
<tr><td><code>--robot</code></td><td>Machine-readable JSON output</td></tr>
|
|
<tr><td><code>-J, --json</code></td><td>JSON shorthand (same as --robot)</td></tr>
|
|
</table>
|
|
|
|
<h3>Robot Mode Detection</h3>
|
|
<div class="card">
|
|
<p style="font-size:13px">Three ways to activate:</p>
|
|
<pre>lore --robot list issues # Explicit flag
|
|
lore list issues | jq . # Auto: stdout not a TTY
|
|
LORE_ROBOT=1 lore list issues # Environment variable</pre>
|
|
<p style="font-size:13px;color:var(--text-muted);margin-top:8px">Robot mode returns JSON: <code>{"ok":true,"data":{...},"meta":{...}}</code></p>
|
|
<p style="font-size:13px;color:var(--text-muted)">Errors go to stderr: <code>{"error":{"code":"...","message":"...","suggestion":"..."}}</code></p>
|
|
</div>
|
|
|
|
<h3>Exit Codes</h3>
|
|
<table>
|
|
<tr><th>Code</th><th>Meaning</th></tr>
|
|
<tr><td>0</td><td>Success</td></tr>
|
|
<tr><td>1</td><td>Internal error</td></tr>
|
|
<tr><td>2</td><td>Config not found</td></tr>
|
|
<tr><td>3</td><td>Config invalid</td></tr>
|
|
<tr><td>4</td><td>Token not set</td></tr>
|
|
<tr><td>5</td><td>GitLab auth failed</td></tr>
|
|
<tr><td>6</td><td>Resource not found</td></tr>
|
|
<tr><td>7</td><td>Rate limited</td></tr>
|
|
<tr><td>8</td><td>Network error</td></tr>
|
|
<tr><td>9</td><td>Database locked</td></tr>
|
|
<tr><td>10</td><td>Database error</td></tr>
|
|
<tr><td>11</td><td>Migration failed</td></tr>
|
|
<tr><td>12</td><td>I/O error</td></tr>
|
|
<tr><td>13</td><td>Transform error</td></tr>
|
|
</table>
|
|
|
|
<h3>Configuration</h3>
|
|
<pre>{
|
|
"gitlab": {
|
|
"baseUrl": "https://gitlab.com",
|
|
"tokenEnvVar": "GITLAB_TOKEN"
|
|
},
|
|
"projects": [
|
|
{ "path": "group/project" }
|
|
],
|
|
"sync": {
|
|
"backfillDays": 14,
|
|
"staleLockMinutes": 10,
|
|
"heartbeatIntervalSeconds": 30,
|
|
"cursorRewindSeconds": 2,
|
|
"primaryConcurrency": 4,
|
|
"dependentConcurrency": 2
|
|
},
|
|
"storage": {
|
|
"dbPath": "~/.local/share/lore/lore.db",
|
|
"compressRawPayloads": true
|
|
}
|
|
}</pre>
|
|
|
|
<h3>Deprecated Commands (Hidden)</h3>
|
|
<table>
|
|
<tr><th>Old</th><th>New</th><th>Notes</th></tr>
|
|
<tr><td><code>lore list</code></td><td><code>lore issues</code> / <code>lore mrs</code></td><td>Shows deprecation warning</td></tr>
|
|
<tr><td><code>lore show</code></td><td><code>lore issues <iid></code></td><td>Shows deprecation warning</td></tr>
|
|
<tr><td><code>lore auth-test</code></td><td><code>lore auth</code></td><td>Alias</td></tr>
|
|
<tr><td><code>lore sync-status</code></td><td><code>lore status</code></td><td>Alias</td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- API ENDPOINTS USED -->
|
|
<div id="api-used" class="panel">
|
|
<h2>GitLab API Endpoints Used by Gitlore</h2>
|
|
<p style="font-size:13px;color:var(--text-muted);margin-bottom:16px">All requests use <code>PRIVATE-TOKEN</code> header authentication. Rate limited at 10 req/s with 0-50ms jitter.</p>
|
|
|
|
<div class="expandable" data-id="ep-issues">
|
|
<div class="expandable-header" onclick="toggleExpand('ep-issues')">
|
|
<span><span class="badge badge-get">GET</span> <code>/api/v4/projects/:id/issues</code> — List project issues</span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="ep-issues-body">
|
|
<h4>Used By</h4>
|
|
<p style="font-size:13px"><code>lore ingest issues</code></p>
|
|
<h4>Query Parameters Used</h4>
|
|
<table>
|
|
<tr><th>Param</th><th>Value</th><th>Purpose</th></tr>
|
|
<tr><td><code>scope</code></td><td><code>all</code></td><td>Include all issues, not just assigned to user</td></tr>
|
|
<tr><td><code>state</code></td><td><code>all</code></td><td>Open and closed</td></tr>
|
|
<tr><td><code>order_by</code></td><td><code>updated_at</code></td><td>Support cursor-based incremental sync</td></tr>
|
|
<tr><td><code>sort</code></td><td><code>asc</code></td><td>Oldest first for cursor correctness</td></tr>
|
|
<tr><td><code>per_page</code></td><td><code>100</code></td><td>Maximum page size</td></tr>
|
|
<tr><td><code>page</code></td><td><code>{n}</code></td><td>Offset pagination</td></tr>
|
|
<tr><td><code>updated_after</code></td><td>ISO 8601</td><td>Cursor: only fetch changes since last sync</td></tr>
|
|
</table>
|
|
<h4>Response Fields Consumed</h4>
|
|
<pre>id, iid, project_id, title, description, state,
|
|
created_at, updated_at, closed_at,
|
|
author { id, username, name },
|
|
assignees[] { id, username, name },
|
|
labels[], milestone { id, iid, title, state, due_date, web_url },
|
|
web_url, due_date</pre>
|
|
<h4>Pagination</h4>
|
|
<p style="font-size:13px">Fallback chain: <code>Link</code> header (RFC 8288) → <code>x-next-page</code> header → full-page heuristic (100 items = more pages)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" data-id="ep-mrs">
|
|
<div class="expandable-header" onclick="toggleExpand('ep-mrs')">
|
|
<span><span class="badge badge-get">GET</span> <code>/api/v4/projects/:id/merge_requests</code> — List project MRs</span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="ep-mrs-body">
|
|
<h4>Used By</h4>
|
|
<p style="font-size:13px"><code>lore ingest mrs</code></p>
|
|
<h4>Query Parameters Used</h4>
|
|
<table>
|
|
<tr><th>Param</th><th>Value</th><th>Purpose</th></tr>
|
|
<tr><td><code>scope</code></td><td><code>all</code></td><td>All MRs in project</td></tr>
|
|
<tr><td><code>state</code></td><td><code>all</code></td><td>opened, merged, closed, locked</td></tr>
|
|
<tr><td><code>order_by</code></td><td><code>updated_at</code></td><td>Cursor-based sync</td></tr>
|
|
<tr><td><code>sort</code></td><td><code>asc</code></td><td>Oldest first</td></tr>
|
|
<tr><td><code>per_page</code></td><td><code>100</code></td><td>Max page size</td></tr>
|
|
<tr><td><code>page</code></td><td><code>{n}</code></td><td>Offset pagination</td></tr>
|
|
<tr><td><code>updated_after</code></td><td>ISO 8601</td><td>Incremental cursor</td></tr>
|
|
</table>
|
|
<h4>Response Fields Consumed</h4>
|
|
<pre>id, iid, project_id, title, description, state,
|
|
draft (preferred) OR work_in_progress (fallback),
|
|
author, assignees[], reviewers[],
|
|
labels[], source_branch, target_branch, sha,
|
|
references { short, full },
|
|
detailed_merge_status (preferred) OR merge_status (fallback),
|
|
merge_user (preferred) OR merged_by (fallback),
|
|
created_at, updated_at, merged_at, closed_at, web_url</pre>
|
|
<h4>Deprecated Field Fallbacks</h4>
|
|
<table>
|
|
<tr><th>Preferred</th><th>Fallback</th></tr>
|
|
<tr><td><code>draft</code></td><td><code>work_in_progress</code></td></tr>
|
|
<tr><td><code>detailed_merge_status</code></td><td><code>merge_status</code></td></tr>
|
|
<tr><td><code>merge_user</code></td><td><code>merged_by</code></td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" data-id="ep-issue-disc">
|
|
<div class="expandable-header" onclick="toggleExpand('ep-issue-disc')">
|
|
<span><span class="badge badge-get">GET</span> <code>/api/v4/projects/:id/issues/:iid/discussions</code> — Issue discussions</span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="ep-issue-disc-body">
|
|
<h4>Used By</h4>
|
|
<p style="font-size:13px"><code>lore ingest issues</code> (dependent phase)</p>
|
|
<h4>Sync Strategy</h4>
|
|
<p style="font-size:13px">Full-refresh: DELETE all existing discussions for the issue, INSERT all fetched discussions. Only triggered when <code>issue.updated_at > issue.discussions_synced_for_updated_at</code>.</p>
|
|
<h4>Response Fields Consumed</h4>
|
|
<pre>id (string), individual_note (bool),
|
|
notes[] {
|
|
id, type (DiscussionNote | DiffNote | null),
|
|
body, author { username, name },
|
|
created_at, updated_at,
|
|
system (bool), resolvable, resolved,
|
|
resolved_by, resolved_at,
|
|
position { old_path, new_path, old_line, new_line,
|
|
position_type, line_range, base_sha, start_sha, head_sha }
|
|
}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" data-id="ep-mr-disc">
|
|
<div class="expandable-header" onclick="toggleExpand('ep-mr-disc')">
|
|
<span><span class="badge badge-get">GET</span> <code>/api/v4/projects/:id/merge_requests/:iid/discussions</code> — MR discussions</span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="ep-mr-disc-body">
|
|
<h4>Used By</h4>
|
|
<p style="font-size:13px"><code>lore ingest mrs</code> (dependent phase)</p>
|
|
<h4>Sync Strategy</h4>
|
|
<p style="font-size:13px">Same full-refresh strategy as issue discussions. Includes DiffNote support (code review comments on specific lines).</p>
|
|
<h4>Additional: DiffNote Position</h4>
|
|
<pre>position {
|
|
old_path, new_path, // file paths
|
|
old_line, new_line, // line numbers
|
|
position_type, // "text" or "image"
|
|
line_range { // multi-line comments
|
|
start { line_code, type, old_line, new_line },
|
|
end { line_code, type, old_line, new_line }
|
|
},
|
|
base_sha, start_sha, head_sha // commit references
|
|
}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" data-id="ep-user">
|
|
<div class="expandable-header" onclick="toggleExpand('ep-user')">
|
|
<span><span class="badge badge-get">GET</span> <code>/api/v4/user</code> — Current authenticated user</span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="ep-user-body">
|
|
<h4>Used By</h4>
|
|
<p style="font-size:13px"><code>lore auth</code>, <code>lore doctor</code></p>
|
|
<h4>Response Fields</h4>
|
|
<pre>id, username, name, email, avatar_url, web_url, created_at, state</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" data-id="ep-project">
|
|
<div class="expandable-header" onclick="toggleExpand('ep-project')">
|
|
<span><span class="badge badge-get">GET</span> <code>/api/v4/projects/:path_encoded</code> — Project details</span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="ep-project-body">
|
|
<h4>Used By</h4>
|
|
<p style="font-size:13px"><code>lore ingest</code> (project resolution during init/sync)</p>
|
|
<h4>Response Fields</h4>
|
|
<pre>id, path_with_namespace, default_branch, web_url,
|
|
created_at, updated_at, name, description, visibility, archived</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" data-id="ep-version">
|
|
<div class="expandable-header" onclick="toggleExpand('ep-version')">
|
|
<span><span class="badge badge-get">GET</span> <code>/api/v4/version</code> — GitLab instance version</span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="ep-version-body">
|
|
<h4>Used By</h4>
|
|
<p style="font-size:13px"><code>lore doctor</code></p>
|
|
<h4>Response Fields</h4>
|
|
<pre>version, revision</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FULL GITLAB API -->
|
|
<div id="api-full" class="panel">
|
|
<h2>Full GitLab REST API v4 Reference</h2>
|
|
<p style="font-size:13px;color:var(--text-muted);margin-bottom:16px">Complete endpoint inventory for the resources relevant to Gitlore. <span class="badge badge-used">USED</span> = consumed by Gitlore.</p>
|
|
|
|
<input type="text" class="search-box" id="api-search" placeholder="Filter endpoints... (e.g. 'issues', 'POST', 'discussions')" oninput="filterAPI()">
|
|
|
|
<div id="api-list">
|
|
<!-- ISSUES -->
|
|
<h3>Issues API</h3>
|
|
<table class="api-table">
|
|
<tr><th style="width:60px">Method</th><th>Endpoint</th><th>Description</th><th style="width:60px">Status</th></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/issues</code></td><td>List all issues (global)</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/groups/:id/issues</code></td><td>List group issues</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr class="used-row"><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/issues</code></td><td>List project issues</td><td><span class="badge badge-used">USED</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/issues/:iid</code></td><td>Get single issue</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/issues</code></td><td>Create issue</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-put">PUT</span></td><td><code>/projects/:id/issues/:iid</code></td><td>Update issue</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-delete">DEL</span></td><td><code>/projects/:id/issues/:iid</code></td><td>Delete issue</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-put">PUT</span></td><td><code>/projects/:id/issues/:iid/reorder</code></td><td>Reorder issue</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/issues/:iid/move</code></td><td>Move issue</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/issues/:iid/clone</code></td><td>Clone issue</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/issues/:iid/subscribe</code></td><td>Subscribe to issue</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/issues/:iid/unsubscribe</code></td><td>Unsubscribe</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/issues/:iid/todo</code></td><td>Create to-do</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/issues/:iid/time_estimate</code></td><td>Set time estimate</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/issues/:iid/add_spent_time</code></td><td>Add spent time</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/issues/:iid/time_stats</code></td><td>Get time stats</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/issues/:iid/related_merge_requests</code></td><td>Related MRs</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/issues/:iid/closed_by</code></td><td>MRs that close issue</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/issues/:iid/participants</code></td><td>List participants</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
</table>
|
|
|
|
<!-- MERGE REQUESTS -->
|
|
<h3>Merge Requests API</h3>
|
|
<table class="api-table">
|
|
<tr><th style="width:60px">Method</th><th>Endpoint</th><th>Description</th><th style="width:60px">Status</th></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/merge_requests</code></td><td>List all MRs (global)</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/groups/:id/merge_requests</code></td><td>List group MRs</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr class="used-row"><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/merge_requests</code></td><td>List project MRs</td><td><span class="badge badge-used">USED</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/merge_requests/:iid</code></td><td>Get single MR</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/merge_requests</code></td><td>Create MR</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-put">PUT</span></td><td><code>/projects/:id/merge_requests/:iid</code></td><td>Update MR</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-delete">DEL</span></td><td><code>/projects/:id/merge_requests/:iid</code></td><td>Delete MR</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-put">PUT</span></td><td><code>/projects/:id/merge_requests/:iid/merge</code></td><td>Merge an MR</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/merge_requests/:iid/cancel_merge</code></td><td>Cancel merge</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-put">PUT</span></td><td><code>/projects/:id/merge_requests/:iid/rebase</code></td><td>Rebase MR</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/merge_requests/:iid/commits</code></td><td>List MR commits</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/merge_requests/:iid/changes</code></td><td>List MR diffs</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/merge_requests/:iid/pipelines</code></td><td>MR pipelines</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/merge_requests/:iid/participants</code></td><td>MR participants</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/merge_requests/:iid/approvals</code></td><td>MR approvals</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/merge_requests/:iid/approve</code></td><td>Approve MR</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
</table>
|
|
|
|
<!-- DISCUSSIONS -->
|
|
<h3>Discussions API</h3>
|
|
<table class="api-table">
|
|
<tr><th style="width:60px">Method</th><th>Endpoint</th><th>Description</th><th style="width:60px">Status</th></tr>
|
|
<tr class="used-row"><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/issues/:iid/discussions</code></td><td>List issue discussions</td><td><span class="badge badge-used">USED</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/issues/:iid/discussions/:did</code></td><td>Get single discussion</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/issues/:iid/discussions</code></td><td>Create issue thread</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/issues/:iid/discussions/:did/notes</code></td><td>Add note to thread</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-put">PUT</span></td><td><code>/projects/:id/issues/:iid/discussions/:did/notes/:nid</code></td><td>Modify note</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-delete">DEL</span></td><td><code>/projects/:id/issues/:iid/discussions/:did/notes/:nid</code></td><td>Delete note</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr class="used-row"><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/merge_requests/:iid/discussions</code></td><td>List MR discussions</td><td><span class="badge badge-used">USED</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/merge_requests/:iid/discussions/:did</code></td><td>Get single MR discussion</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/merge_requests/:iid/discussions</code></td><td>Create MR thread</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-put">PUT</span></td><td><code>/projects/:id/merge_requests/:iid/discussions/:did</code></td><td>Resolve/unresolve thread</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/merge_requests/:iid/discussions/:did/notes</code></td><td>Add note to MR thread</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-put">PUT</span></td><td><code>/projects/:id/merge_requests/:iid/discussions/:did/notes/:nid</code></td><td>Modify MR note</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-delete">DEL</span></td><td><code>/projects/:id/merge_requests/:iid/discussions/:did/notes/:nid</code></td><td>Delete MR note</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/snippets/:sid/discussions</code></td><td>List snippet discussions</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/groups/:id/epics/:eid/discussions</code></td><td>List epic discussions</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/repository/commits/:sha/discussions</code></td><td>List commit discussions</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
</table>
|
|
|
|
<!-- NOTES -->
|
|
<h3>Notes API (Flat, non-threaded)</h3>
|
|
<table class="api-table">
|
|
<tr><th style="width:60px">Method</th><th>Endpoint</th><th>Description</th><th style="width:60px">Status</th></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/issues/:iid/notes</code></td><td>List issue notes</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/issues/:iid/notes</code></td><td>Create issue note</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/merge_requests/:iid/notes</code></td><td>List MR notes</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-post">POST</span></td><td><code>/projects/:id/merge_requests/:iid/notes</code></td><td>Create MR note</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
<tr><td><span class="badge badge-get">GET</span></td><td><code>/projects/:id/snippets/:sid/notes</code></td><td>List snippet notes</td><td><span class="badge badge-unused">--</span></td></tr>
|
|
</table>
|
|
<p style="font-size:12px;color:var(--text-muted);margin-top:8px">Gitlore uses the Discussions API (threaded) instead of the flat Notes API. Notes are extracted from discussion responses.</p>
|
|
|
|
<!-- OTHER USED -->
|
|
<h3>Other APIs Used</h3>
|
|
<table class="api-table">
|
|
<tr><th style="width:60px">Method</th><th>Endpoint</th><th>Description</th><th style="width:60px">Status</th></tr>
|
|
<tr class="used-row"><td><span class="badge badge-get">GET</span></td><td><code>/user</code></td><td>Current authenticated user</td><td><span class="badge badge-used">USED</span></td></tr>
|
|
<tr class="used-row"><td><span class="badge badge-get">GET</span></td><td><code>/projects/:path</code></td><td>Get project by path</td><td><span class="badge badge-used">USED</span></td></tr>
|
|
<tr class="used-row"><td><span class="badge badge-get">GET</span></td><td><code>/version</code></td><td>GitLab instance version</td><td><span class="badge badge-used">USED</span></td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MAPPING -->
|
|
<div id="mapping" class="panel">
|
|
<h2>CLI Command ↔ API Endpoint Mapping</h2>
|
|
<p style="font-size:13px;color:var(--text-muted);margin-bottom:16px">How each CLI command maps to GitLab API calls and local database operations.</p>
|
|
|
|
<div class="card">
|
|
<div class="card-title">lore ingest issues</div>
|
|
<div class="mapping-row">
|
|
<div><strong>Phase 1:</strong> Fetch primary resources</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div><code>GET /projects/:id/issues</code> (paginated, cursor-based)</div>
|
|
</div>
|
|
<div class="mapping-row">
|
|
<div><strong>Phase 2:</strong> Identify stale discussions</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div>SQL: <code>WHERE updated_at > discussions_synced_for_updated_at</code></div>
|
|
</div>
|
|
<div class="mapping-row">
|
|
<div><strong>Phase 3:</strong> Sync discussions</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div><code>GET /projects/:id/issues/:iid/discussions</code> (parallel prefetch)</div>
|
|
</div>
|
|
<div class="mapping-row">
|
|
<div><strong>Storage:</strong> Write to DB</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div>Tables: <code>issues</code>, <code>labels</code>, <code>issue_labels</code>, <code>discussions</code>, <code>notes</code>, <code>raw_payloads</code></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">lore ingest mrs</div>
|
|
<div class="mapping-row">
|
|
<div><strong>Phase 1:</strong> Fetch primary resources</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div><code>GET /projects/:id/merge_requests</code> (paginated, cursor-based)</div>
|
|
</div>
|
|
<div class="mapping-row">
|
|
<div><strong>Phase 2:</strong> Identify stale discussions</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div>SQL: <code>WHERE updated_at > discussions_synced_for_updated_at</code></div>
|
|
</div>
|
|
<div class="mapping-row">
|
|
<div><strong>Phase 3:</strong> Sync discussions</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div><code>GET /projects/:id/merge_requests/:iid/discussions</code> (parallel prefetch)</div>
|
|
</div>
|
|
<div class="mapping-row">
|
|
<div><strong>Storage:</strong> Write to DB</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div>Tables: <code>merge_requests</code>, <code>labels</code>, <code>mr_labels</code>, <code>mr_assignees</code>, <code>mr_reviewers</code>, <code>discussions</code>, <code>notes</code>, <code>raw_payloads</code></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">lore issues / lore mrs</div>
|
|
<div class="mapping-row">
|
|
<div><strong>List mode:</strong> Query local DB</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div>SQL: <code>SELECT ... FROM issues/merge_requests</code> with filters (no API call)</div>
|
|
</div>
|
|
<div class="mapping-row">
|
|
<div><strong>Show mode:</strong> Query local DB by IID</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div>SQL: <code>SELECT ... WHERE iid = ?</code> + join discussions/notes (no API call)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">lore auth</div>
|
|
<div class="mapping-row">
|
|
<div>Verify token works</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div><code>GET /api/v4/user</code></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">lore doctor</div>
|
|
<div class="mapping-row">
|
|
<div>Check auth + GitLab version</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div><code>GET /api/v4/user</code> + <code>GET /api/v4/version</code></div>
|
|
</div>
|
|
<div class="mapping-row">
|
|
<div>Check each configured project</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div><code>GET /api/v4/projects/:path</code></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">lore count / lore status / lore init / lore migrate</div>
|
|
<div class="mapping-row">
|
|
<div>Local-only operations</div>
|
|
<div class="mapping-connector">→</div>
|
|
<div>No API calls. Database queries only.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 style="margin-top:24px">API Capabilities NOT Used by Gitlore</h3>
|
|
<div class="grid-2">
|
|
<div class="card">
|
|
<div class="card-title" style="color:var(--red)">Write Operations</div>
|
|
<ul style="font-size:13px;padding-left:16px;color:var(--text-muted)">
|
|
<li>Create/update/delete issues</li>
|
|
<li>Create/update/delete MRs</li>
|
|
<li>Merge MRs</li>
|
|
<li>Create/reply to discussions</li>
|
|
<li>Resolve/unresolve threads</li>
|
|
<li>Approve MRs</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title" style="color:var(--yellow)">Read Operations</div>
|
|
<ul style="font-size:13px;padding-left:16px;color:var(--text-muted)">
|
|
<li>Single issue/MR fetch (uses list with filters instead)</li>
|
|
<li>MR commits, diffs, pipelines</li>
|
|
<li>Issue/MR participants</li>
|
|
<li>Time tracking stats</li>
|
|
<li>Related MRs / closed-by</li>
|
|
<li>Labels API (extracted from issue/MR responses)</li>
|
|
<li>Milestones API (extracted from issue responses)</li>
|
|
<li>Flat Notes API (uses threaded Discussions API)</li>
|
|
<li>Snippets, Epics, Commits discussions</li>
|
|
<li>Webhooks, CI/CD, Pipelines, Deployments</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DATA MODEL -->
|
|
<div id="data" class="panel">
|
|
<h2>Database Schema</h2>
|
|
<p style="font-size:13px;color:var(--text-muted);margin-bottom:16px">SQLite with WAL mode. 12 tables across 6 migrations.</p>
|
|
|
|
<h3>Entity Relationship</h3>
|
|
<pre style="font-size:11px;line-height:1.4">
|
|
projects ──────────────────────────────────────────────────────────
|
|
│ │
|
|
├──< issues ──< issue_labels >── labels │
|
|
│ │ │
|
|
│ └──< discussions ──< notes │
|
|
│ │
|
|
├──< merge_requests ──< mr_labels >── labels │
|
|
│ │ │ │ │
|
|
│ │ │ └──< mr_reviewers │
|
|
│ │ └──< mr_assignees │
|
|
│ │ │
|
|
│ └──< discussions ──< notes │
|
|
│ │
|
|
├──< raw_payloads │
|
|
├──< sync_cursors │
|
|
└── sync_runs, app_locks, schema_version │
|
|
───────────────────────────────────────────────────────────────────</pre>
|
|
|
|
<h3>Table Details</h3>
|
|
|
|
<div class="expandable" data-id="tbl-projects">
|
|
<div class="expandable-header" onclick="toggleExpand('tbl-projects')">
|
|
<span><strong>projects</strong> <span style="color:var(--text-muted);font-size:12px">— Configured GitLab projects</span></span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="tbl-projects-body">
|
|
<table>
|
|
<tr><th>Column</th><th>Type</th><th>Notes</th></tr>
|
|
<tr><td>id</td><td>INTEGER PK</td><td>Auto-increment</td></tr>
|
|
<tr><td>gitlab_project_id</td><td>INTEGER UNIQUE</td><td>GitLab's project ID</td></tr>
|
|
<tr><td>path_with_namespace</td><td>TEXT</td><td>e.g. "group/project"</td></tr>
|
|
<tr><td>default_branch</td><td>TEXT</td><td>e.g. "main"</td></tr>
|
|
<tr><td>web_url</td><td>TEXT</td><td>Full URL</td></tr>
|
|
<tr><td>created_at, updated_at</td><td>INTEGER</td><td>ms epoch</td></tr>
|
|
<tr><td>raw_payload_id</td><td>INTEGER FK</td><td>Link to raw_payloads</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" data-id="tbl-issues">
|
|
<div class="expandable-header" onclick="toggleExpand('tbl-issues')">
|
|
<span><strong>issues</strong> <span style="color:var(--text-muted);font-size:12px">— Synced GitLab issues</span></span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="tbl-issues-body">
|
|
<table>
|
|
<tr><th>Column</th><th>Type</th><th>Notes</th></tr>
|
|
<tr><td>id</td><td>INTEGER PK</td><td>Auto-increment</td></tr>
|
|
<tr><td>gitlab_id</td><td>INTEGER UNIQUE</td><td>GitLab's issue ID</td></tr>
|
|
<tr><td>project_id</td><td>INTEGER FK</td><td>projects.id</td></tr>
|
|
<tr><td>iid</td><td>INTEGER</td><td>Issue # within project (UNIQUE with project_id)</td></tr>
|
|
<tr><td>title, description</td><td>TEXT</td><td></td></tr>
|
|
<tr><td>state</td><td>TEXT</td><td>'opened' | 'closed'</td></tr>
|
|
<tr><td>author_username</td><td>TEXT</td><td>Denormalized</td></tr>
|
|
<tr><td>created_at, updated_at, closed_at</td><td>INTEGER</td><td>ms epoch</td></tr>
|
|
<tr><td>last_seen_at</td><td>INTEGER</td><td>Orphan detection</td></tr>
|
|
<tr><td>discussions_synced_for_updated_at</td><td>INTEGER</td><td>Discussion sync watermark</td></tr>
|
|
<tr><td>web_url</td><td>TEXT</td><td></td></tr>
|
|
<tr><td>raw_payload_id</td><td>INTEGER FK</td><td></td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" data-id="tbl-mrs">
|
|
<div class="expandable-header" onclick="toggleExpand('tbl-mrs')">
|
|
<span><strong>merge_requests</strong> <span style="color:var(--text-muted);font-size:12px">— Synced GitLab MRs</span></span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="tbl-mrs-body">
|
|
<table>
|
|
<tr><th>Column</th><th>Type</th><th>Notes</th></tr>
|
|
<tr><td>id</td><td>INTEGER PK</td><td></td></tr>
|
|
<tr><td>gitlab_id</td><td>INTEGER UNIQUE</td><td></td></tr>
|
|
<tr><td>project_id</td><td>INTEGER FK</td><td></td></tr>
|
|
<tr><td>iid</td><td>INTEGER</td><td></td></tr>
|
|
<tr><td>title, description, state</td><td>TEXT</td><td>'opened' | 'merged' | 'closed' | 'locked'</td></tr>
|
|
<tr><td>draft</td><td>INTEGER</td><td>0/1</td></tr>
|
|
<tr><td>author_username</td><td>TEXT</td><td></td></tr>
|
|
<tr><td>source_branch, target_branch</td><td>TEXT</td><td></td></tr>
|
|
<tr><td>head_sha</td><td>TEXT</td><td></td></tr>
|
|
<tr><td>references_short, references_full</td><td>TEXT</td><td>e.g. "!42", "group/proj!42"</td></tr>
|
|
<tr><td>detailed_merge_status</td><td>TEXT</td><td></td></tr>
|
|
<tr><td>merge_user_username</td><td>TEXT</td><td></td></tr>
|
|
<tr><td>created_at, updated_at, merged_at, closed_at</td><td>INTEGER</td><td>ms epoch</td></tr>
|
|
<tr><td>last_seen_at</td><td>INTEGER</td><td></td></tr>
|
|
<tr><td>discussions_synced_for_updated_at</td><td>INTEGER</td><td>Watermark</td></tr>
|
|
<tr><td>discussions_sync_last_attempt_at</td><td>INTEGER</td><td>Health telemetry</td></tr>
|
|
<tr><td>discussions_sync_attempts</td><td>INTEGER</td><td>Retry count</td></tr>
|
|
<tr><td>discussions_sync_last_error</td><td>TEXT</td><td></td></tr>
|
|
<tr><td>web_url, raw_payload_id</td><td>TEXT/INT</td><td></td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" data-id="tbl-discussions">
|
|
<div class="expandable-header" onclick="toggleExpand('tbl-discussions')">
|
|
<span><strong>discussions + notes</strong> <span style="color:var(--text-muted);font-size:12px">— Threaded comments</span></span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="tbl-discussions-body">
|
|
<h4>discussions</h4>
|
|
<table>
|
|
<tr><th>Column</th><th>Type</th><th>Notes</th></tr>
|
|
<tr><td>id</td><td>INTEGER PK</td><td></td></tr>
|
|
<tr><td>gitlab_discussion_id</td><td>TEXT</td><td>SHA-like string ID</td></tr>
|
|
<tr><td>project_id</td><td>INTEGER FK</td><td></td></tr>
|
|
<tr><td>issue_id</td><td>INTEGER FK</td><td>XOR with merge_request_id</td></tr>
|
|
<tr><td>merge_request_id</td><td>INTEGER FK</td><td>XOR with issue_id</td></tr>
|
|
<tr><td>noteable_type</td><td>TEXT</td><td>'Issue' | 'MergeRequest'</td></tr>
|
|
<tr><td>individual_note</td><td>INTEGER</td><td>0 = threaded, 1 = standalone</td></tr>
|
|
<tr><td>resolvable, resolved</td><td>INTEGER</td><td>0/1</td></tr>
|
|
<tr><td>first_note_at, last_note_at</td><td>INTEGER</td><td>Computed from notes</td></tr>
|
|
</table>
|
|
<h4>notes</h4>
|
|
<table>
|
|
<tr><th>Column</th><th>Type</th><th>Notes</th></tr>
|
|
<tr><td>id</td><td>INTEGER PK</td><td></td></tr>
|
|
<tr><td>gitlab_id</td><td>INTEGER UNIQUE</td><td></td></tr>
|
|
<tr><td>discussion_id</td><td>INTEGER FK</td><td></td></tr>
|
|
<tr><td>note_type</td><td>TEXT</td><td>'DiscussionNote' | 'DiffNote' | null</td></tr>
|
|
<tr><td>is_system</td><td>INTEGER</td><td>Auto-generated (label changes, etc.)</td></tr>
|
|
<tr><td>author_username, body</td><td>TEXT</td><td></td></tr>
|
|
<tr><td>position</td><td>INTEGER</td><td>0-indexed order within discussion</td></tr>
|
|
<tr><td>resolvable, resolved</td><td>INTEGER</td><td>0/1</td></tr>
|
|
<tr><td>resolved_by, resolved_at</td><td>TEXT/INT</td><td></td></tr>
|
|
<tr><td>position_old/new_path</td><td>TEXT</td><td>DiffNote: file path</td></tr>
|
|
<tr><td>position_old/new_line</td><td>INTEGER</td><td>DiffNote: line number</td></tr>
|
|
<tr><td>position_type</td><td>TEXT</td><td>DiffNote: "text" or "image"</td></tr>
|
|
<tr><td>position_line_range_start/end</td><td>TEXT</td><td>JSON for multi-line</td></tr>
|
|
<tr><td>position_base/start/head_sha</td><td>TEXT</td><td>Commit references</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" data-id="tbl-infra">
|
|
<div class="expandable-header" onclick="toggleExpand('tbl-infra')">
|
|
<span><strong>Infrastructure tables</strong> <span style="color:var(--text-muted);font-size:12px">— sync_cursors, sync_runs, app_locks, raw_payloads</span></span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="tbl-infra-body">
|
|
<h4>sync_cursors</h4>
|
|
<p style="font-size:13px">PK: <code>(project_id, resource_type)</code>. Tracks <code>updated_at_cursor</code> (ms epoch) and <code>tie_breaker_id</code> for incremental sync.</p>
|
|
|
|
<h4>sync_runs</h4>
|
|
<p style="font-size:13px">Audit trail: <code>started_at</code>, <code>finished_at</code>, <code>status</code> ('running'|'succeeded'|'failed'), <code>command</code>, <code>error</code>, <code>metrics_json</code>.</p>
|
|
|
|
<h4>app_locks</h4>
|
|
<p style="font-size:13px">Single-flight mutex: <code>name</code> PK ('sync'), <code>owner</code> (UUIDv4), <code>heartbeat_at</code>. Prevents concurrent sync.</p>
|
|
|
|
<h4>raw_payloads</h4>
|
|
<p style="font-size:13px">Archived API responses. SHA-256 dedup. Optional gzip. <code>UNIQUE(project_id, resource_type, gitlab_id, payload_hash)</code>.</p>
|
|
|
|
<h4>SQLite Pragmas</h4>
|
|
<pre>journal_mode = WAL -- Write-ahead logging
|
|
synchronous = NORMAL -- Safe for WAL
|
|
foreign_keys = ON -- Referential integrity
|
|
busy_timeout = 5000 -- 5s lock wait
|
|
temp_store = MEMORY -- Speed optimization</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- INGESTION PIPELINE -->
|
|
<div id="pipeline" class="panel">
|
|
<h2>Ingestion Pipeline</h2>
|
|
|
|
<h3>Three-Phase Architecture</h3>
|
|
<div class="card">
|
|
<div style="display:flex;gap:16px;flex-wrap:wrap">
|
|
<div class="flow-box highlight" style="flex:1;min-width:200px">
|
|
<strong>Phase 1: Primary Fetch</strong><br>
|
|
<small style="color:var(--text-muted)">Paginated API fetch with cursor-based sync.<br>Stores raw payloads + normalized rows.</small>
|
|
</div>
|
|
<div class="flow-box" style="flex:1;min-width:200px">
|
|
<strong>Phase 2: Identify Stale</strong><br>
|
|
<small style="color:var(--text-muted)">SQL query: which issues/MRs need<br>their discussions refreshed?</small>
|
|
</div>
|
|
<div class="flow-box highlight" style="flex:1;min-width:200px">
|
|
<strong>Phase 3: Discussion Sync</strong><br>
|
|
<small style="color:var(--text-muted)">Parallel prefetch + serial write.<br>Full-refresh per parent entity.</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Cursor-Based Incremental Sync</h3>
|
|
<div class="card">
|
|
<pre style="margin:0">Cursor State: (updated_at_cursor: i64, tie_breaker_id: i64)
|
|
|
|
First sync:
|
|
updated_after = (now - backfillDays)
|
|
|
|
Subsequent syncs:
|
|
updated_after = cursor.updated_at - cursorRewindSeconds
|
|
|
|
For each fetched resource:
|
|
if (gitlab_id, updated_at) <= cursor:
|
|
SKIP (already processed in overlap zone)
|
|
else:
|
|
UPSERT into database
|
|
|
|
After each page boundary:
|
|
UPDATE sync_cursors (crash recovery safe)</pre>
|
|
</div>
|
|
|
|
<h3>Discussion Sync Strategy</h3>
|
|
<div class="card">
|
|
<pre style="margin:0">For each issue/MR where updated_at > discussions_synced_for_updated_at:
|
|
|
|
1. PREFETCH (parallel, configurable concurrency):
|
|
GET /projects/:id/issues/:iid/discussions (all pages)
|
|
|
|
2. WRITE (serial, inside transaction):
|
|
DELETE FROM discussions WHERE issue_id = ?
|
|
DELETE FROM notes WHERE discussion_id IN (...)
|
|
INSERT discussions + notes (fresh data)
|
|
UPDATE issues SET discussions_synced_for_updated_at = updated_at</pre>
|
|
<p style="font-size:12px;color:var(--text-muted);margin-top:8px">Full-refresh avoids complexity of detecting deleted/edited notes. Trade-off: more API calls for heavily-discussed items.</p>
|
|
</div>
|
|
|
|
<h3>Rate Limiting</h3>
|
|
<div class="card">
|
|
<pre style="margin:0">RateLimiter {
|
|
min_interval: 100ms (= 1s / 10 req/s)
|
|
jitter: 0-50ms random
|
|
|
|
acquire():
|
|
elapsed = now - last_request
|
|
if elapsed < min_interval:
|
|
sleep(min_interval - elapsed + random_jitter)
|
|
last_request = now
|
|
}</pre>
|
|
</div>
|
|
|
|
<h3>Pagination</h3>
|
|
<div class="card">
|
|
<p style="font-size:13px">Async stream-based. Fallback chain for next-page detection:</p>
|
|
<ol style="font-size:13px;padding-left:20px;margin-top:8px">
|
|
<li><code>Link</code> header (RFC 8288) — parse <code>rel="next"</code></li>
|
|
<li><code>x-next-page</code> header — direct page number</li>
|
|
<li>Full-page heuristic — if response has 100 items, assume more pages</li>
|
|
</ol>
|
|
</div>
|
|
|
|
<h3>Raw Payload Storage</h3>
|
|
<div class="card">
|
|
<div class="flow-diagram">
|
|
<div class="flow-box">API Response<br><small>JSON bytes</small></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box">SHA-256 Hash<br><small>Dedup check</small></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box">Gzip Compress<br><small>(if enabled)</small></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box highlight">raw_payloads<br><small>BLOB storage</small></div>
|
|
</div>
|
|
<p style="font-size:12px;color:var(--text-muted)">UNIQUE constraint on <code>(project_id, resource_type, gitlab_id, payload_hash)</code> prevents storing identical payloads.</p>
|
|
</div>
|
|
|
|
<h3>Concurrency Model</h3>
|
|
<div class="grid-2">
|
|
<div class="card">
|
|
<div class="card-title">Primary Resource Fetch</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">Single-threaded async stream. Rate-limited. Each page written in a transaction. Cursor updated at page boundaries.</p>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">Discussion Sync</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">Parallel prefetch (configurable, default 2 concurrent). Serial write phase to avoid DB contention. Each parent entity is one transaction.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Single-Flight Lock</h3>
|
|
<div class="card">
|
|
<pre style="margin:0">AppLock (database-enforced mutex):
|
|
name: 'sync' (PK)
|
|
owner: UUIDv4 (unique per process)
|
|
heartbeat_at: updated every 30s
|
|
|
|
Acquire:
|
|
INSERT OR fail if row exists
|
|
Check stale: if heartbeat > staleLockMinutes, force-acquire
|
|
|
|
Release:
|
|
DELETE WHERE owner = my_uuid</pre>
|
|
</div>
|
|
|
|
<h3>Progress Events</h3>
|
|
<table>
|
|
<tr><th>Event</th><th>Description</th></tr>
|
|
<tr><td><code>IssuesFetchStarted</code></td><td>Beginning primary issue fetch</td></tr>
|
|
<tr><td><code>IssueFetched</code></td><td>Each issue processed (for progress bars)</td></tr>
|
|
<tr><td><code>IssuesFetchComplete</code></td><td>All pages consumed</td></tr>
|
|
<tr><td><code>DiscussionSyncStarted</code></td><td>Beginning discussion phase</td></tr>
|
|
<tr><td><code>DiscussionSynced</code></td><td>Each parent's discussions written</td></tr>
|
|
<tr><td><code>DiscussionSyncComplete</code></td><td>All discussions updated</td></tr>
|
|
<tr><td colspan="2" style="color:var(--text-muted);font-size:12px">Same events exist for MRs (MrsFetchStarted, etc.)</td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- FIELD COVERAGE -->
|
|
<div id="fields" class="panel">
|
|
<h2>Field-Level Coverage: API Response vs Gitlore Storage</h2>
|
|
<p style="font-size:13px;color:var(--text-muted);margin-bottom:12px">Every field in every GitLab API response, mapped to what Gitlore does with it. Serde silently drops fields not in the Rust structs.</p>
|
|
|
|
<div class="legend">
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div> Stored in DB</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--yellow)"></div> Used transiently (logic only)</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--purple)"></div> Deserialized but ignored</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--red)"></div> Never deserialized (silently dropped)</div>
|
|
</div>
|
|
|
|
<input type="text" class="search-box" id="field-search" placeholder="Filter fields... (e.g. 'upvotes', 'stored', 'dropped')" oninput="filterFields()">
|
|
|
|
<!-- ISSUES RESPONSE -->
|
|
<div class="expandable" data-id="fld-issue">
|
|
<div class="expandable-header open" onclick="toggleExpand('fld-issue')">
|
|
<span><strong>GET /projects/:id/issues</strong> — Issue response object <span style="color:var(--text-muted)">(15 stored / 30+ total)</span></span>
|
|
<span class="arrow" style="transform:rotate(90deg)">▶</span>
|
|
</div>
|
|
<div class="expandable-body open" id="fld-issue-body">
|
|
<table class="field-table">
|
|
<tr><th style="width:200px">API Field</th><th style="width:90px">Status</th><th style="width:180px">DB Column</th><th>Notes</th></tr>
|
|
<tr class="f-stored"><td><code>id</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issues.gitlab_id</code></td><td>GitLab's global issue ID</td></tr>
|
|
<tr class="f-stored"><td><code>iid</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issues.iid</code></td><td>Project-scoped #</td></tr>
|
|
<tr class="f-stored"><td><code>project_id</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issues.project_id</code></td><td>FK to projects</td></tr>
|
|
<tr class="f-stored"><td><code>title</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issues.title</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>description</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issues.description</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>state</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issues.state</code></td><td>"opened" | "closed"</td></tr>
|
|
<tr class="f-stored"><td><code>created_at</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issues.created_at</code></td><td>Converted to ms epoch</td></tr>
|
|
<tr class="f-stored"><td><code>updated_at</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issues.updated_at</code></td><td>Converted to ms epoch; drives cursor</td></tr>
|
|
<tr class="f-stored"><td><code>closed_at</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issues.closed_at</code></td><td>Nullable, ms epoch</td></tr>
|
|
<tr class="f-stored"><td><code>due_date</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issues.due_date</code></td><td>YYYY-MM-DD string</td></tr>
|
|
<tr class="f-stored"><td><code>web_url</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issues.web_url</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>author.username</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issues.author_username</code></td><td>Only username kept</td></tr>
|
|
<tr class="f-stored"><td><code>assignees[].username</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issue_assignees</code></td><td>Junction table, username only</td></tr>
|
|
<tr class="f-stored"><td><code>labels[]</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>issue_labels</code> → <code>labels</code></td><td>Junction table</td></tr>
|
|
<tr class="f-stored"><td><code>milestone.*</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>milestones</code> table + denorm</td><td>id, iid, title, state, due_date, web_url</td></tr>
|
|
<tr class="f-ignored"><td><code>author.id</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>—</td><td>Deserialized but not stored</td></tr>
|
|
<tr class="f-ignored"><td><code>author.name</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>—</td><td>Deserialized but not stored</td></tr>
|
|
<tr class="f-ignored"><td><code>author.state</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>—</td><td>Deserialized but not stored</td></tr>
|
|
<tr class="f-ignored"><td><code>author.avatar_url</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>—</td><td>Deserialized but not stored</td></tr>
|
|
<tr class="f-ignored"><td><code>author.web_url</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>—</td><td>Deserialized but not stored</td></tr>
|
|
<tr class="f-dropped"><td><code>type</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>"ISSUE" — not deserialized</td></tr>
|
|
<tr class="f-dropped"><td><code>upvotes</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Engagement metric, not captured</td></tr>
|
|
<tr class="f-dropped"><td><code>downvotes</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Engagement metric, not captured</td></tr>
|
|
<tr class="f-dropped"><td><code>merge_requests_count</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Count of linked MRs</td></tr>
|
|
<tr class="f-dropped"><td><code>user_notes_count</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Comment count</td></tr>
|
|
<tr class="f-dropped"><td><code>subscribed</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>User-specific subscription state</td></tr>
|
|
<tr class="f-dropped"><td><code>confidential</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Visibility flag</td></tr>
|
|
<tr class="f-dropped"><td><code>discussion_locked</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Lock state</td></tr>
|
|
<tr class="f-dropped"><td><code>weight</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Premium: issue weight</td></tr>
|
|
<tr class="f-dropped"><td><code>time_stats.*</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>time_estimate, total_time_spent, human_*</td></tr>
|
|
<tr class="f-dropped"><td><code>task_completion_status</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>{count, completed_count}</td></tr>
|
|
<tr class="f-dropped"><td><code>references.*</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>{short, relative, full}</td></tr>
|
|
<tr class="f-dropped"><td><code>closed_by</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>User who closed it</td></tr>
|
|
<tr class="f-dropped"><td><code>assignee</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Deprecated singular; uses assignees[]</td></tr>
|
|
<tr class="f-dropped"><td><code>has_tasks</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Task list presence</td></tr>
|
|
<tr class="f-dropped"><td><code>issue_type</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>"issue" | "incident" | "test_case"</td></tr>
|
|
<tr class="f-dropped"><td><code>severity</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Incident severity</td></tr>
|
|
<tr class="f-dropped"><td><code>imported</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Import flag</td></tr>
|
|
<tr class="f-dropped"><td><code>imported_from</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Import source</td></tr>
|
|
<tr class="f-dropped"><td><code>moved_to_id</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Target if moved</td></tr>
|
|
<tr class="f-dropped"><td><code>_links</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>HATEOAS links</td></tr>
|
|
<tr class="f-dropped"><td><code>epic</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Premium: parent epic</td></tr>
|
|
<tr class="f-dropped"><td><code>iteration</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Premium: iteration</td></tr>
|
|
<tr class="f-dropped"><td><code>health_status</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Ultimate: health</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MR RESPONSE -->
|
|
<div class="expandable" data-id="fld-mr">
|
|
<div class="expandable-header" onclick="toggleExpand('fld-mr')">
|
|
<span><strong>GET /projects/:id/merge_requests</strong> — MR response object <span style="color:var(--text-muted)">(20 stored / 45+ total)</span></span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="fld-mr-body">
|
|
<table class="field-table">
|
|
<tr><th style="width:220px">API Field</th><th style="width:90px">Status</th><th style="width:200px">DB Column</th><th>Notes</th></tr>
|
|
<tr class="f-stored"><td><code>id</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.gitlab_id</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>iid</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.iid</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>project_id</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.project_id</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>title</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.title</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>description</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.description</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>state</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.state</code></td><td>opened|merged|closed|locked</td></tr>
|
|
<tr class="f-stored"><td><code>draft</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.draft</code></td><td>Preferred field</td></tr>
|
|
<tr class="f-stored"><td><code>source_branch</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.source_branch</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>target_branch</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.target_branch</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>sha</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.head_sha</code></td><td>Head commit SHA</td></tr>
|
|
<tr class="f-stored"><td><code>references.short</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.references_short</code></td><td>e.g. "!42"</td></tr>
|
|
<tr class="f-stored"><td><code>references.full</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.references_full</code></td><td>e.g. "group/proj!42"</td></tr>
|
|
<tr class="f-stored"><td><code>detailed_merge_status</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.detailed_merge_status</code></td><td>Preferred field</td></tr>
|
|
<tr class="f-stored"><td><code>author.username</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.author_username</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>merge_user.username</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.merge_user_username</code></td><td>Preferred over merged_by</td></tr>
|
|
<tr class="f-stored"><td><code>assignees[].username</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>mr_assignees</code></td><td>Junction table</td></tr>
|
|
<tr class="f-stored"><td><code>reviewers[].username</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>mr_reviewers</code></td><td>Junction table</td></tr>
|
|
<tr class="f-stored"><td><code>labels[]</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>mr_labels</code> → <code>labels</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>created_at</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.created_at</code></td><td>ms epoch</td></tr>
|
|
<tr class="f-stored"><td><code>updated_at</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.updated_at</code></td><td>ms epoch; drives cursor</td></tr>
|
|
<tr class="f-stored"><td><code>merged_at</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.merged_at</code></td><td>ms epoch, nullable</td></tr>
|
|
<tr class="f-stored"><td><code>closed_at</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.closed_at</code></td><td>ms epoch, nullable</td></tr>
|
|
<tr class="f-stored"><td><code>web_url</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>merge_requests.web_url</code></td><td></td></tr>
|
|
<tr class="f-transient"><td><code>work_in_progress</code></td><td><span class="badge" style="background:#2a2a1a;color:var(--yellow)">TRANSIENT</span></td><td>—</td><td>Deprecated fallback for <code>draft</code></td></tr>
|
|
<tr class="f-transient"><td><code>merge_status</code></td><td><span class="badge" style="background:#2a2a1a;color:var(--yellow)">TRANSIENT</span></td><td>—</td><td>Deprecated fallback for <code>detailed_merge_status</code></td></tr>
|
|
<tr class="f-transient"><td><code>merged_by</code></td><td><span class="badge" style="background:#2a2a1a;color:var(--yellow)">TRANSIENT</span></td><td>—</td><td>Deprecated fallback for <code>merge_user</code></td></tr>
|
|
<tr class="f-dropped"><td><code>upvotes</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Engagement metric</td></tr>
|
|
<tr class="f-dropped"><td><code>downvotes</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Engagement metric</td></tr>
|
|
<tr class="f-dropped"><td><code>user_notes_count</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Comment count</td></tr>
|
|
<tr class="f-dropped"><td><code>source_project_id</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Fork source</td></tr>
|
|
<tr class="f-dropped"><td><code>target_project_id</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Fork target</td></tr>
|
|
<tr class="f-dropped"><td><code>milestone</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Full milestone object (stored for issues, not MRs)</td></tr>
|
|
<tr class="f-dropped"><td><code>merge_when_pipeline_succeeds</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Auto-merge flag</td></tr>
|
|
<tr class="f-dropped"><td><code>merge_commit_sha</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Merge commit reference</td></tr>
|
|
<tr class="f-dropped"><td><code>squash_commit_sha</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Squash commit reference</td></tr>
|
|
<tr class="f-dropped"><td><code>discussion_locked</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Lock state</td></tr>
|
|
<tr class="f-dropped"><td><code>should_remove_source_branch</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Cleanup preference</td></tr>
|
|
<tr class="f-dropped"><td><code>force_remove_source_branch</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Forced cleanup</td></tr>
|
|
<tr class="f-dropped"><td><code>squash</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Squash flag</td></tr>
|
|
<tr class="f-dropped"><td><code>squash_on_merge</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Squash on merge flag</td></tr>
|
|
<tr class="f-dropped"><td><code>has_conflicts</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Merge conflict state</td></tr>
|
|
<tr class="f-dropped"><td><code>blocking_discussions_resolved</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>All blocking threads resolved?</td></tr>
|
|
<tr class="f-dropped"><td><code>time_stats.*</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Time tracking data</td></tr>
|
|
<tr class="f-dropped"><td><code>task_completion_status</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>{count, completed_count}</td></tr>
|
|
<tr class="f-dropped"><td><code>closed_by</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>User who closed</td></tr>
|
|
<tr class="f-dropped"><td><code>prepared_at</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Preparation timestamp</td></tr>
|
|
<tr class="f-dropped"><td><code>merge_after</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Scheduled merge time</td></tr>
|
|
<tr class="f-dropped"><td><code>imported</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Import flag</td></tr>
|
|
<tr class="f-dropped"><td><code>approvals_before_merge</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Deprecated approval count</td></tr>
|
|
<tr class="f-dropped"><td><code>references.relative</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Only short + full stored</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DISCUSSION RESPONSE -->
|
|
<div class="expandable" data-id="fld-disc">
|
|
<div class="expandable-header" onclick="toggleExpand('fld-disc')">
|
|
<span><strong>GET /projects/:id/.../discussions</strong> — Discussion + Note response <span style="color:var(--text-muted)">(18 stored / 28+ total)</span></span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="fld-disc-body">
|
|
<h4>Discussion object</h4>
|
|
<table class="field-table">
|
|
<tr><th style="width:200px">API Field</th><th style="width:90px">Status</th><th style="width:180px">DB Column</th><th>Notes</th></tr>
|
|
<tr class="f-stored"><td><code>id</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>discussions.gitlab_discussion_id</code></td><td>SHA-like string</td></tr>
|
|
<tr class="f-stored"><td><code>individual_note</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>discussions.individual_note</code></td><td>0/1</td></tr>
|
|
<tr class="f-stored"><td><code>notes[]</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes</code> table</td><td>Each note stored individually</td></tr>
|
|
</table>
|
|
<p style="font-size:12px;color:var(--text-muted);margin:8px 0">Computed from notes: <code>first_note_at</code>, <code>last_note_at</code>, <code>resolvable</code> (any note resolvable), <code>resolved</code> (all resolvable notes resolved)</p>
|
|
|
|
<h4>Note object (within discussion)</h4>
|
|
<table class="field-table">
|
|
<tr><th style="width:200px">API Field</th><th style="width:90px">Status</th><th style="width:180px">DB Column</th><th>Notes</th></tr>
|
|
<tr class="f-stored"><td><code>id</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.gitlab_id</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>type</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.note_type</code></td><td>DiscussionNote|DiffNote|null</td></tr>
|
|
<tr class="f-stored"><td><code>body</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.body</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>system</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.is_system</code></td><td>Auto-generated notes</td></tr>
|
|
<tr class="f-stored"><td><code>author.username</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.author_username</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>created_at</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.created_at</code></td><td>ms epoch</td></tr>
|
|
<tr class="f-stored"><td><code>updated_at</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.updated_at</code></td><td>ms epoch</td></tr>
|
|
<tr class="f-stored"><td><code>resolvable</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.resolvable</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>resolved</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.resolved</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>resolved_by.username</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.resolved_by</code></td><td>Username only</td></tr>
|
|
<tr class="f-stored"><td><code>resolved_at</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.resolved_at</code></td><td>ms epoch</td></tr>
|
|
<tr class="f-stored"><td><code>position.old_path</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.position_old_path</code></td><td>DiffNote only</td></tr>
|
|
<tr class="f-stored"><td><code>position.new_path</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.position_new_path</code></td><td>DiffNote only</td></tr>
|
|
<tr class="f-stored"><td><code>position.old_line</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.position_old_line</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>position.new_line</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.position_new_line</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>position.position_type</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.position_type</code></td><td>"text"|"image"|"file"</td></tr>
|
|
<tr class="f-stored"><td><code>position.line_range</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.position_line_range_*</code></td><td>Start/end line numbers extracted</td></tr>
|
|
<tr class="f-stored"><td><code>position.base_sha</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.position_base_sha</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>position.start_sha</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.position_start_sha</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>position.head_sha</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>notes.position_head_sha</code></td><td></td></tr>
|
|
<tr class="f-dropped"><td><code>attachment</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>File attachment metadata</td></tr>
|
|
<tr class="f-dropped"><td><code>noteable_id</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Parent ID (redundant, known from URL)</td></tr>
|
|
<tr class="f-dropped"><td><code>noteable_type</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Parent type (redundant)</td></tr>
|
|
<tr class="f-dropped"><td><code>noteable_iid</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Parent IID (redundant)</td></tr>
|
|
<tr class="f-dropped"><td><code>project_id</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>From note context (redundant)</td></tr>
|
|
<tr class="f-dropped"><td><code>commit_id</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>DiffNote: specific commit</td></tr>
|
|
<tr class="f-dropped"><td><code>position.width</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Image position only</td></tr>
|
|
<tr class="f-dropped"><td><code>position.height</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Image position only</td></tr>
|
|
<tr class="f-dropped"><td><code>position.x</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Image position only</td></tr>
|
|
<tr class="f-dropped"><td><code>position.y</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Image position only</td></tr>
|
|
<tr class="f-dropped"><td><code>suggestions[]</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Code suggestion diffs (from_line, to_line, content)</td></tr>
|
|
<tr class="f-dropped"><td><code>line_range.*.line_code</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Internal line identifier</td></tr>
|
|
<tr class="f-dropped"><td><code>line_range.*.type</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>"old"|"new" side indicator</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PROJECT RESPONSE -->
|
|
<div class="expandable" data-id="fld-proj">
|
|
<div class="expandable-header" onclick="toggleExpand('fld-proj')">
|
|
<span><strong>GET /projects/:path</strong> — Project response <span style="color:var(--text-muted)">(6 stored / 40+ total)</span></span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="fld-proj-body">
|
|
<table class="field-table">
|
|
<tr><th style="width:220px">API Field</th><th style="width:90px">Status</th><th style="width:200px">DB Column</th><th>Notes</th></tr>
|
|
<tr class="f-stored"><td><code>id</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>projects.gitlab_project_id</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>path_with_namespace</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>projects.path_with_namespace</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>default_branch</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>projects.default_branch</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>web_url</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>projects.web_url</code></td><td></td></tr>
|
|
<tr class="f-stored"><td><code>created_at</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>projects.created_at</code></td><td>ms epoch</td></tr>
|
|
<tr class="f-stored"><td><code>updated_at</code></td><td><span class="badge" style="background:#1a3a2a;color:var(--green)">STORED</span></td><td><code>projects.updated_at</code></td><td>ms epoch</td></tr>
|
|
<tr class="f-ignored"><td><code>name</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>—</td><td>Deserialized but not stored</td></tr>
|
|
<tr class="f-ignored"><td><code>description</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>—</td><td>Deserialized but not stored</td></tr>
|
|
<tr class="f-ignored"><td><code>visibility</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>—</td><td>Deserialized but not stored</td></tr>
|
|
<tr class="f-ignored"><td><code>archived</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>—</td><td>Deserialized but not stored</td></tr>
|
|
<tr class="f-dropped"><td><code>forks_count</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td></td></tr>
|
|
<tr class="f-dropped"><td><code>star_count</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td></td></tr>
|
|
<tr class="f-dropped"><td><code>issues_enabled</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Feature toggle</td></tr>
|
|
<tr class="f-dropped"><td><code>merge_requests_enabled</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Feature toggle</td></tr>
|
|
<tr class="f-dropped"><td><code>wiki_enabled</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td></td></tr>
|
|
<tr class="f-dropped"><td><code>http_url_to_repo</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Clone URL</td></tr>
|
|
<tr class="f-dropped"><td><code>ssh_url_to_repo</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Clone URL</td></tr>
|
|
<tr class="f-dropped"><td><code>owner</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Owner user object</td></tr>
|
|
<tr class="f-dropped"><td><code>namespace</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Group/namespace info</td></tr>
|
|
<tr class="f-dropped"><td><code>last_activity_at</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Last push/update</td></tr>
|
|
<tr class="f-dropped"><td><code>statistics</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td>—</td><td>Commit count, storage, etc.</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- USER RESPONSE -->
|
|
<div class="expandable" data-id="fld-user">
|
|
<div class="expandable-header" onclick="toggleExpand('fld-user')">
|
|
<span><strong>GET /user</strong> — Current user response <span style="color:var(--text-muted)">(0 stored, transient only / 20+ total)</span></span>
|
|
<span class="arrow">▶</span>
|
|
</div>
|
|
<div class="expandable-body" id="fld-user-body">
|
|
<table class="field-table">
|
|
<tr><th style="width:200px">API Field</th><th style="width:90px">Status</th><th>Notes</th></tr>
|
|
<tr class="f-transient"><td><code>id</code></td><td><span class="badge" style="background:#2a2a1a;color:var(--yellow)">TRANSIENT</span></td><td>Displayed in auth check</td></tr>
|
|
<tr class="f-transient"><td><code>username</code></td><td><span class="badge" style="background:#2a2a1a;color:var(--yellow)">TRANSIENT</span></td><td>Displayed in auth check</td></tr>
|
|
<tr class="f-transient"><td><code>name</code></td><td><span class="badge" style="background:#2a2a1a;color:var(--yellow)">TRANSIENT</span></td><td>Displayed in auth check</td></tr>
|
|
<tr class="f-ignored"><td><code>email</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>Deserialized but not used</td></tr>
|
|
<tr class="f-ignored"><td><code>avatar_url</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>Deserialized but not used</td></tr>
|
|
<tr class="f-ignored"><td><code>web_url</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>Deserialized but not used</td></tr>
|
|
<tr class="f-ignored"><td><code>created_at</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>Deserialized but not used</td></tr>
|
|
<tr class="f-ignored"><td><code>state</code></td><td><span class="badge" style="background:#2a1a2a;color:var(--purple)">IGNORED</span></td><td>Deserialized but not used</td></tr>
|
|
<tr class="f-dropped"><td><code>bio</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td></td></tr>
|
|
<tr class="f-dropped"><td><code>location</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td></td></tr>
|
|
<tr class="f-dropped"><td><code>public_email</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td></td></tr>
|
|
<tr class="f-dropped"><td><code>organization</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td></td></tr>
|
|
<tr class="f-dropped"><td><code>job_title</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td></td></tr>
|
|
<tr class="f-dropped"><td><code>two_factor_enabled</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td></td></tr>
|
|
<tr class="f-dropped"><td><code>identities</code></td><td><span class="badge" style="background:#2a1a1a;color:var(--red)">DROPPED</span></td><td></td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SUMMARY STATS -->
|
|
<h3 style="margin-top:24px">Field Coverage Summary</h3>
|
|
<div class="grid-2">
|
|
<div class="card">
|
|
<div class="card-title">Issues Response</div>
|
|
<div class="stat-row"><span style="color:var(--green)">Stored</span><span>15 fields</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:36%;background:var(--green)"></div></div>
|
|
<div class="stat-row"><span style="color:var(--purple)">Deserialized, ignored</span><span>5 fields</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:12%;background:var(--purple)"></div></div>
|
|
<div class="stat-row"><span style="color:var(--red)">Never deserialized</span><span>~22 fields</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:52%;background:var(--red)"></div></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">Merge Request Response</div>
|
|
<div class="stat-row"><span style="color:var(--green)">Stored</span><span>23 fields</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:48%;background:var(--green)"></div></div>
|
|
<div class="stat-row"><span style="color:var(--yellow)">Transient (fallbacks)</span><span>3 fields</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:6%;background:var(--yellow)"></div></div>
|
|
<div class="stat-row"><span style="color:var(--red)">Never deserialized</span><span>~22 fields</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:46%;background:var(--red)"></div></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">Discussion/Note Response</div>
|
|
<div class="stat-row"><span style="color:var(--green)">Stored</span><span>23 fields</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:64%;background:var(--green)"></div></div>
|
|
<div class="stat-row"><span style="color:var(--red)">Never deserialized</span><span>~13 fields</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:36%;background:var(--red)"></div></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">Project Response</div>
|
|
<div class="stat-row"><span style="color:var(--green)">Stored</span><span>6 fields</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:14%;background:var(--green)"></div></div>
|
|
<div class="stat-row"><span style="color:var(--purple)">Deserialized, ignored</span><span>4 fields</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:10%;background:var(--purple)"></div></div>
|
|
<div class="stat-row"><span style="color:var(--red)">Never deserialized</span><span>~30 fields</span></div>
|
|
<div class="coverage-bar"><div class="coverage-fill" style="width:76%;background:var(--red)"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:16px;border-color:var(--accent)">
|
|
<div class="card-title">Key insight: Raw payloads preserve everything</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">Although many fields are dropped during transformation, the <code>raw_payloads</code> table stores the complete original JSON response (with SHA-256 dedup and optional gzip). This means all "dropped" data is still recoverable from the blob storage without re-fetching from GitLab. The normalized tables are optimized for query patterns, not completeness.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- EFFICIENCY ANALYSIS -->
|
|
<div id="efficiency" class="panel">
|
|
<h2>Efficiency Analysis & Opportunities</h2>
|
|
<p style="font-size:13px;color:var(--text-muted);margin-bottom:16px">Observations on how gitlore could leverage the GitLab API more efficiently, and data it currently leaves on the table.</p>
|
|
|
|
<h3>Current Efficiency Wins</h3>
|
|
<div class="grid-2">
|
|
<div class="card" style="border-left:3px solid var(--green)">
|
|
<div class="card-title" style="color:var(--green)">Cursor-based incremental sync</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">Uses <code>updated_after</code> + <code>order_by=updated_at&sort=asc</code> to only fetch changed records. Avoids full re-fetch on every sync. This is the single biggest efficiency feature.</p>
|
|
</div>
|
|
<div class="card" style="border-left:3px solid var(--green)">
|
|
<div class="card-title" style="color:var(--green)">Raw payload dedup</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">SHA-256 hashing prevents storing identical payloads. If an issue's <code>updated_at</code> changes but the actual content is identical, the raw blob is deduplicated.</p>
|
|
</div>
|
|
<div class="card" style="border-left:3px solid var(--green)">
|
|
<div class="card-title" style="color:var(--green)">Discussion watermark</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">Only re-syncs discussions for issues/MRs whose <code>updated_at</code> has advanced past their <code>discussions_synced_for_updated_at</code> watermark. Skips unchanged entities.</p>
|
|
</div>
|
|
<div class="card" style="border-left:3px solid var(--green)">
|
|
<div class="card-title" style="color:var(--green)">Parallel discussion prefetch</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">Fetches discussions for multiple issues/MRs concurrently (configurable, default 2). Dramatically reduces wall-clock time for discussion sync.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Potential Inefficiencies</h3>
|
|
|
|
<div class="card" style="border-left:3px solid var(--red)">
|
|
<div class="card-title" style="color:var(--red)">1. Discussion full-refresh strategy</div>
|
|
<p style="font-size:13px">Every time an issue/MR is updated, ALL its discussions are re-fetched and replaced (DELETE + INSERT). For heavily-discussed items (50+ comments), this is expensive.</p>
|
|
<table style="margin-top:8px">
|
|
<tr><th>Scenario</th><th>Current</th><th>Alternative</th></tr>
|
|
<tr><td>Issue with 100 notes gets 1 new comment</td><td>Re-fetch all 100 notes (multiple pages)</td><td>Could use <code>GET .../notes?order_by=updated_at&updated_after=...</code> for incremental note sync</td></tr>
|
|
<tr><td>MR label change (no new comments)</td><td>Re-fetch all discussions anyway</td><td>Could check <code>user_notes_count</code> delta or use Notes API with updated_after</td></tr>
|
|
</table>
|
|
<p style="font-size:12px;color:var(--text-muted);margin-top:8px"><strong>Trade-off:</strong> Full-refresh is simpler and guarantees consistency (catches edits, deletes). Incremental would miss deleted notes.</p>
|
|
</div>
|
|
|
|
<div class="card" style="border-left:3px solid var(--yellow)">
|
|
<div class="card-title" style="color:var(--yellow)">2. Offset pagination instead of keyset</div>
|
|
<p style="font-size:13px">Gitlore uses <code>page=N&per_page=100</code> offset pagination. GitLab supports <strong>keyset pagination</strong> for some endpoints (Issues, MRs), which is more efficient for large datasets and recommended by GitLab.</p>
|
|
<pre style="margin-top:8px">Current: GET /projects/:id/issues?page=5&per_page=100
|
|
Keyset: GET /projects/:id/issues?pagination=keyset&per_page=100
|
|
(uses Link header rel="next" with cursor)</pre>
|
|
<p style="font-size:12px;color:var(--text-muted);margin-top:8px"><strong>Benefit:</strong> Keyset pagination is O(1) per page (vs O(N) for offset). GitLab recommends it for >10,000 records. Gitlore already parses <code>Link</code> headers, so the client-side support partially exists.</p>
|
|
</div>
|
|
|
|
<div class="card" style="border-left:3px solid var(--yellow)">
|
|
<div class="card-title" style="color:var(--yellow)">3. No ETag / conditional request support</div>
|
|
<p style="font-size:13px">GitLab returns <code>ETag</code> headers on API responses. Sending <code>If-None-Match</code> on subsequent requests would return <code>304 Not Modified</code> without consuming rate limit quota on some endpoints. Currently all requests are unconditional.</p>
|
|
<p style="font-size:12px;color:var(--text-muted);margin-top:8px"><strong>Impact:</strong> Moderate. The cursor-based sync already avoids re-fetching unchanged data, so ETag would mainly help with the <code>discussions</code> full-refresh scenario where nothing changed.</p>
|
|
</div>
|
|
|
|
<div class="card" style="border-left:3px solid var(--yellow)">
|
|
<div class="card-title" style="color:var(--yellow)">4. Labels extracted from embedded data, not dedicated API</div>
|
|
<p style="font-size:13px">Gitlore extracts labels from the <code>labels[]</code> string array embedded in issue/MR responses. The dedicated <code>GET /projects/:id/labels</code> endpoint returns richer data:</p>
|
|
<table style="margin-top:8px">
|
|
<tr><th>From issues response</th><th>From Labels API</th></tr>
|
|
<tr><td>Label name (string only)</td><td>name, color, description, text_color, priority, is_project_label, subscribed, open_issues_count, closed_issues_count, open_merge_requests_count</td></tr>
|
|
</table>
|
|
<p style="font-size:12px;color:var(--text-muted);margin-top:8px"><strong>Impact:</strong> The <code>labels</code> table has <code>color</code> and <code>description</code> columns but they may not be populated from the embedded string array. A single Labels API call (one request, non-paginated for most projects) would enrich the local label catalog.</p>
|
|
</div>
|
|
|
|
<h3>Dropped Data Worth Capturing</h3>
|
|
<p style="font-size:13px;color:var(--text-muted);margin-bottom:12px">Fields currently silently dropped that could add value to local queries:</p>
|
|
|
|
<table>
|
|
<tr><th>Field</th><th>Source</th><th>Value Proposition</th><th>Effort</th></tr>
|
|
<tr>
|
|
<td><code>user_notes_count</code></td>
|
|
<td>Issues, MRs</td>
|
|
<td>Could skip discussion re-sync when count hasn't changed. Quick "activity" sort without joining notes table.</td>
|
|
<td style="color:var(--green)">Low</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>upvotes</code> / <code>downvotes</code></td>
|
|
<td>Issues, MRs</td>
|
|
<td>Engagement metrics for triage. "Most upvoted issues" is a common query.</td>
|
|
<td style="color:var(--green)">Low</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>confidential</code></td>
|
|
<td>Issues</td>
|
|
<td>Security-sensitive filtering. Avoid exposing confidential issues in outputs.</td>
|
|
<td style="color:var(--green)">Low</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>weight</code></td>
|
|
<td>Issues</td>
|
|
<td>Effort estimation for sprint planning (Premium/Ultimate only).</td>
|
|
<td style="color:var(--green)">Low</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>time_stats</code></td>
|
|
<td>Issues, MRs</td>
|
|
<td>Time tracking data for project reporting. Already in the response, free to capture.</td>
|
|
<td style="color:var(--green)">Low</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>has_conflicts</code></td>
|
|
<td>MRs</td>
|
|
<td>Identify MRs needing rebase. Useful for "stale MR" alerts.</td>
|
|
<td style="color:var(--green)">Low</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>blocking_discussions_resolved</code></td>
|
|
<td>MRs</td>
|
|
<td>MR readiness indicator without joining discussions table.</td>
|
|
<td style="color:var(--green)">Low</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>merge_commit_sha</code></td>
|
|
<td>MRs</td>
|
|
<td>Trace merged MRs to specific commits. Useful for git correlation.</td>
|
|
<td style="color:var(--green)">Low</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>suggestions[]</code></td>
|
|
<td>Discussion notes</td>
|
|
<td>Code review suggestions with from/to content. Rich data for code review analysis.</td>
|
|
<td style="color:var(--yellow)">Medium</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>task_completion_status</code></td>
|
|
<td>Issues, MRs</td>
|
|
<td>Track task-list checkbox progress without parsing description markdown.</td>
|
|
<td style="color:var(--green)">Low</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>issue_type</code></td>
|
|
<td>Issues</td>
|
|
<td>Distinguish issues vs incidents vs test cases.</td>
|
|
<td style="color:var(--green)">Low</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>discussion_locked</code></td>
|
|
<td>Issues, MRs</td>
|
|
<td>Know if new comments can be added.</td>
|
|
<td style="color:var(--green)">Low</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<h3>Structural Optimization Opportunities</h3>
|
|
|
|
<div class="card" style="border-left:3px solid var(--purple)">
|
|
<div class="card-title" style="color:var(--purple)">5. User denormalization</div>
|
|
<p style="font-size:13px">Currently stores only <code>username</code> for authors, assignees, reviewers. The API returns <code>name</code>, <code>avatar_url</code>, <code>web_url</code>, and <code>state</code> for every user reference. A <code>users</code> table could deduplicate this data and provide richer displays.</p>
|
|
<pre style="margin-top:8px">-- Potential schema
|
|
CREATE TABLE users (
|
|
username TEXT PRIMARY KEY,
|
|
name TEXT,
|
|
gitlab_id INTEGER,
|
|
avatar_url TEXT,
|
|
state TEXT, -- "active", "blocked", etc.
|
|
last_seen_at INTEGER -- auto-updated on encounter
|
|
);</pre>
|
|
<p style="font-size:12px;color:var(--text-muted);margin-top:8px"><strong>Cost:</strong> No additional API calls. Data is already in every issue/MR/note response. Just needs extraction during transform.</p>
|
|
</div>
|
|
|
|
<div class="card" style="border-left:3px solid var(--purple)">
|
|
<div class="card-title" style="color:var(--purple)">6. MR milestone not captured</div>
|
|
<p style="font-size:13px">Milestones are stored for issues but the MR transformer does not extract the <code>milestone</code> object from MR responses, even though GitLab returns it. The <code>merge_requests</code> table has no <code>milestone_id</code> column.</p>
|
|
<p style="font-size:12px;color:var(--text-muted);margin-top:8px"><strong>Impact:</strong> Cannot query "which MRs are in milestone X?" locally. The data is in the raw payload but not indexed.</p>
|
|
</div>
|
|
|
|
<div class="card" style="border-left:3px solid var(--purple)">
|
|
<div class="card-title" style="color:var(--purple)">7. Issue references not captured</div>
|
|
<p style="font-size:13px">MRs store <code>references.short</code> and <code>references.full</code>, but the issue transformer drops the <code>references</code> object entirely. This means issues lack the cross-project reference format (e.g., <code>group/project#42</code>).</p>
|
|
</div>
|
|
|
|
<h3>API Strategies Not Yet Used</h3>
|
|
|
|
<div class="card">
|
|
<div class="card-title">Webhooks (push-based sync)</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">Instead of polling, GitLab can push events via <code>POST /projects/:id/hooks</code>. Would enable near-real-time sync without rate-limit cost. Requires a listener endpoint.</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">Events API (lightweight change detection)</div>
|
|
<p style="font-size:13px;color:var(--text-muted)"><code>GET /projects/:id/events</code> returns a stream of all project activity. Could be used as a fast "has anything changed?" check before running expensive issue/MR sync. Much lighter than fetching full issue lists.</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">GraphQL API (precise field selection)</div>
|
|
<p style="font-size:13px;color:var(--text-muted)">GitLab's GraphQL API allows requesting exactly the fields needed. Would eliminate bandwidth waste from ~50% of response fields being silently dropped. Trade-off: different pagination model, potentially less stable API surface.</p>
|
|
</div>
|
|
|
|
<h3 style="margin-top:24px">Summary Verdict</h3>
|
|
<div class="card" style="border:1px solid var(--accent)">
|
|
<p style="font-size:13px">Gitlore is <strong>well-optimized for its core use case</strong> (read-only local sync). The cursor-based incremental sync and raw payload archival are sophisticated. The main opportunities are:</p>
|
|
<ol style="font-size:13px;padding-left:20px;margin-top:8px">
|
|
<li><strong>Capture more "free" data</strong> — Fields like <code>user_notes_count</code>, <code>upvotes</code>, <code>has_conflicts</code> are already in API responses. Storing them costs zero API calls and enables richer queries.</li>
|
|
<li><strong>Discussion sync efficiency</strong> — The full-refresh strategy is the biggest source of redundant API calls. Even a simple <code>user_notes_count</code> comparison could skip unchanged discussions.</li>
|
|
<li><strong>Keyset pagination</strong> — A meaningful improvement for large projects (>10K issues), and Gitlore already has partial infrastructure for it.</li>
|
|
<li><strong>MR milestone parity</strong> — Low-effort gap to close with issue milestone support.</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /content -->
|
|
|
|
<script>
|
|
function showTab(id) {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
document.getElementById(id).classList.add('active');
|
|
document.querySelector(`.tab[onclick="showTab('${id}')"]`).classList.add('active');
|
|
}
|
|
|
|
function toggleExpand(id) {
|
|
const header = document.querySelector(`[data-id="${id}"] .expandable-header`);
|
|
const body = document.getElementById(`${id}-body`);
|
|
header.classList.toggle('open');
|
|
body.classList.toggle('open');
|
|
}
|
|
|
|
function filterAPI() {
|
|
const query = document.getElementById('api-search').value.toLowerCase();
|
|
document.querySelectorAll('#api-list .api-table tr').forEach(row => {
|
|
if (row.querySelector('th')) return; // skip headers
|
|
const text = row.textContent.toLowerCase();
|
|
row.style.display = text.includes(query) ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
// Style used rows
|
|
document.querySelectorAll('.used-row').forEach(row => {
|
|
row.style.background = 'rgba(88, 166, 255, 0.05)';
|
|
});
|
|
|
|
function filterFields() {
|
|
const query = document.getElementById('field-search').value.toLowerCase();
|
|
document.querySelectorAll('#fields .field-table tr').forEach(row => {
|
|
if (row.querySelector('th')) return;
|
|
const text = row.textContent.toLowerCase();
|
|
row.style.display = text.includes(query) ? '' : 'none';
|
|
});
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|