Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
163 commits
Select commit Hold shift + click to select a range
ab6174d
feat(messaging): add key, owner, source and hash columns to templates
Silthus Oct 23, 2026
29b841c
feat(messaging): upload, lock, adopt and send code-managed email temp…
Silthus Oct 23, 2026
70b94fb
refactor(messaging): save a locked draft on the row it locked
Silthus Oct 23, 2026
98c0ca2
fix(messaging): keep the stored sender and reply-to on an upload
Silthus Oct 23, 2026
c7163d1
fix(messaging): say to send a json object when the body is not one
Silthus Oct 23, 2026
14ec125
test(messaging): keep the upload refusal rows type-checked
Silthus Oct 23, 2026
5b45b9b
fix(messaging): name the template's own sender when an upload is refused
Silthus Oct 23, 2026
745b6a3
feat(workflows): show code-managed email templates read-only
Silthus Oct 23, 2026
9a70ab8
refactor(workflows): label the read-only template preview with LemonF…
Silthus Oct 23, 2026
2d71443
fix(workflows): lock the editor when an upload refuses a draft save
Silthus Oct 23, 2026
362b19c
fix(workflows): ask before leaving a template with refused edits
Silthus Oct 23, 2026
1e51a84
fix(workflows): disable the quill duplicate while a copy is made
Silthus Oct 23, 2026
5fb5f7f
fix(workflows): hide the banner duplicate while refused edits wait
Silthus Oct 23, 2026
170aa20
test(workflows): release a code-managed template through its menu
Silthus Oct 23, 2026
33147f4
fix(workflows): keep the first rescued edits and skip them on a discard
Silthus Oct 23, 2026
d83bc5d
fix(workflows): point duplicate to the refused edits while they wait
Silthus Oct 23, 2026
4b0ca47
fix(workflows): put refused edits back in the form on release
Silthus Oct 23, 2026
c2cbb34
refactor(cdp): move the email send checks into one send policy
Silthus Oct 21, 2026
172ecb0
fix(cdp): keep the missing-recipient skip ahead of suppression
Silthus Oct 21, 2026
a37d275
refactor(cdp): drop the unread send phase from the email send policy
Silthus Oct 23, 2026
69eb215
fix(cdp): log the suspension cause again when a send is skipped
Silthus Oct 23, 2026
91edd68
refactor(cdp): name how delivery presents every policy code
Silthus Oct 23, 2026
8728de2
chore(cdp): say a refused sender maps to fail in the policy table
Silthus Oct 23, 2026
c75d4a3
refactor(messaging): send transactional email through one command and…
Silthus Oct 21, 2026
f3a1031
refactor(messaging): return send input refusals as outcomes, not exce…
Silthus Oct 23, 2026
74d5c7a
test(messaging): cover send outcomes on both routes at the transport
Silthus Oct 23, 2026
03bd47c
refactor(messaging): type the usage outcome as the facade's send outcome
Silthus Oct 23, 2026
300435c
test(messaging): assert the usage outcome of refused and unconfirmed …
Silthus Oct 23, 2026
4b8e516
test(messaging): count a send that breaks after the pipeline answers
Silthus Oct 23, 2026
57b568a
feat(workflows): give a code send key its own unlisted workflow
Silthus Oct 23, 2026
d877db6
fix(workflows): keep a code send key's workflow out of the setup steps
Silthus Oct 23, 2026
3aa2c3a
fix(workflows): send code-rendered links without utm tags
Silthus Oct 23, 2026
daa1545
fix(workflows): prefer the default sender on a code send's domain
Silthus Oct 23, 2026
95dbb81
chore(workflows): regenerate the API types for the code send key origin
Silthus Oct 23, 2026
53471a1
chore(workflows): refresh the MCP snapshots and desktop client for co…
Silthus Oct 23, 2026
9036bf4
feat(cdp): deliver an accepted send through the email step as given
Silthus Oct 23, 2026
a2e5caf
refactor(cdp): infer the accepted send type from its schema
Silthus Oct 23, 2026
3b69977
refactor(cdp): apply a code send's capture choice where its invocatio…
Silthus Oct 23, 2026
18e7dd7
test(cdp): carry a code send's capture choice through the email queue
Silthus Oct 23, 2026
9f4c27a
fix(cdp): keep a sensitive email's rendered content out of its snapshot
Silthus Oct 23, 2026
3e6630b
test(cdp): assert the bounce capture choice at the capture transport
Silthus Oct 23, 2026
c3a8190
docs(cdp): describe the address pattern the send route accepts
Silthus Oct 23, 2026
39ad25c
feat(messaging): send email rendered by the caller's code by key
Silthus Oct 23, 2026
6ad7707
refactor(messaging): name a send's source as a stored template or ren…
Silthus Oct 23, 2026
7e59511
fix(messaging): explain a send body that is not a json object
Silthus Oct 23, 2026
6592a98
test(messaging): assert a code send's workflow adds no utm tags
Silthus Oct 23, 2026
31f1345
test(messaging): send from the default sender on a shared domain
Silthus Oct 23, 2026
a82fbe4
fix(messaging): refuse a recipient the email pipeline would not take
Silthus Oct 23, 2026
0979c76
fix(messaging): check workflow access before a rendered send
Silthus Oct 23, 2026
e2726d8
fix(messaging): refuse unknown fields inside a send's objects
Silthus Oct 23, 2026
28e957c
fix(messaging): refuse a sender address the email worker would ignore
Silthus Oct 23, 2026
e585fb5
fix(messaging): take a blank html or text beside the other body
Silthus Oct 23, 2026
9dc632c
fix(messaging): refuse an empty reply_to on a send
Silthus Oct 23, 2026
45e7d36
test(messaging): pass each send form as case data, not a branch
Silthus Oct 23, 2026
21d7cb1
chore(messaging): regenerate the API types for the send fields
Silthus Oct 23, 2026
d94fc6c
fix(messaging): check a recipient's length before its pattern
Silthus Oct 23, 2026
945c77c
fix(messaging): refuse a reply-to or legacy recipient that isn't bare
Silthus Oct 23, 2026
b20afdb
fix(messaging): explain a nested unknown field as unknown
Silthus Oct 23, 2026
a9207c5
feat(cdp): refuse a transactional email the send checks would skip
Silthus Oct 22, 2026
40d706e
feat(cdp): refuse a transactional email to an address that can't rece…
Silthus Oct 22, 2026
5bf1799
feat(messaging): answer a send the checks refuse with why and fix
Silthus Oct 22, 2026
1e21662
chore(cdp): cover a transactional email edited after it was accepted
Silthus Oct 22, 2026
c625a6b
feat(cdp): send a queued transactional email as it was accepted
Silthus Oct 22, 2026
420ce96
refactor(cdp): check an admission without the dropped send phase
Silthus Oct 23, 2026
20075b3
refactor(cdp): read the accepted send type beside its schema
Silthus Oct 23, 2026
0c70b53
refactor(cdp): admit a transactional email with the core email validator
Silthus Oct 23, 2026
d7ef7af
test(cdp): retry a keyed send after its admission refusal is fixed
Silthus Oct 23, 2026
966ccb8
test(cdp): admit a code send by its own sender over the workflow's
Silthus Oct 23, 2026
8ad1b03
refactor(cdp): fail a delivery refused for an invalid recipient
Silthus Oct 23, 2026
d30255e
fix(cdp): pin a transactional email by its event and keep live safety
Silthus Oct 23, 2026
4686cc8
fix(cdp): say what a send check found without the run-log prefix
Silthus Oct 23, 2026
5690f47
test(cdp): release a refused key by resuming the workflow
Silthus Oct 23, 2026
130507d
refactor(cdp): reuse the email sender configuration type
Silthus Oct 23, 2026
a80d8fc
chore(workflows): name the worker as a reader of revision snapshots
Silthus Oct 23, 2026
1bae007
fix(cdp): hide person data from a transactional email by its own event
Silthus Oct 23, 2026
341ec83
chore(cdp): join the skip prefix to a reason with a sentence break
Silthus Oct 23, 2026
ca8b9f0
chore(cdp): say a policy detail is shown to the caller and the run log
Silthus Oct 23, 2026
4cc9195
chore(messaging): cover an idempotency key on the send email route
Silthus Oct 22, 2026
2621108
feat(messaging): send each email once per idempotency key on send/email
Silthus Oct 22, 2026
ca24ee1
fix(messaging): replay a keyed send after its workflow or default sen…
Silthus Oct 23, 2026
ced0e0b
test(cdp): reload a paused workflow the way django would
Silthus Oct 23, 2026
67dc012
refactor(messaging): read the idempotency key header where it is sent
Silthus Oct 23, 2026
1c50f88
fix(workflows): reload a saved workflow on the workers after commit
Silthus Oct 23, 2026
bb3dfc7
fix(workflows): keep a committed workflow save when its reload publis…
Silthus Oct 23, 2026
ba5d72e
refactor(messaging): drop the duplicate after-commit worker reload
Silthus Oct 23, 2026
081f633
feat(messaging): send cc, bcc, headers and tags with a code send
Silthus Oct 22, 2026
795a02d
chore(messaging): regenerate the API types for cc, bcc, headers and tags
Silthus Oct 22, 2026
aaa9721
fix(messaging): keep every code send header and accept django's copies
Silthus Oct 22, 2026
fe9b293
chore(cdp): cover a code send's copies in its idempotency key and sen…
Silthus Oct 23, 2026
3d02d2e
feat(cdp): check a code send's cc and bcc before the 202
Silthus Oct 23, 2026
54c4b95
fix(messaging): explain a refused cc, bcc, header or tag as that field
Silthus Oct 23, 2026
bbd9845
chore(messaging): regenerate the API types for the send field checks
Silthus Oct 23, 2026
3fbb23f
test(messaging): refuse a reply-to address quoted with a comma
Silthus Oct 23, 2026
c2ee67d
fix(messaging): post a send's headers in one order whatever the caller's
Silthus Oct 23, 2026
2b6e9d4
fix(messaging): refuse a tag key the send route does not take
Silthus Oct 23, 2026
a40cdb1
fix(messaging): explain a refused to without denying cc and bcc
Silthus Oct 23, 2026
1d5af41
test(cdp): check a code send's headers and tags by membership
Silthus Oct 23, 2026
4be44dc
refactor(cdp): name the limit on a code send's copies
Silthus Oct 23, 2026
94b57cb
fix(messaging): fingerprint a send's tags in name order
Silthus Oct 23, 2026
8361308
fix(messaging): name the field that holds a refused recipient
Silthus Oct 23, 2026
3cbf394
fix(messaging): say which sends take cc and bcc copies
Silthus Oct 23, 2026
6a82b47
chore(messaging): regenerate the API types for the send description
Silthus Oct 23, 2026
db17fb1
feat(messaging): send attachments with a code send
Silthus Oct 22, 2026
4d9c7bb
chore(messaging): regenerate the API types for attachments
Silthus Oct 22, 2026
9a45a75
fix(messaging): keep every header in a raw code send
Silthus Oct 22, 2026
f445fff
chore(cdp): cover a code send's attachments in its idempotency key
Silthus Oct 23, 2026
12f6f29
fix(messaging): explain a refused attachment as that field
Silthus Oct 23, 2026
78652b5
refactor(cdp): send code send attachments as native ses attachments
Silthus Oct 23, 2026
573d037
chore(messaging): regenerate the API types after the field checks
Silthus Oct 23, 2026
ba2b4c2
test(messaging): give the attachments row its send form
Silthus Oct 23, 2026
90328fc
fix(messaging): refuse an attachment named only by a blocked extension
Silthus Oct 23, 2026
f8cecc4
fix(messaging): refuse an attachment key the send route does not take
Silthus Oct 23, 2026
d83e04b
fix(messaging): hold an attachment's type and content id to 78 charac…
Silthus Oct 23, 2026
3039b1c
chore(messaging): regenerate the API types for the attachment limits
Silthus Oct 23, 2026
45386bc
fix(cdp): skip rerunning a code send whose attachments were not stored
Silthus Oct 23, 2026
b41402a
chore(messaging): update the send email tool schema for the attachmen…
Silthus Oct 23, 2026
dba78a7
feat(messaging): add the email_status clickhouse table
Silthus Oct 22, 2026
c7842fb
refactor(messaging): build the email status engine inside its table sql
Silthus Oct 23, 2026
5b63b4f
refactor(messaging): point the email status view at its one data table
Silthus Oct 23, 2026
2c2865a
refactor(clickhouse): declare the email status row columns once
Silthus Oct 23, 2026
2494286
fix(messaging): keep email statuses one day past their 30-day retention
Silthus Oct 23, 2026
357d0d9
refactor(messaging): write the email status kafka consumer count as is
Silthus Oct 23, 2026
5e1ea32
chore(cdp): cover email status rows and the recipient hash
Silthus Oct 22, 2026
7f62a4a
feat(cdp): write transactional email status rows to kafka
Silthus Oct 22, 2026
c3dafb1
chore(cdp): cover the transactional email status hooks
Silthus Oct 22, 2026
26a0e38
feat(cdp): record transactional email statuses at each hook
Silthus Oct 22, 2026
9176786
chore(cdp): cover sends without a recipient hash
Silthus Oct 22, 2026
998c97b
fix(cdp): queue transactional emails when no salt key is set
Silthus Oct 22, 2026
ec7c10f
fix(cdp): record skipped transactional emails before the email service
Silthus Oct 23, 2026
7f230b4
test(cdp): cover a complaint and its not-spam correction in email status
Silthus Oct 23, 2026
97d5a49
test(cdp): observe email status rows at the kafka producer
Silthus Oct 23, 2026
dc7be7c
test(cdp): reload a stopped workflow the way django would
Silthus Oct 23, 2026
d37e090
fix(cdp): hash the recipient only for a send the route queues
Silthus Oct 23, 2026
386bc32
refactor(cdp): drop the email status nothing writes
Silthus Oct 23, 2026
a920dbb
fix(cdp): record an ses reject as a failed email
Silthus Oct 23, 2026
413c39b
refactor(cdp): take only the email status output in its writer
Silthus Oct 23, 2026
5761fbc
fix(cdp): move an email status only for events of its recipient
Silthus Oct 23, 2026
98ba4f2
fix(cdp): record a canceled or orphaned transactional email as skipped
Silthus Oct 23, 2026
9df41dd
perf(cdp): read stalled email job state in bounded chunks
Silthus Oct 23, 2026
846368f
fix(cdp): let a rerun replace the failed attempt it reruns
Silthus Oct 23, 2026
3b04c61
fix(cdp): rank a failed attempt with skipped so a rerun failure wins
Silthus Oct 23, 2026
de20050
chore(messaging): cover reading a transactional email's status
Silthus Oct 22, 2026
b6dfecc
feat(messaging): read a transactional email's status by id or recipient
Silthus Oct 22, 2026
be75ecc
fix(messaging): tag the email status reads for clickhouse
Silthus Oct 23, 2026
44aedca
fix(messaging): find emails across salt key rotations and reject futu…
Silthus Oct 23, 2026
db349b4
chore(messaging): regenerate the API types after the restack
Silthus Oct 23, 2026
0dfef59
feat(messaging): look up a recipient's transactional emails by address
Silthus Oct 23, 2026
633a090
chore(messaging): regenerate the API types for the recipient lookup
Silthus Oct 23, 2026
4a52a98
refactor(messaging): page a recipient's emails by the last message id
Silthus Oct 23, 2026
7bee5e4
fix(messaging): describe the email lookup page as one object
Silthus Oct 23, 2026
8480bce
chore(messaging): regenerate the API types for the email lookup page
Silthus Oct 23, 2026
ef2c1fa
refactor(messaging): drop the test-only recipient hash export
Silthus Oct 23, 2026
9fee246
chore(messaging): regenerate the API types after the restack
Silthus Oct 23, 2026
15b7ecf
fix(messaging): read email statuses for the 30 days the api promises
Silthus Oct 23, 2026
86eb6d3
refactor(messaging): drop the email status nothing writes
Silthus Oct 23, 2026
8243da0
chore(messaging): regenerate the API types without the waiting status
Silthus Oct 23, 2026
f024423
chore(messaging): describe email status order the way the api does
Silthus Oct 23, 2026
9bc4a37
fix(messaging): page email statuses at millisecond precision
Silthus Oct 23, 2026
ad1afed
fix(messaging): answer not found for an email older than 30 days
Silthus Oct 23, 2026
a7e7051
test(messaging): seed email status rows with the writer's ranks
Silthus Oct 23, 2026
2a8f386
refactor(messaging): return a recipient's email page as a dataclass
Silthus Oct 23, 2026
973459b
fix(messaging): read email statuses only with a project secret key
Silthus Oct 23, 2026
987b927
fix(messaging): rate limit email status reads apart from sends
Silthus Oct 23, 2026
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
1 change: 1 addition & 0 deletions docker/kafka/topics.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ clickhouse_app_metrics
clickhouse_app_metrics2
clickhouse_billing_usage_records
clickhouse_document_embeddings
clickhouse_email_status
clickhouse_error_tracking_fingerprint_issue_state
clickhouse_error_tracking_issue_fingerprint
clickhouse_events_json
Expand Down
52 changes: 52 additions & 0 deletions frontend/src/lib/components/Git/gitMetadataParser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ describe('GitMetadataParser', () => {
remote_url: 'git@gitlab.com:posthog-bot-group/posthog-bot-project.git',
expected: 'https://gitlab.com/posthog-bot-group/posthog-bot-project/-/commit/commit-sha',
},
{
description: 'should create commit link for bitbucket URL',
remote_url: 'https://bitbucket.org/user/repo.git',
expected: 'https://bitbucket.org/user/repo/commits/commit-sha',
},
{
description: 'should create commit link for gitlab URL',
remote_url: 'invalid_url',
Expand Down Expand Up @@ -51,6 +56,21 @@ describe('GitMetadataParser', () => {
remote_url: 'git@gitlab.com:posthog-bot-group/posthog-bot-project.git',
expected: 'https://gitlab.com/posthog-bot-group/posthog-bot-project/-/tree/branch-name',
},
{
description: 'should create branch link for bitbucket URL',
remote_url: 'https://bitbucket.org/user/repo.git',
expected: 'https://bitbucket.org/user/repo/src/branch-name',
},
{
description: 'should create branch link for a gitlab project in a subgroup',
remote_url: 'gitlab.com/group/subgroup/project',
expected: 'https://gitlab.com/group/subgroup/project/-/tree/branch-name',
},
{
description: 'should create branch link for a gitlab remote with a trailing slash',
remote_url: 'https://gitlab.com/group/project/',
expected: 'https://gitlab.com/group/project/-/tree/branch-name',
},
])('$description', ({ remote_url, expected }) => {
const result = GitMetadataParser.getBranchLink(remote_url, 'branch-name')
expect(result).toBe(expected)
Expand Down Expand Up @@ -80,4 +100,36 @@ describe('GitMetadataParser', () => {
expect(GitMetadataParser.getGitHubRepositorySlug(remote_url)).toBe(expected)
})
})

describe('getFileLink', () => {
it.each([
{
description: 'a GitHub remote without a scheme',
remote_url: 'github.com/example/app',
expected: 'https://github.com/example/app/blob/main/emails/welcome.html',
},
{
description: 'a GitLab SSH remote',
remote_url: 'git@gitlab.com:example/app.git',
expected: 'https://gitlab.com/example/app/-/blob/main/emails/welcome.html',
},
{
description: 'a Bitbucket HTTPS remote',
remote_url: 'https://bitbucket.org/example/app',
expected: 'https://bitbucket.org/example/app/src/main/emails/welcome.html',
},
{ description: 'an unknown host', remote_url: 'git.example.com/example/app', expected: undefined },
])('links the file for $description', ({ remote_url, expected }) => {
expect(GitMetadataParser.getFileLink(remote_url, 'main', './emails/welcome.html')).toBe(expected)
})

it('encodes each part of the ref and the path', () => {
expect(GitMetadataParser.getFileLink('github.com/example/app', 'feat/a b', 'emails/a#b.html')).toBe(
'https://github.com/example/app/blob/feat/a%20b/emails/a%23b.html'
)
expect(GitMetadataParser.getRefLink('github.com/example/app', 'fix#12')).toBe(
'https://github.com/example/app/tree/fix%2312'
)
})
})
})
77 changes: 68 additions & 9 deletions frontend/src/lib/components/Git/gitMetadataParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export type ParsedRemoteUrl = {
}

