guito 1.0.11 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +2 -1
- package/bin/ai-review.js +25 -1
- package/bin/guito-server.js +128 -87
- package/bin/ui/browser/index.html +1 -1
- package/bin/ui/browser/main-NA3UMMCQ.js +67 -0
- package/{guito-1.0.11.vsix → guito-1.0.12.vsix} +0 -0
- package/package.json +1 -1
- package/bin/ui/browser/main-3MOB345X.js +0 -67
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ All notable changes to Guito are documented in this file. The project follows [S
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.0.12] - 2026-10-15
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Azure DevOps is now detected from the repository's remotes instead of configured. Guito derives the project from `origin` (or the first other remote pointing to Azure DevOps) and creates pull requests against that remote; the settings dialog shows the detected project. The `guito.azureDevOpsUrl` VS Code setting and the Server URL field in the settings dialog are removed.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Pull request **Comments** tab now shows code context on inline threads: a focused snippet around the commented line with a **Show code context** loader and an expandable full-file diff, so reviewers see what a comment refers to without leaving the tab.
|
|
16
|
+
- Azure-generated comment activity now reads as plain language: vote entries like "changed status to 10" render as "approved" (with suggestions / waiting for author / rejected mapped the same way) and named status changes use friendly labels like Resolved, with the raw text kept as a tooltip. System entries in an inline thread's diff view render as a quiet one-line note instead of a comment with avatar and markdown, again with the raw text as a tooltip.
|
|
17
|
+
- Filter pull request comments by thread status (with counts), location (General vs Inline), and entry kind (comments vs activity), with a **Clear** reset and a distinct "No comments match these filters" empty state.
|
|
18
|
+
- Mark automated review findings as fixed from the PR dialog's **Review** tab with a per-finding **I fixed it** button. Fixed points move to **Already handled** with a green **Fixed** label and are never raised again by future reviews, like dismissed ones. A **Dim handled** toggle dims the handled section via opacity so pending points stand out; it is on by default and remembered per browser.
|
|
19
|
+
- Decline to review a pull request from the PR dialog's **Review** menu (**Decline to review** / **Resume review**). Reviewers who declined appear in their own **Declined** section of the dialog's reviewer list, and the side panel marks pull requests you declined with a **Declined** badge.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Right-clicking a branch badge in the commit history no longer offers the wrong actions for local branches whose names contain a slash (e.g. `feature/topic`). Badges are now resolved against the loaded branch list instead of guessing from the name, so **Checkout Branch...** opens the local switch dialog and **Delete Branch...** deletes the local branch instead of failing against a misread remote.
|
|
24
|
+
- Detached `HEAD` and symbolic `origin/HEAD` badges no longer offer **Rename**, **Delete** or **Pull** actions that Git would refuse, and **Unselect in Branches Dropdown** now removes only that branch from the history filter (instead of clearing the whole selection) and stays disabled when the branch is not selected.
|
|
25
|
+
|
|
7
26
|
## [1.0.11] - 2026-10-12
|
|
8
27
|
|
|
9
28
|
### Added
|
package/README.md
CHANGED
|
@@ -111,9 +111,10 @@ The settings dialog also manages preferences that have no VS Code equivalent: th
|
|
|
111
111
|
|
|
112
112
|
### Azure DevOps
|
|
113
113
|
|
|
114
|
+
Guito detects Azure DevOps from the repository's remotes: when `origin` (or, failing that, any other remote) has a URL like `https://server/DefaultCollection/Project/_git/Repo`, pull request creation, the pull request panel, and the automated reviewer are enabled for that project. ssh remotes are reached over https on the same host. Requests authenticate with Windows integrated auth.
|
|
115
|
+
|
|
114
116
|
| Setting | Description |
|
|
115
117
|
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
116
|
-
| `guito.azureDevOpsUrl` | Base URL of your on-prem Azure DevOps Server, for example `https://server/DefaultCollection`. Enables pull request creation and the automated reviewer; requests authenticate with Windows integrated auth. |
|
|
117
118
|
| `guito.prBranchNameTemplate` | Branch name template used when Guito creates a new branch for a pull request. Defaults to `pr/${randomstring}`. Variables: `${username}`, `${randomstring}`, `${branch}`, `${targetbranch}`, `${title}`, `${repository}`, `${date}` (UTC `YYYY-MM-DD`), `${time}` (UTC `HHmmss`), and `${timestamp}`. |
|
|
118
119
|
| `guito.allowMerge` | Offer the "No fast-forward (merge commit)" and "Semi-linear merge" pull request completion strategies. On by default. |
|
|
119
120
|
|
package/bin/ai-review.js
CHANGED
|
@@ -346,8 +346,9 @@ export function buildReviewPrompt(input) {
|
|
|
346
346
|
const incremental = Boolean(sinceCommit);
|
|
347
347
|
const perFile = Math.max(2000, Math.floor(input.maxDiffChars / Math.max(diffs.length, 1)));
|
|
348
348
|
const body = diffs.map((diff) => renderFileDiff(diff, perFile)).join('\n');
|
|
349
|
-
const raised = previous.filter((finding) => finding.status
|
|
349
|
+
const raised = previous.filter((finding) => finding.status === 'pending' || finding.status === 'posted');
|
|
350
350
|
const dismissed = previous.filter((finding) => finding.status === 'dismissed');
|
|
351
|
+
const fixed = previous.filter((finding) => finding.status === 'fixed');
|
|
351
352
|
const sections = [
|
|
352
353
|
'You are reviewing an Azure DevOps pull request as a careful senior engineer on this team.',
|
|
353
354
|
'',
|
|
@@ -377,6 +378,13 @@ export function buildReviewPrompt(input) {
|
|
|
377
378
|
.join('\n')
|
|
378
379
|
: ' None.',
|
|
379
380
|
'',
|
|
381
|
+
'POINTS THE USER FIXED (never raise these again):',
|
|
382
|
+
fixed.length
|
|
383
|
+
? fixed
|
|
384
|
+
.map((finding) => ` - ${finding.file || 'pull request'} — ${finding.title}`)
|
|
385
|
+
.join('\n')
|
|
386
|
+
: ' None.',
|
|
387
|
+
'',
|
|
380
388
|
'EXISTING COMMENT THREADS ON THE PULL REQUEST:',
|
|
381
389
|
renderThreads(threads),
|
|
382
390
|
'',
|
|
@@ -755,6 +763,22 @@ export function createAiReviewer(deps, initialConfig = DEFAULT_AI_REVIEW_CONFIG,
|
|
|
755
763
|
return next;
|
|
756
764
|
});
|
|
757
765
|
},
|
|
766
|
+
async fix(pullRequestId, findingIds) {
|
|
767
|
+
const wanted = new Set(findingIds);
|
|
768
|
+
return mutateStore((store) => {
|
|
769
|
+
const current = store.pullRequests[String(pullRequestId)];
|
|
770
|
+
if (!current)
|
|
771
|
+
throw new Error(`Pull request ${pullRequestId} has not been reviewed yet.`);
|
|
772
|
+
const next = {
|
|
773
|
+
...current,
|
|
774
|
+
findings: current.findings.map((finding) => finding.status === 'pending' && wanted.has(finding.id)
|
|
775
|
+
? { ...finding, status: 'fixed', resolvedAt: now().toISOString() }
|
|
776
|
+
: finding),
|
|
777
|
+
};
|
|
778
|
+
store.pullRequests[String(pullRequestId)] = next;
|
|
779
|
+
return next;
|
|
780
|
+
});
|
|
781
|
+
},
|
|
758
782
|
async forget(pullRequestId) {
|
|
759
783
|
await mutateStore((store) => {
|
|
760
784
|
delete store.pullRequests[String(pullRequestId)];
|
package/bin/guito-server.js
CHANGED
|
@@ -61,6 +61,20 @@ const AZURE_THREAD_STATUSES = {
|
|
|
61
61
|
pending: 7,
|
|
62
62
|
};
|
|
63
63
|
const azureThreadStatusName = (value) => Object.entries(AZURE_THREAD_STATUSES).find(([, numeric]) => numeric === value)?.[0] ?? 'unknown';
|
|
64
|
+
/** Azure DevOps comment types, by their numeric REST value. */
|
|
65
|
+
const AZURE_COMMENT_TYPES = {
|
|
66
|
+
unknown: 0,
|
|
67
|
+
text: 1,
|
|
68
|
+
codeChange: 2,
|
|
69
|
+
system: 3,
|
|
70
|
+
};
|
|
71
|
+
const azureCommentTypeName = (value) => {
|
|
72
|
+
if (typeof value === 'string' && value) {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
const numeric = Number(value ?? 0);
|
|
76
|
+
return (Object.entries(AZURE_COMMENT_TYPES).find(([, entry]) => entry === numeric)?.[0] ?? 'unknown');
|
|
77
|
+
};
|
|
64
78
|
/** Pull request reviewer votes, by their numeric REST value. */
|
|
65
79
|
const AZURE_VOTES = {
|
|
66
80
|
reset: 0,
|
|
@@ -111,47 +125,40 @@ export function parseAzureRemoteUrl(remoteUrl) {
|
|
|
111
125
|
return { projectPath, repo };
|
|
112
126
|
}
|
|
113
127
|
/**
|
|
114
|
-
*
|
|
115
|
-
* DevOps REST calls
|
|
116
|
-
*
|
|
128
|
+
* Detects the encoded "{origin}/{collection-or-org}/{project}" prefix for
|
|
129
|
+
* Azure DevOps REST calls from a git remote URL. https remotes keep their
|
|
130
|
+
* scheme, host and port (credentials are dropped); ssh remotes map to https
|
|
131
|
+
* on the same host, since ssh carries no web address.
|
|
117
132
|
*/
|
|
118
|
-
export function buildAzureProjectPrefix(
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
const info = parseAzureRemoteUrl(remoteUrl);
|
|
133
|
+
export function buildAzureProjectPrefix(remoteUrl) {
|
|
134
|
+
const value = (remoteUrl ?? '').trim();
|
|
135
|
+
const info = parseAzureRemoteUrl(value);
|
|
124
136
|
if (!info) {
|
|
125
137
|
return null;
|
|
126
138
|
}
|
|
127
|
-
|
|
139
|
+
let origin;
|
|
140
|
+
const httpMatch = value.match(/^(https?):\/\/(?:[^@/]*@)?([^/]+)\//i);
|
|
141
|
+
if (httpMatch) {
|
|
142
|
+
origin = `${httpMatch[1].toLowerCase()}://${httpMatch[2]}`;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// ssh://[user@]host[:port]/path or scp-style user@host:path.
|
|
146
|
+
const host = value.match(/^ssh:\/\/(?:[^@/]*@)?([^/:]+)/i)?.[1] ?? value.match(/^[^@/]+@([^:]+):/)?.[1];
|
|
147
|
+
if (!host) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
origin = `https://${host}`;
|
|
151
|
+
}
|
|
152
|
+
const path = info.projectPath
|
|
128
153
|
.split('/')
|
|
129
154
|
.filter(Boolean)
|
|
130
155
|
.map((segment) => encodeURIComponent(segment))
|
|
131
156
|
.join('/');
|
|
132
|
-
|
|
133
|
-
const parsed = new URL(base);
|
|
134
|
-
const decode = (path) => path
|
|
135
|
-
.split('/')
|
|
136
|
-
.filter(Boolean)
|
|
137
|
-
.map((segment) => decodeURIComponent(segment));
|
|
138
|
-
const baseParts = decode(parsed.pathname);
|
|
139
|
-
const remoteParts = info.projectPath.split('/').filter(Boolean);
|
|
140
|
-
let overlap = Math.min(baseParts.length, remoteParts.length);
|
|
141
|
-
while (overlap &&
|
|
142
|
-
!baseParts
|
|
143
|
-
.slice(-overlap)
|
|
144
|
-
.every((part, index) => part.toLowerCase() === remoteParts[index].toLowerCase()))
|
|
145
|
-
overlap--;
|
|
146
|
-
return `${parsed.origin}/${[...baseParts, ...remoteParts.slice(overlap)].map(encodeURIComponent).join('/')}`;
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
return `${base}/${encodePath(info.projectPath)}`;
|
|
150
|
-
}
|
|
157
|
+
return `${origin}/${path}`;
|
|
151
158
|
}
|
|
152
|
-
/** Builds the pullrequests REST URL from
|
|
153
|
-
export function buildAzurePullRequestUrl(
|
|
154
|
-
const prefix = buildAzureProjectPrefix(
|
|
159
|
+
/** Builds the pullrequests REST URL from an Azure DevOps git remote URL. */
|
|
160
|
+
export function buildAzurePullRequestUrl(remoteUrl) {
|
|
161
|
+
const prefix = buildAzureProjectPrefix(remoteUrl);
|
|
155
162
|
if (!prefix) {
|
|
156
163
|
return null;
|
|
157
164
|
}
|
|
@@ -251,7 +258,7 @@ const azureErrorMessage = (result) => {
|
|
|
251
258
|
return result.body;
|
|
252
259
|
}
|
|
253
260
|
};
|
|
254
|
-
export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 8080, apiToken,
|
|
261
|
+
export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 8080, apiToken, prBranchNameTemplate, autoReload, diffViewer, showGraph, showTags, showRemoteBranches, grayRefBadges, issueRegex, issueUrl, allowMerge, fileListView, refListView, sidePanelSectionsExpanded, searchMode, searchCaseSensitive, worktreeSort, prSort, aiReview, azureRequestImpl, aiReviewModelImpl, avatarFetchImpl, onLog, }) {
|
|
255
262
|
// Initialize server
|
|
256
263
|
const app = fastify({
|
|
257
264
|
logger: false,
|
|
@@ -496,10 +503,9 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
496
503
|
return branch;
|
|
497
504
|
};
|
|
498
505
|
const repoRoot = async () => (await git.revparse(['--show-toplevel'])).trim();
|
|
499
|
-
// ====================
|
|
506
|
+
// ==================== Settings ====================
|
|
500
507
|
// Settings live in the repository's git directory so they are per-repo and
|
|
501
|
-
// never committed.
|
|
502
|
-
// azureDevOpsUrl, which wins over the file.
|
|
508
|
+
// never committed.
|
|
503
509
|
let settingsPath = '';
|
|
504
510
|
let reviewStorePath = '';
|
|
505
511
|
// Per worktree: each one has its own uncommitted changes.
|
|
@@ -527,33 +533,39 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
527
533
|
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
528
534
|
};
|
|
529
535
|
const azureRequest = azureRequestImpl ?? defaultAzureRequest;
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
return { url, source: url ? 'file' : '' };
|
|
538
|
-
};
|
|
539
|
-
/** Resolves the Azure DevOps project prefix or throws a user-facing error. */
|
|
540
|
-
const azureProjectContext = async () => {
|
|
541
|
-
const effective = await effectiveAzureUrl();
|
|
542
|
-
if (!effective.url) {
|
|
543
|
-
throw new Error('Azure DevOps URL is not configured. Set it in Settings (gear icon) or the guito.azureDevOpsUrl VS Code setting.');
|
|
544
|
-
}
|
|
545
|
-
let remoteUrl = '';
|
|
536
|
+
/**
|
|
537
|
+
* Detects the Azure DevOps remote: "origin" when it points to Azure DevOps,
|
|
538
|
+
* else the first remote that does. Null when no remote is an Azure DevOps
|
|
539
|
+
* git URL.
|
|
540
|
+
*/
|
|
541
|
+
const detectAzureRemote = async () => {
|
|
542
|
+
let remotes = [];
|
|
546
543
|
try {
|
|
547
|
-
|
|
544
|
+
remotes = await git.getRemotes(true);
|
|
548
545
|
}
|
|
549
546
|
catch {
|
|
550
|
-
|
|
547
|
+
return null;
|
|
551
548
|
}
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
549
|
+
const ordered = [
|
|
550
|
+
...remotes.filter((entry) => entry.name === 'origin'),
|
|
551
|
+
...remotes.filter((entry) => entry.name !== 'origin'),
|
|
552
|
+
];
|
|
553
|
+
for (const entry of ordered) {
|
|
554
|
+
const remoteUrl = entry.refs.fetch.trim();
|
|
555
|
+
const prefix = buildAzureProjectPrefix(remoteUrl);
|
|
556
|
+
if (prefix) {
|
|
557
|
+
return { remote: entry.name, remoteUrl, prefix };
|
|
558
|
+
}
|
|
555
559
|
}
|
|
556
|
-
return
|
|
560
|
+
return null;
|
|
561
|
+
};
|
|
562
|
+
/** Resolves the Azure DevOps remote and project prefix or throws a user-facing error. */
|
|
563
|
+
const azureProjectContext = async () => {
|
|
564
|
+
const detected = await detectAzureRemote();
|
|
565
|
+
if (!detected) {
|
|
566
|
+
throw new Error('No remote of this repository points to an Azure DevOps project (expected a URL like https://server/Collection/Project/_git/Repo).');
|
|
567
|
+
}
|
|
568
|
+
return detected;
|
|
557
569
|
};
|
|
558
570
|
/** Human identity of the authenticated Windows/Azure user; cached per run. */
|
|
559
571
|
let cachedAzureMe = null;
|
|
@@ -602,6 +614,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
602
614
|
email: String(entry?.uniqueName ?? ''),
|
|
603
615
|
vote: Number(entry?.vote ?? 0),
|
|
604
616
|
isRequired: entry?.isRequired === true,
|
|
617
|
+
hasDeclined: entry?.hasDeclined === true,
|
|
605
618
|
}));
|
|
606
619
|
const mine = me ? reviewers.find((reviewer) => reviewer.id === me.id) : undefined;
|
|
607
620
|
return {
|
|
@@ -622,6 +635,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
622
635
|
reviewers,
|
|
623
636
|
myVote: mine ? Number(mine.vote ?? 0) : 0,
|
|
624
637
|
requiresMe: mine?.isRequired === true,
|
|
638
|
+
myDeclined: mine?.hasDeclined === true,
|
|
625
639
|
};
|
|
626
640
|
};
|
|
627
641
|
/** REST base of one pull request (".../pullrequests/{id}") plus repo context. */
|
|
@@ -676,7 +690,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
676
690
|
};
|
|
677
691
|
/** Merges the VS Code settings with the server-side settings file. */
|
|
678
692
|
const effectiveSettings = async () => {
|
|
679
|
-
const [file, azure] = await Promise.all([readSettings(),
|
|
693
|
+
const [file, azure] = await Promise.all([readSettings(), detectAzureRemote()]);
|
|
680
694
|
const fileAutoReload = typeof file.autoReload === 'boolean' ? file.autoReload : undefined;
|
|
681
695
|
const fileShowGraph = typeof file.showGraph === 'boolean' ? file.showGraph : undefined;
|
|
682
696
|
const fileShowTags = typeof file.showTags === 'boolean' ? file.showTags : undefined;
|
|
@@ -734,9 +748,8 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
734
748
|
}
|
|
735
749
|
}
|
|
736
750
|
return {
|
|
737
|
-
azureDevOpsUrl: azure
|
|
751
|
+
azureDevOpsUrl: azure?.prefix ?? '',
|
|
738
752
|
prBranchNameTemplate: prBranchNameTemplate?.trim() || filePrBranchNameTemplate || DEFAULT_PR_BRANCH_NAME_TEMPLATE,
|
|
739
|
-
source: azure.source,
|
|
740
753
|
autoReload: typeof autoReload === 'boolean' ? autoReload : (fileAutoReload ?? true),
|
|
741
754
|
diffViewer: diffViewer === 'vscode' ? 'vscode' : 'guito',
|
|
742
755
|
showGraph: typeof showGraph === 'boolean' ? showGraph : (fileShowGraph ?? true),
|
|
@@ -1818,15 +1831,8 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1818
1831
|
try {
|
|
1819
1832
|
const body = req.body ?? {};
|
|
1820
1833
|
const settings = await readSettings();
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
if (url && !/^https?:\/\//i.test(url)) {
|
|
1824
|
-
return resp.status(400).type('application/json').send({
|
|
1825
|
-
error: 'The Azure DevOps URL must start with http:// or https://.',
|
|
1826
|
-
});
|
|
1827
|
-
}
|
|
1828
|
-
settings.azureDevOpsUrl = url;
|
|
1829
|
-
}
|
|
1834
|
+
// Superseded by detection from the remotes; drop values older versions stored.
|
|
1835
|
+
delete settings.azureDevOpsUrl;
|
|
1830
1836
|
if ('prBranchNameTemplate' in body) {
|
|
1831
1837
|
const template = String(body.prBranchNameTemplate ?? '').trim();
|
|
1832
1838
|
if (template.length > 200) {
|
|
@@ -1958,7 +1964,12 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1958
1964
|
if (!targetBranch) {
|
|
1959
1965
|
return resp.status(400).type('application/json').send({ error: 'targetBranch required' });
|
|
1960
1966
|
}
|
|
1961
|
-
const { prefix, remoteUrl } = await azureProjectContext();
|
|
1967
|
+
const { remote, prefix, remoteUrl } = await azureProjectContext();
|
|
1968
|
+
// Branch names may arrive as remote-tracking refs ("origin/main").
|
|
1969
|
+
const stripRemote = (name) => {
|
|
1970
|
+
const branch = name.replace(/^refs\/heads\//, '');
|
|
1971
|
+
return branch.startsWith(`${remote}/`) ? branch.slice(remote.length + 1) : branch;
|
|
1972
|
+
};
|
|
1962
1973
|
const info = parseAzureRemoteUrl(remoteUrl);
|
|
1963
1974
|
if (!info) {
|
|
1964
1975
|
return resp.status(400).type('application/json').send({
|
|
@@ -1983,9 +1994,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
1983
1994
|
username: computerUsername,
|
|
1984
1995
|
randomstring: randomBranchSuffix(),
|
|
1985
1996
|
branch: (await git.revparse(['--abbrev-ref', 'HEAD'])).trim(),
|
|
1986
|
-
targetbranch: String(targetBranch)
|
|
1987
|
-
.replace(/^refs\/heads\//, '')
|
|
1988
|
-
.replace(/^origin\//, ''),
|
|
1997
|
+
targetbranch: stripRemote(String(targetBranch)),
|
|
1989
1998
|
title: String(title ?? '').trim() || 'pull-request',
|
|
1990
1999
|
repository: basename(await repoRoot()),
|
|
1991
2000
|
date: now.toISOString().slice(0, 10),
|
|
@@ -2001,23 +2010,23 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
2001
2010
|
});
|
|
2002
2011
|
}
|
|
2003
2012
|
const existingBranch = await git
|
|
2004
|
-
.raw(['ls-remote', '--heads',
|
|
2013
|
+
.raw(['ls-remote', '--heads', remote, `refs/heads/${name}`])
|
|
2005
2014
|
.catch(() => '');
|
|
2006
2015
|
if (existingBranch.trim()) {
|
|
2007
2016
|
return resp.status(400).send({
|
|
2008
|
-
error: `The pull request branch already exists on
|
|
2017
|
+
error: `The pull request branch already exists on ${remote}: ${name}`,
|
|
2009
2018
|
});
|
|
2010
2019
|
}
|
|
2011
|
-
await git.push([
|
|
2020
|
+
await git.push([remote, `HEAD:refs/heads/${name}`]);
|
|
2012
2021
|
source = name;
|
|
2013
2022
|
}
|
|
2014
2023
|
else {
|
|
2015
2024
|
if (!source) {
|
|
2016
2025
|
return resp.status(400).type('application/json').send({ error: 'sourceBranch required' });
|
|
2017
2026
|
}
|
|
2018
|
-
if (source.startsWith(
|
|
2027
|
+
if (source.startsWith(`${remote}/`)) {
|
|
2019
2028
|
// Remote-tracking branch: the PR source is the branch name on the remote.
|
|
2020
|
-
source = source.slice(
|
|
2029
|
+
source = source.slice(remote.length + 1);
|
|
2021
2030
|
}
|
|
2022
2031
|
else {
|
|
2023
2032
|
// Publish the branch first when the remote does not have it yet.
|
|
@@ -2027,23 +2036,21 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
2027
2036
|
// simple-git, so list the tracking refs instead.
|
|
2028
2037
|
const trackingRefs = await git.raw([
|
|
2029
2038
|
'for-each-ref',
|
|
2030
|
-
|
|
2039
|
+
`refs/remotes/${remote}`,
|
|
2031
2040
|
'--format=%(refname)',
|
|
2032
2041
|
]);
|
|
2033
2042
|
const published = trackingRefs
|
|
2034
2043
|
.split('\n')
|
|
2035
|
-
.some((line) => line.trim() === `refs/remotes
|
|
2044
|
+
.some((line) => line.trim() === `refs/remotes/${remote}/${source}`);
|
|
2036
2045
|
if (!published) {
|
|
2037
|
-
await git.push([
|
|
2046
|
+
await git.push([remote, source]);
|
|
2038
2047
|
}
|
|
2039
2048
|
}
|
|
2040
2049
|
}
|
|
2041
2050
|
const payload = {
|
|
2042
2051
|
isDraft: req.body?.isDraft === true,
|
|
2043
2052
|
sourceRefName: `refs/heads/${source}`,
|
|
2044
|
-
targetRefName: `refs/heads/${String(targetBranch)
|
|
2045
|
-
.replace(/^refs\/heads\//, '')
|
|
2046
|
-
.replace(/^origin\//, '')}`,
|
|
2053
|
+
targetRefName: `refs/heads/${stripRemote(String(targetBranch))}`,
|
|
2047
2054
|
};
|
|
2048
2055
|
const trimmedTitle = String(title ?? '').trim();
|
|
2049
2056
|
const trimmedDescription = String(description ?? '').trim();
|
|
@@ -2480,7 +2487,27 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
2480
2487
|
}
|
|
2481
2488
|
const { base } = await azurePrApiBase(Number(req.params?.id));
|
|
2482
2489
|
const me = await azureMe();
|
|
2483
|
-
await azureJson('PUT', `${base}/reviewers/${encodeURIComponent(me.id)}?api-version=${AZURE_API_VERSION}`, JSON.stringify({ vote: AZURE_VOTES[vote] }));
|
|
2490
|
+
await azureJson('PUT', `${base}/reviewers/${encodeURIComponent(me.id)}?api-version=${AZURE_API_VERSION}`, JSON.stringify({ vote: AZURE_VOTES[vote], hasDeclined: false }));
|
|
2491
|
+
return resp.type('application/json').send({ ok: true });
|
|
2492
|
+
}
|
|
2493
|
+
catch (err) {
|
|
2494
|
+
return resp.status(400).type('application/json').send({ error: err.message });
|
|
2495
|
+
}
|
|
2496
|
+
});
|
|
2497
|
+
// Declines (or resumes) the current user's review on the pull request.
|
|
2498
|
+
// Azure keeps the reviewer on the pull request with hasDeclined set; the
|
|
2499
|
+
// record is read first so the required flag survives the PUT.
|
|
2500
|
+
app.post('/api/azure-devops/pullrequests/:id/decline', async (req, resp) => {
|
|
2501
|
+
try {
|
|
2502
|
+
const declined = req.body?.declined !== false;
|
|
2503
|
+
const { base } = await azurePrApiBase(Number(req.params?.id));
|
|
2504
|
+
const me = await azureMe();
|
|
2505
|
+
const reviewerUrl = `${base}/reviewers/${encodeURIComponent(me.id)}?api-version=${AZURE_API_VERSION}`;
|
|
2506
|
+
const current = await azureJson('GET', reviewerUrl);
|
|
2507
|
+
await azureJson('PUT', reviewerUrl, JSON.stringify({
|
|
2508
|
+
isRequired: current?.isRequired === true,
|
|
2509
|
+
hasDeclined: declined,
|
|
2510
|
+
}));
|
|
2484
2511
|
return resp.type('application/json').send({ ok: true });
|
|
2485
2512
|
}
|
|
2486
2513
|
catch (err) {
|
|
@@ -2508,6 +2535,9 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
2508
2535
|
if (req.body?.vote !== undefined && Number.isFinite(Number(req.body.vote))) {
|
|
2509
2536
|
payload.vote = Number(req.body.vote);
|
|
2510
2537
|
}
|
|
2538
|
+
if (req.body?.hasDeclined !== undefined) {
|
|
2539
|
+
payload.hasDeclined = req.body.hasDeclined === true;
|
|
2540
|
+
}
|
|
2511
2541
|
await azureJson('PUT', reviewerUrl, JSON.stringify(payload));
|
|
2512
2542
|
return resp.type('application/json').send({ ok: true });
|
|
2513
2543
|
}
|
|
@@ -2627,6 +2657,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
2627
2657
|
content: String(comment?.content ?? ''),
|
|
2628
2658
|
createdAt: String(comment?.publishedDate ?? ''),
|
|
2629
2659
|
isDeleted: false,
|
|
2660
|
+
commentType: azureCommentTypeName(comment?.commentType),
|
|
2630
2661
|
})),
|
|
2631
2662
|
};
|
|
2632
2663
|
});
|
|
@@ -3410,6 +3441,17 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
3410
3441
|
return resp.status(400).type('application/json').send({ error: err.message });
|
|
3411
3442
|
}
|
|
3412
3443
|
});
|
|
3444
|
+
// Marks findings as fixed; a fixed point is never raised again for this PR.
|
|
3445
|
+
app.post('/api/azure-devops/pullrequests/:id/ai-review/fix', async (req, resp) => {
|
|
3446
|
+
try {
|
|
3447
|
+
const findingIds = (Array.isArray(req.body?.findingIds) ? req.body.findingIds : []).map((id) => String(id));
|
|
3448
|
+
const state = await aiReviewer.fix(Number(req.params?.id), findingIds);
|
|
3449
|
+
return resp.type('application/json').send({ state });
|
|
3450
|
+
}
|
|
3451
|
+
catch (err) {
|
|
3452
|
+
return resp.status(400).type('application/json').send({ error: err.message });
|
|
3453
|
+
}
|
|
3454
|
+
});
|
|
3413
3455
|
// Drops everything remembered about a pull request, including dismissals.
|
|
3414
3456
|
app.delete('/api/azure-devops/pullrequests/:id/ai-review', async (req, resp) => {
|
|
3415
3457
|
try {
|
|
@@ -3745,7 +3787,6 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
|
|
|
3745
3787
|
await app.close();
|
|
3746
3788
|
},
|
|
3747
3789
|
updateSettings: (settings) => {
|
|
3748
|
-
azureDevOpsUrl = settings.azureDevOpsUrl;
|
|
3749
3790
|
prBranchNameTemplate = settings.prBranchNameTemplate;
|
|
3750
3791
|
autoReload = settings.autoReload;
|
|
3751
3792
|
diffViewer = settings.diffViewer;
|
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
<style>:root{--bg:#1e1e1e;--bg-alt:#232323;--bg-hover:#2a2d2e;--border:#3c3c3c;--text:#cccccc;--text-bright:#e8e8e8;--text-dim:#9d9d9d;--accent:#3794ff;--red:#f14c4c;--mono:ui-monospace, "Cascadia Code", "SF Mono", Menlo, Consolas, monospace}*{box-sizing:border-box}html,body{height:100%;margin:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:13px;color:var(--text);background:var(--bg);overflow:hidden}</style><link rel="stylesheet" href="styles-I7K2YZTW.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-I7K2YZTW.css"></noscript></head>
|
|
10
10
|
<body>
|
|
11
11
|
<app-root></app-root>
|
|
12
|
-
<script src="polyfills-5CFQRCPP.js" type="module"></script><script src="main-
|
|
12
|
+
<script src="polyfills-5CFQRCPP.js" type="module"></script><script src="main-NA3UMMCQ.js" type="module"></script></body>
|
|
13
13
|
</html>
|