Display name improvements (#259)

This commit is contained in:
Brendan Kellam 2025-04-02 17:50:48 -07:00 committed by GitHub
parent d55bf83ac1
commit bbd8b221d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 140 additions and 63 deletions

16
.vscode/sourcebot.code-workspace vendored Normal file
View file

@ -0,0 +1,16 @@
{
"folders": [
{
"path": ".."
},
{
"path": "../vendor/zoekt"
}
],
"settings": {
"files.associations": {
"*.json": "jsonc",
"index.json": "json"
}
}
}

View file

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixes ### Fixes
- Change connection manager upsert timeout to 5 minutes - Change connection manager upsert timeout to 5 minutes
- Fix issue with repo display names being poorly formatted, especially for gerrit. ([#259](https://github.com/sourcebot-dev/sourcebot/pull/259))
## [3.0.1] - 2025-04-01 ## [3.0.1] - 2025-04-01

View file

@ -1,21 +1,15 @@
import { simpleGit, SimpleGitProgressEvent } from 'simple-git'; import { simpleGit, SimpleGitProgressEvent } from 'simple-git';
export const cloneRepository = async (cloneURL: string, path: string, gitConfig?: Record<string, string>, onProgress?: (event: SimpleGitProgressEvent) => void) => { export const cloneRepository = async (cloneURL: string, path: string, onProgress?: (event: SimpleGitProgressEvent) => void) => {
const git = simpleGit({ const git = simpleGit({
progress: onProgress, progress: onProgress,
}); });
const configParams = Object.entries(gitConfig ?? {}).flatMap(
([key, value]) => ['--config', `${key}=${value}`]
);
try { try {
await git.clone( await git.clone(
cloneURL, cloneURL,
path, path,
[ [
"--bare", "--bare",
...configParams
] ]
); );
@ -48,6 +42,26 @@ export const fetchRepository = async (path: string, onProgress?: (event: SimpleG
} }
} }
/**
* Applies the gitConfig to the repo at the given path. Note that this will
* override the values for any existing keys, and append new values for keys
* that do not exist yet. It will _not_ remove any existing keys that are not
* present in gitConfig.
*/
export const upsertGitConfig = async (path: string, gitConfig: Record<string, string>, onProgress?: (event: SimpleGitProgressEvent) => void) => {
const git = simpleGit({
progress: onProgress,
}).cwd(path);
try {
for (const [key, value] of Object.entries(gitConfig)) {
await git.addConfig(key, value);
}
} catch (error) {
throw new Error(`Failed to set git config ${path}`);
}
}
export const getBranches = async (path: string) => { export const getBranches = async (path: string) => {
const git = simpleGit(); const git = simpleGit();
const branches = await git.cwd({ const branches = await git.cwd({

View file

@ -8,6 +8,7 @@ import { WithRequired } from "./types.js"
import { marshalBool } from "./utils.js"; import { marshalBool } from "./utils.js";
import { GerritConnectionConfig, GiteaConnectionConfig, GitlabConnectionConfig } from '@sourcebot/schemas/v3/connection.type'; import { GerritConnectionConfig, GiteaConnectionConfig, GitlabConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
import { RepoMetadata } from './types.js'; import { RepoMetadata } from './types.js';
import path from 'path';
export type RepoData = WithRequired<Prisma.RepoCreateInput, 'connections'>; export type RepoData = WithRequired<Prisma.RepoCreateInput, 'connections'>;
@ -29,10 +30,13 @@ export const compileGithubConfig = async (
const notFound = gitHubReposResult.notFound; const notFound = gitHubReposResult.notFound;
const hostUrl = config.url ?? 'https://github.com'; const hostUrl = config.url ?? 'https://github.com';
const hostname = new URL(hostUrl).hostname; const repoNameRoot = new URL(hostUrl)
.toString()
.replace(/^https?:\/\//, '');
const repos = gitHubRepos.map((repo) => { const repos = gitHubRepos.map((repo) => {
const repoName = `${hostname}/${repo.full_name}`; const repoDisplayName = repo.full_name;
const repoName = path.join(repoNameRoot, repoDisplayName);
const cloneUrl = new URL(repo.clone_url!); const cloneUrl = new URL(repo.clone_url!);
const record: RepoData = { const record: RepoData = {
@ -42,6 +46,7 @@ export const compileGithubConfig = async (
cloneUrl: cloneUrl.toString(), cloneUrl: cloneUrl.toString(),
webUrl: repo.html_url, webUrl: repo.html_url,
name: repoName, name: repoName,
displayName: repoDisplayName,
imageUrl: repo.owner.avatar_url, imageUrl: repo.owner.avatar_url,
isFork: repo.fork, isFork: repo.fork,
isArchived: !!repo.archived, isArchived: !!repo.archived,
@ -67,6 +72,7 @@ export const compileGithubConfig = async (
'zoekt.archived': marshalBool(repo.archived), 'zoekt.archived': marshalBool(repo.archived),
'zoekt.fork': marshalBool(repo.fork), 'zoekt.fork': marshalBool(repo.fork),
'zoekt.public': marshalBool(repo.private === false), 'zoekt.public': marshalBool(repo.private === false),
'zoekt.display-name': repoDisplayName,
}, },
branches: config.revisions?.branches ?? undefined, branches: config.revisions?.branches ?? undefined,
tags: config.revisions?.tags ?? undefined, tags: config.revisions?.tags ?? undefined,
@ -93,13 +99,16 @@ export const compileGitlabConfig = async (
const notFound = gitlabReposResult.notFound; const notFound = gitlabReposResult.notFound;
const hostUrl = config.url ?? 'https://gitlab.com'; const hostUrl = config.url ?? 'https://gitlab.com';
const hostname = new URL(hostUrl).hostname; const repoNameRoot = new URL(hostUrl)
.toString()
.replace(/^https?:\/\//, '');
const repos = gitlabRepos.map((project) => { const repos = gitlabRepos.map((project) => {
const projectUrl = `${hostUrl}/${project.path_with_namespace}`; const projectUrl = `${hostUrl}/${project.path_with_namespace}`;
const cloneUrl = new URL(project.http_url_to_repo); const cloneUrl = new URL(project.http_url_to_repo);
const isFork = project.forked_from_project !== undefined; const isFork = project.forked_from_project !== undefined;
const repoName = `${hostname}/${project.path_with_namespace}`; const repoDisplayName = project.path_with_namespace;
const repoName = path.join(repoNameRoot, repoDisplayName);
const record: RepoData = { const record: RepoData = {
external_id: project.id.toString(), external_id: project.id.toString(),
@ -108,6 +117,7 @@ export const compileGitlabConfig = async (
cloneUrl: cloneUrl.toString(), cloneUrl: cloneUrl.toString(),
webUrl: projectUrl, webUrl: projectUrl,
name: repoName, name: repoName,
displayName: repoDisplayName,
imageUrl: project.avatar_url, imageUrl: project.avatar_url,
isFork: isFork, isFork: isFork,
isArchived: !!project.archived, isArchived: !!project.archived,
@ -130,7 +140,8 @@ export const compileGitlabConfig = async (
'zoekt.gitlab-forks': (project.forks_count ?? 0).toString(), 'zoekt.gitlab-forks': (project.forks_count ?? 0).toString(),
'zoekt.archived': marshalBool(project.archived), 'zoekt.archived': marshalBool(project.archived),
'zoekt.fork': marshalBool(isFork), 'zoekt.fork': marshalBool(isFork),
'zoekt.public': marshalBool(project.private === false) 'zoekt.public': marshalBool(project.private === false),
'zoekt.display-name': repoDisplayName,
}, },
branches: config.revisions?.branches ?? undefined, branches: config.revisions?.branches ?? undefined,
tags: config.revisions?.tags ?? undefined, tags: config.revisions?.tags ?? undefined,
@ -157,11 +168,14 @@ export const compileGiteaConfig = async (
const notFound = giteaReposResult.notFound; const notFound = giteaReposResult.notFound;
const hostUrl = config.url ?? 'https://gitea.com'; const hostUrl = config.url ?? 'https://gitea.com';
const hostname = new URL(hostUrl).hostname; const repoNameRoot = new URL(hostUrl)
.toString()
.replace(/^https?:\/\//, '');
const repos = giteaRepos.map((repo) => { const repos = giteaRepos.map((repo) => {
const cloneUrl = new URL(repo.clone_url!); const cloneUrl = new URL(repo.clone_url!);
const repoName = `${hostname}/${repo.full_name!}`; const repoDisplayName = repo.full_name!;
const repoName = path.join(repoNameRoot, repoDisplayName);
const record: RepoData = { const record: RepoData = {
external_id: repo.id!.toString(), external_id: repo.id!.toString(),
@ -170,6 +184,7 @@ export const compileGiteaConfig = async (
cloneUrl: cloneUrl.toString(), cloneUrl: cloneUrl.toString(),
webUrl: repo.html_url, webUrl: repo.html_url,
name: repoName, name: repoName,
displayName: repoDisplayName,
imageUrl: repo.owner?.avatar_url, imageUrl: repo.owner?.avatar_url,
isFork: repo.fork!, isFork: repo.fork!,
isArchived: !!repo.archived, isArchived: !!repo.archived,
@ -191,6 +206,7 @@ export const compileGiteaConfig = async (
'zoekt.archived': marshalBool(repo.archived), 'zoekt.archived': marshalBool(repo.archived),
'zoekt.fork': marshalBool(repo.fork!), 'zoekt.fork': marshalBool(repo.fork!),
'zoekt.public': marshalBool(repo.internal === false && repo.private === false), 'zoekt.public': marshalBool(repo.internal === false && repo.private === false),
'zoekt.display-name': repoDisplayName,
}, },
branches: config.revisions?.branches ?? undefined, branches: config.revisions?.branches ?? undefined,
tags: config.revisions?.tags ?? undefined, tags: config.revisions?.tags ?? undefined,
@ -212,27 +228,32 @@ export const compileGerritConfig = async (
orgId: number) => { orgId: number) => {
const gerritRepos = await getGerritReposFromConfig(config); const gerritRepos = await getGerritReposFromConfig(config);
const hostUrl = (config.url ?? 'https://gerritcodereview.com').replace(/\/$/, ''); // Remove trailing slash const hostUrl = config.url;
const hostname = new URL(hostUrl).hostname; const repoNameRoot = new URL(hostUrl)
.toString()
.replace(/^https?:\/\//, '');
const repos = gerritRepos.map((project) => { const repos = gerritRepos.map((project) => {
const repoId = `${hostname}/${project.name}`; const cloneUrl = new URL(path.join(hostUrl, encodeURIComponent(project.name)));
const cloneUrl = new URL(`${config.url}/${encodeURIComponent(project.name)}`); const repoDisplayName = project.name;
const repoName = path.join(repoNameRoot, repoDisplayName);
const webUrl = (() => {
if (!project.web_links || project.web_links.length === 0) {
return null;
}
let webUrl = "https://www.gerritcodereview.com/";
// Gerrit projects can have multiple web links; use the first one
if (project.web_links) {
const webLink = project.web_links[0]; const webLink = project.web_links[0];
if (webLink) { const webUrl = webLink.url;
webUrl = webLink.url;
}
}
// Handle case where webUrl is just a gitiles path // Handle case where webUrl is just a gitiles path
// https://github.com/GerritCodeReview/plugins_gitiles/blob/5ee7f57/src/main/java/com/googlesource/gerrit/plugins/gitiles/GitilesWeblinks.java#L50 // https://github.com/GerritCodeReview/plugins_gitiles/blob/5ee7f57/src/main/java/com/googlesource/gerrit/plugins/gitiles/GitilesWeblinks.java#L50
if (webUrl.startsWith('/plugins/gitiles/')) { if (webUrl.startsWith('/plugins/gitiles/')) {
webUrl = `${hostUrl}${webUrl}`; return path.join(hostUrl, webUrl);
} else {
return webUrl;
} }
})();
const record: RepoData = { const record: RepoData = {
external_id: project.id.toString(), external_id: project.id.toString(),
@ -240,7 +261,8 @@ export const compileGerritConfig = async (
external_codeHostUrl: hostUrl, external_codeHostUrl: hostUrl,
cloneUrl: cloneUrl.toString(), cloneUrl: cloneUrl.toString(),
webUrl: webUrl, webUrl: webUrl,
name: project.name, name: repoName,
displayName: repoDisplayName,
isFork: false, isFork: false,
isArchived: false, isArchived: false,
org: { org: {
@ -256,11 +278,12 @@ export const compileGerritConfig = async (
metadata: { metadata: {
gitConfig: { gitConfig: {
'zoekt.web-url-type': 'gitiles', 'zoekt.web-url-type': 'gitiles',
'zoekt.web-url': webUrl, 'zoekt.web-url': webUrl ?? '',
'zoekt.name': repoId, 'zoekt.name': repoName,
'zoekt.archived': marshalBool(false), 'zoekt.archived': marshalBool(false),
'zoekt.fork': marshalBool(false), 'zoekt.fork': marshalBool(false),
'zoekt.public': marshalBool(true), 'zoekt.public': marshalBool(true),
'zoekt.display-name': repoDisplayName,
}, },
} satisfies RepoMetadata, } satisfies RepoMetadata,
}; };

View file

@ -3,9 +3,9 @@ import { Redis } from 'ioredis';
import { createLogger } from "./logger.js"; import { createLogger } from "./logger.js";
import { Connection, PrismaClient, Repo, RepoToConnection, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db"; import { Connection, PrismaClient, Repo, RepoToConnection, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db";
import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig } from '@sourcebot/schemas/v3/connection.type'; import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
import { AppContext, Settings, RepoMetadata } from "./types.js"; import { AppContext, Settings, repoMetadataSchema } from "./types.js";
import { getRepoPath, getTokenFromConfig, measure, getShardPrefix } from "./utils.js"; import { getRepoPath, getTokenFromConfig, measure, getShardPrefix } from "./utils.js";
import { cloneRepository, fetchRepository } from "./git.js"; import { cloneRepository, fetchRepository, upsertGitConfig } from "./git.js";
import { existsSync, readdirSync, promises } from 'fs'; import { existsSync, readdirSync, promises } from 'fs';
import { indexGitRepository } from "./zoekt.js"; import { indexGitRepository } from "./zoekt.js";
import { PromClient } from './promClient.js'; import { PromClient } from './promClient.js';
@ -200,8 +200,7 @@ export class RepoManager implements IRepoManager {
let cloneDuration_s: number | undefined = undefined; let cloneDuration_s: number | undefined = undefined;
const repoPath = getRepoPath(repo, this.ctx); const repoPath = getRepoPath(repo, this.ctx);
const metadata = repo.metadata as RepoMetadata; const metadata = repoMetadataSchema.parse(repo.metadata);
// If the repo was already in the indexing state, this job was likely killed and picked up again. As a result, // If the repo was already in the indexing state, this job was likely killed and picked up again. As a result,
// to ensure the repo state is valid, we delete the repo if it exists so we get a fresh clone // to ensure the repo state is valid, we delete the repo if it exists so we get a fresh clone
@ -240,7 +239,7 @@ export class RepoManager implements IRepoManager {
} }
} }
const { durationMs } = await measure(() => cloneRepository(cloneUrl.toString(), repoPath, metadata.gitConfig, ({ method, stage, progress }) => { const { durationMs } = await measure(() => cloneRepository(cloneUrl.toString(), repoPath, ({ method, stage, progress }) => {
this.logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.id}`) this.logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.id}`)
})); }));
cloneDuration_s = durationMs / 1000; cloneDuration_s = durationMs / 1000;
@ -249,6 +248,13 @@ export class RepoManager implements IRepoManager {
this.logger.info(`Cloned ${repo.id} in ${cloneDuration_s}s`); this.logger.info(`Cloned ${repo.id} in ${cloneDuration_s}s`);
} }
// Regardless of clone or fetch, always upsert the git config for the repo.
// This ensures that the git config is always up to date for whatever we
// have in the DB.
if (metadata.gitConfig) {
await upsertGitConfig(repoPath, metadata.gitConfig);
}
this.logger.info(`Indexing ${repo.id}...`); this.logger.info(`Indexing ${repo.id}...`);
const { durationMs } = await measure(() => indexGitRepository(repo, this.settings, this.ctx)); const { durationMs } = await measure(() => indexGitRepository(repo, this.settings, this.ctx));
const indexDuration_s = durationMs / 1000; const indexDuration_s = durationMs / 1000;

View file

@ -1,4 +1,5 @@
import { Settings as SettingsSchema } from "@sourcebot/schemas/v3/index.type"; import { Settings as SettingsSchema } from "@sourcebot/schemas/v3/index.type";
import { z } from "zod";
export type AppContext = { export type AppContext = {
/** /**
@ -16,28 +17,32 @@ export type AppContext = {
export type Settings = Required<SettingsSchema>; export type Settings = Required<SettingsSchema>;
/** // Structure of the `metadata` field in the `Repo` table.
* Structure of the `metadata` field in the `Repo` table. //
*/ // @WARNING: If you modify this schema, please make sure it is backwards
export type RepoMetadata = { // compatible with any prior versions of the schema!!
// @NOTE: If you move this schema, please update the comment in schema.prisma
// to point to the new location.
export const repoMetadataSchema = z.object({
/** /**
* A set of key-value pairs that will be used as git config * A set of key-value pairs that will be used as git config
* variables when cloning the repo. * variables when cloning the repo.
* @see: https://git-scm.com/docs/git-clone#Documentation/git-clone.txt-code--configcodecodeltkeygtltvaluegtcode * @see: https://git-scm.com/docs/git-clone#Documentation/git-clone.txt-code--configcodecodeltkeygtltvaluegtcode
*/ */
gitConfig?: Record<string, string>; gitConfig: z.record(z.string(), z.string()).optional(),
/** /**
* A list of branches to index. Glob patterns are supported. * A list of branches to index. Glob patterns are supported.
*/ */
branches?: string[]; branches: z.array(z.string()).optional(),
/** /**
* A list of tags to index. Glob patterns are supported. * A list of tags to index. Glob patterns are supported.
*/ */
tags?: string[]; tags: z.array(z.string()).optional(),
} });
export type RepoMetadata = z.infer<typeof repoMetadataSchema>;
// @see : https://stackoverflow.com/a/61132308 // @see : https://stackoverflow.com/a/61132308
export type DeepPartial<T> = T extends object ? { export type DeepPartial<T> = T extends object ? {

View file

@ -1,5 +1,5 @@
import { exec } from "child_process"; import { exec } from "child_process";
import { AppContext, RepoMetadata, Settings } from "./types.js"; import { AppContext, repoMetadataSchema, Settings } from "./types.js";
import { Repo } from "@sourcebot/db"; import { Repo } from "@sourcebot/db";
import { getRepoPath } from "./utils.js"; import { getRepoPath } from "./utils.js";
import { getShardPrefix } from "./utils.js"; import { getShardPrefix } from "./utils.js";
@ -17,7 +17,7 @@ export const indexGitRepository = async (repo: Repo, settings: Settings, ctx: Ap
const repoPath = getRepoPath(repo, ctx); const repoPath = getRepoPath(repo, ctx);
const shardPrefix = getShardPrefix(repo.orgId, repo.id); const shardPrefix = getShardPrefix(repo.orgId, repo.id);
const metadata = repo.metadata as RepoMetadata; const metadata = repoMetadataSchema.parse(repo.metadata);
if (metadata.branches) { if (metadata.branches) {
const branchGlobs = metadata.branches const branchGlobs = metadata.branches
@ -57,7 +57,17 @@ export const indexGitRepository = async (repo: Repo, settings: Settings, ctx: Ap
revisions = revisions.slice(0, 64); revisions = revisions.slice(0, 64);
} }
const command = `zoekt-git-index -allow_missing_branches -index ${ctx.indexPath} -max_trigram_count ${settings.maxTrigramCount} -file_limit ${settings.maxFileSize} -branches ${revisions.join(',')} -tenant_id ${repo.orgId} -shard_prefix ${shardPrefix} ${repoPath}`; const command = [
'zoekt-git-index',
'-allow_missing_branches',
`-index ${ctx.indexPath}`,
`-max_trigram_count ${settings.maxTrigramCount}`,
`-file_limit ${settings.maxFileSize}`,
`-branches ${revisions.join(',')}`,
`-tenant_id ${repo.orgId}`,
`-shard_prefix ${shardPrefix}`,
repoPath
].join(' ');
return new Promise<{ stdout: string, stderr: string }>((resolve, reject) => { return new Promise<{ stdout: string, stderr: string }>((resolve, reject) => {
exec(command, (error, stdout, stderr) => { exec(command, (error, stdout, stderr) => {

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Repo" ADD COLUMN "displayName" TEXT;

View file

@ -38,12 +38,13 @@ enum StripeSubscriptionStatus {
model Repo { model Repo {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String name String
displayName String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
indexedAt DateTime? indexedAt DateTime?
isFork Boolean isFork Boolean
isArchived Boolean isArchived Boolean
metadata Json metadata Json // For schema see repoMetadataSchema in packages/backend/src/types.ts
cloneUrl String cloneUrl String
webUrl String? webUrl String?
connections RepoToConnection[] connections RepoToConnection[]

View file

@ -429,6 +429,7 @@ export const getRepos = async (domain: string, filter: { status?: RepoIndexingSt
codeHostType: repo.external_codeHostType, codeHostType: repo.external_codeHostType,
repoId: repo.id, repoId: repo.id,
repoName: repo.name, repoName: repo.name,
repoDisplayName: repo.displayName ?? undefined,
repoCloneUrl: repo.cloneUrl, repoCloneUrl: repo.cloneUrl,
webUrl: repo.webUrl ?? undefined, webUrl: repo.webUrl ?? undefined,
linkedConnections: repo.connections.map(({ connection }) => ({ linkedConnections: repo.connections.map(({ connection }) => ({

View file

@ -73,7 +73,7 @@ const RepositoryBadge = ({
return { return {
repoIcon: <FileIcon className="w-4 h-4" />, repoIcon: <FileIcon className="w-4 h-4" />,
displayName: repo.repoName.split('/').slice(-2).join('/'), displayName: repo.repoName,
repoLink: undefined, repoLink: undefined,
} }
})(); })();

View file

@ -39,7 +39,7 @@ export const RepositoryTable = ({ isAddNewRepoButtonVisible }: RepositoryTablePr
if (!repos) return []; if (!repos) return [];
return repos.map((repo): RepositoryColumnInfo => ({ return repos.map((repo): RepositoryColumnInfo => ({
name: repo.repoName.split('/').length > 2 ? repo.repoName.split('/').slice(-2).join('/') : repo.repoName, name: repo.repoDisplayName ?? repo.repoName,
imageUrl: repo.imageUrl, imageUrl: repo.imageUrl,
connections: repo.linkedConnections, connections: repo.linkedConnections,
repoIndexingStatus: repo.repoIndexingStatus as RepoIndexingStatus, repoIndexingStatus: repo.repoIndexingStatus as RepoIndexingStatus,

View file

@ -168,6 +168,7 @@ export const repositoryQuerySchema = z.object({
codeHostType: z.string(), codeHostType: z.string(),
repoId: z.number(), repoId: z.number(),
repoName: z.string(), repoName: z.string(),
repoDisplayName: z.string().optional(),
repoCloneUrl: z.string(), repoCloneUrl: z.string(),
webUrl: z.string().optional(), webUrl: z.string().optional(),
linkedConnections: z.array(z.object({ linkedConnections: z.array(z.object({

View file

@ -47,22 +47,19 @@ export const getRepoCodeHostInfo = (repo?: Repository): CodeHostInfo | undefined
return undefined; return undefined;
} }
const webUrlType = repo.RawConfig ? repo.RawConfig['web-url-type'] : undefined; if (!repo.RawConfig) {
if (!webUrlType) {
return undefined; return undefined;
} }
const url = new URL(repo.URL); // @todo : use zod to validate config schema
const displayName = url.pathname.slice(1); const webUrlType = repo.RawConfig['web-url-type']!;
const displayName = repo.RawConfig['display-name'] ?? repo.RawConfig['name']!;
return _getCodeHostInfoInternal(webUrlType, displayName, repo.URL); return _getCodeHostInfoInternal(webUrlType, displayName, repo.URL);
} }
export const getRepoQueryCodeHostInfo = (repo?: RepositoryQuery): CodeHostInfo | undefined => { export const getRepoQueryCodeHostInfo = (repo: RepositoryQuery): CodeHostInfo | undefined => {
if (!repo) { const displayName = repo.repoDisplayName ?? repo.repoName;
return undefined;
}
const displayName = repo.repoName.split('/').slice(-2).join('/');
return _getCodeHostInfoInternal(repo.codeHostType, displayName, repo.webUrl ?? repo.repoCloneUrl); return _getCodeHostInfoInternal(repo.codeHostType, displayName, repo.webUrl ?? repo.repoCloneUrl);
} }