const GITHUB_SLUG_PATTERN = /^[A-Za-z0-9-]+\/[A-Za-z0-9._-]+$/
const COMMIT_SHA_PATTERN = /^[0-9a-f]{7,40}$/i

function encodePathSegments(value: string): string {
return value.split('/').map(encodeURIComponent).join('/')
}

export class GitMetadataParser {
static getCommitLink(remote_url?: string, commit_id?: string): string | undefined {
Expand All @@ -33,6 +38,28 @@ export class GitMetadataParser {
return this.buildBranchLink(parsedRemoteUrl, branch)
}

static getRefLink(remote_url?: string, ref?: string): string | undefined {
if (!ref) {
return undefined
}
return this.isCommitSha(ref) ? this.getCommitLink(remote_url, ref) : this.getBranchLink(remote_url, ref)
}

static getFileLink(remote_url?: string, ref?: string, path?: string): string | undefined {
if (!remote_url || !ref || !path) {
return undefined
}
const parsedRemoteUrl = this.parseRemoteUrl(remote_url)
if (!parsedRemoteUrl) {
return undefined
}
return this.buildFileLink(parsedRemoteUrl, ref, path)
}

static isCommitSha(ref: string): boolean {
return COMMIT_SHA_PATTERN.test(ref)
}

static getRepoLink(remote_url?: string): string | undefined {
if (!remote_url) {
return undefined
Expand All @@ -57,7 +84,11 @@ export class GitMetadataParser {
}

static parseRemoteUrl(remoteUrl: string): ParsedRemoteUrl | undefined {
return this.parseSshRemoteUrl(remoteUrl) || this.parseHttpsRemoteUrl(remoteUrl)
return (
this.parseSshRemoteUrl(remoteUrl) ||
this.parseHttpsRemoteUrl(remoteUrl) ||
this.parseSchemelessRemoteUrl(remoteUrl)
)
}

private static buildRemoteLink(parsedUrl: ParsedRemoteUrl): string | undefined {
Expand All @@ -74,10 +105,11 @@ export class GitMetadataParser {
private static buildBranchLink(parsedUrl: ParsedRemoteUrl, branch: string): string | undefined {
switch (parsedUrl.provider) {
case 'github':
case 'bitbucket':
return `${parsedUrl.providerUrl}/${parsedUrl.owner}/${parsedUrl.repository}/tree/${branch}`
return `${parsedUrl.providerUrl}/${parsedUrl.owner}/${parsedUrl.repository}/tree/${encodePathSegments(branch)}`
case 'gitlab':
return `${parsedUrl.providerUrl}/${parsedUrl.owner}/${parsedUrl.repository}/-/tree/${branch}`
return `${parsedUrl.providerUrl}/${parsedUrl.owner}/${parsedUrl.repository}/-/tree/${encodePathSegments(branch)}`
case 'bitbucket':
return `${parsedUrl.providerUrl}/${parsedUrl.owner}/${parsedUrl.repository}/src/${encodePathSegments(branch)}`
default:
return undefined
}
Expand All @@ -86,15 +118,31 @@ export class GitMetadataParser {
private static buildCommitLink(parsedUrl: ParsedRemoteUrl, commitSha: string): string | undefined {
switch (parsedUrl.provider) {
case 'github':
case 'bitbucket':
return `${parsedUrl.providerUrl}/${parsedUrl.owner}/${parsedUrl.repository}/commit/${commitSha}`
case 'bitbucket':
return `${parsedUrl.providerUrl}/${parsedUrl.owner}/${parsedUrl.repository}/commits/${commitSha}`
case 'gitlab':
return `${parsedUrl.providerUrl}/${parsedUrl.owner}/${parsedUrl.repository}/-/commit/${commitSha}`
default:
return undefined
}
}

private static buildFileLink(parsedUrl: ParsedRemoteUrl, ref: string, path: string): string | undefined {
const base = `${parsedUrl.providerUrl}/${parsedUrl.owner}/${parsedUrl.repository}`
const refAndPath = `${encodePathSegments(ref)}/${encodePathSegments(path.replace(/^\.?\//, ''))}`
switch (parsedUrl.provider) {
case 'github':
return `${base}/blob/${refAndPath}`
case 'gitlab':
return `${base}/-/blob/${refAndPath}`
case 'bitbucket':
return `${base}/src/${refAndPath}`
default:
return undefined
}
}

private static parseSshRemoteUrl(remoteUrl: string): ParsedRemoteUrl | undefined {
// git@github.com:user/repo.git

Expand Down Expand Up @@ -136,23 +184,34 @@ export class GitMetadataParser {
}

const domain = withoutProtocol.slice(0, firstSlashIdx)
const pathParts = withoutProtocol.slice(firstSlashIdx + 1).split('/')
const pathParts = withoutProtocol
.slice(firstSlashIdx + 1)
.split('/')
.filter((part) => part !== '')

if (pathParts.length < 2) {
return undefined
}

const [provider, providerUrl] = this.parseDomain(domain)
const owner = pathParts[0]
let repository = pathParts[1]
// GitLab nests projects in subgroups, so everything after the owner names the project.
let repository = provider === 'gitlab' ? pathParts.slice(1).join('/') : pathParts[1]
if (repository.endsWith('.git')) {
repository = repository.slice(0, -4)
}

const [provider, providerUrl] = this.parseDomain(domain)

return { provider, owner, repository, providerUrl }
}

private static parseSchemelessRemoteUrl(remoteUrl: string): ParsedRemoteUrl | undefined {
// github.com/user/repo
if (remoteUrl.includes('://') || remoteUrl.includes('@')) {
return undefined
}
return this.parseHttpsRemoteUrl(`https://${remoteUrl}`)
}

private static parseDomain(domain: string): [GitProvider, string | undefined] {
switch (domain) {
case 'github.com':
Expand Down
Loading