mirror of
https://github.com/firstsun-dev/git-files-sync.git
synced 2026-07-22 17:20:30 +00:00
Refresh failed with the cryptic "Unexpected token '<', "<!DOCTYPE ..." is not valid JSON" whenever the Git server returned an HTML page (login, SSO redirect, or proxy/error page) on a 2xx/3xx response. safeRequest only threw on status >= 400, so such bodies slipped through and crashed at response.json. Add BaseGitService.parseJson() which detects non-JSON/HTML bodies and throws an actionable message, and use it for all response.json reads in the GitHub and GitLab services. Also harden parseErrorResponse so HTML error pages produce a clear message instead of dumping the raw document. Add tests covering HTML 2xx responses, HTML detection by leading '<', malformed JSON, and HTML error pages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
import { GitServiceInterface } from './git-service-interface';
|
|
import { BaseGitService, GitFile, GitLabFileResponse, GitLabTreeItem } from './git-service-base';
|
|
|
|
export class GitLabService extends BaseGitService implements GitServiceInterface {
|
|
private baseUrl: string = 'https://gitlab.com';
|
|
private projectId: string = '';
|
|
|
|
updateConfig(baseUrl: string, token: string, projectId: string, rootPath: string = '') {
|
|
this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
this.token = token;
|
|
this.projectId = projectId;
|
|
this.rootPath = rootPath;
|
|
}
|
|
|
|
protected addAuthHeader(headers: Record<string, string>): void {
|
|
headers['PRIVATE-TOKEN'] = this.token;
|
|
}
|
|
|
|
private getApiUrl(path: string): string {
|
|
const fullPath = this.getFullPath(path);
|
|
const encodedPath = encodeURIComponent(fullPath);
|
|
const encodedProjectId = encodeURIComponent(this.projectId);
|
|
return `${this.baseUrl}/api/v4/projects/${encodedProjectId}/repository/files/${encodedPath}`;
|
|
}
|
|
|
|
async getFile(path: string, branch: string): Promise<GitFile> {
|
|
try {
|
|
const url = `${this.getApiUrl(path)}?ref=${branch}`;
|
|
const response = await this.safeRequest(url, 'GET');
|
|
const data = this.parseJson<GitLabFileResponse>(response);
|
|
|
|
return {
|
|
content: this.decodeContent(data.content, path),
|
|
sha: data.last_commit_id
|
|
};
|
|
} catch (e) {
|
|
return this.handleFileNotFound(e);
|
|
}
|
|
}
|
|
|
|
async pushFile(path: string, content: string | ArrayBuffer, branch: string, message: string, sha?: string): Promise<{ path: string, sha?: string }> {
|
|
const url = this.getApiUrl(path);
|
|
const body = {
|
|
branch,
|
|
content: this.encodeContent(content),
|
|
encoding: 'base64',
|
|
commit_message: message,
|
|
last_commit_id: sha
|
|
};
|
|
|
|
const method = sha ? 'PUT' : 'POST';
|
|
const response = await this.safeRequest(url, method, body);
|
|
const data = this.parseJson<GitLabFileResponse>(response);
|
|
return { path: data.file_path };
|
|
}
|
|
|
|
async listFiles(branch: string, useFilter = true): Promise<string[]> {
|
|
const encodedProjectId = encodeURIComponent(this.projectId);
|
|
let allPaths: string[] = [];
|
|
let page = 1;
|
|
const perPage = 100;
|
|
|
|
while (true) {
|
|
const url = `${this.baseUrl}/api/v4/projects/${encodedProjectId}/repository/tree?ref=${branch}&recursive=true&per_page=${perPage}&page=${page}`;
|
|
const response = await this.safeRequest(url, 'GET');
|
|
const data = this.parseJson<GitLabTreeItem[]>(response);
|
|
|
|
if (!data || data.length === 0) break;
|
|
|
|
const paths = data
|
|
.filter(item => item.type === 'blob')
|
|
.map(item => item.path);
|
|
|
|
if (useFilter) {
|
|
const filtered = paths.filter(p => {
|
|
if (!this.rootPath) return true;
|
|
const cleanRoot = this.rootPath.endsWith('/') ? this.rootPath : `${this.rootPath}/`;
|
|
return p === this.rootPath || p.startsWith(cleanRoot);
|
|
});
|
|
allPaths = allPaths.concat(filtered);
|
|
} else {
|
|
allPaths = allPaths.concat(paths);
|
|
}
|
|
|
|
if (data.length < perPage) break;
|
|
page++;
|
|
}
|
|
|
|
return allPaths;
|
|
}
|
|
|
|
async deleteFile(path: string, branch: string, message: string): Promise<void> {
|
|
const url = this.getApiUrl(path);
|
|
const body = {
|
|
branch,
|
|
commit_message: message
|
|
};
|
|
|
|
await this.safeRequest(url, 'DELETE', body);
|
|
}
|
|
|
|
async testConnection(): Promise<boolean> {
|
|
try {
|
|
const encodedProjectId = encodeURIComponent(this.projectId);
|
|
const url = `${this.baseUrl}/api/v4/projects/${encodedProjectId}`;
|
|
await this.safeRequest(url, 'GET');
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
}
|