diff --git a/public/app/features/provisioning/Wizard/ConnectStep.tsx b/public/app/features/provisioning/Wizard/ConnectStep.tsx index bd1e775a596..4a3f882c1b4 100644 --- a/public/app/features/provisioning/Wizard/ConnectStep.tsx +++ b/public/app/features/provisioning/Wizard/ConnectStep.tsx @@ -25,7 +25,11 @@ export function ConnectStep() { // We don't need to dynamically react on repo type changes, so we use getValues for it const type = getValues('repository.type'); - const [repositoryUrl = '', repositoryToken = ''] = watch(['repository.url', 'repository.token']); + const [repositoryUrl = '', repositoryToken = '', repositoryTokenUser = ''] = watch([ + 'repository.url', + 'repository.token', + 'repository.tokenUser', + ]); const isGitBased = isGitProvider(type); const { @@ -36,6 +40,7 @@ export function ConnectStep() { repositoryType: type, repositoryUrl, repositoryToken, + repositoryTokenUser, }); const gitFields = isGitBased ? getGitProviderFields(type) : null; diff --git a/public/app/features/provisioning/hooks/useBranchOptions.ts b/public/app/features/provisioning/hooks/useBranchOptions.ts index fc0f6b7dfc9..e4af6ab1f58 100644 --- a/public/app/features/provisioning/hooks/useBranchOptions.ts +++ b/public/app/features/provisioning/hooks/useBranchOptions.ts @@ -14,11 +14,18 @@ export interface UseBranchOptionsProps { repositoryType: RepoType; repositoryUrl: string; repositoryToken: string; + repositoryTokenUser?: string; } -export function useBranchOptions({ repositoryType, repositoryUrl = '', repositoryToken = '' }: UseBranchOptionsProps) { +export function useBranchOptions({ + repositoryType, + repositoryUrl = '', + repositoryToken = '', + repositoryTokenUser = '', +}: UseBranchOptionsProps) { const trimmedUrl = repositoryUrl.trim(); const trimmedToken = repositoryToken.trim(); + const trimmedTokenUser = repositoryTokenUser.trim(); const hasRequiredData = useMemo(() => { if (!isSupportedGitProvider(repositoryType)) { @@ -27,10 +34,11 @@ export function useBranchOptions({ repositoryType, repositoryUrl = '', repositor const hasUrl = trimmedUrl.length > 0; const hasToken = trimmedToken.length > 0; + const hasTokenUser = repositoryType === 'bitbucket' ? trimmedTokenUser.length > 0 : true; const repoInfo = hasUrl ? parseRepositoryUrl(trimmedUrl, repositoryType) : null; - return hasUrl && hasToken && repoInfo !== null; - }, [trimmedUrl, trimmedToken, repositoryType]); + return hasUrl && hasToken && hasTokenUser && repoInfo !== null; + }, [trimmedUrl, trimmedToken, trimmedTokenUser, repositoryType]); const fetchOptions = useMemo( () => async (): Promise> => { @@ -44,14 +52,16 @@ export function useBranchOptions({ repositoryType, repositoryUrl = '', repositor throw new Error('Invalid repository URL format'); } - const branchData = await fetchAllBranches(repositoryType, repoInfo.owner, repoInfo.repo, trimmedToken); + // For Bitbucket, combine username and app password for Basic auth + const authToken = repositoryType === 'bitbucket' ? `${trimmedTokenUser}:${trimmedToken}` : trimmedToken; + const branchData = await fetchAllBranches(repositoryType, repoInfo.owner, repoInfo.repo, authToken); return branchData.map((branch) => ({ label: branch.name, value: branch.name, })); }, - [hasRequiredData, trimmedUrl, trimmedToken, repositoryType] + [hasRequiredData, trimmedUrl, trimmedToken, trimmedTokenUser, repositoryType] ); const asyncState = useAsync(fetchOptions, [fetchOptions]); diff --git a/public/app/features/provisioning/utils/httpUtils.ts b/public/app/features/provisioning/utils/httpUtils.ts index f433173892a..6e9cbc4e933 100644 --- a/public/app/features/provisioning/utils/httpUtils.ts +++ b/public/app/features/provisioning/utils/httpUtils.ts @@ -50,7 +50,7 @@ export function getProviderHeaders(repositoryType: string, token: string): Recor case 'gitlab': return { 'Private-Token': token }; case 'bitbucket': - return { Authorization: `Bearer ${token}` }; + return { Authorization: `Basic ${btoa(token)}` }; default: throw new Error( t('provisioning.http-utils.unsupported-repository-type', 'Unsupported repository type: {{repositoryType}}', { @@ -82,7 +82,7 @@ export async function makeApiRequest(request: ApiRequest) { return response.json(); } -// GitHub and GitLab limit results to 100 items per page, so we need to paginate +// GitHub, GitLab, and Bitbucket limit results to 100 items per page, so we need to paginate async function fetchWithPagination( buildUrl: (page: number) => string, headers: Record @@ -95,9 +95,12 @@ async function fetchWithPagination( const url = buildUrl(page); const data = await makeApiRequest({ url, headers }); - if (Array.isArray(data) && data.length > 0) { - allBranches.push(...data); - hasMorePages = data.length === 100; + // Handle GitHub/GitLab format (direct array) and Bitbucket format ({ values: [...] }) + const branches = Array.isArray(data) ? data : data?.values; + + if (Array.isArray(branches) && branches.length > 0) { + allBranches.push(...branches); + hasMorePages = branches.length === 100; page++; } else { hasMorePages = false; @@ -135,14 +138,10 @@ export async function fetchAllBitbucketBranches( repo: string, headers: Record ): Promise> { - const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=1000`; - const data = await makeApiRequest({ url, headers }); - - if (data && Array.isArray(data.values)) { - return data.values; - } - - return []; + return fetchWithPagination( + (page) => `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=100&page=${page}`, + headers + ); } export async function fetchAllBranches(