Skip to content

Commit b36294d

Browse files
Merge pull request #33 from nventive/msyo/fix/improve-sync-error-messages
fix: improve sync error messages to distinguish bad URLs from incompatible repo structures
2 parents b2f75d0 + 8ac7b8e commit b36294d

4 files changed

Lines changed: 94 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
All notable changes to the "promptitude" extension will be documented in this file.
44

5-
## [Unreleased]
5+
## [1.5.5] - 2026-03-18
6+
7+
### Improved
8+
9+
- Sync failure messages now distinguish **invalid repository URLs** (e.g. sub-paths like `/tree/main/...`) from **incompatible repository structures** (repo is reachable but has no supported prompt folders). The summary toast shows a categorised count, and "Show Details" lists the specific reason per repository.
610

711
## [1.5.4] - 2026-01-12
812

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "promptitude-extension",
33
"displayName": "Promptitude",
44
"description": "Sync GitHub Copilot prompts, chatmodes and instructions from git repositories",
5-
"version": "1.5.4",
5+
"version": "1.5.5",
66
"publisher": "logientnventive",
77
"icon": "resources/promptitude-icon.png",
88
"repository": {

src/syncManager.ts

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,50 @@ export class SyncManager {
325325
}
326326
}
327327

328+
/**
329+
* Validates that a repository URL is a well-formed repo root (not a sub-path like /tree/main/...).
330+
* Returns an error string if invalid, or undefined if valid.
331+
*/
332+
private validateRepositoryUrl(repoUrl: string): string | undefined {
333+
try {
334+
const parsed = new URL(repoUrl);
335+
const normalizedPath = parsed.pathname.replace(/\.git$/, '').replace(/\/+$/, '');
336+
const segments = normalizedPath.split('/').filter(Boolean);
337+
338+
// GitHub: expect exactly owner/repo (2 segments)
339+
if (parsed.hostname === 'github.com' && segments.length < 2) {
340+
return `Invalid URL – expected https://github.com/owner/repo but the path "${parsed.pathname}" is missing the owner and/or repo name`;
341+
}
342+
if (parsed.hostname === 'github.com' && segments.length > 2) {
343+
return `Invalid URL – expected https://github.com/owner/repo but got extra sub-path segments in "${parsed.pathname}". Remove extra path segments like /tree/…`;
344+
}
345+
346+
// Azure DevOps (dev.azure.com): expect org/project/_git/repo (4 segments)
347+
if (parsed.hostname === 'dev.azure.com') {
348+
if (segments.length < 4 || segments[2] !== '_git') {
349+
return `Invalid URL – expected https://dev.azure.com/org/project/_git/repo but the path "${parsed.pathname}" does not match the expected format`;
350+
}
351+
if (segments.length > 4) {
352+
return `Invalid URL – expected https://dev.azure.com/org/project/_git/repo but got extra sub-path segments in "${parsed.pathname}". Remove extra path segments`;
353+
}
354+
}
355+
356+
// Azure DevOps (visualstudio.com): expect project/_git/repo (3 segments)
357+
if (parsed.hostname.endsWith('.visualstudio.com')) {
358+
if (segments.length < 3 || segments[1] !== '_git') {
359+
return `Invalid URL – expected https://org.visualstudio.com/project/_git/repo but the path "${parsed.pathname}" does not match the expected format`;
360+
}
361+
if (segments.length > 3) {
362+
return `Invalid URL – expected https://org.visualstudio.com/project/_git/repo but got extra sub-path segments in "${parsed.pathname}". Remove extra path segments`;
363+
}
364+
}
365+
366+
return undefined;
367+
} catch {
368+
return `Invalid URL format – could not parse "${repoUrl}" as a repository URL`;
369+
}
370+
}
371+
328372
private async syncMultipleRepositories(repositories: string[]): Promise<MultiRepositorySyncResult> {
329373
const results: RepositorySyncResult[] = [];
330374
let totalItemsUpdated = 0;
@@ -338,6 +382,20 @@ export class SyncManager {
338382
try {
339383
this.logger.debug(`Syncing repository: ${repoUrl}`);
340384

385+
// Early validation: reject malformed repository URLs
386+
const urlError = this.validateRepositoryUrl(repoUrl);
387+
if (urlError) {
388+
this.logger.warn(`Skipping repository ${repoUrl}: ${urlError}`);
389+
results.push({
390+
repository: repoUrl,
391+
success: false,
392+
itemsUpdated: 0,
393+
error: urlError
394+
});
395+
errors.push(`${repoUrl}: ${urlError}`);
396+
continue;
397+
}
398+
341399
// Get or create Git API manager for this repository
342400
let gitApi = this.gitProviders.get(repoUrl);
343401
if (!gitApi) {
@@ -379,14 +437,20 @@ export class SyncManager {
379437

380438
if (relevantFiles.length === 0) {
381439
this.logger.warn(`No relevant files found to sync in ${repoUrl} based on current settings`);
382-
const promptLocation = `${REPO_SYNC_CHAT_MODE_PATH}, ${REPO_SYNC_CHAT_MODE_LEGACY_PATH}, ${REPO_SYNC_CHAT_MODE_LEGACY_SINGULAR_PATH}, ${REPO_SYNC_INSTRUCTIONS_PATH}, ${REPO_SYNC_PROMPT_PATH}`;
440+
const enabledTypes: string[] = [];
441+
if (this.config.syncChatmode) { enabledTypes.push(REPO_SYNC_CHAT_MODE_PATH); }
442+
if (this.config.syncInstructions) { enabledTypes.push(REPO_SYNC_INSTRUCTIONS_PATH); }
443+
if (this.config.syncPrompt) { enabledTypes.push(REPO_SYNC_PROMPT_PATH); }
444+
const structureError = enabledTypes.length > 0
445+
? `Incompatible repository – no .md/.txt files found under ${enabledTypes.join(', ')} on branch "${branch}". Check that the repo uses a supported folder layout.`
446+
: `No sync types enabled – enable at least one of syncChatmode, syncInstructions, or syncPrompt in settings.`;
383447
results.push({
384448
repository: repoUrl,
385449
success: false,
386450
itemsUpdated: 0,
387-
error: `No relevant files found, make sure prompts are in valid directories: ${promptLocation}`
451+
error: structureError
388452
});
389-
errors.push(`${repoUrl}: No relevant files found`);
453+
errors.push(`${repoUrl}: ${structureError}`);
390454
continue;
391455
}
392456
this.logger.debug(`Found ${relevantFiles.length} relevant files to sync for ${repoUrl}`);

src/utils/notifications.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,27 @@ export class NotificationManager {
4747
}
4848

4949
async showPartialSyncSuccess(itemsCount: number, successCount: number, totalCount: number, errors: string[]): Promise<void> {
50-
const message = `⚠️ Partial sync completed! ${itemsCount} items updated from ${successCount}/${totalCount} repositories.`;
50+
// Categorise failures so the summary toast is immediately actionable
51+
let badUrlCount = 0;
52+
let badStructureCount = 0;
53+
let otherCount = 0;
54+
for (const err of errors) {
55+
if (err.includes('Invalid URL')) {
56+
badUrlCount++;
57+
} else if (err.includes('Incompatible repository') || err.includes('No sync types enabled')) {
58+
badStructureCount++;
59+
} else {
60+
otherCount++;
61+
}
62+
}
63+
64+
const failParts: string[] = [];
65+
if (badUrlCount > 0) { failParts.push(`${badUrlCount} bad URL${badUrlCount > 1 ? 's' : ''}`); }
66+
if (badStructureCount > 0) { failParts.push(`${badStructureCount} incompatible structure${badStructureCount > 1 ? 's' : ''}`); }
67+
if (otherCount > 0) { failParts.push(`${otherCount} other error${otherCount > 1 ? 's' : ''}`); }
68+
69+
const failSummary = failParts.length > 0 ? ` Failures: ${failParts.join(', ')}.` : '';
70+
const message = `⚠️ Partial sync: ${itemsCount} items updated from ${successCount}/${totalCount} repos.${failSummary}`;
5171
const result = await this.showWarning(
5272
message,
5373
'Show Details',

0 commit comments

Comments
 (0)