refactor: Update Gitea provider components and API handling

- Adjusted GiteaProviderSchema to ensure watchPaths are correctly initialized and validated.
- Refactored SaveGiteaProvider and SaveGiteaProviderCompose components for improved state management and UI consistency.
- Simplified API router methods for Gitea, enhancing readability and error handling.
- Updated database schema and service functions for better clarity and maintainability.
- Removed unnecessary comments and improved logging for better debugging.
This commit is contained in:
Mauricio Siu 2025-03-23 03:27:19 -06:00
parent fc7eff94b6
commit 9359ee7a04
6 changed files with 316 additions and 539 deletions

View File

@ -67,13 +67,13 @@ const GiteaProviderSchema = z.object({
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
giteaPathNamespace: z.string().min(1),
giteaPathNamespace: z.string(),
id: z.number().nullable(),
watchPaths: z.array(z.string()).default([]),
})
.required(),
branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]),
});
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
@ -97,10 +97,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
repo: "",
giteaPathNamespace: "",
id: null,
watchPaths: [],
},
giteaId: "",
branch: "",
watchPaths: [],
},
resolver: zodResolver(GiteaProviderSchema),
});
@ -146,10 +146,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
owner: data.giteaOwner || "",
giteaPathNamespace: data.giteaPathNamespace || "",
id: data.giteaProjectId,
watchPaths: data.watchPaths || [],
},
buildPath: data.giteaBuildPath || "/",
giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [],
});
}
}, [form.reset, data, form]);
@ -164,7 +164,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
applicationId,
giteaProjectId: data.repository.id,
giteaPathNamespace: data.repository.giteaPathNamespace,
watchPaths: data.repository.watchPaths,
watchPaths: data.watchPaths,
})
.then(async () => {
toast.success("Service Provider Saved");
@ -198,7 +198,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
repo: "",
id: null,
giteaPathNamespace: "",
watchPaths: [],
});
form.setValue("branch", "");
}}
@ -285,7 +284,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
repo: repo.name,
id: repo.id,
giteaPathNamespace: repo.name,
watchPaths: [],
});
form.setValue("branch", "");
}}
@ -413,7 +411,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
/>
<FormField
control={form.control}
name="repository.watchPaths"
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">

View File

