ExternalSyncController.java

package com.taxonomy.workspace.controller;

import com.taxonomy.workspace.model.RepositoryTopologyMode;
import com.taxonomy.workspace.model.SystemRepository;
import com.taxonomy.workspace.service.ExternalGitSyncService;
import com.taxonomy.workspace.service.SystemRepositoryService;
import com.taxonomy.workspace.service.WorkspaceResolver;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * REST API for external Git repository synchronization.
 *
 * <p>Provides endpoints to fetch from, push to, and fully sync with an
 * external Git remote when the system is configured in
 * {@link RepositoryTopologyMode#EXTERNAL_CANONICAL} mode.
 */
@RestController
@RequestMapping("/api/workspace/external")
@Tag(name = "External Git Sync")
public class ExternalSyncController {

    private final ExternalGitSyncService externalGitSyncService;
    private final SystemRepositoryService systemRepositoryService;
    private final WorkspaceResolver workspaceResolver;

    public ExternalSyncController(ExternalGitSyncService externalGitSyncService,
                                  SystemRepositoryService systemRepositoryService,
                                  WorkspaceResolver workspaceResolver) {
        this.externalGitSyncService = externalGitSyncService;
        this.systemRepositoryService = systemRepositoryService;
        this.workspaceResolver = workspaceResolver;
    }

    @PostMapping("/fetch")
    @Operation(summary = "Fetch from external remote",
            description = "Fetches all branches from the configured external Git remote " +
                    "into the system repository. Requires EXTERNAL_CANONICAL topology mode.")
    public ResponseEntity<Map<String, Object>> fetchFromExternal() {
        try {
            var result = externalGitSyncService.fetchFromExternal();
            Map<String, Object> response = new LinkedHashMap<>();
            response.put("success", true);
            response.put("updates", result.getTrackingRefUpdates().size());
            return ResponseEntity.ok(response);
        } catch (IllegalStateException e) {
            return ResponseEntity.badRequest().body(Map.of(
                    "error", "Configuration error",
                    "message", e.getMessage()
            ));
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body(Map.of(
                    "error", "Fetch failed",
                    "message", e.getMessage()
            ));
        }
    }

    @PostMapping("/push")
    @Operation(summary = "Push to external remote",
            description = "Pushes the shared branch to the configured external Git remote.")
    public ResponseEntity<Map<String, Object>> pushToExternal(
            @RequestParam(required = false) String branch) {
        try {
            String targetBranch = branch != null ? branch : systemRepositoryService.getSharedBranch();
            var result = externalGitSyncService.pushToExternal(targetBranch);
            Map<String, Object> response = new LinkedHashMap<>();
            response.put("success", true);
            response.put("branch", targetBranch);
            return ResponseEntity.ok(response);
        } catch (IllegalStateException e) {
            return ResponseEntity.badRequest().body(Map.of(
                    "error", "Configuration error",
                    "message", e.getMessage()
            ));
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body(Map.of(
                    "error", "Push failed",
                    "message", e.getMessage()
            ));
        }
    }

    @PostMapping("/full-sync")
    @Operation(summary = "Full sync with external remote",
            description = "Fetches from the external remote and merges changes into the shared branch.")
    public ResponseEntity<Map<String, Object>> fullSync() {
        try {
            String username = workspaceResolver.resolveCurrentUsername();
            String commitId = externalGitSyncService.fullSync(username);
            Map<String, Object> response = new LinkedHashMap<>();
            response.put("success", true);
            response.put("commitId", commitId);
            return ResponseEntity.ok(response);
        } catch (IllegalStateException e) {
            return ResponseEntity.badRequest().body(Map.of(
                    "error", "Configuration error",
                    "message", e.getMessage()
            ));
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body(Map.of(
                    "error", "Full sync failed",
                    "message", e.getMessage()
            ));
        }
    }

    @GetMapping("/status")
    @Operation(summary = "Get external sync status",
            description = "Returns the current external sync configuration and timestamps.")
    public ResponseEntity<Map<String, Object>> getStatus() {
        var status = externalGitSyncService.getStatus();
        Map<String, Object> response = new LinkedHashMap<>();
        response.put("externalEnabled", status.externalEnabled());
        response.put("externalUrl", status.externalUrl());
        response.put("lastFetchAt", status.lastFetchAt());
        response.put("lastPushAt", status.lastPushAt());
        response.put("lastFetchCommit", status.lastFetchCommit());
        return ResponseEntity.ok(response);
    }

    @PutMapping("/configure")
    @PreAuthorize("hasRole('ADMIN')")
    @Operation(summary = "Configure external repository",
            description = "Set the external URL and topology mode for the system repository.")
    public ResponseEntity<Map<String, Object>> configure(
            @RequestParam(required = false) String externalUrl,
            @RequestParam(required = false) String topologyMode) {
        try {
            SystemRepository sysRepo = systemRepositoryService.getPrimaryRepository();

            if (externalUrl != null) {
                sysRepo.setExternalUrl(externalUrl);
            }
            if (topologyMode != null) {
                sysRepo.setTopologyMode(RepositoryTopologyMode.valueOf(topologyMode));
            }

            systemRepositoryService.save(sysRepo);

            Map<String, Object> response = new LinkedHashMap<>();
            response.put("success", true);
            response.put("topologyMode", sysRepo.getTopologyMode().name());
            response.put("externalUrl", sysRepo.getExternalUrl());
            return ResponseEntity.ok(response);
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body(Map.of(
                    "error", "Invalid parameter",
                    "message", e.getMessage()
            ));
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body(Map.of(
                    "error", "Configuration failed",
                    "message", e.getMessage()
            ));
        }
    }
}