From 3a238d138dcaadbf45e35fbe3072ffce868f56f1 Mon Sep 17 00:00:00 2001 From: Steve Alexander Date: Mon, 16 Dec 2024 08:32:33 -0800 Subject: [PATCH 01/11] yarn: add @coderabbitai/bitbucket --- packages/backend/package.json | 1 + yarn.lock | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/backend/package.json b/packages/backend/package.json index 2514cd85..3ec64eac 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -25,6 +25,7 @@ "@gitbeaker/rest": "^40.5.1", "@octokit/rest": "^21.0.2", "argparse": "^2.0.1", + "@coderabbitai/bitbucket": "^1.1.1", "cross-fetch": "^4.0.0", "dotenv": "^16.4.5", "gitea-js": "^1.22.0", diff --git a/yarn.lock b/yarn.lock index 3747f32e..bbbef7bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -339,6 +339,13 @@ style-mod "^4.1.0" w3c-keyname "^2.2.4" +"@coderabbitai/bitbucket@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@coderabbitai/bitbucket/-/bitbucket-1.1.1.tgz#4093593d38cd3dab4a6b2bb9cd30b0affe4c9a8e" + integrity sha512-ZEOTVjecyIdr9Gz0wlrMO79h48T8HbtNLCLlo3kVf7zG5YoP73x0r95MzP2VHCKmlzDIilcjHCE4oxgLF7OytA== + dependencies: + openapi-fetch "^0.12.2" + "@colors/colors@1.6.0", "@colors/colors@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" @@ -4718,6 +4725,18 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" +openapi-fetch@^0.12.2: + version "0.12.5" + resolved "https://registry.yarnpkg.com/openapi-fetch/-/openapi-fetch-0.12.5.tgz#b0cabd3fe2d423f44b83a0ce99a20e1aa4067287" + integrity sha512-FnAMWLt0MNL6ComcL4q/YbB1tUgyz5YnYtwA1+zlJ5xcucmK5RlWsgH1ynxmEeu8fGJkYjm8armU/HVpORc9lw== + dependencies: + openapi-typescript-helpers "^0.0.15" + +openapi-typescript-helpers@^0.0.15: + version "0.0.15" + resolved "https://registry.yarnpkg.com/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz#96ffa762a5e01ef66a661b163d5f1109ed1967ed" + integrity sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw== + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" From 8134f4f58e338a6913eae4665bf0df8d0d98c659 Mon Sep 17 00:00:00 2001 From: Steve Alexander Date: Mon, 16 Dec 2024 08:33:03 -0800 Subject: [PATCH 02/11] ignore: ignore vim swap files --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d62fb792..35527fef 100644 --- a/.gitignore +++ b/.gitignore @@ -163,4 +163,5 @@ dist .sourcebot /bin /config.json -.DS_Store \ No newline at end of file +.DS_Store +.*.sw* From a8d6f60678ddaac1ca453913832dae16b1ee800d Mon Sep 17 00:00:00 2001 From: Steve Alexander Date: Mon, 16 Dec 2024 08:33:24 -0800 Subject: [PATCH 03/11] editorconfig: add initial revision --- .editorconfig | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..850eb1b6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,py,ts}] +charset = utf-8 + +# Tab indentation (no size specified) +# 'go fmt' uses tabs +[{Makefile,*.mk,*.go}] +indent_style = tab + +# Indentation override for JS +[{*.js,*.ts,*.json}] +indent_style = space +indent_size = 4 From 260e114111b98c0baec5084bfb1ec189313de560 Mon Sep 17 00:00:00 2001 From: Steve Alexander Date: Mon, 16 Dec 2024 08:35:39 -0800 Subject: [PATCH 04/11] public: add bitbucket.svg - from: https://www.svgrepo.com/svg/349308/bitbucket - MIT license --- packages/web/public/bitbucket.svg | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/web/public/bitbucket.svg diff --git a/packages/web/public/bitbucket.svg b/packages/web/public/bitbucket.svg new file mode 100644 index 00000000..894ed83b --- /dev/null +++ b/packages/web/public/bitbucket.svg @@ -0,0 +1,7 @@ + + \ No newline at end of file From e76c224c0df7bb671bf85f328a7a65513f338c13 Mon Sep 17 00:00:00 2001 From: Steve Alexander Date: Mon, 16 Dec 2024 08:37:05 -0800 Subject: [PATCH 05/11] backend: add bitbucket client - cloud-only at present --- packages/backend/src/bitbucket.ts | 305 ++++++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 packages/backend/src/bitbucket.ts diff --git a/packages/backend/src/bitbucket.ts b/packages/backend/src/bitbucket.ts new file mode 100644 index 00000000..c6414b35 --- /dev/null +++ b/packages/backend/src/bitbucket.ts @@ -0,0 +1,305 @@ +import { createBitbucketCloudClient } from "@coderabbitai/bitbucket/cloud"; +import { toBase64 } from "@coderabbitai/bitbucket"; +import { SchemaBranch, SchemaProject, SchemaRepository, SchemaTag, SchemaWorkspace } from "@coderabbitai/bitbucket/cloud/openapi"; +import { BitbucketConfig } from "./schemas/v2.js"; +import { excludeArchivedRepos, excludeForkedRepos, excludeReposByName, excludeReposByTopic, getTokenFromConfig, includeReposByTopic, marshalBool, measure } from "./utils.js"; +import { createLogger } from "./logger.js"; +import { AppContext, GitRepository } from "./types.js"; +import path from 'path'; +import micromatch from "micromatch"; + +const logger = createLogger('Bitbucket'); +const BITBUCKET_CLOUD_HOSTNAME = 'bitbucket.org'; // note, not 'api.bitbucket.org' +const BITBUCKET_CLOUD_API = 'https://api.bitbucket.org/2.0'; + +interface Repo { + workspace: string; + project: string; + name: string; + isFork: boolean; + isArchived: boolean; + isPublic: boolean; + user: string; + token: string; + stars: number; + forks: number; +}; + +export const getBitbucketReposFromConfig = async (config: BitbucketConfig, ctx: AppContext) => { + const token = config.token ? getTokenFromConfig(config.token, ctx) : undefined; + const user = config.user ? config.user : undefined; + const basic = toBase64(user + ':' + token); + const url = config.url ? config.url : BITBUCKET_CLOUD_API; + + logger.debug('BASE URL ' + url); + + const clientOptions = { + baseUrl: url, + headers: { + Accept: 'application/json', + Authorization: `Basic ${basic}`, + }, + }; + + const client = createBitbucketCloudClient(clientOptions); + + const hostname = config.url ? new URL(config.url).hostname : BITBUCKET_CLOUD_HOSTNAME; + + let foundRepos: Repo[] = []; + + const workspaces = await getWorkspaces(client, url); + for (const workspace of workspaces) { + const projects = await getProjects(client, url, workspace); + + for (const project of projects) { + const repos = await getRepos(client, url, workspace, project); + + for (const repo of repos) { + logger.debug('REPO ' + JSON.stringify(repo)); + if (repo.name == null) { + continue; + } + + var stars: number = 0; + var forks: number = 0; + + // counting these can cause rate limiting + if (config.countMisc) { + // not sure the suggested mapping of watchers to stars makes much sense + stars = await countWatchers(client, url, `${workspace}/${repo.name}`); + forks = await countForks(client, url, `${workspace}/${repo.name}`); + } + + var repository: Repo = { + forks: forks, + isArchived: false, + isFork: repo.parent != null, + isPublic: !repo.is_private, + name: repo.name ? repo.name : 'unknown', + project: project, + stars: stars, + token: token ? token : 'unknown', + user: user ? user : 'unknown', + workspace: workspace, + }; + + foundRepos.push(repository); + } + } + }; + + + let repos: GitRepository[] = foundRepos + .map((project) => { + const repoId = `https://${hostname}/${project.workspace}/${project.name}`; + const repoPath = path.resolve(path.join(ctx.reposPath, `${repoId}.git`)) + + const cloneUrl = new URL(repoId); + if (token) { + cloneUrl.username = project.user; + cloneUrl.password = project.token; + } + const webUrl = new URL(repoId); // we don't want user & token here + + return { + vcs: 'git', + codeHost: 'bitbucket', + name: `${project.workspace}/${project.name}`, + id: repoId, + cloneUrl: cloneUrl.toString(), + path: repoPath, + isStale: false, + isFork: project.isFork, + isArchived: project.isArchived, + gitConfigMetadata: { + // + // using 'bitbucket-server' lets 'zoekt' generate the correct commit + // links -- /commits, not /commit + // + 'zoekt.web-url-type': 'bitbucket-server', + 'zoekt.web-url': webUrl.toString(), + 'zoekt.name': repoId, + 'zoekt.bitbucket-stars': project.stars?.toString() ?? '0', + 'zoekt.bitbucket-forks': project.forks?.toString() ?? '0', + 'zoekt.archived': marshalBool(project.isArchived), + 'zoekt.fork': marshalBool(project.isFork), + 'zoekt.public': marshalBool(project.isPublic), + }, + branches: [], + tags: [], + } satisfies GitRepository; + }); + + if (config.exclude) { + if (!!config.exclude.forks) { + repos = excludeForkedRepos(repos, logger); + } + + if (config.exclude.workspaces) { + repos = excludeReposByName(repos, config.exclude.workspaces, logger); + } + + if (config.exclude.projects) { + repos = excludeReposByName(repos, config.exclude.projects, logger); + } + } + + logger.debug(`Found ${repos.length} total repositories.`); + + if (config.revisions) { + if (config.revisions.branches) { + const branchGlobs = config.revisions.branches; + repos = await Promise.all(repos.map(async (repo) => { + logger.debug(`Fetching branches for repo ${repo.name}...`); + let branches = await getBranches(client, url, repo.name); + logger.debug(`Found ${branches.length} branches in repo ${repo.name}`); + + branches = micromatch.match(branches, branchGlobs); + + return { + ...repo, + branches, + }; + })); + } + + if (config.revisions.tags) { + const tagGlobs = config.revisions.tags; + repos = await Promise.all(repos.map(async (repo) => { + logger.debug(`Fetching tags for repo ${repo.name}...`); + let tags = await getTags(client, url, repo.name); + logger.debug(`Found ${tags.length} tags in repo ${repo.name}`); + + tags = micromatch.match(tags, tagGlobs); + + return { + ...repo, + tags, + }; + })); + } + } + + return repos; +} + +async function getWorkspaces(client: any, baseUrl: string): Promise { + var info: object[] = await getPaginatedResult(client, baseUrl, '/workspaces'); + var results: string[] = []; + + for (var i = 0; i < info.length; i++) { + const workspace = info[i]; + // we return 'slug' here, since sometimes 'name' doesn't match; no idea why + if (workspace.slug != null) { + results.push(workspace.slug); + } + } + + logger.info(`FOUND ${results.length} WORKSPACES`); + return results; +} + +async function getPaginatedResult(client: any, baseUrl: string, path: string): Promise { + var results: object[] = []; + + while (true) { + logger.debug('URL ' + path); + const page = await client.GET(path); + + logger.debug('PAGE ' + JSON.stringify(page)); + if (page.error != null) { + // rate limit? + await delay(2000); + continue; + } + + if (page.data == null || page.data.values == null || page.data.values.length == 0) { + break; + } + + for (var i = 0; i < page.data.values.length; i++) { + results.push(page.data.values[i]); + } + + if (page.data?.next == null) { + break; + } + + path = page.data.next.replace(baseUrl, ''); + } + + return results; +} + +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function getProjects(client: any, baseUrl: string, workspace: string): Promise { + var info: object[] = await getPaginatedResult(client, baseUrl, '/workspaces/' + workspace + '/projects'); + var results: string[] = []; + + for (var i = 0; i < info.length; i++) { + const proj = info[i]; + logger.debug('PROJ ' + JSON.stringify(proj)); + if (proj.key != null) { + results.push(proj.key); + } + } + + logger.info(` FOUND ${results.length} PROJECTS IN ${workspace}`); + return results; +} + +async function getRepos(client: any, baseUrl: string, workspace: string, project: string): Promise { + var info: object[] = await getPaginatedResult(client, baseUrl, '/repositories/' + workspace + '?q=project.key="' + project + '"'); + var results: SchemaRepository[] = []; + + for (var i = 0; i < info.length; i++) { + const repo = info[i]; + results.push(repo); + } + + logger.info(` FOUND ${results.length} REPOS IN ${workspace}/${project}`); + return results; +} + +async function getBranches(client: any, baseUrl: string, repo: string): Promise { + var info: object[] = await getPaginatedResult(client, baseUrl, '/repositories/' + repo + '/refs/branches'); + var results: string[] = []; + + for (var i = 0; i < info.length; i++) { + const branch = info[i]; + results.push(branch.name ? branch.name : 'unknown'); + } + + logger.info(` FOUND ${results.length} BRANCHES IN ${repo}`); + return results; +} + +async function getTags(client: any, baseUrl: string, repo: string): Promise { + var info: object[] = await getPaginatedResult(client, baseUrl, '/repositories/' + repo + '/refs/tags'); + var results: string[] = []; + + for (var i = 0; i < info.length; i++) { + const tag = info[i]; + results.push(tag.name ? tag.name : 'unknown'); + } + + logger.info(` FOUND ${results.length} TAGS IN ${repo}`); + return results; +} + +async function countForks(client: any, baseUrl: string, repo: string): Promise { + var info: object[] = await getPaginatedResult(client, baseUrl, '/repositories/' + repo + '/forks'); + + logger.info(` FOUND ${info.length} FORKS FOR ${repo}`); + return info.length; +} + +async function countWatchers(client: any, baseUrl: string, repo: string): Promise { + var info: object[] = await getPaginatedResult(client, baseUrl, '/repositories/' + repo + '/watchers'); + + logger.info(` FOUND ${info.length} WATCHERS FOR ${repo}`); + return info.length; +} From 447fe6ace597a45f213de981e566b00e04604abb Mon Sep 17 00:00:00 2001 From: Steve Alexander Date: Mon, 16 Dec 2024 09:02:10 -0800 Subject: [PATCH 06/11] schemas: add bitbucket support --- packages/backend/src/schemas/v2.ts | 58 +++++++++++++++- schemas/v2/index.json | 105 +++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/schemas/v2.ts b/packages/backend/src/schemas/v2.ts index 67897cd8..9fe94beb 100644 --- a/packages/backend/src/schemas/v2.ts +++ b/packages/backend/src/schemas/v2.ts @@ -1,6 +1,6 @@ // THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! -export type Repos = GitHubConfig | GitLabConfig | GiteaConfig | GerritConfig | LocalConfig; +export type Repos = GitHubConfig | GitLabConfig | GiteaConfig | GerritConfig | LocalConfig | BitbucketConfig; /** * A Sourcebot configuration file outlines which repositories Sourcebot should sync and index. @@ -268,3 +268,59 @@ export interface LocalConfig { paths?: string[]; }; } +export interface BitbucketConfig { + /** + * Bitbucket configuration + */ + type: "bitbucket"; + /** + * Count watchers and forks + */ + countMisc?: boolean; + /** + * Bitbucket URL + */ + url?: string; + /** + * server type + */ + serverType?: "cloud" | "server"; + exclude?: { + /** + * Exclude archived repositories from syncing. + */ + archived?: boolean; + /** + * Exclude forked repositories from syncing. + */ + forks?: boolean; + /** + * List of specific workspaces to exclude from syncing. + */ + workspaces?: string[]; + /** + * List of specific projects to exclude from syncing. + */ + projects?: string[]; + /** + * List of specific repos to exclude from syncing. + */ + repos?: string[]; + }; + /** + * A bitbucket API key. + */ + token: + | string + | { + /** + * The name of the environment variable that contains the token. + */ + env: string; + }; + /** + * User name for authentication + */ + user: string; + revisions?: GitRevisions; +} diff --git a/schemas/v2/index.json b/schemas/v2/index.json index ffdcff56..cdc38db9 100644 --- a/schemas/v2/index.json +++ b/schemas/v2/index.json @@ -516,6 +516,108 @@ ], "additionalProperties": false }, + "BitbucketConfig": { + "type": "object", + "properties": { + "type": { + "const": "bitbucket", + "description": "Bitbucket configuration" + }, + "countMisc": { + "type": "boolean", + "default": false, + "description": "Count watchers and forks" + }, + "url": { + "type": "string", + "format": "url", + "default": "https://api.bitbucket.org/2.0", + "description": "Bitbucket URL" + }, + "serverType": { + "type": "string", + "enum": ["cloud", "server"], + "default": "cloud", + "description": "server type" + }, + "exclude": { + "type": "object", + "properties": { + "archived": { + "type": "boolean", + "default": false, + "description": "Exclude archived repositories from syncing." + }, + "forks": { + "type": "boolean", + "default": false, + "description": "Exclude forked repositories from syncing." + }, + "workspaces": { + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + [ + "workspace1/repo1", + "workspace2/**" + ] + ], + "description": "List of specific workspaces to exclude from syncing." + }, + "projects": { + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + [ + "project1/repo1", + "project2/**" + ] + ], + "description": "List of specific projects to exclude from syncing." + }, + "repos": { + "type": "array", + "items": { + "type": "string" + }, + "examples": [ + [ + "workspace1/repo1", + "workspace2/**" + ] + ], + "description": "List of specific repos to exclude from syncing." + } + }, + "additionalProperties": false + }, + "token": { + "$ref": "#/definitions/Token", + "description": "A bitbucket API key.", + "examples": [ + "secret-token", + { "env": "ENV_VAR_CONTAINING_TOKEN" } + ] + }, + "user": { + "type": "string", + "description": "User name for authentication" + }, + "revisions": { + "$ref": "#/definitions/GitRevisions" + } + }, + "required": [ + "token", + "type", + "user" + ], + "additionalProperties": false + }, "Repos": { "anyOf": [ { @@ -532,6 +634,9 @@ }, { "$ref": "#/definitions/LocalConfig" + }, + { + "$ref": "#/definitions/BitbucketConfig" } ] }, From 75563c88684fd688b48401df519a6b0cf661ee71 Mon Sep 17 00:00:00 2001 From: Steve Alexander Date: Mon, 16 Dec 2024 09:02:43 -0800 Subject: [PATCH 07/11] main: add bitbucket support --- packages/backend/src/main.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/backend/src/main.ts b/packages/backend/src/main.ts index c5aa2b66..d80d775f 100644 --- a/packages/backend/src/main.ts +++ b/packages/backend/src/main.ts @@ -5,6 +5,7 @@ import { getGitHubReposFromConfig } from "./github.js"; import { getGitLabReposFromConfig } from "./gitlab.js"; import { getGiteaReposFromConfig } from "./gitea.js"; import { getGerritReposFromConfig } from "./gerrit.js"; +import { getBitbucketReposFromConfig } from "./bitbucket.js"; import { AppContext, LocalRepository, GitRepository, Repository, Settings } from "./types.js"; import { cloneRepository, fetchRepository } from "./git.js"; import { createLogger } from "./logger.js"; @@ -245,6 +246,11 @@ const syncConfig = async (configPath: string, db: Database, signal: AbortSignal, configRepos.push(repo); break; } + case 'bitbucket': { + const bitbucketRepos = await getBitbucketReposFromConfig(repoConfig, ctx); + configRepos.push(...bitbucketRepos); + break; + } } } From c5fdc8837d1c08e87a6486b5e3bc7b3987940e16 Mon Sep 17 00:00:00 2001 From: Steve Alexander Date: Mon, 16 Dec 2024 09:03:00 -0800 Subject: [PATCH 08/11] web: add bitbucket support --- packages/web/src/lib/utils.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/web/src/lib/utils.ts b/packages/web/src/lib/utils.ts index b63b784a..785a2ac0 100644 --- a/packages/web/src/lib/utils.ts +++ b/packages/web/src/lib/utils.ts @@ -4,6 +4,7 @@ import githubLogo from "../../public/github.svg"; import gitlabLogo from "../../public/gitlab.svg"; import giteaLogo from "../../public/gitea.svg"; import gerritLogo from "../../public/gerrit.svg"; +import bitbucketLogo from "../../public/bitbucket.svg"; import { ServiceError } from "./serviceError"; import { Repository } from "./types"; @@ -32,7 +33,7 @@ export const createPathWithQueryParams = (path: string, ...queryParams: [string, } type CodeHostInfo = { - type: "github" | "gitlab" | "gitea" | "gerrit"; + type: "github" | "gitlab" | "gitea" | "gerrit" | "bitbucket"; displayName: string; costHostName: string; repoLink: string; @@ -86,6 +87,14 @@ export const getRepoCodeHostInfo = (repo?: Repository): CodeHostInfo | undefined repoLink: repo.URL, icon: gerritLogo, } + case 'bitbucket': + return { + type: "bitbucket", + displayName: displayName, + costHostName: "Bitbucket", + repoLink: repo.URL, + icon: bitbucketLogo, + } } } From 57d55cc8516e55924c7657afebfdaae43ceb5632 Mon Sep 17 00:00:00 2001 From: Steve Alexander Date: Mon, 16 Dec 2024 09:03:16 -0800 Subject: [PATCH 09/11] configs: add sample config for bitbucket cloud --- configs/bitbucket-cloud.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 configs/bitbucket-cloud.json diff --git a/configs/bitbucket-cloud.json b/configs/bitbucket-cloud.json new file mode 100644 index 00000000..27bcd677 --- /dev/null +++ b/configs/bitbucket-cloud.json @@ -0,0 +1,14 @@ +{ + "$schema": "../schemas/v2/index.json", + // Note: to include private repositories, you must provide an authentication token. + // See: configs/auth.json for a example. + "repos": [ + // From bitbucket, include: + // - all repos in workspaces owned by user + { + "type": "bitbucket", + "token": "BB_TOKEN", + "user": "user1" + } + ] +} From 417633d32f707b5ba16c9ae4783fccb9cddbae37 Mon Sep 17 00:00:00 2001 From: Steve Alexander Date: Wed, 18 Dec 2024 13:37:52 -0800 Subject: [PATCH 10/11] bitbucket: re-factor for cloud and server support - this is pretty ugly - server support has only been tested with a prism mock - watcher counting is not implemented yet --- configs/bitbucket-server.json | 21 ++ packages/backend/src/bitbucket.ts | 510 +++++++++++++++++++++++++----- 2 files changed, 445 insertions(+), 86 deletions(-) create mode 100644 configs/bitbucket-server.json diff --git a/configs/bitbucket-server.json b/configs/bitbucket-server.json new file mode 100644 index 00000000..a43bc600 --- /dev/null +++ b/configs/bitbucket-server.json @@ -0,0 +1,21 @@ +{ + "$schema": "../schemas/v2/index.json", + // Note: to include private repositories, you must provide an authentication token. + // See: configs/auth.json for a example. + "repos": [ + // From bitbucket, include: + // - all repos in workspaces owned by user + { + "type": "bitbucket", + "url": "http://localhost:4010", + "token": "", + "serverType": "server", + "revisions": { + "branches": [ + "main", + "master" + ] + } + } + ] +} diff --git a/packages/backend/src/bitbucket.ts b/packages/backend/src/bitbucket.ts index c6414b35..c18e9132 100644 --- a/packages/backend/src/bitbucket.ts +++ b/packages/backend/src/bitbucket.ts @@ -1,64 +1,106 @@ import { createBitbucketCloudClient } from "@coderabbitai/bitbucket/cloud"; +import { createBitbucketServerClient } from "@coderabbitai/bitbucket/server"; +import type { ClientOptions } from "openapi-fetch"; import { toBase64 } from "@coderabbitai/bitbucket"; -import { SchemaBranch, SchemaProject, SchemaRepository, SchemaTag, SchemaWorkspace } from "@coderabbitai/bitbucket/cloud/openapi"; +import { + SchemaBranch as CloudBranch, + SchemaProject as CloudProject, + SchemaRepository as CloudRepository, + SchemaTag as CloudTag, + SchemaWorkspace as CloudWorkspace +} from "@coderabbitai/bitbucket/cloud/openapi"; +import { + SchemaRestBranch as ServerBranch, + SchemaProject as ServerProject, + SchemaRepository as ServerRepository, + SchemaRestTag as ServerTag +} from "@coderabbitai/bitbucket/server/openapi"; import { BitbucketConfig } from "./schemas/v2.js"; -import { excludeArchivedRepos, excludeForkedRepos, excludeReposByName, excludeReposByTopic, getTokenFromConfig, includeReposByTopic, marshalBool, measure } from "./utils.js"; +import { + excludeArchivedRepos, + excludeForkedRepos, + excludeReposByName, + getTokenFromConfig, + marshalBool, + measure +} from "./utils.js"; import { createLogger } from "./logger.js"; import { AppContext, GitRepository } from "./types.js"; import path from 'path'; import micromatch from "micromatch"; const logger = createLogger('Bitbucket'); -const BITBUCKET_CLOUD_HOSTNAME = 'bitbucket.org'; // note, not 'api.bitbucket.org' +const BITBUCKET_CLOUD_GIT = 'https://bitbucket.org'; const BITBUCKET_CLOUD_API = 'https://api.bitbucket.org/2.0'; +const SERVER_API_PATH = 'rest/api/latest'; +const CLOUD = 'cloud'; +const SERVER = 'server'; +const CONTENT_TYPE = 'application/json'; +const UNKNOWN = 'unknown'; +const RETRY_TIME = 2000; // ms interface Repo { + // these two are set when the Repo object is initialized in newRepo() workspace: string; project: string; + // these four are set by the per-type mapping functions name: string; - isFork: boolean; isArchived: boolean; + isFork: boolean; isPublic: boolean; + // these four are set in common processing user: string; token: string; stars: number; forks: number; }; -export const getBitbucketReposFromConfig = async (config: BitbucketConfig, ctx: AppContext) => { - const token = config.token ? getTokenFromConfig(config.token, ctx) : undefined; - const user = config.user ? config.user : undefined; - const basic = toBase64(user + ':' + token); - const url = config.url ? config.url : BITBUCKET_CLOUD_API; - - logger.debug('BASE URL ' + url); +interface BitbucketClient { + serverType: string; + token: string; + user: string | undefined; // not used for server auth + apiClient: any; // can't figure out a better way to declare this + baseUrl: string; // API URL + gitUrl: string; // git repo URL + // methods + getPaginated: (client: BitbucketClient, path: string) => Promise; + getWorkspaces: (client: BitbucketClient) => Promise; + getProjects: (client: BitbucketClient, workspace: string) => Promise; + getRepos: (client: BitbucketClient, workspace: string, project: string) => Promise; + countForks: (client: BitbucketClient, repo: Repo) => Promise; + countWatchers: (client: BitbucketClient, repo: Repo) => Promise; + getBranches: (client: BitbucketClient, repo: string) => Promise; + getTags: (client: BitbucketClient, repo: string) => Promise; +} - const clientOptions = { - baseUrl: url, - headers: { - Accept: 'application/json', - Authorization: `Basic ${basic}`, - }, - }; +export const getBitbucketReposFromConfig = async (config: BitbucketConfig, ctx: AppContext) => { + const serverType = config.serverType ? config.serverType : CLOUD; + var client: BitbucketClient; - const client = createBitbucketCloudClient(clientOptions); + if (serverType === SERVER) { + client = serverClient(config, ctx); + } else { + client = cloudClient(config, ctx); + } - const hostname = config.url ? new URL(config.url).hostname : BITBUCKET_CLOUD_HOSTNAME; + logger.info(`Base URL: ${client.baseUrl}`); + logger.info(`Git URL: ${client.gitUrl}`); let foundRepos: Repo[] = []; - const workspaces = await getWorkspaces(client, url); + const { durationMs: workspaceMs, data: workspaces } = await measure(() => client.getWorkspaces(client)); + logger.info(`Found ${workspaces.length} workspaces in ${workspaceMs} ms`); + for (const workspace of workspaces) { - const projects = await getProjects(client, url, workspace); + const { durationMs: projectMs, data: projects } = await measure(() => client.getProjects(client, workspace)); + logger.info(` Found ${projects.length} projects in ${workspace} in ${projectMs} ms`); for (const project of projects) { - const repos = await getRepos(client, url, workspace, project); + const { durationMs, data: repos } = await measure(() => client.getRepos(client, workspace, project)); + logger.info(` Found ${repos.length} repos in ${project} in ${durationMs} ms`); for (const repo of repos) { logger.debug('REPO ' + JSON.stringify(repo)); - if (repo.name == null) { - continue; - } var stars: number = 0; var forks: number = 0; @@ -66,40 +108,37 @@ export const getBitbucketReposFromConfig = async (config: BitbucketConfig, ctx: // counting these can cause rate limiting if (config.countMisc) { // not sure the suggested mapping of watchers to stars makes much sense - stars = await countWatchers(client, url, `${workspace}/${repo.name}`); - forks = await countForks(client, url, `${workspace}/${repo.name}`); + const { durationMs: starMs, data: foundStars } = await measure(() => client.countWatchers(client, repo)); + stars = foundStars + logger.info(` Found ${stars} watchers for ${repo.name} in ${starMs} ms`); + const { durationMs: forkMs, data: foundForks } = await measure(() => client.countForks(client, repo)); + forks = foundForks + logger.info(` Found ${forks} forks for ${repo.name} in ${forkMs} ms`); } - var repository: Repo = { - forks: forks, - isArchived: false, - isFork: repo.parent != null, - isPublic: !repo.is_private, - name: repo.name ? repo.name : 'unknown', - project: project, - stars: stars, - token: token ? token : 'unknown', - user: user ? user : 'unknown', - workspace: workspace, - }; + repo.forks = forks; + repo.stars = stars; + repo.token = client.token ? client.token : UNKNOWN; + repo.user = client.user ? client.user : UNKNOWN; - foundRepos.push(repository); + foundRepos.push(repo); } } }; - let repos: GitRepository[] = foundRepos .map((project) => { - const repoId = `https://${hostname}/${project.workspace}/${project.name}`; + const repoId = `${client.gitUrl}/${project.workspace}/${project.name}`; const repoPath = path.resolve(path.join(ctx.reposPath, `${repoId}.git`)) + const webUrl = new URL(repoId); // we don't want user & token here const cloneUrl = new URL(repoId); - if (token) { + if (project.user) { cloneUrl.username = project.user; + } + if (project.token) { cloneUrl.password = project.token; } - const webUrl = new URL(repoId); // we don't want user & token here return { vcs: 'git', @@ -131,6 +170,10 @@ export const getBitbucketReposFromConfig = async (config: BitbucketConfig, ctx: }); if (config.exclude) { + if (!!config.exclude.archived) { + repos = excludeArchivedRepos(repos, logger); + } + if (!!config.exclude.forks) { repos = excludeForkedRepos(repos, logger); } @@ -150,9 +193,9 @@ export const getBitbucketReposFromConfig = async (config: BitbucketConfig, ctx: if (config.revisions.branches) { const branchGlobs = config.revisions.branches; repos = await Promise.all(repos.map(async (repo) => { - logger.debug(`Fetching branches for repo ${repo.name}...`); - let branches = await getBranches(client, url, repo.name); - logger.debug(`Found ${branches.length} branches in repo ${repo.name}`); + logger.debug(` Fetching branches for repo ${repo.name}...`); + let { durationMs, data: branches } = await measure(() => client.getBranches(client, repo.name)); + logger.info(` Found ${branches.length} branches in repo ${repo.name} in ${durationMs} ms`); branches = micromatch.match(branches, branchGlobs); @@ -166,9 +209,9 @@ export const getBitbucketReposFromConfig = async (config: BitbucketConfig, ctx: if (config.revisions.tags) { const tagGlobs = config.revisions.tags; repos = await Promise.all(repos.map(async (repo) => { - logger.debug(`Fetching tags for repo ${repo.name}...`); - let tags = await getTags(client, url, repo.name); - logger.debug(`Found ${tags.length} tags in repo ${repo.name}`); + logger.debug(` Fetching tags for repo ${repo.name}...`); + let { durationMs, data: tags } = await measure(() => client.getTags(client, repo.name)); + logger.info(` Found ${tags.length} tags in repo ${repo.name} in ${durationMs} ms`); tags = micromatch.match(tags, tagGlobs); @@ -183,33 +226,77 @@ export const getBitbucketReposFromConfig = async (config: BitbucketConfig, ctx: return repos; } -async function getWorkspaces(client: any, baseUrl: string): Promise { - var info: object[] = await getPaginatedResult(client, baseUrl, '/workspaces'); +// +// cloud support +// +function cloudClient(config: BitbucketConfig, ctx: AppContext): BitbucketClient { + const token = config.token ? getTokenFromConfig(config.token, ctx) : undefined; + const user = config.user ? config.user : undefined; + const basic = toBase64(`${user}:${token}`); + + const clientOptions: ClientOptions = { + baseUrl: BITBUCKET_CLOUD_API, + headers: { + Accept: CONTENT_TYPE, + Authorization: `Basic ${basic}`, + }, + }; + + const apiClient = createBitbucketCloudClient(clientOptions); + var client: BitbucketClient = { + serverType: CLOUD, + user: user, + token: token ? token : 'junk', + apiClient: apiClient, + baseUrl: BITBUCKET_CLOUD_API, + gitUrl: BITBUCKET_CLOUD_GIT, + getPaginated: cloudGetPaginatedResult, + getWorkspaces: cloudGetWorkspaces, + getProjects: cloudGetProjects, + getRepos: cloudGetRepos, + countForks: cloudCountForks, + countWatchers: cloudCountWatchers, + getBranches: cloudGetBranches, + getTags: cloudGetTags, + } + + return client; +} + +async function cloudGetWorkspaces(client: BitbucketClient): Promise { + var info: object[]; var results: string[] = []; + try { + info = await client.getPaginated(client, '/workspaces'); + } catch (e) { + logger.error(`Failed to fetch workspaces.`, e); + return results; + } + for (var i = 0; i < info.length; i++) { - const workspace = info[i]; + const workspace = info[i]; // we return 'slug' here, since sometimes 'name' doesn't match; no idea why if (workspace.slug != null) { results.push(workspace.slug); } } - logger.info(`FOUND ${results.length} WORKSPACES`); + logger.debug(`FOUND ${results.length} WORKSPACES`); return results; } -async function getPaginatedResult(client: any, baseUrl: string, path: string): Promise { +async function cloudGetPaginatedResult(client: BitbucketClient, path: string): Promise { var results: object[] = []; while (true) { - logger.debug('URL ' + path); - const page = await client.GET(path); + logger.debug(`URL ${path}`); + const page = await client.apiClient.GET(path); logger.debug('PAGE ' + JSON.stringify(page)); if (page.error != null) { // rate limit? - await delay(2000); + await delay(RETRY_TIME); continue; } @@ -225,7 +312,7 @@ async function getPaginatedResult(client: any, baseUrl: string, path: string): P break; } - path = page.data.next.replace(baseUrl, ''); + path = page.data.next.replace(client.baseUrl, ''); } return results; @@ -235,71 +322,322 @@ function delay(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } -async function getProjects(client: any, baseUrl: string, workspace: string): Promise { - var info: object[] = await getPaginatedResult(client, baseUrl, '/workspaces/' + workspace + '/projects'); +async function cloudGetProjects(client: BitbucketClient, workspace: string): Promise { + var info: object[]; var results: string[] = []; + try { + info = await client.getPaginated(client, `/workspaces/${workspace}/projects`); + } catch (e) { + logger.error(`Failed to fetch projects for workspace ${workspace}.`, e); + return results; + } + for (var i = 0; i < info.length; i++) { - const proj = info[i]; + const proj = info[i]; logger.debug('PROJ ' + JSON.stringify(proj)); if (proj.key != null) { results.push(proj.key); } } - logger.info(` FOUND ${results.length} PROJECTS IN ${workspace}`); + logger.debug(` FOUND ${results.length} PROJECTS IN ${workspace}`); return results; } -async function getRepos(client: any, baseUrl: string, workspace: string, project: string): Promise { - var info: object[] = await getPaginatedResult(client, baseUrl, '/repositories/' + workspace + '?q=project.key="' + project + '"'); - var results: SchemaRepository[] = []; +async function cloudGetRepos(client: BitbucketClient, workspace: string, project: string): Promise { + var info: object[]; + var results: Repo[] = []; + + try { + info = await client.getPaginated(client, `/repositories/${workspace}?q=project.key="${project}"`); + } catch (e) { + logger.error(`Failed to fetch repos for workspace ${workspace}/${project}.`, e); + return results; + } for (var i = 0; i < info.length; i++) { - const repo = info[i]; - results.push(repo); + const repo = info[i]; + results.push(mapCloudRepo(repo, workspace, project)); } - logger.info(` FOUND ${results.length} REPOS IN ${workspace}/${project}`); + logger.debug(` FOUND ${results.length} REPOS IN ${workspace}/${project}`); return results; } -async function getBranches(client: any, baseUrl: string, repo: string): Promise { - var info: object[] = await getPaginatedResult(client, baseUrl, '/repositories/' + repo + '/refs/branches'); +function mapCloudRepo(orig: CloudRepository, workspace: string, project: string): Repo { + var repo: Repo = newRepo(workspace, project); + + repo.name = orig.name ? orig.name : UNKNOWN; + repo.isArchived = false; // no archiving in cloud + repo.isFork = orig.parent != null; + repo.isPublic = !orig.is_private; + + return repo; +} + +function newRepo(workspace: string, project: string): Repo { + var repo: Repo = { + name: UNKNOWN, + isArchived: false, + isFork: false, + isPublic: false, + project: project, + workspace: workspace, + user: UNKNOWN, + token: UNKNOWN, + stars: 0, + forks: 0, + }; + + return repo; +} + +async function cloudGetBranches(client: BitbucketClient, repo: string): Promise { + var info: object[]; var results: string[] = []; + try { + info = await client.getPaginated(client, `/repositories/${repo}/refs/branches`); + } catch (e) { + logger.error(`Failed to fetch branches for repo ${repo}.`, e); + return results; + } + for (var i = 0; i < info.length; i++) { - const branch = info[i]; - results.push(branch.name ? branch.name : 'unknown'); + const branch = info[i]; + results.push(branch.name ? branch.name : UNKNOWN); } - logger.info(` FOUND ${results.length} BRANCHES IN ${repo}`); + logger.debug(` FOUND ${results.length} BRANCHES IN ${repo}`); return results; } -async function getTags(client: any, baseUrl: string, repo: string): Promise { - var info: object[] = await getPaginatedResult(client, baseUrl, '/repositories/' + repo + '/refs/tags'); +async function cloudGetTags(client: BitbucketClient, repo: string): Promise { + var info: object[]; var results: string[] = []; + try { + info = await client.getPaginated(client, `/repositories/${repo}/refs/tags`); + } catch (e) { + logger.error(`Failed to fetch tags for repo ${repo}.`, e); + return results; + } + for (var i = 0; i < info.length; i++) { - const tag = info[i]; - results.push(tag.name ? tag.name : 'unknown'); + const tag = info[i]; + results.push(tag.name ? tag.name : UNKNOWN); } - logger.info(` FOUND ${results.length} TAGS IN ${repo}`); + logger.debug(` FOUND ${results.length} TAGS IN ${repo}`); return results; } -async function countForks(client: any, baseUrl: string, repo: string): Promise { - var info: object[] = await getPaginatedResult(client, baseUrl, '/repositories/' + repo + '/forks'); +async function cloudCountForks(client: BitbucketClient, repo: Repo): Promise { + var name: string = `${repo.workspace}/${repo.name}` + var info: object[]; + + try { + info = await client.getPaginated(client, `/repositories/${name}/forks`); + } catch (e) { + logger.error(`Failed to count forks for repo ${name}.`, e); + return 0; + } - logger.info(` FOUND ${info.length} FORKS FOR ${repo}`); + logger.debug(` FOUND ${info.length} FORKS FOR ${name}`); return info.length; } -async function countWatchers(client: any, baseUrl: string, repo: string): Promise { - var info: object[] = await getPaginatedResult(client, baseUrl, '/repositories/' + repo + '/watchers'); +async function cloudCountWatchers(client: BitbucketClient, repo: Repo): Promise { + var name: string = `${repo.workspace}/${repo.name}` + var info: object[]; + + try { + info = await client.getPaginated(client, `/repositories/${name}/watchers`); + } catch (e) { + logger.error(`Failed to count watchers for repo ${name}.`, e); + return 0; + } - logger.info(` FOUND ${info.length} WATCHERS FOR ${repo}`); + logger.debug(` FOUND ${info.length} WATCHERS FOR ${name}`); return info.length; } + +// +// server support +// +function serverClient(config: BitbucketConfig, ctx: AppContext): BitbucketClient { + const token = config.token ? getTokenFromConfig(config.token, ctx) : undefined; + const url = config.url ? config.url : 'https://junk'; + + const clientOptions: ClientOptions = { + baseUrl: `${url}/${SERVER_API_PATH}`, + headers: { + Accept: CONTENT_TYPE, + Authorization: `Bearer ${token}`, + }, + }; + + const apiClient = createBitbucketServerClient(clientOptions); + var client: BitbucketClient = { + serverType: SERVER, + user: '', + token: token ? token : 'junk', + apiClient: apiClient, + baseUrl: `${url}/${SERVER_API_PATH}`, + gitUrl: `${url}`, + getPaginated: serverGetPaginatedResult, + getWorkspaces: serverGetWorkspaces, + getProjects: serverGetProjects, + getRepos: serverGetRepos, + countForks: serverCountForks, + countWatchers: serverCountWatchers, + getBranches: serverGetBranches, + getTags: serverGetTags, + }; + + return client; +} + +// no workspace support in server as far as I can see +async function serverGetWorkspaces(_: BitbucketClient): Promise { + return ['default']; +} + +async function serverGetPaginatedResult(client: BitbucketClient, path: string): Promise { + var results: object[] = []; + const origPath: string = path; + + while (true) { + logger.debug(`URL ${path}`); + const page = await client.apiClient.GET(path); + + logger.debug('PAGE ' + JSON.stringify(page)); + if (page.error != null) { + // rate limit? + await delay(RETRY_TIME); + continue; + } + + if (page.data == null || page.data.values == null || page.data.values.length === 0) { + break; + } + + for (var i = 0; i < page.data.values.length; i++) { + results.push(page.data.values[i]); + } + + // mock testing with prism can return a negative result + if (page.data?.nextPageStart == null || page.data?.nextPageStart <= 0) { + break; + } + + path = origPath + `?start=${page.data.nextPageStart}`; + } + + return results; +} + +async function serverGetProjects(client: BitbucketClient, _: string): Promise { + var info: object[]; + var results: string[] = []; + + try { + info = await client.getPaginated(client, '/projects'); + } catch (e) { + logger.error(`Failed to fetch projects.`, e); + return results; + } + + for (var i = 0; i < info.length; i++) { + const proj = info[i]; + logger.debug('PROJ ' + JSON.stringify(proj)); + if (proj.key != null) { + results.push(proj.key); + } + } + + logger.debug(` FOUND ${results.length} PROJECTS`); + return results; +} + +async function serverGetRepos(client: BitbucketClient, _: string, project: string): Promise { + var info: object[]; + var results: Repo[] = []; + + try { + info = await client.getPaginated(client, `/projects/${project}/repos`); + } catch (e) { + logger.error(`Failed to fetch repos for project ${project}.`, e); + return results; + } + + for (var i = 0; i < info.length; i++) { + const repo = info[i]; + results.push(mapServerRepo(repo, project, project)); + } + + logger.debug(` FOUND ${results.length} REPOS IN ${project}`); + return results; +} + +function mapServerRepo(orig: ServerRepository, workspace: string, project: string): Repo { + // server has no workspace; set workspace to project + var repo: Repo = newRepo(project, project); + + repo.name = orig.name ? orig.name : UNKNOWN; + repo.isArchived = orig.archived ? orig.archived : false; + repo.isFork = false; + repo.isPublic = orig.public ? orig.public : false; + + return repo; +} + +async function serverGetBranches(client: BitbucketClient, repo: string): Promise { + const comps: string[] = repo.split('/'); + var info: object[]; + var results: string[] = []; + + try { + info = await client.getPaginated(client, `/projects/${comps[0]}/repos/${comps[1]}/branches`); + } catch (e) { + logger.error(`Failed to fetch branches for repo ${repo}.`, e); + return results; + } + for (var i = 0; i < info.length; i++) { + const branch = info[i]; + results.push(branch.displayId ? branch.displayId : UNKNOWN); + } + + logger.debug(` FOUND ${results.length} BRANCHES IN ${repo}`); + return results; +} + +async function serverGetTags(client: BitbucketClient, repo: string): Promise { + const comps: string[] = repo.split('/'); + var info: object[]; + var results: string[] = []; + + try { + info = await client.getPaginated(client, `/projects/${comps[0]}/repos/${comps[1]}/tags`); + } catch (e) { + logger.error(`Failed to fetch tags for repo ${repo}.`, e); + return results; + } + + for (var i = 0; i < info.length; i++) { + const tag = info[i]; + results.push(tag.displayId ? tag.displayId : UNKNOWN); + } + + logger.debug(` FOUND ${results.length} TAGS IN ${repo}`); + return results; +} + +async function serverCountForks(client: BitbucketClient, repo: Repo): Promise { + return 0; +} + +async function serverCountWatchers(client: BitbucketClient, repo: Repo): Promise { + return 0; +} From 433ce9bc305cbe59f95029cdac89b402d1b40218 Mon Sep 17 00:00:00 2001 From: Steve Alexander Date: Tue, 17 Dec 2024 12:13:09 -0800 Subject: [PATCH 11/11] README: add bitbucket - still needs screen shots and proofreading --- README.md | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0c2dbe23..e2dea5b2 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ https://github.com/user-attachments/assets/98d46192-5469-430f-ad9e-5c042adbb10d ## Features - 💻 **One-command deployment**: Get started instantly using Docker on your own machine. -- 🔍 **Multi-repo search**: Effortlessly index and search through multiple public and private repositories in GitHub, GitLab, Gitea, or Gerrit. +- 🔍 **Multi-repo search**: Effortlessly index and search through multiple public and private repositories in GitHub, GitLab, Gitea, Gerrit, or Bitbucket (Cloud or Data Center). - ⚡**Lightning fast performance**: Built on top of the powerful [Zoekt](https://github.com/sourcegraph/zoekt) search engine. - 📂 **Full file visualization**: Instantly view the entire file when selecting any search result. - 🎨 **Modern web app**: Enjoy a sleek interface with features like syntax highlighting, light/dark mode, and vim-style navigation @@ -62,7 +62,7 @@ Sourcebot supports indexing and searching through public and private repositorie GitHub icon - GitHub, GitLab, Gitea, and Gerrit. This section will guide you through configuring the repositories that Sourcebot indexes. + GitHub, GitLab, Gitea, Gerrit, and Bitbucket. This section will guide you through configuring the repositories that Sourcebot indexes. 1. Create a new folder on your machine that stores your configs and `.sourcebot` cache, and navigate into it: ```sh @@ -266,6 +266,103 @@ docker run -e GITEA_TOKEN=my-secret-token /* additional args */ ghcr.io/s Gerrit authentication is not yet currently supported. +
+ Bitbucket Cloud + +Generate an API key [here](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/). At a minimum, you'll need to select the `read:repository` scope, but `read:user` and `read:organization` are required for the `user` and `org` fields of your config file: + +![Bitbucket Cloud Access token creation](.github/images/gitea-pat-creation.png) + +Next, update your configuration with the `token` field: +```json +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v2/index.json", + "repos": [ + { + "type": "bitbucket", + "user": "bitbucket-user", + "token": "my-secret-token", + ... + } + ] +} +``` + +You can also pass tokens as environment variables: +```json +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v2/index.json", + "repos": [ + { + "type": "bitbucket", + "user": "bitbucket-user", + "token": { + // note: this env var can be named anything. It + // doesn't need to be `BITBUCKET_TOKEN`. + "env": "BITBUCKET_TOKEN" + }, + ... + } + ] +} +``` + +You'll need to pass this environment variable each time you run Sourcebot: + +
+docker run -e BITBUCKET_TOKEN=my-secret-token /* additional args */ ghcr.io/sourcebot-dev/sourcebot:latest
+
+ +
+ +
+ Bitbucket Data Center + +Generate an API key [here](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/). At a minimum, you'll need to select the `read:repository` scope, but `read:user` and `read:organization` are required for the `user` and `org` fields of your config file: + +![Bitbucket Data Center Access token creation](.github/images/gitea-pat-creation.png) + +Next, update your configuration with the `token` field: +```json +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v2/index.json", + "repos": [ + { + "type": "bitbucket", + "serverType": "server", + "token": "my-secret-token", + ... + } + ] +} +``` + +You can also pass tokens as environment variables: +```json +{ + "$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v2/index.json", + "repos": [ + { + "type": "bitbucket", + "serverType": "server", + "token": { + // note: this env var can be named anything. It + // doesn't need to be `BITBUCKET_TOKEN`. + "env": "BITBUCKET_TOKEN" + }, + ... + } + ] +} +``` + +You'll need to pass this environment variable each time you run Sourcebot: + +
+docker run -e BITBUCKET_TOKEN=my-secret-token /* additional args */ ghcr.io/sourcebot-dev/sourcebot:latest
+
+ +