Skip to content

Add REST API to fall back an encrypted repository to a file-based repository#1276

Open
minwoox wants to merge 3 commits intoline:mainfrom
minwoox:fallback_fileRepo
Open

Add REST API to fall back an encrypted repository to a file-based repository#1276
minwoox wants to merge 3 commits intoline:mainfrom
minwoox:fallback_fileRepo

Conversation

@minwoox
Copy link
Contributor

@minwoox minwoox commented Mar 13, 2026

Motivation:
After migrating a repository to an encrypted repository, there was no way to revert it back to the original file-based git repository.

Modifications:

  • Add FallbackToFileRepositoryCommand and CommandType.FALLBACK_TO_FILE_REPOSITORY
  • Add POST /projects/{projectName}/repos/{repoName}/migrate/file endpoint, which falls back an encrypted repository.

Result:

  • System administrators can revert an encrypted repository back to the original file-based git state.

…ository

Motivation:
After migrating a repository to an encrypted repository, there was no way to revert it back to the original file-based git repository.

Modifications:
- Add `FallbackToFileRepositoryCommand` and `CommandType.FALLBACK_TO_FILE_REPOSITORY`
- Add `POST /projects/{projectName}/repos/{repoName}/migrate/file` endpoint, which sets fall back an encrypted repository.

Result:
- System administrators can revert an encrypted repository back to the original file-based git state.
@minwoox minwoox added this to the 0.81.0 milestone Mar 13, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

Adds a new "fallback to file repository" feature: a Command type and command class, executor handling, API endpoint, RepositoryManager and GitRepositoryManager support, a wrapper method, and comprehensive tests to migrate an encrypted repository back to file-based storage.

Changes

Cohort / File(s) Summary
Command types & factory
server/src/main/java/com/linecorp/centraldogma/server/command/Command.java, server/src/main/java/com/linecorp/centraldogma/server/command/CommandType.java
Adds new command type FALLBACK_TO_FILE_REPOSITORY, JSON subtype mapping, and static factory fallbackToFileRepository(...).
Command implementation
server/src/main/java/com/linecorp/centraldogma/server/command/FallbackToFileRepositoryCommand.java
New serializable FallbackToFileRepositoryCommand class with constructor, accessor, equals/hashCode, and toStringHelper.
Command execution flow
server/src/main/java/com/linecorp/centraldogma/server/command/StandaloneCommandExecutor.java
Routes new command to a fallbackToFileRepository(...) handler that validates encryption state and schedules async repository fallback on repositoryWorker.
Public API
server/src/main/java/com/linecorp/centraldogma/server/internal/api/RepositoryServiceV1.java
New POST endpoint /projects/{projectName}/repos/{repoName}/migrate/file and server-side flow: set READ_ONLY, issue fallback command, handle failures, and restore ACTIVE status on success.
Repository manager surface
server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/RepositoryManagerWrapper.java, server/src/main/java/com/linecorp/centraldogma/server/storage/repository/RepositoryManager.java
Adds fallbackToFileRepository(String) to wrapper and interface, delegating to underlying manager and updating wrapped map.
Git-backed manager implementation
server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryManager.java
Implements fallbackToFileRepository(...): validates state, removes placeholder, reopens file repo, replaces child, closes encrypted repo, and attempts cleanup with error handling and logging.
Tests
server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/FallbackToFileRepositoryTest.java
New comprehensive tests covering content/history restoration, encryption data cleanup, placeholder removal, usability after fallback, manager restart behavior, and isolation of other repos' encryption data.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant API as RepositoryServiceV1
  participant Executor as StandaloneCommandExecutor
  participant Manager as RepositoryManager
  participant GitMgr as GitRepositoryManager
  participant Worker as RepositoryWorker

  Client->>API: POST /projects/{p}/repos/{r}/migrate/file
  API->>API: set repository status -> READ_ONLY
  API->>Executor: submit FallbackToFileRepositoryCommand
  Executor->>Manager: get repository & validate encrypted
  Executor->>Worker: schedule fallbackToFileRepository(repositoryName)
  Worker->>GitMgr: fallbackToFileRepository(repositoryName)
  GitMgr->>GitMgr: remove placeholder, reopen file repo, replace child, close encrypted repo, cleanup
  GitMgr-->>Worker: completion / error
  Worker-->>Executor: completion
  Executor-->>API: notify result
  API->>API: set repository status -> ACTIVE (or revert on failure)
  API-->>Client: return updated RepositoryDto
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

