feat(forge): pluggable forge abstraction + GitLab and Gitea/Forgejo/Codeberg (#1913)

* refactor(forge): forge-neutral foundation (GitHub-only)

Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged.

- Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry.
- GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved.
- forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client).
- createPullRequest reports typed CLI/auth errors instead of a generic message.
- forge-resolver host/remote caches are LRU-bounded.
- Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com.
- Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology.
- Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs.

* feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals

GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals.

- threadIsResolved is part of the neutral timeline item.
- Pipeline load failures show an error instead of an empty section.
- Manual pipeline jobs render as pending.
- Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request).

* feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg)

One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts.

- CI status aggregates commit statuses and Actions runs together.
- Gitea's terminal "warning" state maps to failure on server and client.
- Gitea Actions check details are reachable from the PR pane by workflowRunId.

* refactor(forge): localize compatibility handling

* test(forge): expect normalized GitLab facts

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
nllptrx
2026-07-17 15:03:26 +08:00
committed by GitHub
co-authored by Mohamed Boudra
parent 04d1ebdce0
commit a8ebd390fa
187 changed files with 20988 additions and 2907 deletions
+50 -11
View File
@@ -102,6 +102,7 @@ export const es: TranslationResources = {
addImage: "Agregar imagen",
addFile: "Upload file",
addIssueOrPr: "Agregar problema oPR",
addIssueOrPr_mr: "Agregar problema o MR",
dropImagesHere: "Suelta imágenes aquí",
dropFilesHere: "Drop files here",
editQueuedMessage: "Editar mensaje en cola",
@@ -109,8 +110,8 @@ export const es: TranslationResources = {
openImage: "Abrir imagen adjunta",
removeImage: "Quitar imagen adjunta",
removeFile: "Remove file attachment",
openGithub: "Abrir{{kind}}#{{number}}",
removeGithub: "Quitar{{kind}}#{{number}}",
openGithub: "Abrir {{kind}} {{number}}",
removeGithub: "Quitar {{kind}} {{number}}",
element: "Elemento",
openBrowserElement: "Abrir archivo adjunto de elemento del navegador",
removeBrowserElement: "Eliminar el archivo adjunto del elemento del navegador",
@@ -134,7 +135,9 @@ export const es: TranslationResources = {
searching: "Búsqueda...",
noResults: "No se encontraron resultados.",
searchPlaceholder: "Problemas de búsqueda y relaciones públicas...",
searchPlaceholder_mr: "Problemas de búsqueda y MRs...",
title: "Adjuntar problema oPR",
title_mr: "Adjuntar problema o MR",
},
},
agentControls: {
@@ -624,6 +627,9 @@ export const es: TranslationResources = {
label: "CrearPR",
pending: "CreandoPR...",
success: "PRcreado",
label_mr: "Crear MR",
pending_mr: "Creando MR...",
success_mr: "MR creado",
},
mergeBranch: {
label: "Fusionar localmente",
@@ -646,7 +652,13 @@ export const es: TranslationResources = {
rebase: "Fusionar PR (rebase)",
pending: "Fusionando PR...",
success: "PR fusionado",
squash_mr: "Fusionar MR (squash)",
merge_mr: "Fusionar MR (merge)",
rebase_mr: "Fusionar MR (rebase)",
pending_mr: "Fusionando MR...",
success_mr: "MR fusionado",
},
viewPr_mr: "Ver MR",
autoMerge: {
enableSquash: "Fusión automática (squash)",
enableMerge: "Fusión automática (merge)",
@@ -657,7 +669,8 @@ export const es: TranslationResources = {
disabled: "Fusión automática deshabilitada",
},
unavailable: {
viewPrNoGithub: "VerPRno está disponible en este momento porqueGitHubno está conectado",
viewPrNoForge:
"Ver {{noun}} no está disponible en este momento porque {{brand}} no está conectado",
pullNoRemote:
"Pull no está disponible aquí porque esta rama aún no está conectada a un control remoto",
pullDirty:
@@ -672,10 +685,14 @@ export const es: TranslationResources = {
"Tirar y empujar no está disponible aquí porque esta rama aún no está conectada a un control remoto",
pullAndPushDirty:
"Tirar y empujar no está disponible mientras tenga cambios locales, así que confírmelos o guárdelos primero",
pullAndPushNoIncoming:
"Pull and push no está disponible porque no hay cambios entrantes que traer primero",
pullAndPushInSync:
"Pull and push no está disponible porque esta rama ya está sincronizada",
createPrNoGithub:
"CrearPRno está disponible en este momento porqueGitHubno está conectado",
pullAndPushNothingToPush:
"Pull and push no está disponible porque no hay nada nuevo que enviar después de pull",
createPrNoForge:
"Crear {{noun}} no está disponible en este momento porque {{brand}} no está conectado",
createPrNoCommits:
"CrearPRno está disponible porque esta rama aún no tiene nuevas confirmaciones",
mergeNoBase:
@@ -692,6 +709,10 @@ export const es: TranslationResources = {
"La actualización no está disponible porque esta rama ya está actualizada con{{baseRef}}",
mergePrNoGithub:
"FusionarPRno está disponible en este momento porqueGitHubno está conectado",
archiveNotWorktree:
"El archivo no está disponible aquí porque este espacio de trabajo no se creó como un árbol de trabajoPaseo",
mergePrNoForge:
"Fusionar {{noun}} no está disponible en este momento porque {{brand}} no está conectado",
mergePrMissing:
"FusionarPRno está disponible porque aún no hay una solicitud de extracción",
mergePrDraft:
@@ -705,7 +726,7 @@ export const es: TranslationResources = {
mergePrQueue:
"FusionarPRno está disponible aquí porque este repositorio utiliza una cola de fusión",
mergePrNotReady:
"FusionarPRno está disponible hasta queGitHubinforme que la solicitud de extracción está lista para fusionarse",
"Fusionar {{noun}} no está disponible hasta que {{brand}} informe que {{noun}} está listo para fusionarse",
autoMergeCannotDisable:
"La combinación automática está habilitada, pero esta cuenta no puede deshabilitarla",
},
@@ -756,7 +777,7 @@ export const es: TranslationResources = {
expandAllFolders: "Expandir todas las carpetas",
refreshing: "Refrescante",
refresh: "Refrescar",
refreshState: "Actualizar el estado de git yGitHub",
refreshState: "Actualizar el estado de git y {{brand}}",
failedRefresh: "No se pudo actualizar el estado de git.",
emptyHiddenWhitespace: "No hay cambios visibles después de ocultar espacios en blanco",
emptyUncommitted: "Sin cambios no confirmados",
@@ -792,13 +813,23 @@ export const es: TranslationResources = {
pr: {
actions: {
viewPullRequest: "Ver",
openOn: "Abrir en {{brand}}",
},
sections: {
checks: "cheques",
pipeline: "Pipeline",
reviews: "Reseñas",
},
empty: {
noJobs: "Sin trabajos",
loadingPipeline: "Cargando pipeline...",
pipelineJobsLoadFailed: "No se pudieron cargar los trabajos del pipeline",
allowedToFail: "permitido fallar",
},
approvals: "{{given}} de {{required}} aprobaciones",
accessibility: {
pullRequest: "Solicitud de extracción n.°{{number}}",
pullRequest_mr: "Solicitud de fusión !{{number}}",
},
states: {
draft: "Borrador",
@@ -815,11 +846,19 @@ export const es: TranslationResources = {
time: {
justNow: "En este momento",
},
thread: {
discussion: "Hilo de discusión",
},
errors: {
statusLoadFailed: "No se puede cargar el estado de la solicitud de extracción",
activityLoadFailed: "No se puede cargar la actividad de solicitud de extracción",
},
},
forgeSetup: {
installCli: "Instala la CLI de {{cli}} para usar las funciones de {{brand}}.",
signIn: "Ejecuta {{command}} para usar las funciones de {{brand}}.",
generic: "Configura {{brand}} en este host para usar sus funciones.",
},
},
},
sidebar: {
@@ -951,10 +990,10 @@ export const es: TranslationResources = {
refPicker: {
startingRef: "Árbitro inicial",
chooseStart: "Elige por dónde empezar",
checkoutHint: "¿MiraPR#{{number}}?",
checkoutPr: "Echa un vistazo aPR#{{number}}",
dismissCheckoutHint: "Descartar la sugerencia de pago dePR#{{number}}",
intoBase: "en{{baseRef}}",
checkoutHint: "¿Mira {{noun}} {{numberPrefix}}{{number}}?",
checkoutPr: "Echa un vistazo a {{noun}} {{numberPrefix}}{{number}}",
dismissCheckoutHint: "Descartar la sugerencia de pago de {{noun}} {{numberPrefix}}{{number}}",
intoBase: "en {{baseRef}}",
searching: "Búsqueda...",
noMatchingRefs: "No hay árbitros coincidentes.",
searchPlaceholder: "Buscar sucursales y relaciones públicas",