Skip to content

Commit 55b0e5b

Browse files
olafmaltha017Olaf Malthafarnabaz
authoredMar 7, 2025··
feat(collection): add support for Bitbucket repository (#3226)
--------- Co-authored-by: Olaf Maltha <olafoliver.maltha@gmail.com> Co-authored-by: Farnabaz <farnabaz@gmail.com>
1 parent 3dbdb3d commit 55b0e5b

File tree

5 files changed

+224
-2
lines changed

5 files changed

+224
-2
lines changed
 

‎src/types/collection.ts

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export type CollectionSource = {
1313
exclude?: string[]
1414
repository?: string
1515
authToken?: string
16+
authBasic?: {
17+
username: string
18+
password: string
19+
}
1620
cwd?: string
1721
}
1822

‎src/utils/collection.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { hash } from 'ohash'
33
import type { Collection, ResolvedCollection, CollectionSource, DefinedCollection, ResolvedCollectionSource, CustomCollectionSource, ResolvedCustomCollectionSource } from '../types/collection'
44
import { getOrderedSchemaKeys } from '../runtime/internal/schema'
55
import type { ParsedContentFile } from '../types'
6-
import { defineLocalSource, defineGitHubSource } from './source'
6+
import { defineLocalSource, defineGitHubSource, defineBitbucketSource } from './source'
77
import { metaSchema, pageSchema } from './schema'
88
import type { ZodFieldType } from './zod'
99
import { getUnderlyingType, ZodToSqlFieldTypes, z, getUnderlyingTypeName } from './zod'
@@ -122,6 +122,9 @@ function resolveSource(source: string | CollectionSource | CollectionSource[] |
122122
}
123123

124124
if (source.repository) {
125+
if (source.repository.startsWith('https://bitbucket.org/')) {
126+
return defineBitbucketSource(source)
127+
}
125128
return defineGitHubSource(source)
126129
}
127130

‎src/utils/git.ts

+27
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,33 @@ export function parseGitHubUrl(url: string) {
9090
return null
9191
}
9292

93+
export function parseBitBucketUrl(url: string) {
94+
const bitbucketRegex = /https:\/\/bitbucket\.org\/([^/]+)\/([^/]+)(?:\/src\/([^/]+))?(?:\/(.+))?/
95+
const bitbucketMatch = url.match(bitbucketRegex)
96+
97+
if (bitbucketMatch) {
98+
const org = bitbucketMatch[1]
99+
const repo = bitbucketMatch[2]
100+
let branch = bitbucketMatch[3] || 'main' // Default to 'main' if no branch is provided
101+
let path = bitbucketMatch[4] || ''
102+
103+
if (['fix', 'feat', 'chore', 'test', 'docs'].includes(branch)) {
104+
const pathParts = path.split('/')
105+
branch = join(branch, pathParts[0])
106+
path = pathParts.slice(1).join('/')
107+
}
108+
109+
return {
110+
org: org,
111+
repo: repo,
112+
branch: branch,
113+
path: path,
114+
}
115+
}
116+
117+
return null
118+
}
119+
93120
export async function getLocalGitInfo(rootDir: string): Promise<GitInfo | undefined> {
94121
const remote = await getLocalGitRemote(rootDir)
95122
if (!remote) {

‎src/utils/source.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { join } from 'pathe'
33
import { withLeadingSlash, withoutTrailingSlash } from 'ufo'
44
import FastGlob from 'fast-glob'
55
import type { CollectionSource, ResolvedCollectionSource } from '../types/collection'
6-
import { downloadRepository, parseGitHubUrl } from './git'
6+
import { downloadRepository, parseBitBucketUrl, parseGitHubUrl } from './git'
77
import { logger } from './dev'
88

99
export function defineLocalSource(source: CollectionSource | ResolvedCollectionSource): ResolvedCollectionSource {
@@ -62,6 +62,41 @@ export function defineGitHubSource(source: CollectionSource): ResolvedCollection
6262
return resolvedSource
6363
}
6464

65+
export function defineBitbucketSource(
66+
source: CollectionSource,
67+
): ResolvedCollectionSource {
68+
const resolvedSource = defineLocalSource(source)
69+
70+
resolvedSource.prepare = async ({ rootDir }) => {
71+
const repository
72+
= source?.repository && parseBitBucketUrl(source.repository!)
73+
if (repository) {
74+
const { org, repo, branch } = repository
75+
resolvedSource.cwd = join(
76+
rootDir,
77+
'.data',
78+
'content',
79+
`bitbucket-${org}-${repo}-${branch}`,
80+
)
81+
82+
let headers: Record<string, string> = {}
83+
if (resolvedSource.authBasic) {
84+
const credentials = `${resolvedSource.authBasic.username}:${resolvedSource.authBasic.password}`
85+
const encodedCredentials = btoa(credentials)
86+
headers = {
87+
Authorization: `Basic ${encodedCredentials}`,
88+
}
89+
}
90+
91+
const url = `https://bitbucket.org/${org}/${repo}/get/${branch}.tar.gz`
92+
93+
await downloadRepository(url, resolvedSource.cwd!, { headers })
94+
}
95+
}
96+
97+
return resolvedSource
98+
}
99+
65100
export function parseSourceBase(source: CollectionSource) {
66101
const [fixPart, ...rest] = source.include.includes('*') ? source.include.split('*') : ['', source.include]
67102
return {

‎test/unit/git/url.test.ts

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { expect, describe, test } from 'vitest'
2+
3+
import { parseBitBucketUrl, parseGitHubUrl } from '../../../src/utils/git'
4+
5+
describe('parseGitHubUrl', () => {
6+
test('parses a valid GitHub URL with organization and repository', () => {
7+
const url = 'https://github.com/organization/repository'
8+
const result = parseGitHubUrl(url)
9+
expect(result).toEqual({
10+
org: 'organization',
11+
repo: 'repository',
12+
branch: 'main',
13+
path: '',
14+
})
15+
})
16+
17+
test('parses a valid GitHub URL with trailing slash', () => {
18+
const url = 'https://github.com/organization/repository/'
19+
const result = parseGitHubUrl(url)
20+
expect(result).toEqual({
21+
org: 'organization',
22+
repo: 'repository',
23+
branch: 'main',
24+
path: '',
25+
})
26+
})
27+
28+
test('parses a valid GitHub URL with additional path segments', () => {
29+
const url = 'https://github.com/organization/repository/tree/develop/components'
30+
const result = parseGitHubUrl(url)
31+
expect(result).toEqual({
32+
org: 'organization',
33+
repo: 'repository',
34+
branch: 'develop',
35+
path: 'components',
36+
})
37+
})
38+
39+
test('returns null for invalid GitHub URL (missing repository)', () => {
40+
const url = 'https://github.com/organization'
41+
const result = parseGitHubUrl(url)
42+
expect(result).toBeNull()
43+
})
44+
45+
test('returns null for non-GitHub URL', () => {
46+
const url = 'https://gitlab.com/organization/repository'
47+
const result = parseGitHubUrl(url)
48+
expect(result).toBeNull()
49+
})
50+
51+
test('returns null for malformed URL', () => {
52+
const url = 'not-a-url'
53+
const result = parseGitHubUrl(url)
54+
expect(result).toBeNull()
55+
})
56+
57+
test('parses a GitHub URL with feature branch', () => {
58+
const url = 'https://github.com/organization/repository/tree/feat/something'
59+
const result = parseGitHubUrl(url)
60+
expect(result).toEqual({
61+
org: 'organization',
62+
repo: 'repository',
63+
branch: 'feat/something',
64+
path: '',
65+
})
66+
})
67+
68+
test('parses a GitHub URL with feature branch and path', () => {
69+
const url = 'https://github.com/organization/repository/tree/feat/something/components'
70+
const result = parseGitHubUrl(url)
71+
expect(result).toEqual({
72+
org: 'organization',
73+
repo: 'repository',
74+
branch: 'feat/something',
75+
path: 'components',
76+
})
77+
})
78+
})
79+
80+
describe('parseBitBucketUrl', () => {
81+
test('parses a valid BitBucket URL with workspace and repository', () => {
82+
const url = 'https://bitbucket.org/workspace/repository'
83+
const result = parseBitBucketUrl(url)
84+
expect(result).toEqual({
85+
org: 'workspace',
86+
repo: 'repository',
87+
branch: 'main',
88+
path: '',
89+
})
90+
})
91+
92+
test('parses a valid BitBucket URL with trailing slash', () => {
93+
const url = 'https://bitbucket.org/workspace/repository/'
94+
const result = parseBitBucketUrl(url)
95+
expect(result).toEqual({
96+
org: 'workspace',
97+
repo: 'repository',
98+
branch: 'main',
99+
path: '',
100+
})
101+
})
102+
103+
test('parses a valid BitBucket URL with additional path segments', () => {
104+
const url = 'https://bitbucket.org/workspace/repository/src/develop/components'
105+
const result = parseBitBucketUrl(url)
106+
expect(result).toEqual({
107+
org: 'workspace',
108+
repo: 'repository',
109+
branch: 'develop',
110+
path: 'components',
111+
})
112+
})
113+
114+
test('returns null for invalid BitBucket URL (missing repository)', () => {
115+
const url = 'https://bitbucket.org/workspace'
116+
const result = parseBitBucketUrl(url)
117+
expect(result).toBeNull()
118+
})
119+
120+
test('returns null for non-BitBucket URL', () => {
121+
const url = 'https://github.com/organization/repository'
122+
const result = parseBitBucketUrl(url)
123+
expect(result).toBeNull()
124+
})
125+
126+
test('returns null for malformed URL', () => {
127+
const url = 'not-a-url'
128+
const result = parseBitBucketUrl(url)
129+
expect(result).toBeNull()
130+
})
131+
132+
test('parses a BitBucket URL with feature branch', () => {
133+
const url = 'https://bitbucket.org/organization/repository/src/feat/something'
134+
const result = parseBitBucketUrl(url)
135+
expect(result).toEqual({
136+
org: 'organization',
137+
repo: 'repository',
138+
branch: 'feat/something',
139+
path: '',
140+
})
141+
})
142+
143+
test('parses a BitBucket URL with feature branch and path', () => {
144+
const url = 'https://bitbucket.org/organization/repository/src/feat/something/components'
145+
const result = parseBitBucketUrl(url)
146+
expect(result).toEqual({
147+
org: 'organization',
148+
repo: 'repository',
149+
branch: 'feat/something',
150+
path: 'components',
151+
})
152+
})
153+
})

0 commit comments

Comments
 (0)
Please sign in to comment.