"I hopped through branches, nibbling byte and file,
Switched encrypto burrows back to plain tile.
Commits safe and sound, history kept neat,
Placeholder gone — oh what a treat! 🐰
— A rabbit who loves a tidy repo smile"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a REST API endpoint to revert encrypted repositories to file-based repositories.
Description check ✅ Passed The description comprehensively covers the motivation, modifications, and expected result, all directly related to the changeset which adds fallback functionality and a new REST API endpoint.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use OpenGrep to find security vulnerabilities and bugs across 17+ programming languages.

OpenGrep is compatible with Semgrep configurations. Add an opengrep.yml or semgrep.yml configuration file to your project to enable OpenGrep analysis.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@server/src/main/java/com/linecorp/centraldogma/server/command/StandaloneCommandExecutor.java`:
- Around line 372-382: The pre-check Repository.isEncrypted() is done on the
calling thread using a captured Repository, which can become stale; move the
encryption-state check into the repositoryWorker task so it re-fetches the
current Repository instance under the same execution context. In
fallbackToFileRepository(FallbackToFileRepositoryCommand), remove the early
isEncrypted() call and, inside the CompletableFuture.supplyAsync(...) block
(which runs on repositoryWorker), call repositoryManager.get(c.repositoryName())
again, verify isEncrypted() there and throw the IllegalStateException from
inside the async block (or complete exceptionally) before calling
repositoryManager.fallbackToFileRepository(c.repositoryName()).

In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/api/RepositoryServiceV1.java`:
- Around line 326-335: The POST handler
RepositoryServiceV1.fallbackToFileRepository exposes a destructive revert
without explicit user confirmation and without checking for divergence between
the encrypted repo and the preserved Git repo; update the API and implementation
so the operation requires an explicit confirmation flag (e.g., boolean
forceFallback or confirmFallback) or fails if the encrypted Git has diverged.
Concretely: extend the method signature (and incoming request parsing) to accept
a confirmation token/flag, modify validateFallbackPrerequisites to also check
divergence via GitRepositoryManager.fallbackToFileRepository or a new
GitRepositoryManager.hasDiverged(...) call, and enforce that fallback(author,
project, repository) is only invoked when the confirmation flag is present OR
divergence check passes; also apply the same change to the other endpoint(s)
covering lines 337-359 to ensure consistent validation and gating before calling
setRepositoryStatus(..., RepositoryStatus.READ_ONLY) and fallback(...).

In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryManager.java`:
- Around line 226-232: The current fallback flow logs, closes the encrypted repo
via ((GitRepository) encryptedRepository).close(...), and then calls
encryptionStorageManager().deleteRepositoryData(...) which may throw and cause
the whole fallback to be reported as failed; change deleteRepositoryData to be
best-effort: wrap the call in a try/catch that logs any exception (including
context like projectRepositoryName(repositoryName)) at warn/error and does not
rethrow, and consider delegating retries/asynchronous cleanup to a background
executor if transient errors are expected so the command completes and
RepositoryManagerWrapper can refresh its cache after the file-based repo is
already live.

In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/RepositoryManagerWrapper.java`:
- Around line 66-70: The wrapper must perform an atomic handoff so the wrapped
cache is swapped before the old repo is closed: change the workflow so
RepositoryManagerWrapper.fallbackToFileRepository calls a modified
delegate.fallbackToFileRepository that returns the reopened Repository instance
(instead of void), then immediately do repos.replace(repositoryName,
repoWrapper.apply(reopenedRepo)) with that returned instance, and only after
that allow the delegate to finalize/close the old repo; ensure the delegate-side
fallback implementation is updated to return the reopened repo and defer closing
the old repository until after the caller has swapped in the new one.

