Merge pull request #2525 from divaltor/bitbucket-api-token

feat(bitbucket): Deprecate App password and replace it with API token
This commit is contained in:
Mauricio Siu
2025-09-21 15:18:40 -06:00
committed by GitHub
14 changed files with 13519 additions and 167 deletions
@@ -1,7 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -27,18 +26,12 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
username: z.string().min(1, {
message: "Username is required",
}),
password: z.string().min(1, {
message: "App Password is required",
}),
name: z.string().min(1, { message: "Name is required" }),
username: z.string().min(1, { message: "Username is required" }),
email: z.string().email().optional(),
apiToken: z.string().min(1, { message: "API Token is required" }),
workspaceName: z.string().optional(),
});
@@ -47,14 +40,12 @@ type Schema = z.infer<typeof Schema>;
export const AddBitbucketProvider = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const _url = useUrl();
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
const { data: auth } = api.user.get.useQuery();
const _router = useRouter();
const form = useForm<Schema>({
defaultValues: {
username: "",
password: "",
apiToken: "",
workspaceName: "",
},
resolver: zodResolver(Schema),
@@ -63,7 +54,8 @@ export const AddBitbucketProvider = () => {
useEffect(() => {
form.reset({
username: "",
password: "",
email: "",
apiToken: "",
workspaceName: "",
});
}, [form, isOpen]);
@@ -71,10 +63,11 @@ export const AddBitbucketProvider = () => {
const onSubmit = async (data: Schema) => {
await mutateAsync({
bitbucketUsername: data.username,
appPassword: data.password,
apiToken: data.apiToken,
bitbucketWorkspaceName: data.workspaceName || "",
authId: auth?.id || "",
name: data.name || "",
bitbucketEmail: data.email || "",
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
@@ -113,37 +106,46 @@ export const AddBitbucketProvider = () => {
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<AlertBlock type="warning">
Bitbucket App Passwords are deprecated for new providers. Use
an API Token instead. Existing providers with App Passwords
will continue to work until 9th June 2026.
</AlertBlock>
<div className="mt-1 text-sm">
Manage tokens in
<Link
href="https://id.atlassian.com/manage-profile/security/api-tokens"
target="_blank"
className="inline-flex items-center gap-1 ml-1"
>
<span>Bitbucket settings</span>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</div>
<ul className="list-disc list-inside ml-4 text-sm text-muted-foreground">
<li className="text-muted-foreground text-sm">
Click on Create API token with scopes
</li>
<li className="text-muted-foreground text-sm">
Select the expiration date (Max 1 year)
</li>
<li className="text-muted-foreground text-sm">
Select Bitbucket product.
</li>
</ul>
<p className="text-muted-foreground text-sm">
To integrate your Bitbucket account, you need to create a new
App Password in your Bitbucket settings. Follow these steps:
Select the following scopes:
</p>
<ol className="list-decimal list-inside text-sm text-muted-foreground">
<li className="flex flex-row gap-2 items-center">
Create new App Password{" "}
<Link
href="https://bitbucket.org/account/settings/app-passwords/new"
target="_blank"
>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</li>
<li>
When creating the App Password, ensure you grant the
following permissions:
<ul className="list-disc list-inside ml-4">
<li>Account: Read</li>
<li>Workspace membership: Read</li>
<li>Projects: Read</li>
<li>Repositories: Read</li>
<li>Pull requests: Read</li>
<li>Webhooks: Read and write</li>
</ul>
</li>
<li>
After creating, you'll receive an App Password. Copy it and
paste it below along with your Bitbucket username.
</li>
</ol>
<ul className="list-disc list-inside ml-4 text-sm text-muted-foreground">
<li>read:repository:bitbucket</li>
<li>read:pullrequest:bitbucket</li>
<li>read:webhook:bitbucket</li>
<li>read:workspace:bitbucket</li>
<li>write:webhook:bitbucket</li>
</ul>
<FormField
control={form.control}
name="name"
@@ -152,7 +154,7 @@ export const AddBitbucketProvider = () => {
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Random Name eg(my-personal-account)"
placeholder="Your Bitbucket Provider, eg: my-personal-account"
{...field}
/>
</FormControl>
@@ -179,14 +181,27 @@ export const AddBitbucketProvider = () => {
<FormField
control={form.control}
name="password"
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>App Password</FormLabel>
<FormLabel>Bitbucket Email</FormLabel>
<FormControl>
<Input placeholder="Your Bitbucket email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="apiToken"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="ATBBPDYUC94nR96Nj7Cqpp4pfwKk03573DD2"
placeholder="Paste your Bitbucket API token"
{...field}
/>
</FormControl>
@@ -200,7 +215,7 @@ export const AddBitbucketProvider = () => {
name="workspaceName"
render={({ field }) => (
<FormItem>
<FormLabel>Workspace Name (Optional)</FormLabel>
<FormLabel>Workspace Name (optional)</FormLabel>
<FormControl>
<Input
placeholder="For organization accounts"
@@ -33,7 +33,10 @@ const Schema = z.object({
username: z.string().min(1, {
message: "Username is required",
}),
email: z.string().email().optional(),
workspaceName: z.string().optional(),
apiToken: z.string().optional(),
appPassword: z.string().optional(),
});
type Schema = z.infer<typeof Schema>;
@@ -60,19 +63,28 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
const form = useForm<Schema>({
defaultValues: {
username: "",
email: "",
workspaceName: "",
apiToken: "",
appPassword: "",
},
resolver: zodResolver(Schema),
});
const username = form.watch("username");
const email = form.watch("email");
const workspaceName = form.watch("workspaceName");
const apiToken = form.watch("apiToken");
const appPassword = form.watch("appPassword");
useEffect(() => {
form.reset({
username: bitbucket?.bitbucketUsername || "",
email: bitbucket?.bitbucketEmail || "",
workspaceName: bitbucket?.bitbucketWorkspaceName || "",
name: bitbucket?.gitProvider.name || "",
apiToken: bitbucket?.apiToken || "",
appPassword: bitbucket?.appPassword || "",
});
}, [form, isOpen, bitbucket]);
@@ -81,8 +93,11 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
bitbucketId,
gitProviderId: bitbucket?.gitProviderId || "",
bitbucketUsername: data.username,
bitbucketEmail: data.email || "",
bitbucketWorkspaceName: data.workspaceName || "",
name: data.name || "",
apiToken: data.apiToken || "",
appPassword: data.appPassword || "",
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
@@ -121,6 +136,12 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<p className="text-muted-foreground text-sm">
Update your Bitbucket authentication. Use API Token for
enhanced security (recommended) or App Password for legacy
support.
</p>
<FormField
control={form.control}
name="name"
@@ -154,6 +175,24 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email (Required for API Tokens)</FormLabel>
<FormControl>
<Input
type="email"
placeholder="Your Bitbucket email address"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="workspaceName"
@@ -171,6 +210,49 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
)}
/>
<div className="flex flex-col gap-2 border-t pt-4">
<h3 className="text-sm font-medium mb-2">
Authentication (Update to use API Token)
</h3>
<FormField
control={form.control}
name="apiToken"
render={({ field }) => (
<FormItem>
<FormLabel>API Token (Recommended)</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your Bitbucket API Token"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appPassword"
render={({ field }) => (
<FormItem>
<FormLabel>
App Password (Legacy - will be deprecated June 2026)
</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your Bitbucket App Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-between gap-4 mt-4">
<Button
type="button"
@@ -180,7 +262,10 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
await testConnection({
bitbucketId,
bitbucketUsername: username,
bitbucketEmail: email,
workspaceName: workspaceName,
apiToken: apiToken,
appPassword: appPassword,
})
.then(async (message) => {
toast.info(`Message: ${message}`);
@@ -157,7 +157,13 @@ export const ShowGitProviders = () => {
</div>
</div>
<div className="flex flex-row gap-1">
<div className="flex flex-row gap-1 items-center">
{isBitbucket &&
gitProvider.bitbucket?.appPassword &&
!gitProvider.bitbucket?.apiToken ? (
<Badge variant="yellow">Deprecated</Badge>
) : null}
{!haveGithubRequirements && isGithub && (
<div className="flex flex-row gap-1 items-center">
<Badge
@@ -0,0 +1 @@
ALTER TABLE "bitbucket" ADD COLUMN "apiToken" text;
@@ -0,0 +1 @@
ALTER TABLE "bitbucket" ADD COLUMN "bitbucketEmail" text;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14
View File
@@ -778,6 +778,20 @@
"when": 1757189541734,
"tag": "0110_red_psynapse",
"breakpoints": true
},
{
"idx": 111,
"version": "7",
"when": 1758445844561,
"tag": "0111_mushy_wolfsbane",
"breakpoints": true
},
{
"idx": 112,
"version": "7",
"when": 1758483520214,
"tag": "0112_freezing_skrulls",
"breakpoints": true
}
]
}
+13 -11
View File
@@ -1,4 +1,9 @@
import { IS_CLOUD, shouldDeploy } from "@dokploy/server";
import {
type Bitbucket,
getBitbucketHeaders,
IS_CLOUD,
shouldDeploy,
} from "@dokploy/server";
import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import { db } from "@/server/db";
@@ -146,10 +151,10 @@ export default async function handler(
const commitedPaths = await extractCommitedPaths(
req.body,
application.bitbucketOwner,
application.bitbucket?.appPassword || "",
application.bitbucket,
application.bitbucketRepository || "",
);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
commitedPaths,
@@ -354,9 +359,8 @@ export const getProviderByHeader = (headers: any) => {
export const extractCommitedPaths = async (
body: any,
bitbucketUsername: string | null,
bitbucketAppPassword: string | null,
repository: string | null,
bitbucket: Bitbucket | null,
repository: string,
) => {
const changes = body.push?.changes || [];
@@ -365,18 +369,16 @@ export const extractCommitedPaths = async (
.filter(Boolean);
const commitedPaths: string[] = [];
for (const commit of commitHashes) {
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucketUsername}/${repository}/diffstat/${commit}`;
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucket?.bitbucketUsername}/${repository}/diffstat/${commit}`;
try {
const response = await fetch(url, {
headers: {
Authorization: `Basic ${Buffer.from(`${bitbucketUsername}:${bitbucketAppPassword}`).toString("base64")}`,
},
headers: getBitbucketHeaders(bitbucket!),
});
const data = await response.json();
for (const value of data.values) {
commitedPaths.push(value.new?.path);
if (value?.new?.path) commitedPaths.push(value.new.path);
}
} catch (error) {
console.error(
@@ -99,8 +99,7 @@ export default async function handler(
const commitedPaths = await extractCommitedPaths(
req.body,
composeResult.bitbucketOwner,
composeResult.bitbucket?.appPassword || "",
composeResult.bitbucket,
composeResult.bitbucketRepository || "",
);
+18 -1
View File
@@ -11,7 +11,9 @@ export const bitbucket = pgTable("bitbucket", {
.primaryKey()
.$defaultFn(() => nanoid()),
bitbucketUsername: text("bitbucketUsername"),
bitbucketEmail: text("bitbucketEmail"),
appPassword: text("appPassword"),
apiToken: text("apiToken"),
bitbucketWorkspaceName: text("bitbucketWorkspaceName"),
gitProviderId: text("gitProviderId")
.notNull()
@@ -29,7 +31,9 @@ const createSchema = createInsertSchema(bitbucket);
export const apiCreateBitbucket = createSchema.extend({
bitbucketUsername: z.string().optional(),
bitbucketEmail: z.string().email().optional(),
appPassword: z.string().optional(),
apiToken: z.string().optional(),
bitbucketWorkspaceName: z.string().optional(),
gitProviderId: z.string().optional(),
authId: z.string().min(1),
@@ -46,9 +50,19 @@ export const apiBitbucketTestConnection = createSchema
.extend({
bitbucketId: z.string().min(1),
bitbucketUsername: z.string().optional(),
bitbucketEmail: z.string().email().optional(),
workspaceName: z.string().optional(),
apiToken: z.string().optional(),
appPassword: z.string().optional(),
})
.pick({ bitbucketId: true, bitbucketUsername: true, workspaceName: true });
.pick({
bitbucketId: true,
bitbucketUsername: true,
bitbucketEmail: true,
workspaceName: true,
apiToken: true,
appPassword: true,
});
export const apiFindBitbucketBranches = z.object({
owner: z.string(),
@@ -60,6 +74,9 @@ export const apiUpdateBitbucket = createSchema.extend({
bitbucketId: z.string().min(1),
name: z.string().min(1),
bitbucketUsername: z.string().optional(),
bitbucketEmail: z.string().email().optional(),
appPassword: z.string().optional(),
apiToken: z.string().optional(),
bitbucketWorkspaceName: z.string().optional(),
organizationId: z.string().optional(),
});
+18 -2
View File
@@ -68,10 +68,26 @@ export const updateBitbucket = async (
input: typeof apiUpdateBitbucket._type,
) => {
return await db.transaction(async (tx) => {
// First get the current bitbucket provider to get gitProviderId
const currentProvider = await tx.query.bitbucket.findFirst({
where: eq(bitbucket.bitbucketId, bitbucketId),
});
if (!currentProvider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Bitbucket provider not found",
});
}
const result = await tx
.update(bitbucket)
.set({
...input,
bitbucketUsername: input.bitbucketUsername,
bitbucketEmail: input.bitbucketEmail,
appPassword: input.appPassword,
apiToken: input.apiToken,
bitbucketWorkspaceName: input.bitbucketWorkspaceName,
})
.where(eq(bitbucket.bitbucketId, bitbucketId))
.returning();
@@ -83,7 +99,7 @@ export const updateBitbucket = async (
name: input.name,
organizationId: input.organizationId,
})
.where(eq(gitProvider.gitProviderId, input.gitProviderId))
.where(eq(gitProvider.gitProviderId, currentProvider.gitProviderId))
.returning();
}
@@ -5,7 +5,10 @@ import type {
apiBitbucketTestConnection,
apiFindBitbucketBranches,
} from "@dokploy/server/db/schema";
import { findBitbucketById } from "@dokploy/server/services/bitbucket";
import {
type Bitbucket,
findBitbucketById,
} from "@dokploy/server/services/bitbucket";
import type { Compose } from "@dokploy/server/services/compose";
import type { InferResultType } from "@dokploy/server/types/with";
import { TRPCError } from "@trpc/server";
@@ -23,6 +26,39 @@ export type ComposeWithBitbucket = InferResultType<
{ bitbucket: true }
>;
export const getBitbucketCloneUrl = (
bitbucketProvider: {
apiToken?: string | null;
bitbucketUsername?: string | null;
appPassword?: string | null;
} | null,
repoClone: string,
) => {
if (!bitbucketProvider) {
throw new Error("Bitbucket provider is required");
}
return bitbucketProvider.apiToken
? `https://x-token-auth:${bitbucketProvider.apiToken}@${repoClone}`
: `https://${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}@${repoClone}`;
};
export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
if (bitbucketProvider.apiToken) {
// For API tokens, use HTTP Basic auth with email and token
// According to Bitbucket docs: email:token for API calls
const email =
bitbucketProvider.bitbucketEmail || bitbucketProvider.bitbucketUsername;
return {
Authorization: `Basic ${Buffer.from(`${email}:${bitbucketProvider.apiToken}`).toString("base64")}`,
};
}
// For app passwords, use HTTP Basic auth with username and app password
return {
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
};
};
export const cloneBitbucketRepository = async (
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
logPath: string,
@@ -51,7 +87,7 @@ export const cloneBitbucketRepository = async (
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
try {
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
const cloneArgs = [
@@ -103,7 +139,7 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
try {
const cloneArgs = [
@@ -153,7 +189,7 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
try {
const cloneCommand = `
@@ -206,7 +242,7 @@ export const getBitbucketCloneCommand = async (
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
const cloneCommand = `
rm -rf ${outputPath};
@@ -241,9 +277,7 @@ export const getBitbucketRepositories = async (bitbucketId?: string) => {
while (url) {
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
},
headers: getBitbucketHeaders(bitbucketProvider),
});
if (!response.ok) {
@@ -279,35 +313,43 @@ export const getBitbucketBranches = async (
}
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
const { owner, repo } = input;
const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=100`;
let url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=1`;
let allBranches: {
name: string;
commit: {
sha: string;
};
}[] = [];
try {
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
},
});
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `HTTP error! status: ${response.status}`,
while (url) {
const response = await fetch(url, {
method: "GET",
headers: getBitbucketHeaders(bitbucketProvider),
});
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `HTTP error! status: ${response.status}`,
});
}
const data = await response.json();
const mappedData = data.values.map((branch: any) => {
return {
name: branch.name,
commit: {
sha: branch.target.hash,
},
};
});
allBranches = allBranches.concat(mappedData);
url = data.next || null;
}
const data = await response.json();
const mappedData = data.values.map((branch: any) => {
return {
name: branch.name,
commit: {
sha: branch.target.hash,
},
};
});
return mappedData as {
return allBranches as {
name: string;
commit: {
sha: string;
@@ -335,9 +377,7 @@ export const testBitbucketConnection = async (
try {
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
},
headers: getBitbucketHeaders(bitbucketProvider),
});
if (!response.ok) {
+84 -64
View File
@@ -362,17 +362,22 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
}
const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
const limit = 30;
let allRepos = 0;
let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`;
while (nextUrl) {
const response = await fetch(nextUrl, {
headers: {
Accept: "application/json",
Authorization: `token ${provider.accessToken}`,
// Use /user/repos to get authenticated user's repositories with pagination
let allRepos = 0;
let page = 1;
const limit = 50; // Max per page
while (true) {
const response = await fetch(
`${baseUrl}/api/v1/user/repos?page=${page}&limit=${limit}`,
{
headers: {
Accept: "application/json",
Authorization: `token ${provider.accessToken}`,
},
},
});
);
if (!response.ok) {
throw new Error(
@@ -381,22 +386,18 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
}
const repos = await response.json();
allRepos += repos.data.length;
const linkHeader = response.headers.get("link");
nextUrl = "";
if (linkHeader) {
const nextLink = linkHeader
.split(",")
.find((link) => link.includes('rel="next"'));
if (nextLink) {
const matches = nextLink.match(/<([^>]+)>/);
if (matches?.[1]) {
nextUrl = matches[1];
}
}
if (!Array.isArray(repos) || repos.length === 0) {
break; // No more repositories
}
allRepos += repos.length;
// Check if there are more pages
if (repos.length < limit) {
break; // Last page (fewer results than limit)
}
page++;
}
await updateGitea(giteaId, {
@@ -418,17 +419,22 @@ export const getGiteaRepositories = async (giteaId?: string) => {
const giteaProvider = await findGiteaById(giteaId);
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
const limit = 30;
let allRepositories: any[] = [];
let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`;
while (nextUrl) {
const response = await fetch(nextUrl, {
headers: {
Accept: "application/json",
Authorization: `token ${giteaProvider.accessToken}`,
// Use /user/repos to get authenticated user's repositories with pagination
let allRepositories: any[] = [];
let page = 1;
const limit = 50; // Max per page
while (true) {
const response = await fetch(
`${baseUrl}/api/v1/user/repos?page=${page}&limit=${limit}`,
{
headers: {
Accept: "application/json",
Authorization: `token ${giteaProvider.accessToken}`,
},
},
});
);
if (!response.ok) {
throw new TRPCError({
@@ -437,23 +443,19 @@ export const getGiteaRepositories = async (giteaId?: string) => {
});
}
const result = await response.json();
allRepositories = [...allRepositories, ...result.data];
const linkHeader = response.headers.get("link");
nextUrl = "";
if (linkHeader) {
const nextLink = linkHeader
.split(",")
.find((link) => link.includes('rel="next"'));
if (nextLink) {
const matches = nextLink.match(/<([^>]+)>/);
if (matches?.[1]) {
nextUrl = matches[1];
}
}
const repos = await response.json();
if (!Array.isArray(repos) || repos.length === 0) {
break; // No more repositories
}
allRepositories = [...allRepositories, ...repos];
// Check if there are more pages
if (repos.length < limit) {
break; // Last page (fewer results than limit)
}
page++;
}
return (
@@ -482,25 +484,43 @@ export const getGiteaBranches = async (input: {
const giteaProvider = await findGiteaById(input.giteaId);
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
const url = `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches`;
const response = await fetch(url, {
headers: {
Accept: "application/json",
Authorization: `token ${giteaProvider.accessToken}`,
},
});
// Handle pagination for branches
let allBranches: any[] = [];
let page = 1;
const limit = 50; // Max per page
if (!response.ok) {
throw new Error(`Failed to fetch branches: ${response.statusText}`);
while (true) {
const response = await fetch(
`${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches?page=${page}&limit=${limit}`,
{
headers: {
Accept: "application/json",
Authorization: `token ${giteaProvider.accessToken}`,
},
},
);
if (!response.ok) {
throw new Error(`Failed to fetch branches: ${response.statusText}`);
}
const branches = await response.json();
if (!Array.isArray(branches) || branches.length === 0) {
break; // No more branches
}
allBranches = [...allBranches, ...branches];
// Check if there are more pages
if (branches.length < limit) {
break; // Last page (fewer results than limit)
}
page++;
}
const branches = await response.json();
if (!branches) {
return [];
}
return branches?.map((branch: any) => ({
return allBranches?.map((branch: any) => ({
id: branch.name,
name: branch.name,
commit: {