Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Bitbucket repositories #1695

Merged
merged 7 commits into from Sep 18, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
### Features

- Flag option types like `validation` can now be set to true/false to enable/disable all flags within them.
- Source code links now work with Bitbucket repositories.

## v0.22.3 (2021-08-12)

Expand Down
2 changes: 1 addition & 1 deletion examples/self/run
@@ -1,3 +1,3 @@
#!/bin/sh
cd ${0%/*}
node ../../bin/typedoc --mode file --name "TypeDoc Documentation" --tsconfig ../../tsconfig.json --readme ../../README.md --out doc/ ../../src/lib/
node ../../bin/typedoc --name "TypeDoc Documentation" --tsconfig ../../tsconfig.json --readme ../../README.md --out doc/ --entryPointStrategy expand ../../src/lib/
2 changes: 1 addition & 1 deletion examples/self/run.bat
Expand Up @@ -2,6 +2,6 @@
set curr_dir=%cd%
chdir /D "%~dp0"

node ..\..\bin\typedoc --mode file --name "TypeDoc Documentation" --tsconfig ../../tsconfig.json --readme ../../README.md --out doc\ ..\..\src\lib\
node ..\..\bin\typedoc --name "TypeDoc Documentation" --tsconfig ../../tsconfig.json --readme ../../README.md --out doc\ --entryPointStrategy expand ..\..\src\lib\

chdir /D "%curr_dir%"
86 changes: 57 additions & 29 deletions src/lib/converter/plugins/GitHubPlugin.ts
Expand Up @@ -7,6 +7,7 @@ import { BasePath } from "../utils/base-path";
import { Converter } from "../converter";
import type { Context } from "../context";
import { BindOption } from "../../utils";
import { RepositoryType } from "../../models";

function git(...args: string[]) {
return spawnSync("git", args, { encoding: "utf-8", windowsHide: true });
Expand Down Expand Up @@ -34,22 +35,27 @@ export class Repository {
/**
* The user/organization name of this repository on GitHub.
*/
gitHubUser?: string;
user?: string;

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

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

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

/**
* Create a new Repository instance.
Expand All @@ -61,25 +67,33 @@ export class Repository {
this.branch = gitRevision || "master";

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

if (url) {
this.gitHubHostname = url[1];
this.gitHubUser = url[2];
this.gitHubProject = url[3];
if (this.gitHubProject.substr(-4) === ".git") {
this.gitHubProject = this.gitHubProject.substr(
if (!match) {
match = /(bitbucket.org)[:/]([^/]+)\/(.*)/.exec(repoLinks[i]);
}

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

if (this.hostname.includes("bitbucket.org"))
this.type = RepositoryType.Bitbucket;
else this.type = RepositoryType.GitHub;

let out = git("-C", path, "ls-files");
if (out.status === 0) {
out.stdout.split("\n").forEach((file) => {
Expand Down Expand Up @@ -108,25 +122,21 @@ export class Repository {
}

/**
* Get the URL of the given file on GitHub.
* Get the URL of the given file on GitHub or Bitbucket.
*
* @param fileName The file whose GitHub URL should be determined.
* @returns An url pointing to the web preview of the given file or NULL.
* @param fileName The file whose URL should be determined.
* @returns A URL pointing to the web preview of the given file or undefined.
*/
getGitHubURL(fileName: string): string | undefined {
if (
!this.gitHubUser ||
!this.gitHubProject ||
!this.contains(fileName)
) {
getURL(fileName: string): string | undefined {
if (!this.user || !this.project || !this.contains(fileName)) {
return;
}

return [
`https://${this.gitHubHostname}`,
this.gitHubUser,
this.gitHubProject,
"blob",
`https://${this.hostname}`,
this.user,
this.project,
this.type === "github" ? "blob" : "src",
this.branch,
fileName.substr(this.path.length + 1),
].join("/");
Expand Down Expand Up @@ -159,6 +169,19 @@ export class Repository {
remotesOutput.stdout.split("\n")
);
}

static getLineNumberAnchor(
lineNumber: number,
repositoryType: RepositoryType | undefined
): string {
switch (repositoryType) {
default:
case RepositoryType.GitHub:
return "L" + lineNumber;
case RepositoryType.Bitbucket:
return "lines-" + lineNumber;
}
}
}

/**
Expand Down Expand Up @@ -248,9 +271,8 @@ export class GitHubPlugin extends ConverterComponent {
project.files.forEach((sourceFile) => {
const repository = this.getRepository(sourceFile.fullFileName);
if (repository) {
sourceFile.url = repository.getGitHubURL(
sourceFile.fullFileName
);
sourceFile.url = repository.getURL(sourceFile.fullFileName);
sourceFile.repositoryType = repository.type;
}
});

Expand All @@ -259,7 +281,13 @@ export class GitHubPlugin extends ConverterComponent {
if (reflection.sources) {
reflection.sources.forEach((source: SourceReference) => {
if (source.file && source.file.url) {
source.url = source.file.url + "#L" + source.line;
source.url =
source.file.url +
"#" +
Repository.getLineNumberAnchor(
source.line,
source.file.repositoryType
);
}
});
}
Expand Down
8 changes: 7 additions & 1 deletion src/lib/models/sources/file.ts
Expand Up @@ -3,6 +3,7 @@ import * as Path from "path";
import type { Reflection } from "../reflections/abstract";
import type { ReflectionGroup } from "../ReflectionGroup";
import type { SourceDirectory } from "./directory";
import type { RepositoryType } from "./repository";

/**
* Represents references of reflections to their defining source files.
Expand Down Expand Up @@ -61,10 +62,15 @@ export class SourceFile {
name: string;

/**
* A url pointing to a page displaying the contents of this file.
* A URL pointing to a page displaying the contents of this file.
*/
url?: string;

/**
* The type of repository where this file is hosted.
*/
repositoryType?: RepositoryType;

/**
* The representation of the parent directory of this source file.
*/
Expand Down
1 change: 1 addition & 0 deletions src/lib/models/sources/index.ts
@@ -1,3 +1,4 @@
export { SourceDirectory } from "./directory";
export { SourceFile } from "./file";
export type { SourceReference } from "./file";
export { RepositoryType } from "./repository";
4 changes: 4 additions & 0 deletions src/lib/models/sources/repository.ts
@@ -0,0 +1,4 @@
export enum RepositoryType {
GitHub = "github",
Bitbucket = "bitbucket",
}
4 changes: 2 additions & 2 deletions src/lib/utils/options/sources/typedoc.ts
Expand Up @@ -232,11 +232,11 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
});
options.addDeclaration({
name: "gitRevision",
help: "Use specified revision instead of the last revision for linking to GitHub source files.",
help: "Use specified revision instead of the last revision for linking to GitHub/Bitbucket source files.",
});
options.addDeclaration({
name: "gitRemote",
help: "Use the specified remote for linking to GitHub source files.",
help: "Use the specified remote for linking to GitHub/Bitbucket source files.",
defaultValue: "origin",
});
options.addDeclaration({
Expand Down
99 changes: 99 additions & 0 deletions src/test/converter3/plugins/GitHubPlugin.test.ts
@@ -0,0 +1,99 @@
import * as github from "../../../lib/converter/plugins/GitHubPlugin";
import { RepositoryType } from "../../../lib/models";
import Assert = require("assert");

describe("Repository", function () {
describe("constructor", function () {
it("defaults to github.com hostname", function () {
const repository = new github.Repository("", "", []);

Assert.equal(repository.hostname, "github.com");
Assert.equal(repository.type, RepositoryType.GitHub);
});

it("handles a personal GitHub HTTPS URL", function () {
const mockRemotes = ["https://github.com/joebloggs/foobar.git"];

const repository = new github.Repository("", "", mockRemotes);

Assert.equal(repository.hostname, "github.com");
Assert.equal(repository.user, "joebloggs");
Assert.equal(repository.project, "foobar");
Assert.equal(repository.type, RepositoryType.GitHub);
});

it("handles an enterprise GitHub URL", function () {
const mockRemotes = ["git@github.acme.com:joebloggs/foobar.git"];

const repository = new github.Repository("", "", mockRemotes);

Assert.equal(repository.hostname, "github.acme.com");
Assert.equal(repository.user, "joebloggs");
Assert.equal(repository.project, "foobar");
Assert.equal(repository.type, RepositoryType.GitHub);
});

it("handles a Bitbucket HTTPS URL", function () {
const mockRemotes = [
"https://joebloggs@bitbucket.org/joebloggs/foobar.git",
];

const repository = new github.Repository("", "", mockRemotes);

Assert.equal(repository.hostname, "bitbucket.org");
Assert.equal(repository.user, "joebloggs");
Assert.equal(repository.project, "foobar");
Assert.equal(repository.type, RepositoryType.Bitbucket);
});

it("handles a Bitbucket SSH URL", function () {
const mockRemotes = ["git@bitbucket.org:joebloggs/foobar.git"];

const repository = new github.Repository("", "", mockRemotes);

Assert.equal(repository.hostname, "bitbucket.org");
Assert.equal(repository.user, "joebloggs");
Assert.equal(repository.project, "foobar");
Assert.equal(repository.type, RepositoryType.Bitbucket);
});
});

describe("getGitHubURL", () => {
srmagura marked this conversation as resolved.
Show resolved Hide resolved
const repositoryPath = "C:/Projects/foobar";
const filePath = repositoryPath + "/src/index.ts";

it("returns a GitHub URL", function () {
const mockRemotes = ["https://github.com/joebloggs/foobar.git"];

const repository = new github.Repository(
repositoryPath,
"main",
mockRemotes
);
repository.files = [filePath];

Assert.equal(
repository.getURL(filePath),
"https://github.com/joebloggs/foobar/blob/main/src/index.ts"
);
});

it("returns a Bitbucket URL", function () {
const mockRemotes = [
"https://joebloggs@bitbucket.org/joebloggs/foobar.git",
];

const repository = new github.Repository(
repositoryPath,
"main",
mockRemotes
);
repository.files = [filePath];

Assert.equal(
repository.getURL(filePath),
"https://bitbucket.org/joebloggs/foobar/src/main/src/index.ts"
);
});
});
});
20 changes: 0 additions & 20 deletions src/test/githubplugin.test.ts

This file was deleted.