|
| 1 | +import { Injectable, Autowired } from '@opensumi/di'; |
| 2 | +import { localize, IReporterService, formatLocalize, MessageType } from '@opensumi/ide-core-common'; |
| 3 | +import { request, RequestOptions } from '@alipay/alex-shared'; |
| 4 | +import { API } from './types'; |
| 5 | +import { HelperService } from '../common/service'; |
| 6 | +import { CODE_PLATFORM_CONFIG } from '../common/config'; |
| 7 | +import type { |
| 8 | + TreeEntry, |
| 9 | + EntryParam, |
| 10 | + ICodeAPIService, |
| 11 | + IRepositoryModel, |
| 12 | + BranchOrTag, |
| 13 | + CommitParams, |
| 14 | + Branch, |
| 15 | + Project, |
| 16 | +} from '../common/types'; |
| 17 | +import { CodePlatform, CommitFileStatus } from '../common/types'; |
| 18 | + |
| 19 | +const toType = (d: API.ResponseCommitFileChange) => { |
| 20 | + if (d.new_file) return CommitFileStatus.Added; |
| 21 | + else if (d.deleted_file) return CommitFileStatus.Deleted; |
| 22 | + else if (d.renamed_file) return CommitFileStatus.Renamed; |
| 23 | + return CommitFileStatus.Modified; |
| 24 | +}; |
| 25 | +const toChangeLines = (diff: string) => { |
| 26 | + const diffLines = diff ? diff.split('\n') : []; |
| 27 | + return diffLines.reduce( |
| 28 | + (obj, line) => { |
| 29 | + // +++,--- 在首行为文件名 |
| 30 | + if (line.startsWith('+') && !line.startsWith('+++')) { |
| 31 | + obj.additions += 1; |
| 32 | + } else if (line.startsWith('-') && !line.startsWith('---')) { |
| 33 | + obj.deletions += 1; |
| 34 | + } |
| 35 | + return obj; |
| 36 | + }, |
| 37 | + { additions: 0, deletions: 0 } |
| 38 | + ); |
| 39 | +}; |
| 40 | +/* |
| 41 | + * 为保证生产测试一致 暂时使用的是gitlink的生产区接口 https://www.gitlink.org.cn/docs/api |
| 42 | + * TODO 待后续测试区接口上线 https://testgitea2.trustie.net/api/swagger#/ |
| 43 | + */ |
| 44 | + |
| 45 | +@Injectable() |
| 46 | +export class GitLinkAPIService implements ICodeAPIService { |
| 47 | + @Autowired(IReporterService) |
| 48 | + reporter: IReporterService; |
| 49 | + |
| 50 | + @Autowired() |
| 51 | + helper: HelperService; |
| 52 | + |
| 53 | + private config = CODE_PLATFORM_CONFIG[CodePlatform.gitlink]; |
| 54 | + |
| 55 | + private _PRIVATE_TOKEN: string | null; |
| 56 | + |
| 57 | + shouldShowView = false; |
| 58 | + |
| 59 | + get PRIVATE_TOKEN() { |
| 60 | + return this._PRIVATE_TOKEN; |
| 61 | + } |
| 62 | + |
| 63 | + constructor() { |
| 64 | + this._PRIVATE_TOKEN = this.config.token || ''; |
| 65 | + } |
| 66 | + getUser(repo: IRepositoryModel): Promise<Branch> { |
| 67 | + throw new Error('Method not implemented.'); |
| 68 | + } |
| 69 | + |
| 70 | + async available() { |
| 71 | + return true; |
| 72 | + } |
| 73 | + |
| 74 | + private showErrorMessage(symbol: string, message?: string, status?: number) { |
| 75 | + this.helper.showMessage(CodePlatform.gitlink, { |
| 76 | + type: MessageType.Error, |
| 77 | + status, |
| 78 | + symbol, |
| 79 | + message, |
| 80 | + }); |
| 81 | + } |
| 82 | + |
| 83 | + private projectIdMap = new Map<string, Promise<string>>(); |
| 84 | + |
| 85 | + async getProjectId(repo: IRepositoryModel) { |
| 86 | + const projectPath = this.getProjectPath(repo); |
| 87 | + if (!this.projectIdMap.has(projectPath)) { |
| 88 | + this.projectIdMap.set( |
| 89 | + projectPath, |
| 90 | + this.getProject(repo).then(({ id }) => id) |
| 91 | + ); |
| 92 | + } |
| 93 | + return this.projectIdMap.get(projectPath)!; |
| 94 | + } |
| 95 | + |
| 96 | + private getProjectPath(repo: IRepositoryModel) { |
| 97 | + return `${repo.owner}/${repo.name}`; |
| 98 | + } |
| 99 | + |
| 100 | + // 静态资源路径 |
| 101 | + transformStaticResource(repo: IRepositoryModel, path: string) { |
| 102 | + return `${this.config.origin}/${this.getProjectPath(repo)}/raw/${repo.commit}/${path}`; |
| 103 | + } |
| 104 | + |
| 105 | + protected async request<T>(path: string, options?: RequestOptions): Promise<T> { |
| 106 | + try { |
| 107 | + const { headers, ...rest } = options || {}; |
| 108 | + const privateToken = this.PRIVATE_TOKEN; |
| 109 | + return await request(path, { |
| 110 | + baseURL: this.config.endpoint, |
| 111 | + responseType: 'json', |
| 112 | + headers: { |
| 113 | + ...(privateToken |
| 114 | + ? { |
| 115 | + 'PRIVATE-TOKEN': privateToken, |
| 116 | + } |
| 117 | + : {}), |
| 118 | + ...headers, |
| 119 | + }, |
| 120 | + ...rest, |
| 121 | + }); |
| 122 | + } catch (err: any) { |
| 123 | + const status = err.response?.status; |
| 124 | + let messageKey = 'error.request'; |
| 125 | + if (status === 401) { |
| 126 | + messageKey = 'gitlink.unauthorized'; |
| 127 | + } else if (status === 404) { |
| 128 | + messageKey = 'error.resource-not-found'; |
| 129 | + } |
| 130 | + this.showErrorMessage(messageKey, status); |
| 131 | + throw err; |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + async getProject(repo: IRepositoryModel) { |
| 136 | + const data = await this.request<API.Project>(`/api/${this.getProjectPath(repo)}/detail.json`); |
| 137 | + if (!data?.identifier) { |
| 138 | + const message = formatLocalize('api.response.project-not-found', this.getProjectPath(repo)); |
| 139 | + this.showErrorMessage('', message, 404); |
| 140 | + throw new Error(message); |
| 141 | + } |
| 142 | + return { |
| 143 | + ...data, |
| 144 | + id: data.identifier, |
| 145 | + }; |
| 146 | + } |
| 147 | + |
| 148 | + async getCommit(repo: IRepositoryModel, ref: string) { |
| 149 | + return ( |
| 150 | + await this.request<API.ResponseGetCommit>( |
| 151 | + `/api/${this.getProjectPath(repo)}/commits/${encodeURIComponent(ref)}` |
| 152 | + ) |
| 153 | + ).commit.sha; |
| 154 | + } |
| 155 | + |
| 156 | + // getTree getBlob getBlobByCommitPath 暂时都使用同一接口 |
| 157 | + async getTree(repo: IRepositoryModel, path: string) { |
| 158 | + const data = await this.request<API.ResponseGetTree>( |
| 159 | + `/api/${this.getProjectPath(repo)}/sub_entries.json`, |
| 160 | + { |
| 161 | + params: { |
| 162 | + ref: repo.commit, |
| 163 | + filepath: path, |
| 164 | + type: 'dir', |
| 165 | + }, |
| 166 | + } |
| 167 | + ); |
| 168 | + if (data.entries) { |
| 169 | + return data.entries.map<TreeEntry>((item) => { |
| 170 | + const entry: TreeEntry = { |
| 171 | + ...item, |
| 172 | + // gitlink 文件类型暂时只有 dir 和 file |
| 173 | + type: item.type === 'dir' ? 'tree' : 'blob', |
| 174 | + }; |
| 175 | + return entry; |
| 176 | + }); |
| 177 | + } else { |
| 178 | + throw new Error('[can not find entries]'); |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + async getBlob(repo: IRepositoryModel, entry: EntryParam) { |
| 183 | + const buf = await this.request<API.ResponseGetSubTree>( |
| 184 | + `/api/${this.getProjectPath(repo)}/sub_entries.json`, |
| 185 | + { |
| 186 | + params: { |
| 187 | + filepath: entry.path, |
| 188 | + ref: repo.commit, |
| 189 | + type: 'file', |
| 190 | + }, |
| 191 | + } |
| 192 | + ); |
| 193 | + let content = (buf.entries as API.Entry).content; |
| 194 | + return Buffer.from(content); |
| 195 | + } |
| 196 | + |
| 197 | + async getBlobByCommitPath(repo: IRepositoryModel, commit: string, path: string) { |
| 198 | + const buf = await this.request<API.ResponseGetSubTree>( |
| 199 | + `/api/${this.getProjectPath(repo)}/sub_entries.json`, |
| 200 | + { |
| 201 | + params: { |
| 202 | + filepath: path, |
| 203 | + ref: commit, |
| 204 | + type: 'file', |
| 205 | + }, |
| 206 | + } |
| 207 | + ); |
| 208 | + let content = (buf.entries as API.Entry).content; |
| 209 | + return Buffer.from(content); |
| 210 | + } |
| 211 | + |
| 212 | + // 此处应该使用后续的api/v1 里的branch接口 |
| 213 | + async getBranches(repo: IRepositoryModel): Promise<BranchOrTag[]> { |
| 214 | + const branchSlice = await this.request<API.BranchSlice>( |
| 215 | + `/api/${this.getProjectPath(repo)}/branches_slice.json` |
| 216 | + ); |
| 217 | + const branches: BranchOrTag[] = []; |
| 218 | + branchSlice.forEach((slices) => { |
| 219 | + slices.list.forEach((branch) => { |
| 220 | + branches.push({ |
| 221 | + commit: { |
| 222 | + id: branch.last_commit.sha, |
| 223 | + }, |
| 224 | + name: branch.name, |
| 225 | + }); |
| 226 | + }); |
| 227 | + }); |
| 228 | + return branches; |
| 229 | + } |
| 230 | + |
| 231 | + async getTags(repo: IRepositoryModel): Promise<BranchOrTag[]> { |
| 232 | + const tags = await this.request<API.ResponseGetRefs>( |
| 233 | + `/api/${this.getProjectPath(repo)}/tags.json` |
| 234 | + ); |
| 235 | + return tags.map((tag) => ({ |
| 236 | + commit: { |
| 237 | + id: tag.id, |
| 238 | + }, |
| 239 | + name: tag.name, |
| 240 | + })); |
| 241 | + } |
| 242 | + |
| 243 | + /** |
| 244 | + * gitlab 接口不支持搜索 |
| 245 | + */ |
| 246 | + async searchContent() { |
| 247 | + return []; |
| 248 | + } |
| 249 | + |
| 250 | + async searchFile() { |
| 251 | + return []; |
| 252 | + } |
| 253 | + |
| 254 | + // 不支持 |
| 255 | + async getFileBlame() { |
| 256 | + return Uint8Array.from([]); |
| 257 | + } |
| 258 | + |
| 259 | + async getCommits(repo: IRepositoryModel, params: CommitParams) { |
| 260 | + return []; |
| 261 | + } |
| 262 | + |
| 263 | + async getCommitDiff(repo: IRepositoryModel, sha: string) { |
| 264 | + return []; |
| 265 | + } |
| 266 | + |
| 267 | + async getCommitCompare(repo: IRepositoryModel, from: string, to: string) { |
| 268 | + return []; |
| 269 | + } |
| 270 | + |
| 271 | + async bulkChangeFiles() { |
| 272 | + return []; |
| 273 | + } |
| 274 | + |
| 275 | + async getFiles() { |
| 276 | + return []; |
| 277 | + } |
| 278 | + createBranch(repo: IRepositoryModel, newBranch: string): Promise<any> { |
| 279 | + throw new Error('Method not implemented.'); |
| 280 | + } |
| 281 | +} |
0 commit comments