In
`@server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/FallbackToFileRepositoryTest.java`:
- Around line 76-80: The tearDown() method currently only closes
encryptionStorageManager; update tearDown() to also close any open repository
managers (e.g., reloadedManager and any other RepositoryManager-like fields used
in tests) to avoid leaking JGit/RocksDB handles, and modify the test
fallbackRepoReloadsCorrectlyAfterManagerRestart() to ensure reloadedManager is
closed (either in a finally block or by delegating to tearDown cleanup) so all
repository managers are closed after each test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 66bee47f-29cf-454a-a320-4712ab0649be

📥 Commits

Reviewing files that changed from the base of the PR and between 70ccfa9 and 17a81b1.

📒 Files selected for processing (9)
  • server/src/main/java/com/linecorp/centraldogma/server/command/Command.java
  • server/src/main/java/com/linecorp/centraldogma/server/command/CommandType.java
  • server/src/main/java/com/linecorp/centraldogma/server/command/FallbackToFileRepositoryCommand.java
  • server/src/main/java/com/linecorp/centraldogma/server/command/StandaloneCommandExecutor.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/api/RepositoryServiceV1.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/RepositoryManagerWrapper.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryManager.java
  • server/src/main/java/com/linecorp/centraldogma/server/storage/repository/RepositoryManager.java
  • server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/FallbackToFileRepositoryTest.java

Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 👍

logger.warn("Failed to restore the encrypted repository placeholder file at: {}",
repoDir, ex);
}
throw new StorageException(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note) I wasn't able to find where these exceptions would be logged - no problem as long as they are logged somewhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/FallbackToFileRepositoryTest.java (1)

221-247: Add a negative-path test for repos that were never migrated.

This block already creates otherRepo as an encrypted-from-scratch repository. A dedicated assertion that fallbackToFileRepository(otherRepo) fails and keeps .encryption-repo-placeholder intact would lock down the unsupported case that the new production path currently has to roll back.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/FallbackToFileRepositoryTest.java`
around lines 221 - 247, Add a negative-path assertion for repos that were never
migrated: in FallbackToFileRepositoryTest (e.g., in or alongside
fallbackPreservesOtherRepoEncryptionData) call
gitRepositoryManager.fallbackToFileRepository(otherRepo) wrapped in an
assertThrows (expecting the appropriate runtime/illegal state exception) to
confirm the operation fails, then assert
gitRepositoryManager.get(otherRepo).isEncrypted() remains true and the
repository still contains the ".encryption-repo-placeholder" marker (verify the
placeholder file exists in the repo storage or via the same storage API used
elsewhere) to ensure the placeholder is not removed on a failed fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryManager.java`:
- Around line 202-208: The restore of the durable placeholder file in
GitRepositoryManager must not be downgraded to a warn; when
Files.createFile(Paths.get(..., ENCRYPTED_REPO_PLACEHOLDER_FILE)) fails inside
the rollback paths, attach that failure to the original rollback exception and
rethrow so callers of openChild() see the combined failure. Concretely, in the
rollback blocks where you currently catch IOException and logger.warn(...),
instead catch the IOException, call
originalException.addSuppressed(restoreIOException) or wrap both into a new
IOException (or RuntimeException) that includes both messages, and rethrow the
original/combined exception; apply the same change to both restore sites shown
around the placeholder-restore code paths so the restore failure surfaces to
callers.
- Around line 184-195: Before deleting ENCRYPTED_REPO_PLACEHOLDER_FILE, guard
that the repository is actually an encrypted-migrated repo by checking
encryptedRepository.isEncrypted() and verifying the preserved file-based git
data exists in repoDir (e.g. presence of the .git directory or whatever marker
used by migrateToEncryptedRepository()); if either check fails, throw a
StorageException and do not mutate the repoDir so openFileRepository()/rollback
paths won’t run on an invalid state. Update the block around
get(repositoryName)/repoDir() to perform the isEncrypted() and repo-data-exists
checks before calling Files.delete(...) and keep error handling consistent with
existing StorageException usage.

---

