Skip to content

Commit

Permalink
Support specifying base url for source code
Browse files Browse the repository at this point in the history
Resolves #2069
  • Loading branch information
Gerrit0 committed Oct 10, 2022
1 parent 9aece44 commit afd4afb
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 271 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Features

- Added support for specifying the tsconfig.json file in packages mode with `{ "typedoc": { "tsconfig": "tsconfig.lib.json" }}` in package.json, #2061.
- Added support for specifying the base file url for links to source code, #2068.

### Bug Fixes

Expand Down
3 changes: 2 additions & 1 deletion src/lib/converter/plugins/SourcePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ export class SourcePlugin extends ConverterComponent {
const repository = Repository.tryCreateRepository(
dirName,
this.gitRevision,
this.gitRemote
this.gitRemote,
this.application.logger
);
if (repository) {
this.repositories[repository.path.toLowerCase()] = repository;
Expand Down
240 changes: 104 additions & 136 deletions src/lib/converter/utils/repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { spawnSync } from "child_process";
import { RepositoryType } from "../../models";
import type { Logger } from "../../utils";
import { BasePath } from "../utils/base-path";

const TEN_MEGABYTES: number = 1024 * 10000;
Expand All @@ -19,131 +19,43 @@ export const gitIsInstalled = git("--version").status === 0;
*/
export class Repository {
/**
* The root path of this repository.
* The path of this repository on disk.
*/
path: string;

/**
* The name of the branch this repository is on right now.
* All files tracked by the repository.
*/
branch: string;
files = new Set<string>();

/**
* A list of all files tracked by the repository.
* The base url for link creation.
*/
files: string[] = [];
baseUrl: string;

/**
* The user/organization name of this repository on GitHub.
* The anchor prefix used to select lines, usually `L`
*/
user?: string;

/**
* The project name of this repository on GitHub.
*/
project?: string;

/**
* The hostname for this GitHub/Bitbucket/.etc project.
*
* Defaults to: `github.com` (for normal, public GitHub instance projects)
*
* Can be the hostname for an enterprise version of GitHub, e.g. `github.acme.com`
* (if found as a match in the list of git remotes).
*/
hostname = "github.com";

/**
* Whether this is a GitHub, Bitbucket, or other type of repository.
*/
type: RepositoryType = RepositoryType.GitHub;

private urlCache = new Map<string, string>();
anchorPrefix: string;

/**
* Create a new Repository instance.
*
* @param path The root path of the repository.
*/
constructor(path: string, gitRevision: string, repoLinks: string[]) {
constructor(path: string, baseUrl: string) {
this.path = path;
this.branch = gitRevision || "master";

for (let i = 0, c = repoLinks.length; i < c; i++) {
let match =
/(github(?!.us)(?:\.[a-z]+)*\.[a-z]{2,})[:/]([^/]+)\/(.*)/.exec(
repoLinks[i]
);

// Github Enterprise
if (!match) {
match = /(\w+\.githubprivate.com)[:/]([^/]+)\/(.*)/.exec(
repoLinks[i]
);
}

// Github Enterprise
if (!match) {
match = /(\w+\.ghe.com)[:/]([^/]+)\/(.*)/.exec(repoLinks[i]);
}

// Github Enterprise
if (!match) {
match = /(\w+\.github.us)[:/]([^/]+)\/(.*)/.exec(repoLinks[i]);
}

if (!match) {
match = /(bitbucket.org)[:/]([^/]+)\/(.*)/.exec(repoLinks[i]);
}

if (!match) {
match = /(gitlab.com)[:/]([^/]+)\/(.*)/.exec(repoLinks[i]);
}

if (match) {
this.hostname = match[1];
this.user = match[2];
this.project = match[3];
if (this.project.endsWith(".git")) {
this.project = this.project.slice(0, -4);
}
break;
}
}

if (this.hostname.includes("bitbucket.org")) {
this.type = RepositoryType.Bitbucket;
} else if (this.hostname.includes("gitlab.com")) {
this.type = RepositoryType.GitLab;
} else {
this.type = RepositoryType.GitHub;
}
this.baseUrl = baseUrl;
this.anchorPrefix = guessAnchorPrefix(this.baseUrl);

let out = git("-C", path, "ls-files");
if (out.status === 0) {
out.stdout.split("\n").forEach((file) => {
if (file !== "") {
this.files.push(BasePath.normalize(path + "/" + file));
this.files.add(BasePath.normalize(path + "/" + file));
}
});
}

if (!gitRevision) {
out = git("-C", path, "rev-parse", "--short", "HEAD");
if (out.status === 0) {
this.branch = out.stdout.replace("\n", "");
}
}
}

/**
* Check whether the given file is tracked by this repository.
*
* @param fileName The name of the file to test for.
* @returns TRUE when the file is part of the repository, otherwise FALSE.
*/
contains(fileName: string): boolean {
return this.files.includes(fileName);
}

/**
Expand All @@ -153,39 +65,15 @@ export class Repository {
* @returns A URL pointing to the web preview of the given file or undefined.
*/
getURL(fileName: string): string | undefined {
if (this.urlCache.has(fileName)) {
return this.urlCache.get(fileName)!;
}

if (!this.user || !this.project || !this.contains(fileName)) {
if (!this.files.has(fileName)) {
return;
}

const url = [
`https://${this.hostname}`,
this.user,
this.project,
this.type === RepositoryType.GitLab ? "-" : undefined,
this.type === RepositoryType.Bitbucket ? "src" : "blob",
this.branch,
fileName.substring(this.path.length + 1),
]
.filter((s) => !!s)
.join("/");

this.urlCache.set(fileName, url);
return url;
return `${this.baseUrl}/${fileName.substring(this.path.length + 1)}`;
}

getLineNumberAnchor(lineNumber: number): string {
switch (this.type) {
default:
case RepositoryType.GitHub:
case RepositoryType.GitLab:
return "L" + lineNumber;
case RepositoryType.Bitbucket:
return "lines-" + lineNumber;
}
return `${this.anchorPrefix}${lineNumber}`;
}

/**
Expand All @@ -200,19 +88,99 @@ export class Repository {
static tryCreateRepository(
path: string,
gitRevision: string,
gitRemote: string
gitRemote: string,
logger: Logger
): Repository | undefined {
const out = git("-C", path, "rev-parse", "--show-toplevel");
const remotesOutput = git("-C", path, "remote", "get-url", gitRemote);

if (out.status !== 0 || remotesOutput.status !== 0) {
return;
const topLevel = git("-C", path, "rev-parse", "--show-toplevel");
if (topLevel.status !== 0) return;

gitRevision ||= git(
"-C",
path,
"rev-parse",
"--short",
"HEAD"
).stdout.trim();
if (!gitRevision) return; // Will only happen in a repo with no commits.

let baseUrl: string | undefined;
if (/^https?:\/\//.test(gitRemote)) {
baseUrl = `${gitRemote}/${gitRevision}`;
} else {
const remotesOut = git("-C", path, "remote", "get-url", gitRemote);
if (remotesOut.status === 0) {
baseUrl = guessBaseUrl(
gitRevision,
remotesOut.stdout.split("\n")
);
} else {
logger.warn(
`The provided git remote "${gitRemote}" was not valid. Source links will be broken.`
);
}
}

if (!baseUrl) return;

return new Repository(
BasePath.normalize(out.stdout.replace("\n", "")),
gitRevision,
remotesOutput.stdout.split("\n")
BasePath.normalize(topLevel.stdout.replace("\n", "")),
baseUrl
);
}
}

// Should have three capturing groups:
// 1. hostname
// 2. user
// 3. project
const repoExpressions = [
/(github(?!.us)(?:\.[a-z]+)*\.[a-z]{2,})[:/]([^/]+)\/(.*)/,
/(\w+\.githubprivate.com)[:/]([^/]+)\/(.*)/, // GitHub enterprise
/(\w+\.ghe.com)[:/]([^/]+)\/(.*)/, // GitHub enterprise
/(\w+\.github.us)[:/]([^/]+)\/(.*)/, // GitHub enterprise
/(bitbucket.org)[:/]([^/]+)\/(.*)/,
/(gitlab.com)[:/]([^/]+)\/(.*)/,
];

export function guessBaseUrl(
gitRevision: string,
remotes: string[]
): string | undefined {
let hostname = "";
let user = "";
let project = "";
outer: for (const repoLink of remotes) {
for (const regex of repoExpressions) {
const match = regex.exec(repoLink);
if (match) {
hostname = match[1];
user = match[2];
project = match[3];
break outer;
}
}
}

if (!hostname) return;

if (project.endsWith(".git")) {
project = project.slice(0, -4);
}

let sourcePath = "blob";
if (hostname.includes("gitlab")) {
sourcePath = "-/blob";
} else if (hostname.includes("bitbucket")) {
sourcePath = "src";
}

return `https://${hostname}/${user}/${project}/${sourcePath}/${gitRevision}`;
}

function guessAnchorPrefix(url: string) {
if (url.includes("bitbucket")) {
return "lines-";
}

return "L";
}
1 change: 0 additions & 1 deletion src/lib/models/sources/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { SourceReference } from "./file";
export { RepositoryType } from "./repository";
5 changes: 0 additions & 5 deletions src/lib/models/sources/repository.ts

This file was deleted.

Loading

0 comments on commit afd4afb

Please sign in to comment.