Fixes found by the live pull/push e2e: - CRITICAL: driveCycle never checked out the 'docmost' branch before applyPullActions, so Docmost content was written straight onto 'main', clobbering local file edits before push could diff them. Now checkout 'docmost' before pull (applyPullActions commits there then checks out main + merges) — mirrors the engine's pull main(). Round-trip now works both ways. - add an unresolved-merge guard (SPEC §9): skip the cycle if the vault is mid-merge instead of failing on checkout. - SAFETY: enabledSpaces() is now STRICT opt-in — only spaces with settings.gitSync.enabled===true; removed the all-spaces fallback that synced every space (incl. a 92-page one) the moment GIT_SYNC_ENABLED flipped. - SAFETY: per-cycle delete cap (GIT_SYNC_MAX_DELETES_PER_CYCLE, default 5): dry-run the push, and if planned deletes exceed the cap, run the apply with deletePage neutralized — phantom absence-deletions from a non-convergent vault can't soft-delete real pages. Fails safe if the dry-run throws. - fix manual trigger: TriggerGitSyncDto.spaceId needs @IsUUID or the global whitelist ValidationPipe strips it (arrived undefined -> vault 'undefined'). Live-verified on an isolated flagged space: push (vault file edit -> Docmost content, stamped lastUpdatedSource='git-sync') and pull (Docmost rename -> vault file + meta) both work; an unrelated 92-page space stayed untouched throughout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
ForbiddenException,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Post,
|
|
Get,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
|
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
|
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
|
import WorkspaceAbilityFactory from '../../core/casl/abilities/workspace-ability.factory';
|
|
import {
|
|
WorkspaceCaslAction,
|
|
WorkspaceCaslSubject,
|
|
} from '../../core/casl/interfaces/workspace-ability.type';
|
|
import { EnvironmentService } from '../environment/environment.service';
|
|
import { IsUUID } from 'class-validator';
|
|
import {
|
|
GitSyncOrchestrator,
|
|
GitSyncRunStatus,
|
|
} from './services/git-sync.orchestrator';
|
|
|
|
/** Body for the manual one-shot trigger. */
|
|
class TriggerGitSyncDto {
|
|
// The global ValidationPipe runs with whitelist:true, which STRIPS any field
|
|
// lacking a validation decorator — without this @IsUUID the spaceId would be
|
|
// dropped and arrive as undefined.
|
|
@IsUUID()
|
|
spaceId: string;
|
|
}
|
|
|
|
/**
|
|
* Ops/testing endpoints for the git-sync control plane (plan §6). Admin-guarded
|
|
* (workspace Manage/Settings, mirroring WorkspaceController) so only workspace
|
|
* admins can force a cycle. Mounted under the global `/api` prefix:
|
|
* - POST /api/git-sync/trigger { spaceId } — run one cycle now (await result),
|
|
* - GET /api/git-sync/status — report whether sync is enabled + config.
|
|
*/
|
|
@UseGuards(JwtAuthGuard)
|
|
@Controller('git-sync')
|
|
export class GitSyncController {
|
|
constructor(
|
|
private readonly orchestrator: GitSyncOrchestrator,
|
|
private readonly environmentService: EnvironmentService,
|
|
private readonly workspaceAbility: WorkspaceAbilityFactory,
|
|
) {}
|
|
|
|
/** Throw unless the caller is a workspace admin (Manage Settings). */
|
|
private assertAdmin(user: User, workspace: Workspace): void {
|
|
const ability = this.workspaceAbility.createForUser(user, workspace);
|
|
if (
|
|
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Settings)
|
|
) {
|
|
throw new ForbiddenException();
|
|
}
|
|
}
|
|
|
|
@HttpCode(HttpStatus.OK)
|
|
@Post('trigger')
|
|
async trigger(
|
|
@Body() dto: TriggerGitSyncDto,
|
|
@AuthUser() user: User,
|
|
@AuthWorkspace() workspace: Workspace,
|
|
): Promise<GitSyncRunStatus> {
|
|
this.assertAdmin(user, workspace);
|
|
// Use the workspace from the request context (never client-supplied).
|
|
return this.orchestrator.runOnce(dto.spaceId, workspace.id);
|
|
}
|
|
|
|
@HttpCode(HttpStatus.OK)
|
|
@Get('status')
|
|
async status(
|
|
@AuthUser() user: User,
|
|
@AuthWorkspace() workspace: Workspace,
|
|
): Promise<{
|
|
enabled: boolean;
|
|
dataDir: string;
|
|
pollIntervalMs: number;
|
|
debounceMs: number;
|
|
serviceUserConfigured: boolean;
|
|
}> {
|
|
this.assertAdmin(user, workspace);
|
|
return {
|
|
enabled: this.environmentService.isGitSyncEnabled(),
|
|
dataDir: this.environmentService.getGitSyncDataDir(),
|
|
pollIntervalMs: this.environmentService.getGitSyncPollIntervalMs(),
|
|
debounceMs: this.environmentService.getGitSyncDebounceMs(),
|
|
serviceUserConfigured: Boolean(
|
|
this.environmentService.getGitSyncServiceUserId(),
|
|
),
|
|
};
|
|
}
|
|
}
|