Nitpick comments:
In
`@server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/FallbackToFileRepositoryTest.java`:
- Around line 221-247: Add a negative-path assertion for repos that were never
migrated: in FallbackToFileRepositoryTest (e.g., in or alongside
fallbackPreservesOtherRepoEncryptionData) call
gitRepositoryManager.fallbackToFileRepository(otherRepo) wrapped in an
assertThrows (expecting the appropriate runtime/illegal state exception) to
confirm the operation fails, then assert
gitRepositoryManager.get(otherRepo).isEncrypted() remains true and the
repository still contains the ".encryption-repo-placeholder" marker (verify the
placeholder file exists in the repo storage or via the same storage API used
elsewhere) to ensure the placeholder is not removed on a failed fallback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c1446fa5-5faa-4637-bced-7f5d089be8d1

📥 Commits

Reviewing files that changed from the base of the PR and between 17a81b1 and 6522453.

📒 Files selected for processing (2)
  • server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryManager.java
  • server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/FallbackToFileRepositoryTest.java

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryManager.java (1)

210-215: ⚠️ Potential issue | 🟠 Major

Don’t hide placeholder-restore failures in rollback.

After Line 198, .encryption-repo-placeholder is the durable repo-type switch. If either Files.createFile(...) fails here, restart behavior can diverge from the in-memory child, but callers only see the original reopen/replace failure. Add the restore IOException as suppressed and rethrow the combined failure from both rollback paths.

🩹 Proposed fix
         } catch (Throwable t) {
             // Restore the placeholder file so the manager stays in a consistent state.
             try {
-                Files.createFile(Paths.get(repoDir.getPath(), ENCRYPTED_REPO_PLACEHOLDER_FILE));
+                Files.createFile(placeholderPath);
             } catch (IOException ex) {
-                logger.warn("Failed to restore the encrypted repository placeholder file at: {}",
-                            repoDir, ex);
+                t.addSuppressed(ex);
             }
             throw new StorageException("failed to reopen the file-based repository after fallback. " +
                                        "repositoryName: " + projectRepositoryName(repositoryName), t);
         }
@@
         if (!replaceChild(repositoryName, encryptedRepository, fileRepository)) {
             fileRepository.internalClose();
+            final StorageException cause = new StorageException(
+                    "failed to replace the encrypted repository with the file-based repository. " +
+                    "repositoryName: " + projectRepositoryName(repositoryName));
             try {
-                Files.createFile(Paths.get(repoDir.getPath(), ENCRYPTED_REPO_PLACEHOLDER_FILE));
+                Files.createFile(placeholderPath);
             } catch (IOException ex) {
-                logger.warn("Failed to restore the encrypted repository placeholder file at: {}",
-                            repoDir, ex);
+                cause.addSuppressed(ex);
             }
-            throw new StorageException(
-                    "failed to replace the encrypted repository with the file-based repository. " +
-                    "repositoryName: " + projectRepositoryName(repositoryName));
+            throw cause;
         }

Also applies to: 223-226

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryManager.java`
around lines 210 - 215, The rollback code in GitRepositoryManager that calls
Files.createFile(Paths.get(repoDir.getPath(), ENCRYPTED_REPO_PLACEHOLDER_FILE))
currently swallows IOException by logging only; instead, catch that IOException,
add it as a suppressed exception to the original reopen/replace failure (the
primary exception caught earlier in the rollback path), and then rethrow the
original exception so callers see the combined failure; apply the same change to
the other rollback location that restores ENCRYPTED_REPO_PLACEHOLDER_FILE so
both rollback paths attach the placeholder-restore IOException as suppressed and
rethrow the primary exception.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryManager.java`:
- Around line 210-215: The rollback code in GitRepositoryManager that calls
Files.createFile(Paths.get(repoDir.getPath(), ENCRYPTED_REPO_PLACEHOLDER_FILE))
currently swallows IOException by logging only; instead, catch that IOException,
add it as a suppressed exception to the original reopen/replace failure (the
primary exception caught earlier in the rollback path), and then rethrow the
original exception so callers see the combined failure; apply the same change to
the other rollback location that restores ENCRYPTED_REPO_PLACEHOLDER_FILE so
both rollback paths attach the placeholder-restore IOException as suppressed and
rethrow the primary exception.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c04e7a7d-7a8e-4266-a700-9687a76786c9

📥 Commits

Reviewing files that changed from the base of the PR and between 6522453 and 8a70567.

📒 Files selected for processing (1)
  • server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryManager.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants