Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 137 additions & 2 deletions src/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getRepoInfo,
isMergeCommit,
normalizePathspec,
parseRepoUrl,
} from "./git";

describe("normalizePathspec", () => {
Expand Down Expand Up @@ -124,12 +125,146 @@ describe("extractBranchName", () => {
});

describe("getRepoInfo", () => {
// Skip: reads from actual git remote, will pass once in linear-release repo
it.skip("should return the repo info", () => {
it("should return the repo info", () => {
const result = getRepoInfo();
expect(result).toBeDefined();
expect(result?.owner).toBe("linear");
expect(result?.name).toBe("linear-release");
expect(result?.provider).toBe("github");
expect(result?.url).toBe("https://github.com/linear/linear-release");
});
});

describe("parseRepoUrl", () => {
describe("HTTPS URLs", () => {
it("should parse github.com HTTPS URL", () => {
const result = parseRepoUrl("https://github.com/linear/linear-app.git");
expect(result).toEqual({
owner: "linear",
name: "linear-app",
provider: "github",
url: "https://github.com/linear/linear-app",
});
});

it("should parse github.com HTTPS URL without .git suffix", () => {
const result = parseRepoUrl("https://github.com/linear/linear-app");
expect(result).toEqual({
owner: "linear",
name: "linear-app",
provider: "github",
url: "https://github.com/linear/linear-app",
});
});

it("should parse gitlab.com HTTPS URL", () => {
const result = parseRepoUrl("https://gitlab.com/myorg/myrepo.git");
expect(result).toEqual({
owner: "myorg",
name: "myrepo",
provider: "gitlab",
url: "https://gitlab.com/myorg/myrepo",
});
});

it("should parse GitHub Enterprise HTTPS URL", () => {
const result = parseRepoUrl("https://github.mycompany.com/engineering/platform.git");
expect(result).toEqual({
owner: "engineering",
name: "platform",
provider: "github",
url: "https://github.mycompany.com/engineering/platform",
});
});

it("should parse self-hosted GitLab HTTPS URL", () => {
const result = parseRepoUrl("https://gitlab.internal.io/team/service.git");
expect(result).toEqual({
owner: "team",
name: "service",
provider: "gitlab",
url: "https://gitlab.internal.io/team/service",
});
});

it("should parse HTTPS URL with credentials", () => {
const result = parseRepoUrl("https://[email protected]/linear/linear-app.git");
expect(result).toEqual({
owner: "linear",
name: "linear-app",
provider: "github",
url: "https://github.com/linear/linear-app",
});
});
});

describe("SSH URLs", () => {
it("should parse github.com SSH URL", () => {
const result = parseRepoUrl("[email protected]:linear/linear-app.git");
expect(result).toEqual({
owner: "linear",
name: "linear-app",
provider: "github",
url: "https://github.com/linear/linear-app",
});
});

it("should parse github.com SSH URL without .git suffix", () => {
const result = parseRepoUrl("[email protected]:linear/linear-app");
expect(result).toEqual({
owner: "linear",
name: "linear-app",
provider: "github",
url: "https://github.com/linear/linear-app",
});
});

it("should parse gitlab.com SSH URL", () => {
const result = parseRepoUrl("[email protected]:myorg/myrepo.git");
expect(result).toEqual({
owner: "myorg",
name: "myrepo",
provider: "gitlab",
url: "https://gitlab.com/myorg/myrepo",
});
});

it("should parse GitHub Enterprise SSH URL", () => {
const result = parseRepoUrl("[email protected]:engineering/platform.git");
expect(result).toEqual({
owner: "engineering",
name: "platform",
provider: "github",
url: "https://github.mycompany.com/engineering/platform",
});
});

it("should parse self-hosted GitLab SSH URL", () => {
const result = parseRepoUrl("[email protected]:team/service.git");
expect(result).toEqual({
owner: "team",
name: "service",
provider: "gitlab",
url: "https://gitlab.internal.io/team/service",
});
});
});

describe("unknown providers", () => {
it("should return null provider for unknown hosts", () => {
const result = parseRepoUrl("https://example.com/myorg/myrepo.git");
expect(result).toEqual({
owner: "myorg",
name: "myrepo",
provider: null,
url: "https://example.com/myorg/myrepo",
});
});

it("should return null for unrecognized URL formats", () => {
expect(parseRepoUrl("not-a-url")).toBeNull();
expect(parseRepoUrl("")).toBeNull();
});
});
});

Expand Down
70 changes: 49 additions & 21 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,54 @@ export function getCommitContextsBetweenShas(
return commits;
}

function hostToProvider(host: string): string | null {
if (host === "gitlab.com" || host.includes("gitlab")) {
return "gitlab";
}
if (host === "github.com" || host.includes("github")) {
return "github";
}
return null;
}

/**
* Parses a git remote URL (HTTPS or SSH) into repo information.
*
* @param remoteUrl The raw git remote URL string.
* @returns Parsed repo info, or null if the URL could not be parsed.
*/
export function parseRepoUrl(remoteUrl: string): RepoInfo | null {
// Handle HTTPS URLs: https://github.com/owner/repo.git
const httpsMatch = remoteUrl.match(/^https?:\/\/(?:[^@]+@)?([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/);
if (httpsMatch) {
const host = httpsMatch[1];
const owner = httpsMatch[2] || null;
const name = httpsMatch[3]?.replace(/\.git$/, "") || null;
return {
owner,
name,
provider: hostToProvider(host),
url: owner && name ? `https://${host}/${owner}/${name}` : null,
};
}

// Handle SSH URLs: [email protected]:owner/repo.git
const sshMatch = remoteUrl.match(/^git@([^:]+):([^/]+)\/([^/]+?)(?:\.git)?$/);
if (sshMatch) {
const host = sshMatch[1];
const owner = sshMatch[2] || null;
const name = sshMatch[3]?.replace(/\.git$/, "") || null;
return {
owner,
name,
provider: hostToProvider(host),
url: owner && name ? `https://${host}/${owner}/${name}` : null,
};
}

return null;
}

export function getRepoInfo(remote: string = "origin", cwd: string = process.cwd()): RepoInfo | null {
try {
const url = execSync(`git remote get-url ${remote}`, {
Expand All @@ -311,27 +359,7 @@ export function getRepoInfo(remote: string = "origin", cwd: string = process.cwd
encoding: "utf8",
}).trim();

// Handle HTTPS URLs: https://github.com/owner/repo.git or https://github.com/owner/repo
const httpsMatch = url.match(
/^https?:\/\/(?:[^@]+@)?(?:github\.com|gitlab\.com|bitbucket\.org)[/:]([^/]+)\/([^/]+?)(?:\.git)?$/,
);
if (httpsMatch) {
return {
owner: httpsMatch[1] || null,
name: httpsMatch[2]?.replace(/\.git$/, "") || null,
};
}

// Handle SSH URLs: [email protected]:owner/repo.git or [email protected]:owner/repo
const sshMatch = url.match(/^git@(?:github\.com|gitlab\.com|bitbucket\.org):([^/]+)\/([^/]+?)(?:\.git)?$/);
if (sshMatch) {
return {
owner: sshMatch[1] || null,
name: sshMatch[2]?.replace(/\.git$/, "") || null,
};
}

return null;
return parseRepoUrl(url);
} catch (error) {
console.error(`Error getting repo info: ${error}`);
return null;
Expand Down
33 changes: 21 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
AccessKeyUpdateByPipelineResponse,
CommitContext,
DebugSink,
IssueReference,
IssueSource,
PullRequestSource,
RepoInfo,
Expand Down Expand Up @@ -113,11 +114,11 @@ function scanCommits(
commits: CommitContext[],
includePaths: string[] | null,
): {
issueIdentifiers: string[];
issueReferences: IssueReference[];
prNumbers: number[];
debugSink: DebugSink;
} {
const seen = new Set<string>();
const seen = new Map<string, IssueReference>();
const prNumbersSet = new Set<number>();
const debugSink: DebugSink = {
inspectedShas: [],
Expand Down Expand Up @@ -151,7 +152,7 @@ function scanCommits(
continue;
}

seen.add(key);
seen.set(key, { identifier: key, commitSha: commit.sha });
log(`Detected issue key ${key} from branch name "${commit.branchName ?? ""}"`);
}

Expand All @@ -177,7 +178,7 @@ function scanCommits(
continue;
}

seen.add(key);
seen.set(key, { identifier: key, commitSha: commit.sha });
log(`Detected issue key ${key} from commit message "${commit.message ?? ""}"`);
}

Expand All @@ -198,7 +199,7 @@ function scanCommits(
}

return {
issueIdentifiers: Array.from(seen),
issueReferences: Array.from(seen.values()),
prNumbers: Array.from(prNumbersSet),
debugSink,
};
Expand Down Expand Up @@ -276,21 +277,21 @@ async function syncCommand(): Promise<{
return null;
}

const { issueIdentifiers, prNumbers, debugSink } = scanCommits(commits, effectiveIncludePaths);
const { issueReferences, prNumbers, debugSink } = scanCommits(commits, effectiveIncludePaths);

log(`Debug sink: ${JSON.stringify(debugSink, null, 2)}`);

if (issueIdentifiers.length === 0) {
if (issueReferences.length === 0) {
log("No issue keys found");
} else {
log(`Retrieved issue keys: ${issueIdentifiers.join(", ")}`);
log(`Retrieved issue keys: ${issueReferences.map((f) => f.identifier).join(", ")}`);
}

const repoInfo = getRepoInfo();

const release = await syncRelease(issueIdentifiers, prNumbers, repoInfo, debugSink);
const release = await syncRelease(issueReferences, prNumbers, repoInfo, debugSink);
log(
`Issues [${issueIdentifiers.join(", ")}] and pull requests [${prNumbers.join(
`Issues [${issueReferences.map((f) => f.identifier).join(", ")}] and pull requests [${prNumbers.join(
", ",
)}] have been added to release ${release.name}`,
);
Expand Down Expand Up @@ -426,7 +427,7 @@ async function getPipelineSettings(): Promise<{ includePathPatterns: string[] }>
}

async function syncRelease(
issueIdentifiers: string[],
issueReferences: IssueReference[],
prNumbers: number[],
repoInfo: RepoInfo | null,
debugSink: DebugSink,
Expand Down Expand Up @@ -463,12 +464,20 @@ async function syncRelease(
name: releaseName,
version: releaseVersion,
commitSha: currentSha,
issueIdentifiers,
issueReferences,
pullRequestReferences: prNumbers.map((number) => ({
repositoryOwner: owner,
repositoryName: name,
number,
})),
repository: repoInfo
? {
owner: repoInfo.owner,
name: repoInfo.name,
provider: repoInfo.provider,
url: repoInfo.url,
}
: undefined,
debugSink,
},
},
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ export type GitInfo = {
export type RepoInfo = {
owner: string | null;
name: string | null;
provider: string | null;
url: string | null;
};

export type IssueReference = {
identifier: string;
commitSha: string;
};

// Debug sink types
Expand Down