@ -41,7 +41,7 @@ import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import type { Branch, Repository } from "@/utils/gitea-utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import { CheckIcon, ChevronsUpDown, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
@ -55,7 +55,7 @@ const GiteaProviderSchema = z.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
id: z.number().nullable(),
giteaPathNamespace: z.string().min(1),
giteaPathNamespace: z.string(),
})
.required(),
branch: z.string().min(1, "Branch is required"),
@ -95,6 +95,8 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
const repository = form.watch("repository");
const giteaId = form.watch("giteaId");
console.log(repository);
const {
data: repositories,
isLoading: isLoadingRepositories,
@ -126,6 +128,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
useEffect(() => {
if (data) {
console.log(data.giteaRepository);
form.reset({
branch: data.giteaBranch || "",
repository: {
@ -452,20 +455,20 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
/>
<Button
type="button"
variant="secondary"
variant="outline"
size="icon"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder*="Enter a path"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {
const newPaths = [...(field.value || []), value];
form.setValue("watchPaths", newPaths);
const path = input.value.trim();
if (path) {
field.onChange([...field.value, path]);
input.value = "";
}
}}
>
Add
<Plus className="size-4" />
</Button>
</div>
</FormControl>
@ -475,13 +478,11 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
/>
</div>
<Button
type="submit"
className="w-full"
disabled={isSavingGiteaProvider || !form.formState.isDirty}
>
{isSavingGiteaProvider ? "Saving..." : "Save Gitea Provider"}
</Button>
<div className="flex justify-end">
<Button type="submit" isLoading={isSavingGiteaProvider}>
Save
</Button>
</div>
</form>
</Form>
</div>

View File

@ -22,49 +22,36 @@ import {
import { TRPCError } from "@trpc/server";
export const giteaRouter = createTRPCRouter({
// Create a new Gitea provider
create: protectedProcedure
.input(apiCreateGitea)
.mutation(
async ({
input,
ctx,
}: { input: typeof apiCreateGitea._input; ctx: any }) => {
try {
return await createGitea(input, ctx.session.activeOrganizationId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating this Gitea provider",
cause: error,
});
}
},
),
.mutation(async ({ input, ctx }) => {
try {
return await createGitea(input, ctx.session.activeOrganizationId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating this Gitea provider",
cause: error,
});
}
}),
// Fetch a specific Gitea provider by ID
one: protectedProcedure
.input(apiFindOneGitea)
.query(
async ({
input,
ctx,
}: { input: typeof apiFindOneGitea._input; ctx: any }) => {
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
return giteaProvider;
},
),
.query(async ({ input, ctx }) => {
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
return giteaProvider;
}),
// Fetch all Gitea providers for the active organization
giteaProviders: protectedProcedure.query(async ({ ctx }: { ctx: any }) => {
let result = await db.query.gitea.findMany({
with: {
@ -72,14 +59,12 @@ export const giteaRouter = createTRPCRouter({
},
});
// Filter by organization ID
result = result.filter(
(provider) =>
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId,
);
// Filter providers that meet the requirements
const filtered = result
.filter((provider) => haveGiteaRequirements(provider))
.map((provider) => {
@ -94,64 +79,88 @@ export const giteaRouter = createTRPCRouter({
return filtered;
}),
// Fetch repositories from Gitea provider
getGiteaRepositories: protectedProcedure
.input(apiFindOneGitea)
.query(
async ({
input,
ctx,
}: { input: typeof apiFindOneGitea._input; ctx: any }) => {
const { giteaId } = input;
.query(async ({ input, ctx }) => {
const { giteaId } = input;
if (!giteaId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Gitea provider ID is required.",
});
}
if (!giteaId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Gitea provider ID is required.",
});
}
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
try {
return await getGiteaRepositories(giteaId);
} catch (error) {
console.error("Error fetching Gitea repositories:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error),
});
}
},
),
try {
const repositories = await getGiteaRepositories(giteaId);
console.log(repositories);
return repositories;
} catch (error) {
console.error("Error fetching Gitea repositories:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error),
});
}
}),
// Fetch branches of a specific Gitea repository
getGiteaBranches: protectedProcedure
.input(apiFindGiteaBranches)
.query(
async ({
input,
ctx,
}: { input: typeof apiFindGiteaBranches._input; ctx: any }) => {
const { giteaId, owner, repositoryName } = input;
.query(async ({ input, ctx }) => {
const { giteaId, owner, repositoryName } = input;
if (!giteaId || !owner || !repositoryName) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Gitea provider ID, owner, and repository name are required.",
});
}
if (!giteaId || !owner || !repositoryName) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Gitea provider ID, owner, and repository name are required.",
});
}
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
try {
return await getGiteaBranches({
giteaId,
owner,
repo: repositoryName,
id: 0,
});
} catch (error) {
console.error("Error fetching Gitea branches:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error),
});
}
}),
testConnection: protectedProcedure
.input(apiGiteaTestConnection)
.mutation(async ({ input, ctx }) => {
const giteaId = input.giteaId ?? "";
try {
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
@ -163,97 +172,49 @@ export const giteaRouter = createTRPCRouter({
});
}
try {
return await getGiteaBranches({
giteaId,
owner,
repo: repositoryName,
id: 0, // Provide a default value for the optional id
});
} catch (error) {
console.error("Error fetching Gitea branches:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error),
});
}
},
),
const result = await testGiteaConnection({
giteaId,
});
// Test connection to Gitea provider
testConnection: protectedProcedure
.input(apiGiteaTestConnection)
.mutation(
async ({
input,
ctx,
}: { input: typeof apiGiteaTestConnection._input; ctx: any }) => {
const giteaId = input.giteaId ?? "";
return `Found ${result} repositories`;
} catch (error) {
console.error("Gitea connection test error:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error),
});
}
}),
try {
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
const result = await testGiteaConnection({
giteaId,
});
return `Found ${result} repositories`;
} catch (error) {
console.error("Gitea connection test error:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error),
});
}
},
),
// Update an existing Gitea provider
update: protectedProcedure
.input(apiUpdateGitea)
.mutation(
async ({
input,
ctx,
}: { input: typeof apiUpdateGitea._input; ctx: any }) => {
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
.mutation(async ({ input, ctx }) => {
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
console.log("Updating Gitea provider:", input);
if (input.name) {
await updateGitProvider(input.gitProviderId, {
name: input.name,
organizationId: ctx.session.activeOrganizationId,
});
if (input.name) {
await updateGitProvider(input.gitProviderId, {
name: input.name,
organizationId: ctx.session.activeOrganizationId,
});
await updateGitea(input.giteaId, {
...input,
});
} else {
await updateGitea(input.giteaId, {
...input,
});
}
await updateGitea(input.giteaId, {
...input,
});
} else {
await updateGitea(input.giteaId, {
...input,
});
}
return { success: true };
},
),
return { success: true };
}),
});

View File

@ -5,13 +5,12 @@ import { nanoid } from "nanoid";
import { z } from "zod";
import { gitProvider } from "./git-provider";
// Gitea table definition
export const gitea = pgTable("gitea", {
giteaId: text("giteaId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()), // Using nanoid for unique ID
giteaUrl: text("giteaUrl").default("https://gitea.com").notNull(), // Default URL for Gitea
.$defaultFn(() => nanoid()),
giteaUrl: text("giteaUrl").default("https://gitea.com").notNull(),
redirectUri: text("redirect_uri"),
clientId: text("client_id"),
clientSecret: text("client_secret"),
@ -26,7 +25,6 @@ export const gitea = pgTable("gitea", {
lastAuthenticatedAt: integer("last_authenticated_at"),
});
// Gitea relations with gitProvider
export const giteaProviderRelations = relations(gitea, ({ one }) => ({
gitProvider: one(gitProvider, {
fields: [gitea.gitProviderId],
@ -34,10 +32,8 @@ export const giteaProviderRelations = relations(gitea, ({ one }) => ({
}),
}));
// Create schema for Gitea
const createSchema = createInsertSchema(gitea);
// API schema for creating a Gitea instance
export const apiCreateGitea = createSchema.extend({
clientId: z.string().optional(),
clientSecret: z.string().optional(),
@ -54,14 +50,12 @@ export const apiCreateGitea = createSchema.extend({
lastAuthenticatedAt: z.number().optional(),
});
// API schema for finding one Gitea instance
export const apiFindOneGitea = createSchema
.extend({
giteaId: z.string().min(1),
})
.pick({ giteaId: true });
// API schema for testing Gitea connection
export const apiGiteaTestConnection = createSchema
.extend({
organizationName: z.string().optional(),
@ -70,7 +64,6 @@ export const apiGiteaTestConnection = createSchema
export type ApiGiteaTestConnection = z.infer<typeof apiGiteaTestConnection>;
// API schema for finding branches in Gitea
export const apiFindGiteaBranches = z.object({
id: z.number().optional(),
owner: z.string().min(1),
@ -78,7 +71,6 @@ export const apiFindGiteaBranches = z.object({
giteaId: z.string().optional(),
});
// API schema for updating Gitea instance
export const apiUpdateGitea = createSchema.extend({
clientId: z.string().optional(),
clientSecret: z.string().optional(),

View File

@ -1,6 +1,4 @@
// @ts-ignore: Cannot find module errors
import { db } from "@dokploy/server/db";
// @ts-ignore: Cannot find module errors
import {
type apiCreateGitea,
gitProvider,
@ -15,7 +13,6 @@ export const createGitea = async (
input: typeof apiCreateGitea._type,
organizationId: string,
) => {
// @ts-ignore - Complex transaction type - Added because proper typing in Drizzle in not sufficient
return await db.transaction(async (tx) => {
const newGitProvider = await tx
.insert(gitProvider)
@ -25,7 +22,7 @@ export const createGitea = async (
name: input.name,
})
.returning()
.then((response: (typeof gitProvider.$inferSelect)[]) => response[0]);
.then((response) => response[0]);
if (!newGitProvider) {
throw new TRPCError({
@ -50,7 +47,6 @@ export const createGitea = async (
});
}
// Return just the essential data needed by the frontend
return {
giteaId: giteaProvider.giteaId,
clientId: giteaProvider.clientId,
@ -69,7 +65,6 @@ export const findGiteaById = async (giteaId: string) => {
});
if (!giteaProviderResult) {
console.error("No Gitea Provider found:", { giteaId });
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
@ -78,21 +73,11 @@ export const findGiteaById = async (giteaId: string) => {
return giteaProviderResult;
} catch (error) {
console.error("Error finding Gitea Provider:", error);
throw error;
}
};
export const updateGitea = async (giteaId: string, input: Partial<Gitea>) => {
console.log("Updating Gitea Provider:", {
giteaId,
updateData: {
accessTokenPresent: !!input.accessToken,
refreshTokenPresent: !!input.refreshToken,
expiresAt: input.expiresAt,
},
});
try {
const updateResult = await db
.update(gitea)
@ -103,13 +88,11 @@ export const updateGitea = async (giteaId: string, input: Partial<Gitea>) => {
const result = updateResult[0] as Gitea | undefined;
if (!result) {
console.error("No rows were updated", { giteaId, input });
throw new Error(`Failed to update Gitea provider with ID ${giteaId}`);
}
return result;
} catch (error) {
console.error("Error updating Gitea provider:", error);
throw error;
}
};

View File

@ -1,46 +1,18 @@
import { createWriteStream } from "node:fs";
import * as fs from "node:fs/promises";
import { join } from "node:path";
// @ts-ignore: Cannot find module errors
import { paths } from "@dokploy/server/constants";
// @ts-ignore: Cannot find module errors
import { findGiteaById, updateGitea } from "@dokploy/server/services/gitea";
import {
findGiteaById,
type Gitea,
updateGitea,
} from "@dokploy/server/services/gitea";
import { TRPCError } from "@trpc/server";
import { recreateDirectory } from "../filesystem/directory";
import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
import type { Compose } from "@dokploy/server/services/compose";
import type { InferResultType } from "@dokploy/server/types/with";
/**
* Wrapper function to maintain compatibility with the existing implementation
*/
export const fetchGiteaBranches = async (
giteaId: string,
repoFullName: string,
) => {
// Ensure owner and repo are non-empty strings
const parts = repoFullName.split("/");
// Validate that we have exactly two parts
if (parts.length !== 2 || !parts[0] || !parts[1]) {
throw new Error(
`Invalid repository name format: ${repoFullName}. Expected format: owner/repo`,
);
}
const [owner, repo] = parts;
// Call the existing getGiteaBranches function with the correct object structure
return await getGiteaBranches({
giteaId,
owner,
repo,
id: 0, // Provide a default value for optional id
});
};
/**
* Helper function to check if the required fields are filled for Gitea repository operations
*/
export const getErrorCloneRequirements = (entity: {
giteaRepository?: string | null;
giteaOwner?: string | null;
@ -138,11 +110,15 @@ export const refreshGiteaToken = async (giteaProviderId: string) => {
}
};
/**
* Generate a secure Git clone command with proper validation
*/
export type ApplicationWithGitea = InferResultType<
"applications",
{ gitea: true }
>;
export type ComposeWithGitea = InferResultType<"compose", { gitea: true }>;
export const getGiteaCloneCommand = async (
entity: any,
entity: ApplicationWithGitea | ComposeWithGitea,
logPath: string,
isCompose = false,
) => {
@ -153,6 +129,7 @@ export const getGiteaCloneCommand = async (
giteaOwner,
giteaRepository,
serverId,
gitea,
} = entity;
if (!serverId) {
@ -163,6 +140,12 @@ export const getGiteaCloneCommand = async (
}
if (!giteaId) {
const command = `
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
@ -171,282 +154,189 @@ export const getGiteaCloneCommand = async (
// Use paths(true) for remote operations
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
await refreshGiteaToken(giteaId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
const giteaProvider = await findGiteaById(giteaId);
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
const baseUrl = gitea?.giteaUrl.replace(/^https?:\/\//, "");
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
const cloneUrl = `https://oauth2:${gitea?.accessToken}@${baseUrl}/${repoClone}`;
const cloneCommand = `
# Ensure output directory exists and is empty
rm -rf ${outputPath};
mkdir -p ${outputPath};
# Clone with detailed logging
echo "Cloning repository to ${outputPath}" >> ${logPath};
echo "Repository: ${repoClone}" >> ${logPath};
if ! git clone --branch ${giteaBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
exit 1;
fi
# Verify clone
CLONE_COUNT=$(find ${outputPath} -type f | wc -l)
echo "Files cloned: $CLONE_COUNT" >> ${logPath};
if [ "$CLONE_COUNT" -eq 0 ]; then
echo "⚠️ WARNING: No files cloned" >> ${logPath};
exit 1;
fi
echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath};
`;
return cloneCommand;
};
/**
* Main function to clone a Gitea repository with improved validation and robust directory handling
*/
interface CloneGiteaRepository {
appName: string;
giteaBranch: string;
giteaId: string;
giteaOwner: string;
giteaRepository: string;
}
export const cloneGiteaRepository = async (
entity: any,
logPath?: string,
entity: CloneGiteaRepository,
logPath: string,
isCompose = false,
) => {
// If logPath is not provided, generate a default log path
const actualLogPath =
logPath ||
join(
paths()[isCompose ? "COMPOSE_PATH" : "APPLICATIONS_PATH"],
entity.appName,
"clone.log",
);
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(actualLogPath, { flags: "a" });
const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository } = entity;
if (!giteaId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
});
}
await refreshGiteaToken(giteaId);
const giteaProvider = await findGiteaById(giteaId);
if (!giteaProvider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea provider not found in the database",
});
}
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
try {
if (!giteaId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
});
}
// Refresh the access token
await refreshGiteaToken(giteaId);
// Fetch the Gitea provider
const giteaProvider = await findGiteaById(giteaId);
if (!giteaProvider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea provider not found in the database",
});
}
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
// Log path information
writeStream.write("\nPath Information:\n");
writeStream.write(`Base Path: ${basePath}\n`);
writeStream.write(`Output Path: ${outputPath}\n`);
writeStream.write(`\nRecreating directory: ${outputPath}\n`);
await recreateDirectory(outputPath);
// Additional step - verify directory exists and is empty
try {
const filesCheck = await fs.readdir(outputPath);
writeStream.write(
`Directory after cleanup - files: ${filesCheck.length}\n`,
);
if (filesCheck.length > 0) {
writeStream.write("WARNING: Directory not empty after cleanup!\n");
// Force remove with shell command if recreateDirectory didn't work
if (entity.serverId) {
writeStream.write("Attempting forceful cleanup via shell command\n");
await execAsyncRemote(
entity.serverId,
`rm -rf "${outputPath}" && mkdir -p "${outputPath}"`,
(data) => writeStream.write(`Cleanup output: ${data}\n`),
);
} else {
// Fallback to direct fs operations if serverId not available
writeStream.write("Attempting direct fs removal\n");
await fs.rm(outputPath, { recursive: true, force: true });
await fs.mkdir(outputPath, { recursive: true });
await spawnAsync(
"git",
[
"clone",
"--branch",
giteaBranch,
"--depth",
"1",
"--recurse-submodules",
cloneUrl,
outputPath,
"--progress",
],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
}
} catch (verifyError) {
writeStream.write(`Error verifying directory: ${verifyError}\n`);
// Continue anyway - the clone operation might handle this
}
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
writeStream.write(
`Clone URL (masked): https://oauth2:***@${baseUrl}/${repoClone}\n`,
},
);
// First try standard git clone
try {
await spawnAsync(
"git",
[
"clone",
"--branch",
giteaBranch,
"--depth",
"1",
"--recurse-submodules",
cloneUrl,
outputPath,
"--progress",
],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
writeStream.write("\nStandard git clone succeeded\n");
} catch (cloneError) {
writeStream.write(`\nStandard git clone failed: ${cloneError}\n`);
writeStream.write("Falling back to git init + fetch approach...\n");
// Retry cleanup one more time
if (entity.serverId) {
await execAsyncRemote(
entity.serverId,
`rm -rf "${outputPath}" && mkdir -p "${outputPath}"`,
(data) => writeStream.write(`Cleanup retry: ${data}\n`),
);
} else {
await fs.rm(outputPath, { recursive: true, force: true });
await fs.mkdir(outputPath, { recursive: true });
}
// Initialize git repo
writeStream.write("Initializing git repository...\n");
await spawnAsync("git", ["init", outputPath], (data) =>
writeStream.write(data),
);
// Set remote origin
writeStream.write("Setting remote origin...\n");
await spawnAsync(
"git",
["-C", outputPath, "remote", "add", "origin", cloneUrl],
(data) => writeStream.write(data),
);
// Fetch branch
writeStream.write(`Fetching branch: ${giteaBranch}...\n`);
await spawnAsync(
"git",
["-C", outputPath, "fetch", "--depth", "1", "origin", giteaBranch],
(data) => writeStream.write(data),
);
// Checkout branch
writeStream.write(`Checking out branch: ${giteaBranch}...\n`);
await spawnAsync(
"git",
["-C", outputPath, "checkout", "FETCH_HEAD"],
(data) => writeStream.write(data),
);
writeStream.write("Git init and fetch completed successfully\n");
}
// Verify clone
const files = await fs.readdir(outputPath);
writeStream.write("\nClone Verification:\n");
writeStream.write(`Files found: ${files.length}\n`);
if (files.length > 0) {
// Using a for loop instead of forEach
for (let i = 0; i < Math.min(files.length, 10); i++) {
writeStream.write(`- ${files[i]}\n`);
}
}
if (files.length === 0) {
throw new Error("Repository clone failed - directory is empty");
}
writeStream.write(`\nCloned ${repoClone} successfully: ✅\n`);
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
} catch (error) {
writeStream.write(`\nClone Error: ${error}\n`);
writeStream.write(`ERROR Clonning: ${error}: ❌`);
throw error;
} finally {
writeStream.end();
}
};
/**
* Clone a Gitea repository locally for a Compose configuration
* Leverages the existing comprehensive cloneGiteaRepository function
*/
export const cloneRawGiteaRepository = async (entity: any) => {
// Merge the existing entity with compose-specific properties
const composeEntity = {
...entity,
sourceType: "compose",
isCompose: true,
};
export const cloneRawGiteaRepository = async (entity: Compose) => {
const { appName, giteaRepository, giteaOwner, giteaBranch, giteaId } = entity;
const { COMPOSE_PATH } = paths();
// Call cloneGiteaRepository with the modified entity
await cloneGiteaRepository(composeEntity);
if (!giteaId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
});
}
await refreshGiteaToken(giteaId);
const giteaProvider = await findGiteaById(giteaId);
if (!giteaProvider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea provider not found in the database",
});
}
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
try {
await spawnAsync("git", [
"clone",
"--branch",
giteaBranch!,
"--depth",
"1",
"--recurse-submodules",
cloneUrl,
outputPath,
"--progress",
]);
} catch (error) {
throw error;
}
};
/**
* Clone a Gitea repository remotely for a Compose configuration
* Uses the existing getGiteaCloneCommand function for remote cloning
*/
export const cloneRawGiteaRepositoryRemote = async (compose: any) => {
const { COMPOSE_PATH } = paths(true);
const logPath = join(COMPOSE_PATH, compose.appName, "clone.log");
export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
const {
appName,
giteaRepository,
giteaOwner,
giteaBranch,
giteaId,
serverId,
} = compose;
// Reuse the existing getGiteaCloneCommand function
const command = await getGiteaCloneCommand(
{
...compose,
isCompose: true,
},
logPath,
true,
);
if (!compose.serverId) {
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
// Execute the clone command on the remote server
await execAsyncRemote(compose.serverId, command);
if (!giteaId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
});
}
const { COMPOSE_PATH } = paths(true);
const giteaProvider = await findGiteaById(giteaId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${giteaBranch} --depth 1 ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {
throw error;
}
};
// Helper function to check if a Gitea provider meets the necessary requirements
export const haveGiteaRequirements = (giteaProvider: any) => {
export const haveGiteaRequirements = (giteaProvider: Gitea) => {
return !!(giteaProvider?.clientId && giteaProvider?.clientSecret);
};
/**
* Function to test the connection to a Gitea provider
*/
export const testGiteaConnection = async (input: { giteaId: string }) => {
try {
const { giteaId } = input;
@ -455,7 +345,6 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
throw new Error("Gitea provider not found");
}
// Fetch the Gitea provider from the database
const giteaProvider = await findGiteaById(giteaId);
if (!giteaProvider) {
throw new TRPCError({
@ -464,16 +353,8 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
});
}
console.log("Gitea Provider Found:", {
id: giteaProvider.giteaId,
url: giteaProvider.giteaUrl,
hasAccessToken: !!giteaProvider.accessToken,
});
// Refresh the token if needed
await refreshGiteaToken(giteaId);
// Fetch the provider again in case the token was refreshed
const provider = await findGiteaById(giteaId);
if (!provider || !provider.accessToken) {
throw new TRPCError({
@ -482,15 +363,9 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
});
}
// Make API request to test connection
console.log("Making API request to test connection...");
// Construct proper URL for the API request
const baseUrl = provider.giteaUrl.replace(/\/+$/, ""); // Remove trailing slashes
const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
const url = `${baseUrl}/api/v1/user/repos`;
console.log(`Testing connection to: ${url}`);
const response = await fetch(url, {
headers: {
Accept: "application/json",
@ -499,45 +374,31 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
});
if (!response.ok) {
const errorText = await response.text();
console.error("Repository API failed:", errorText);
throw new Error(
`Failed to connect to Gitea API: ${response.status} ${response.statusText}`,
);
}
const repos = await response.json();
console.log(
`Successfully connected to Gitea API. Found ${repos.length} repositories.`,
);
// Update lastAuthenticatedAt
await updateGitea(giteaId, {
lastAuthenticatedAt: Math.floor(Date.now() / 1000),
});
return repos.length;
} catch (error) {
console.error("Gitea Connection Test Error:", error);
throw error;
}
};
/**
* Function to fetch repositories from a Gitea provider
*/
export const getGiteaRepositories = async (giteaId?: string) => {
if (!giteaId) {
return [];
}
// Refresh the token
await refreshGiteaToken(giteaId);
// Fetch the Gitea provider
const giteaProvider = await findGiteaById(giteaId);
// Construct the URL for fetching repositories
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
const url = `${baseUrl}/api/v1/user/repos`;
@ -557,7 +418,6 @@ export const getGiteaRepositories = async (giteaId?: string) => {
const repositories = await response.json();
// Map repositories to a consistent format
const mappedRepositories = repositories.map((repo: any) => ({
id: repo.id,
name: repo.name,
@ -570,9 +430,6 @@ export const getGiteaRepositories = async (giteaId?: string) => {
return mappedRepositories;
};
/**
* Function to fetch branches for a specific Gitea repository
*/
export const getGiteaBranches = async (input: {
id?: number;
giteaId?: string;
@ -583,10 +440,8 @@ export const getGiteaBranches = async (input: {
return [];
}
// Fetch the Gitea provider
const giteaProvider = await findGiteaById(input.giteaId);
// Construct the URL for fetching branches
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
const url = `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches`;
@ -603,7 +458,6 @@ export const getGiteaBranches = async (input: {
const branches = await response.json();
// Map branches to a consistent format
return branches.map((branch: any) => ({
id: branch.name,
name: branch.name,
@ -612,15 +466,3 @@ export const getGiteaBranches = async (input: {
},
}));
};
export default {
cloneGiteaRepository,
cloneRawGiteaRepository,
cloneRawGiteaRepositoryRemote,
refreshGiteaToken,
haveGiteaRequirements,
testGiteaConnection,
getGiteaRepositories,
getGiteaBranches,
fetchGiteaBranches,
};