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({ .object({
repo: z.string().min(1, "Repo is required"), repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"), owner: z.string().min(1, "Owner is required"),
giteaPathNamespace: z.string().min(1), giteaPathNamespace: z.string(),
id: z.number().nullable(), id: z.number().nullable(),
watchPaths: z.array(z.string()).default([]),
}) })
.required(), .required(),
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"), giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]),
}); });
type GiteaProvider = z.infer<typeof GiteaProviderSchema>; type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
@ -97,10 +97,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
repo: "", repo: "",
giteaPathNamespace: "", giteaPathNamespace: "",
id: null, id: null,
watchPaths: [],
}, },
giteaId: "", giteaId: "",
branch: "", branch: "",
watchPaths: [],
}, },
resolver: zodResolver(GiteaProviderSchema), resolver: zodResolver(GiteaProviderSchema),
}); });
@ -146,10 +146,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
owner: data.giteaOwner || "", owner: data.giteaOwner || "",
giteaPathNamespace: data.giteaPathNamespace || "", giteaPathNamespace: data.giteaPathNamespace || "",
id: data.giteaProjectId, id: data.giteaProjectId,
watchPaths: data.watchPaths || [],
}, },
buildPath: data.giteaBuildPath || "/", buildPath: data.giteaBuildPath || "/",
giteaId: data.giteaId || "", giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [],
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@ -164,7 +164,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
applicationId, applicationId,
giteaProjectId: data.repository.id, giteaProjectId: data.repository.id,
giteaPathNamespace: data.repository.giteaPathNamespace, giteaPathNamespace: data.repository.giteaPathNamespace,
watchPaths: data.repository.watchPaths, watchPaths: data.watchPaths,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provider Saved"); toast.success("Service Provider Saved");
@ -198,7 +198,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
repo: "", repo: "",
id: null, id: null,
giteaPathNamespace: "", giteaPathNamespace: "",
watchPaths: [],
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
@ -285,7 +284,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
repo: repo.name, repo: repo.name,
id: repo.id, id: repo.id,
giteaPathNamespace: repo.name, giteaPathNamespace: repo.name,
watchPaths: [],
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
@ -413,7 +411,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
/> />
<FormField <FormField
control={form.control} control={form.control}
name="repository.watchPaths" name="watchPaths"
render={({ field }) => ( render={({ field }) => (
<FormItem className="md:col-span-2"> <FormItem className="md:col-span-2">
<div className="flex items-center gap-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 { api } from "@/utils/api";
import type { Branch, Repository } from "@/utils/gitea-utils"; import type { Branch, Repository } from "@/utils/gitea-utils";
import { zodResolver } from "@hookform/resolvers/zod"; 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 Link from "next/link";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@ -55,7 +55,7 @@ const GiteaProviderSchema = z.object({
repo: z.string().min(1, "Repo is required"), repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"), owner: z.string().min(1, "Owner is required"),
id: z.number().nullable(), id: z.number().nullable(),
giteaPathNamespace: z.string().min(1), giteaPathNamespace: z.string(),
}) })
.required(), .required(),
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
@ -95,6 +95,8 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
const repository = form.watch("repository"); const repository = form.watch("repository");
const giteaId = form.watch("giteaId"); const giteaId = form.watch("giteaId");
console.log(repository);
const { const {
data: repositories, data: repositories,
isLoading: isLoadingRepositories, isLoading: isLoadingRepositories,
@ -126,6 +128,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
useEffect(() => { useEffect(() => {
if (data) { if (data) {
console.log(data.giteaRepository);
form.reset({ form.reset({
branch: data.giteaBranch || "", branch: data.giteaBranch || "",
repository: { repository: {
@ -452,20 +455,20 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
/> />
<Button <Button
type="button" type="button"
variant="secondary" variant="outline"
size="icon"
onClick={() => { onClick={() => {
const input = document.querySelector( const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]', 'input[placeholder*="Enter a path"]',
) as HTMLInputElement; ) as HTMLInputElement;
const value = input.value.trim(); const path = input.value.trim();
if (value) { if (path) {
const newPaths = [...(field.value || []), value]; field.onChange([...field.value, path]);
form.setValue("watchPaths", newPaths);
input.value = ""; input.value = "";
} }
}} }}
> >
Add <Plus className="size-4" />
</Button> </Button>
</div> </div>
</FormControl> </FormControl>
@ -475,13 +478,11 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
/> />
</div> </div>
<Button <div className="flex justify-end">
type="submit" <Button type="submit" isLoading={isSavingGiteaProvider}>
className="w-full" Save
disabled={isSavingGiteaProvider || !form.formState.isDirty}
>
{isSavingGiteaProvider ? "Saving..." : "Save Gitea Provider"}
</Button> </Button>
</div>
</form> </form>
</Form> </Form>
</div> </div>

View File

@ -22,14 +22,9 @@ import {
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
export const giteaRouter = createTRPCRouter({ export const giteaRouter = createTRPCRouter({
// Create a new Gitea provider
create: protectedProcedure create: protectedProcedure
.input(apiCreateGitea) .input(apiCreateGitea)
.mutation( .mutation(async ({ input, ctx }) => {
async ({
input,
ctx,
}: { input: typeof apiCreateGitea._input; ctx: any }) => {
try { try {
return await createGitea(input, ctx.session.activeOrganizationId); return await createGitea(input, ctx.session.activeOrganizationId);
} catch (error) { } catch (error) {
@ -39,17 +34,11 @@ export const giteaRouter = createTRPCRouter({
cause: error, cause: error,
}); });
} }
}, }),
),
// Fetch a specific Gitea provider by ID
one: protectedProcedure one: protectedProcedure
.input(apiFindOneGitea) .input(apiFindOneGitea)
.query( .query(async ({ input, ctx }) => {
async ({
input,
ctx,
}: { input: typeof apiFindOneGitea._input; ctx: any }) => {
const giteaProvider = await findGiteaById(input.giteaId); const giteaProvider = await findGiteaById(input.giteaId);
if ( if (
giteaProvider.gitProvider.organizationId !== giteaProvider.gitProvider.organizationId !==
@ -61,10 +50,8 @@ export const giteaRouter = createTRPCRouter({
}); });
} }
return giteaProvider; return giteaProvider;
}, }),
),
// Fetch all Gitea providers for the active organization
giteaProviders: protectedProcedure.query(async ({ ctx }: { ctx: any }) => { giteaProviders: protectedProcedure.query(async ({ ctx }: { ctx: any }) => {
let result = await db.query.gitea.findMany({ let result = await db.query.gitea.findMany({
with: { with: {
@ -72,14 +59,12 @@ export const giteaRouter = createTRPCRouter({
}, },
}); });
// Filter by organization ID
result = result.filter( result = result.filter(
(provider) => (provider) =>
provider.gitProvider.organizationId === provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId, ctx.session.activeOrganizationId,
); );
// Filter providers that meet the requirements
const filtered = result const filtered = result
.filter((provider) => haveGiteaRequirements(provider)) .filter((provider) => haveGiteaRequirements(provider))
.map((provider) => { .map((provider) => {
@ -94,14 +79,9 @@ export const giteaRouter = createTRPCRouter({
return filtered; return filtered;
}), }),
// Fetch repositories from Gitea provider
getGiteaRepositories: protectedProcedure getGiteaRepositories: protectedProcedure
.input(apiFindOneGitea) .input(apiFindOneGitea)
.query( .query(async ({ input, ctx }) => {
async ({
input,
ctx,
}: { input: typeof apiFindOneGitea._input; ctx: any }) => {
const { giteaId } = input; const { giteaId } = input;
if (!giteaId) { if (!giteaId) {
@ -123,7 +103,9 @@ export const giteaRouter = createTRPCRouter({
} }
try { try {
return await getGiteaRepositories(giteaId); const repositories = await getGiteaRepositories(giteaId);
console.log(repositories);
return repositories;
} catch (error) { } catch (error) {
console.error("Error fetching Gitea repositories:", error); console.error("Error fetching Gitea repositories:", error);
throw new TRPCError({ throw new TRPCError({
@ -131,17 +113,11 @@ export const giteaRouter = createTRPCRouter({
message: error instanceof Error ? error.message : String(error), message: error instanceof Error ? error.message : String(error),
}); });
} }
}, }),
),
// Fetch branches of a specific Gitea repository
getGiteaBranches: protectedProcedure getGiteaBranches: protectedProcedure
.input(apiFindGiteaBranches) .input(apiFindGiteaBranches)
.query( .query(async ({ input, ctx }) => {
async ({
input,
ctx,
}: { input: typeof apiFindGiteaBranches._input; ctx: any }) => {
const { giteaId, owner, repositoryName } = input; const { giteaId, owner, repositoryName } = input;
if (!giteaId || !owner || !repositoryName) { if (!giteaId || !owner || !repositoryName) {
@ -168,7 +144,7 @@ export const giteaRouter = createTRPCRouter({
giteaId, giteaId,
owner, owner,
repo: repositoryName, repo: repositoryName,
id: 0, // Provide a default value for the optional id id: 0,
}); });
} catch (error) { } catch (error) {
console.error("Error fetching Gitea branches:", error); console.error("Error fetching Gitea branches:", error);
@ -177,17 +153,11 @@ export const giteaRouter = createTRPCRouter({
message: error instanceof Error ? error.message : String(error), message: error instanceof Error ? error.message : String(error),
}); });
} }
}, }),
),
// Test connection to Gitea provider
testConnection: protectedProcedure testConnection: protectedProcedure
.input(apiGiteaTestConnection) .input(apiGiteaTestConnection)
.mutation( .mutation(async ({ input, ctx }) => {
async ({
input,
ctx,
}: { input: typeof apiGiteaTestConnection._input; ctx: any }) => {
const giteaId = input.giteaId ?? ""; const giteaId = input.giteaId ?? "";
try { try {
@ -214,17 +184,11 @@ export const giteaRouter = createTRPCRouter({
message: error instanceof Error ? error.message : String(error), message: error instanceof Error ? error.message : String(error),
}); });
} }
}, }),
),
// Update an existing Gitea provider
update: protectedProcedure update: protectedProcedure
.input(apiUpdateGitea) .input(apiUpdateGitea)
.mutation( .mutation(async ({ input, ctx }) => {
async ({
input,
ctx,
}: { input: typeof apiUpdateGitea._input; ctx: any }) => {
const giteaProvider = await findGiteaById(input.giteaId); const giteaProvider = await findGiteaById(input.giteaId);
if ( if (
giteaProvider.gitProvider.organizationId !== giteaProvider.gitProvider.organizationId !==
@ -236,8 +200,6 @@ export const giteaRouter = createTRPCRouter({
}); });
} }
console.log("Updating Gitea provider:", input);
if (input.name) { if (input.name) {
await updateGitProvider(input.gitProviderId, { await updateGitProvider(input.gitProviderId, {
name: input.name, name: input.name,
@ -254,6 +216,5 @@ export const giteaRouter = createTRPCRouter({
} }
return { success: true }; return { success: true };
}, }),
),
}); });

View File

@ -5,13 +5,12 @@ import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { gitProvider } from "./git-provider"; import { gitProvider } from "./git-provider";
// Gitea table definition
export const gitea = pgTable("gitea", { export const gitea = pgTable("gitea", {
giteaId: text("giteaId") giteaId: text("giteaId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), // Using nanoid for unique ID .$defaultFn(() => nanoid()),
giteaUrl: text("giteaUrl").default("https://gitea.com").notNull(), // Default URL for Gitea giteaUrl: text("giteaUrl").default("https://gitea.com").notNull(),
redirectUri: text("redirect_uri"), redirectUri: text("redirect_uri"),
clientId: text("client_id"), clientId: text("client_id"),
clientSecret: text("client_secret"), clientSecret: text("client_secret"),
@ -26,7 +25,6 @@ export const gitea = pgTable("gitea", {
lastAuthenticatedAt: integer("last_authenticated_at"), lastAuthenticatedAt: integer("last_authenticated_at"),
}); });
// Gitea relations with gitProvider
export const giteaProviderRelations = relations(gitea, ({ one }) => ({ export const giteaProviderRelations = relations(gitea, ({ one }) => ({
gitProvider: one(gitProvider, { gitProvider: one(gitProvider, {
fields: [gitea.gitProviderId], fields: [gitea.gitProviderId],
@ -34,10 +32,8 @@ export const giteaProviderRelations = relations(gitea, ({ one }) => ({
}), }),
})); }));
// Create schema for Gitea
const createSchema = createInsertSchema(gitea); const createSchema = createInsertSchema(gitea);
// API schema for creating a Gitea instance
export const apiCreateGitea = createSchema.extend({ export const apiCreateGitea = createSchema.extend({
clientId: z.string().optional(), clientId: z.string().optional(),
clientSecret: z.string().optional(), clientSecret: z.string().optional(),
@ -54,14 +50,12 @@ export const apiCreateGitea = createSchema.extend({
lastAuthenticatedAt: z.number().optional(), lastAuthenticatedAt: z.number().optional(),
}); });
// API schema for finding one Gitea instance
export const apiFindOneGitea = createSchema export const apiFindOneGitea = createSchema
.extend({ .extend({
giteaId: z.string().min(1), giteaId: z.string().min(1),
}) })
.pick({ giteaId: true }); .pick({ giteaId: true });
// API schema for testing Gitea connection
export const apiGiteaTestConnection = createSchema export const apiGiteaTestConnection = createSchema
.extend({ .extend({
organizationName: z.string().optional(), organizationName: z.string().optional(),
@ -70,7 +64,6 @@ export const apiGiteaTestConnection = createSchema
export type ApiGiteaTestConnection = z.infer<typeof apiGiteaTestConnection>; export type ApiGiteaTestConnection = z.infer<typeof apiGiteaTestConnection>;
// API schema for finding branches in Gitea
export const apiFindGiteaBranches = z.object({ export const apiFindGiteaBranches = z.object({
id: z.number().optional(), id: z.number().optional(),
owner: z.string().min(1), owner: z.string().min(1),
@ -78,7 +71,6 @@ export const apiFindGiteaBranches = z.object({
giteaId: z.string().optional(), giteaId: z.string().optional(),
}); });
// API schema for updating Gitea instance
export const apiUpdateGitea = createSchema.extend({ export const apiUpdateGitea = createSchema.extend({
clientId: z.string().optional(), clientId: z.string().optional(),
clientSecret: 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"; import { db } from "@dokploy/server/db";
// @ts-ignore: Cannot find module errors
import { import {
type apiCreateGitea, type apiCreateGitea,
gitProvider, gitProvider,
@ -15,7 +13,6 @@ export const createGitea = async (
input: typeof apiCreateGitea._type, input: typeof apiCreateGitea._type,
organizationId: string, organizationId: string,
) => { ) => {
// @ts-ignore - Complex transaction type - Added because proper typing in Drizzle in not sufficient
return await db.transaction(async (tx) => { return await db.transaction(async (tx) => {
const newGitProvider = await tx const newGitProvider = await tx
.insert(gitProvider) .insert(gitProvider)
@ -25,7 +22,7 @@ export const createGitea = async (
name: input.name, name: input.name,
}) })
.returning() .returning()
.then((response: (typeof gitProvider.$inferSelect)[]) => response[0]); .then((response) => response[0]);
if (!newGitProvider) { if (!newGitProvider) {
throw new TRPCError({ throw new TRPCError({
@ -50,7 +47,6 @@ export const createGitea = async (
}); });
} }
// Return just the essential data needed by the frontend
return { return {
giteaId: giteaProvider.giteaId, giteaId: giteaProvider.giteaId,
clientId: giteaProvider.clientId, clientId: giteaProvider.clientId,
@ -69,7 +65,6 @@ export const findGiteaById = async (giteaId: string) => {
}); });
if (!giteaProviderResult) { if (!giteaProviderResult) {
console.error("No Gitea Provider found:", { giteaId });
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "Gitea Provider not found", message: "Gitea Provider not found",
@ -78,21 +73,11 @@ export const findGiteaById = async (giteaId: string) => {
return giteaProviderResult; return giteaProviderResult;
} catch (error) { } catch (error) {
console.error("Error finding Gitea Provider:", error);
throw error; throw error;
} }
}; };
export const updateGitea = async (giteaId: string, input: Partial<Gitea>) => { 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 { try {
const updateResult = await db const updateResult = await db
.update(gitea) .update(gitea)
@ -103,13 +88,11 @@ export const updateGitea = async (giteaId: string, input: Partial<Gitea>) => {
const result = updateResult[0] as Gitea | undefined; const result = updateResult[0] as Gitea | undefined;
if (!result) { if (!result) {
console.error("No rows were updated", { giteaId, input });
throw new Error(`Failed to update Gitea provider with ID ${giteaId}`); throw new Error(`Failed to update Gitea provider with ID ${giteaId}`);
} }
return result; return result;
} catch (error) { } catch (error) {
console.error("Error updating Gitea provider:", error);
throw error; throw error;
} }
}; };

View File

@ -1,46 +1,18 @@
import { createWriteStream } from "node:fs"; import { createWriteStream } from "node:fs";
import * as fs from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
// @ts-ignore: Cannot find module errors
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
// @ts-ignore: Cannot find module errors import {
import { findGiteaById, updateGitea } from "@dokploy/server/services/gitea"; findGiteaById,
type Gitea,
updateGitea,
} from "@dokploy/server/services/gitea";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { recreateDirectory } from "../filesystem/directory"; import { recreateDirectory } from "../filesystem/directory";
import { execAsyncRemote } from "../process/execAsync"; import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync"; 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: { export const getErrorCloneRequirements = (entity: {
giteaRepository?: string | null; giteaRepository?: string | null;
giteaOwner?: string | null; giteaOwner?: string | null;
@ -138,11 +110,15 @@ export const refreshGiteaToken = async (giteaProviderId: string) => {
} }
}; };
/** export type ApplicationWithGitea = InferResultType<
* Generate a secure Git clone command with proper validation "applications",
*/ { gitea: true }
>;
export type ComposeWithGitea = InferResultType<"compose", { gitea: true }>;
export const getGiteaCloneCommand = async ( export const getGiteaCloneCommand = async (
entity: any, entity: ApplicationWithGitea | ComposeWithGitea,
logPath: string, logPath: string,
isCompose = false, isCompose = false,
) => { ) => {
@ -153,6 +129,7 @@ export const getGiteaCloneCommand = async (
giteaOwner, giteaOwner,
giteaRepository, giteaRepository,
serverId, serverId,
gitea,
} = entity; } = entity;
if (!serverId) { if (!serverId) {
@ -163,6 +140,12 @@ export const getGiteaCloneCommand = async (
} }
if (!giteaId) { if (!giteaId) {
const command = `
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "Gitea Provider not found", message: "Gitea Provider not found",
@ -171,64 +154,46 @@ export const getGiteaCloneCommand = async (
// Use paths(true) for remote operations // Use paths(true) for remote operations
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
await refreshGiteaToken(giteaId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
const giteaProvider = await findGiteaById(giteaId); const baseUrl = gitea?.giteaUrl.replace(/^https?:\/\//, "");
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
const repoClone = `${giteaOwner}/${giteaRepository}.git`; const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`; const cloneUrl = `https://oauth2:${gitea?.accessToken}@${baseUrl}/${repoClone}`;
const cloneCommand = ` const cloneCommand = `
# Ensure output directory exists and is empty
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${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 if ! git clone --branch ${giteaBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath}; echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
exit 1; exit 1;
fi 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}; echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath};
`; `;
return cloneCommand; return cloneCommand;
}; };
/** interface CloneGiteaRepository {
* Main function to clone a Gitea repository with improved validation and robust directory handling appName: string;
*/ giteaBranch: string;
giteaId: string;
giteaOwner: string;
giteaRepository: string;
}
export const cloneGiteaRepository = async ( export const cloneGiteaRepository = async (
entity: any, entity: CloneGiteaRepository,
logPath?: string, logPath: string,
isCompose = false, isCompose = false,
) => { ) => {
// If logPath is not provided, generate a default log path const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const actualLogPath =
logPath ||
join(
paths()[isCompose ? "COMPOSE_PATH" : "APPLICATIONS_PATH"],
entity.appName,
"clone.log",
);
const writeStream = createWriteStream(actualLogPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository } = entity; const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository } = entity;
try {
if (!giteaId) { if (!giteaId) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
@ -236,10 +201,7 @@ export const cloneGiteaRepository = async (
}); });
} }
// Refresh the access token
await refreshGiteaToken(giteaId); await refreshGiteaToken(giteaId);
// Fetch the Gitea provider
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
if (!giteaProvider) { if (!giteaProvider) {
throw new TRPCError({ throw new TRPCError({
@ -248,58 +210,16 @@ export const cloneGiteaRepository = async (
}); });
} }
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code"); 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); 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 });
}
}
} catch (verifyError) {
writeStream.write(`Error verifying directory: ${verifyError}\n`);
// Continue anyway - the clone operation might handle this
}
const repoClone = `${giteaOwner}/${giteaRepository}.git`; const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, ""); const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`; const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`); writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
writeStream.write(
`Clone URL (masked): https://oauth2:***@${baseUrl}/${repoClone}\n`,
);
// First try standard git clone
try { try {
await spawnAsync( await spawnAsync(
"git", "git",
@ -320,142 +240,26 @@ export const cloneGiteaRepository = async (
} }
}, },
); );
writeStream.write("\nStandard git clone succeeded\n"); writeStream.write(`\nCloned ${repoClone}: ✅\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`);
} catch (error) { } catch (error) {
writeStream.write(`\nClone Error: ${error}\n`); writeStream.write(`ERROR Clonning: ${error}: ❌`);
throw error; throw error;
} finally { } finally {
writeStream.end(); writeStream.end();
} }
}; };
/** export const cloneRawGiteaRepository = async (entity: Compose) => {
* Clone a Gitea repository locally for a Compose configuration const { appName, giteaRepository, giteaOwner, giteaBranch, giteaId } = entity;
* Leverages the existing comprehensive cloneGiteaRepository function const { COMPOSE_PATH } = paths();
*/
export const cloneRawGiteaRepository = async (entity: any) => {
// Merge the existing entity with compose-specific properties
const composeEntity = {
...entity,
sourceType: "compose",
isCompose: true,
};
// Call cloneGiteaRepository with the modified entity
await cloneGiteaRepository(composeEntity);
};
/**
* 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");
// Reuse the existing getGiteaCloneCommand function
const command = await getGiteaCloneCommand(
{
...compose,
isCompose: true,
},
logPath,
true,
);
if (!compose.serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
// Execute the clone command on the remote server
await execAsyncRemote(compose.serverId, command);
};
// Helper function to check if a Gitea provider meets the necessary requirements
export const haveGiteaRequirements = (giteaProvider: any) => {
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;
if (!giteaId) { if (!giteaId) {
throw new Error("Gitea provider not found"); throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
});
} }
await refreshGiteaToken(giteaId);
// Fetch the Gitea provider from the database
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
if (!giteaProvider) { if (!giteaProvider) {
throw new TRPCError({ throw new TRPCError({
@ -464,16 +268,93 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
}); });
} }
console.log("Gitea Provider Found:", { const basePath = COMPOSE_PATH;
id: giteaProvider.giteaId, const outputPath = join(basePath, appName, "code");
url: giteaProvider.giteaUrl, await recreateDirectory(outputPath);
hasAccessToken: !!giteaProvider.accessToken,
}); 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;
}
};
export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
const {
appName,
giteaRepository,
giteaOwner,
giteaBranch,
giteaId,
serverId,
} = compose;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
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;
}
};
export const haveGiteaRequirements = (giteaProvider: Gitea) => {
return !!(giteaProvider?.clientId && giteaProvider?.clientSecret);
};
export const testGiteaConnection = async (input: { giteaId: string }) => {
try {
const { giteaId } = input;
if (!giteaId) {
throw new Error("Gitea provider not found");
}
const giteaProvider = await findGiteaById(giteaId);
if (!giteaProvider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea provider not found in the database",
});
}
// Refresh the token if needed
await refreshGiteaToken(giteaId); await refreshGiteaToken(giteaId);
// Fetch the provider again in case the token was refreshed
const provider = await findGiteaById(giteaId); const provider = await findGiteaById(giteaId);
if (!provider || !provider.accessToken) { if (!provider || !provider.accessToken) {
throw new TRPCError({ throw new TRPCError({
@ -482,15 +363,9 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
}); });
} }
// Make API request to test connection const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
console.log("Making API request to test connection...");
// Construct proper URL for the API request
const baseUrl = provider.giteaUrl.replace(/\/+$/, ""); // Remove trailing slashes
const url = `${baseUrl}/api/v1/user/repos`; const url = `${baseUrl}/api/v1/user/repos`;
console.log(`Testing connection to: ${url}`);
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
Accept: "application/json", Accept: "application/json",
@ -499,45 +374,31 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
}); });
if (!response.ok) { if (!response.ok) {
const errorText = await response.text();
console.error("Repository API failed:", errorText);
throw new Error( throw new Error(
`Failed to connect to Gitea API: ${response.status} ${response.statusText}`, `Failed to connect to Gitea API: ${response.status} ${response.statusText}`,
); );
} }
const repos = await response.json(); const repos = await response.json();
console.log(
`Successfully connected to Gitea API. Found ${repos.length} repositories.`,
);
// Update lastAuthenticatedAt
await updateGitea(giteaId, { await updateGitea(giteaId, {
lastAuthenticatedAt: Math.floor(Date.now() / 1000), lastAuthenticatedAt: Math.floor(Date.now() / 1000),
}); });
return repos.length; return repos.length;
} catch (error) { } catch (error) {
console.error("Gitea Connection Test Error:", error);
throw error; throw error;
} }
}; };
/**
* Function to fetch repositories from a Gitea provider
*/
export const getGiteaRepositories = async (giteaId?: string) => { export const getGiteaRepositories = async (giteaId?: string) => {
if (!giteaId) { if (!giteaId) {
return []; return [];
} }
// Refresh the token
await refreshGiteaToken(giteaId); await refreshGiteaToken(giteaId);
// Fetch the Gitea provider
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
// Construct the URL for fetching repositories
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, ""); const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
const url = `${baseUrl}/api/v1/user/repos`; const url = `${baseUrl}/api/v1/user/repos`;
@ -557,7 +418,6 @@ export const getGiteaRepositories = async (giteaId?: string) => {
const repositories = await response.json(); const repositories = await response.json();
// Map repositories to a consistent format
const mappedRepositories = repositories.map((repo: any) => ({ const mappedRepositories = repositories.map((repo: any) => ({
id: repo.id, id: repo.id,
name: repo.name, name: repo.name,
@ -570,9 +430,6 @@ export const getGiteaRepositories = async (giteaId?: string) => {
return mappedRepositories; return mappedRepositories;
}; };
/**
* Function to fetch branches for a specific Gitea repository
*/
export const getGiteaBranches = async (input: { export const getGiteaBranches = async (input: {
id?: number; id?: number;
giteaId?: string; giteaId?: string;
@ -583,10 +440,8 @@ export const getGiteaBranches = async (input: {
return []; return [];
} }
// Fetch the Gitea provider
const giteaProvider = await findGiteaById(input.giteaId); const giteaProvider = await findGiteaById(input.giteaId);
// Construct the URL for fetching branches
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, ""); const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
const url = `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches`; 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(); const branches = await response.json();
// Map branches to a consistent format
return branches.map((branch: any) => ({ return branches.map((branch: any) => ({
id: branch.name, id: branch.name,
name: 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,
};