diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/api/v1/MirrorDto.java b/common/src/main/java/com/linecorp/centraldogma/internal/api/v1/MirrorDto.java index 92c1eccca..098a99087 100644 --- a/common/src/main/java/com/linecorp/centraldogma/internal/api/v1/MirrorDto.java +++ b/common/src/main/java/com/linecorp/centraldogma/internal/api/v1/MirrorDto.java @@ -36,6 +36,7 @@ public final class MirrorDto { private final String id; private final boolean enabled; private final String projectName; + @Nullable private final String schedule; private final String direction; private final String localRepo; @@ -52,7 +53,7 @@ public final class MirrorDto { public MirrorDto(@JsonProperty("id") String id, @JsonProperty("enabled") @Nullable Boolean enabled, @JsonProperty("projectName") String projectName, - @JsonProperty("schedule") String schedule, + @JsonProperty("schedule") @Nullable String schedule, @JsonProperty("direction") String direction, @JsonProperty("localRepo") String localRepo, @JsonProperty("localPath") String localPath, @@ -65,7 +66,7 @@ public MirrorDto(@JsonProperty("id") String id, this.id = requireNonNull(id, "id"); this.enabled = firstNonNull(enabled, true); this.projectName = requireNonNull(projectName, "projectName"); - this.schedule = requireNonNull(schedule, "schedule"); + this.schedule = schedule; this.direction = requireNonNull(direction, "direction"); this.localRepo = requireNonNull(localRepo, "localRepo"); this.localPath = requireNonNull(localPath, "localPath"); @@ -92,6 +93,7 @@ public String projectName() { return projectName; } + @Nullable @JsonProperty("schedule") public String schedule() { return schedule; @@ -155,7 +157,7 @@ public boolean equals(Object o) { return id.equals(mirrorDto.id) && enabled == mirrorDto.enabled && projectName.equals(mirrorDto.projectName) && - schedule.equals(mirrorDto.schedule) && + Objects.equals(schedule, mirrorDto.schedule) && direction.equals(mirrorDto.direction) && localRepo.equals(mirrorDto.localRepo) && localPath.equals(mirrorDto.localPath) && diff --git a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java index 19b84e04b..5013ea069 100644 --- a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java +++ b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java @@ -62,6 +62,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.FetchResult; +import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.TagOpt; import org.eclipse.jgit.transport.URIish; @@ -86,8 +87,11 @@ import com.linecorp.centraldogma.server.MirrorException; import com.linecorp.centraldogma.server.command.Command; import com.linecorp.centraldogma.server.command.CommandExecutor; +import com.linecorp.centraldogma.server.command.CommitResult; import com.linecorp.centraldogma.server.credential.Credential; import com.linecorp.centraldogma.server.mirror.MirrorDirection; +import com.linecorp.centraldogma.server.mirror.MirrorResult; +import com.linecorp.centraldogma.server.mirror.MirrorStatus; import com.linecorp.centraldogma.server.storage.StorageException; import com.linecorp.centraldogma.server.storage.repository.Repository; @@ -115,7 +119,7 @@ abstract class AbstractGitMirror extends AbstractMirror { @Nullable private IgnoreNode ignoreNode; - AbstractGitMirror(String id, boolean enabled, Cron schedule, MirrorDirection direction, + AbstractGitMirror(String id, boolean enabled, @Nullable Cron schedule, MirrorDirection direction, Credential credential, Repository localRepo, String localPath, URI remoteRepoUri, String remotePath, String remoteBranch, @Nullable String gitignore) { @@ -169,7 +173,7 @@ GitWithAuth openGit(File workDir, } } - void mirrorLocalToRemote( + MirrorResult mirrorLocalToRemote( GitWithAuth git, int maxNumFiles, long maxNumBytes) throws GitAPIException, IOException { // TODO(minwoox): Early return if the remote does not have any updates. final Ref headBranchRef = getHeadBranchRef(git); @@ -177,6 +181,7 @@ void mirrorLocalToRemote( final ObjectId headCommitId = fetchRemoteHeadAndGetCommitId(git, headBranchRefName); final org.eclipse.jgit.lib.Repository gitRepository = git.getRepository(); + String description; try (ObjectReader reader = gitRepository.newObjectReader(); TreeWalk treeWalk = new TreeWalk(reader); RevWalk revWalk = new RevWalk(reader)) { @@ -190,9 +195,12 @@ void mirrorLocalToRemote( final Revision remoteCurrentRevision = remoteCurrentRevision(reader, treeWalk, mirrorStatePath); if (localHead.equals(remoteCurrentRevision)) { // The remote repository is up-to date. - logger.debug("The remote repository '{}#{}' already at {}. Local repository: '{}'", - remoteRepoUri(), remoteBranch(), localHead, localRepo().name()); - return; + description = String.format( + "The remote repository '%s#%s' already at %s. Local repository: '%s/%s'", + remoteRepoUri(), remoteBranch(), localHead, + localRepo().parent().name(), localRepo().name()); + logger.debug(description); + return newMirrorResult(MirrorStatus.UP_TO_DATE, description); } // Reset to traverse the tree from the first. @@ -217,19 +225,27 @@ dirCache, new InsertText(mirrorStatePath.substring(1), // Strip the leading '/'. Jackson.writeValueAsPrettyString(mirrorState) + '\n')); } + final String summary = "Mirror '" + localRepo().name() + "' at " + localHead + + " to the repository '" + remoteRepoUri() + '#' + remoteBranch() + "'\n"; + description = summary; final ObjectId nextCommitId = - commit(gitRepository, dirCache, headCommitId, localHead); + commit(gitRepository, dirCache, headCommitId, summary); + logger.info(summary); updateRef(gitRepository, revWalk, headBranchRefName, nextCommitId); } - git.push() - .setRefSpecs(new RefSpec(headBranchRefName)) - .setAtomic(true) - .setTimeout(GIT_TIMEOUT_SECS) - .call(); + final Iterable pushResults = + git.push() + .setRefSpecs(new RefSpec(headBranchRefName)) + .setAtomic(true) + .setTimeout(GIT_TIMEOUT_SECS) + .call(); + final PushResult pushResult = pushResults.iterator().next(); + // TODO(ikhoon): Append remove ref to description; + return newMirrorResult(MirrorStatus.SUCCESS, description); } - void mirrorRemoteToLocal( + MirrorResult mirrorRemoteToLocal( GitWithAuth git, CommandExecutor executor, int maxNumFiles, long maxNumBytes) throws Exception { final String summary; final String detail; @@ -239,7 +255,13 @@ void mirrorRemoteToLocal( final String mirrorStatePath = localPath() + MIRROR_STATE_FILE_NAME; final Revision localRev = localRepo().normalizeNow(Revision.HEAD); if (!needsFetch(headBranchRef, mirrorStatePath, localRev)) { - return; + final String abbrId = headBranchRef.getObjectId().abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name(); + final String message = String.format("Repository '%s/%s' already at %s, %s#%s", + localRepo().parent().name(), localRepo().name(), abbrId, + remoteRepoUri(), remoteBranch()); + // The local repository is up-to date. + logger.debug(message); + return newMirrorResult(MirrorStatus.UP_TO_DATE, message); } // Update the head commit ID again because there's a chance a commit is pushed between the @@ -301,14 +323,12 @@ void mirrorRemoteToLocal( if (++numFiles > maxNumFiles) { throwMirrorException(maxNumFiles, "files"); - return; } final ObjectId objectId = treeWalk.getObjectId(0); final long contentLength = reader.getObjectSize(objectId, ObjectReader.OBJ_ANY); if (numBytes > maxNumBytes - contentLength) { throwMirrorException(maxNumBytes, "bytes"); - return; } numBytes += contentLength; @@ -337,9 +357,11 @@ void mirrorRemoteToLocal( } }); - executor.execute(Command.push( + final CommitResult commitResult = executor.execute(Command.push( MIRROR_AUTHOR, localRepo().parent().name(), localRepo().name(), Revision.HEAD, summary, detail, Markup.PLAINTEXT, changes.values())).join(); + final String description = summary + ", Revision: " + commitResult.revision(); + return newMirrorResult(MirrorStatus.SUCCESS, description); } private boolean needsFetch(Ref headBranchRef, String mirrorStatePath, Revision localRev) @@ -355,9 +377,6 @@ private boolean needsFetch(Ref headBranchRef, String mirrorStatePath, Revision l final ObjectId headCommitId = headBranchRef.getObjectId(); if (headCommitId.name().equals(localSourceRevision)) { - final String abbrId = headCommitId.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name(); - logger.info("Repository '{}' already at {}, {}#{}", localRepo().name(), abbrId, - remoteRepoUri(), remoteBranch()); return false; } return true; @@ -675,7 +694,7 @@ private static String sanitizeText(String text) { } private ObjectId commit(org.eclipse.jgit.lib.Repository gitRepository, DirCache dirCache, - ObjectId headCommitId, Revision localHead) throws IOException { + ObjectId headCommitId, String message) throws IOException { try (ObjectInserter inserter = gitRepository.newObjectInserter()) { // flush the current index to repository and get the result tree object id. final ObjectId nextTreeId = dirCache.writeTree(inserter); @@ -691,11 +710,7 @@ private ObjectId commit(org.eclipse.jgit.lib.Repository gitRepository, DirCache commitBuilder.setTreeId(nextTreeId); commitBuilder.setEncoding(UTF_8); commitBuilder.setParentId(headCommitId); - - final String summary = "Mirror '" + localRepo().name() + "' at " + localHead + - " to the repository '" + remoteRepoUri() + '#' + remoteBranch() + "'\n"; - logger.info(summary); - commitBuilder.setMessage(summary); + commitBuilder.setMessage(message); final ObjectId nextCommitId = inserter.insert(commitBuilder); inserter.flush(); diff --git a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultGitMirror.java b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultGitMirror.java index 069fe1375..6e1f8edf1 100644 --- a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultGitMirror.java +++ b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultGitMirror.java @@ -36,13 +36,14 @@ import com.linecorp.centraldogma.server.internal.credential.AccessTokenCredential; import com.linecorp.centraldogma.server.internal.credential.PasswordCredential; import com.linecorp.centraldogma.server.mirror.MirrorDirection; +import com.linecorp.centraldogma.server.mirror.MirrorResult; import com.linecorp.centraldogma.server.storage.repository.Repository; final class DefaultGitMirror extends AbstractGitMirror { private static final Consumer> NOOP_CONFIGURATOR = command -> {}; - DefaultGitMirror(String id, boolean enabled, Cron schedule, MirrorDirection direction, + DefaultGitMirror(String id, boolean enabled, @Nullable Cron schedule, MirrorDirection direction, Credential credential, Repository localRepo, String localPath, URI remoteRepoUri, String remotePath, String remoteBranch, @Nullable String gitignore) { @@ -51,9 +52,9 @@ final class DefaultGitMirror extends AbstractGitMirror { } @Override - protected void mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes) throws Exception { + protected MirrorResult mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes) throws Exception { try (GitWithAuth git = openGit(workDir, transportCommandConfigurator())) { - mirrorLocalToRemote(git, maxNumFiles, maxNumBytes); + return mirrorLocalToRemote(git, maxNumFiles, maxNumBytes); } } @@ -78,10 +79,10 @@ protected void mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumByt } @Override - protected void mirrorRemoteToLocal(File workDir, CommandExecutor executor, - int maxNumFiles, long maxNumBytes) throws Exception { + protected MirrorResult mirrorRemoteToLocal(File workDir, CommandExecutor executor, + int maxNumFiles, long maxNumBytes) throws Exception { try (GitWithAuth git = openGit(workDir, transportCommandConfigurator())) { - mirrorRemoteToLocal(git, executor, maxNumFiles, maxNumBytes); + return mirrorRemoteToLocal(git, executor, maxNumFiles, maxNumBytes); } } diff --git a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/SshGitMirror.java b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/SshGitMirror.java index 690fd51f6..e53daae20 100644 --- a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/SshGitMirror.java +++ b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/SshGitMirror.java @@ -61,6 +61,7 @@ import com.linecorp.centraldogma.server.internal.credential.PasswordCredential; import com.linecorp.centraldogma.server.internal.credential.PublicKeyCredential; import com.linecorp.centraldogma.server.mirror.MirrorDirection; +import com.linecorp.centraldogma.server.mirror.MirrorResult; import com.linecorp.centraldogma.server.storage.repository.Repository; final class SshGitMirror extends AbstractGitMirror { @@ -79,7 +80,7 @@ final class SshGitMirror extends AbstractGitMirror { // We might create multiple BouncyCastleRandom later and poll them, if necessary. private static final BouncyCastleRandom bounceCastleRandom = new BouncyCastleRandom(); - SshGitMirror(String id, boolean enabled, Cron schedule, MirrorDirection direction, + SshGitMirror(String id, boolean enabled, @Nullable Cron schedule, MirrorDirection direction, Credential credential, Repository localRepo, String localPath, URI remoteRepoUri, String remotePath, String remoteBranch, @Nullable String gitignore) { @@ -89,28 +90,29 @@ final class SshGitMirror extends AbstractGitMirror { } @Override - protected void mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes) throws Exception { + protected MirrorResult mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes) + throws Exception { final URIish remoteUri = remoteUri(); try (SshClient sshClient = createSshClient(); ClientSession session = createSession(sshClient, remoteUri)) { final DefaultGitSshdSessionFactory sessionFactory = new DefaultGitSshdSessionFactory(sshClient, session); try (GitWithAuth git = openGit(workDir, remoteUri, sessionFactory::configureCommand)) { - mirrorLocalToRemote(git, maxNumFiles, maxNumBytes); + return mirrorLocalToRemote(git, maxNumFiles, maxNumBytes); } } } @Override - protected void mirrorRemoteToLocal(File workDir, CommandExecutor executor, - int maxNumFiles, long maxNumBytes) throws Exception { + protected MirrorResult mirrorRemoteToLocal(File workDir, CommandExecutor executor, + int maxNumFiles, long maxNumBytes) throws Exception { final URIish remoteUri = remoteUri(); try (SshClient sshClient = createSshClient(); ClientSession session = createSession(sshClient, remoteUri)) { final DefaultGitSshdSessionFactory sessionFactory = new DefaultGitSshdSessionFactory(sshClient, session); try (GitWithAuth git = openGit(workDir, remoteUri, sessionFactory::configureCommand)) { - mirrorRemoteToLocal(git, executor, maxNumFiles, maxNumBytes); + return mirrorRemoteToLocal(git, executor, maxNumFiles, maxNumBytes); } } } diff --git a/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringServiceTest.java b/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringServiceTest.java index 8751a2704..34de24e2e 100644 --- a/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringServiceTest.java +++ b/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringServiceTest.java @@ -39,6 +39,8 @@ import com.linecorp.centraldogma.server.credential.Credential; import com.linecorp.centraldogma.server.mirror.Mirror; import com.linecorp.centraldogma.server.mirror.MirrorDirection; +import com.linecorp.centraldogma.server.mirror.MirrorResult; +import com.linecorp.centraldogma.server.mirror.MirrorStatus; import com.linecorp.centraldogma.server.storage.project.Project; import com.linecorp.centraldogma.server.storage.project.ProjectManager; import com.linecorp.centraldogma.server.storage.repository.MetaRepository; @@ -74,14 +76,17 @@ void mirroringTaskShouldNeverBeRejected() { Credential.FALLBACK, r, "/", URI.create("unused://uri"), "/", "", null) { @Override - protected void mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes) {} + protected MirrorResult mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes) { + return newMirrorResult(MirrorStatus.UP_TO_DATE, null); + } @Override - protected void mirrorRemoteToLocal(File workDir, CommandExecutor executor, - int maxNumFiles, long maxNumBytes) throws Exception { + protected MirrorResult mirrorRemoteToLocal(File workDir, CommandExecutor executor, + int maxNumFiles, long maxNumBytes) throws Exception { // Sleep longer than mirroring interval so that the workers fall behind. taskCounter.incrementAndGet(); Thread.sleep(2000); + return newMirrorResult(MirrorStatus.SUCCESS, null); } }; diff --git a/server/src/main/java/com/linecorp/centraldogma/server/CentralDogma.java b/server/src/main/java/com/linecorp/centraldogma/server/CentralDogma.java index 4c4e19642..c11ff9f43 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/CentralDogma.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/CentralDogma.java @@ -813,7 +813,8 @@ private void configureHttpApi(ServerBuilder sb, .annotatedService(new RepositoryServiceV1(executor, mds)); if (GIT_MIRROR_ENABLED) { - apiV1ServiceBuilder.annotatedService(new MirroringServiceV1(projectApiManager, executor)) + apiV1ServiceBuilder.annotatedService( + new MirroringServiceV1(projectApiManager, executor, cfg.dataDir())) .annotatedService(new CredentialServiceV1(projectApiManager, executor)); } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java index a92c591a9..e2da49ca6 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java @@ -19,10 +19,15 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableList.toImmutableList; +import java.io.File; import java.net.URI; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import com.cronutils.model.Cron; + +import com.linecorp.armeria.server.annotation.Blocking; import com.linecorp.armeria.server.annotation.ConsumesJson; import com.linecorp.armeria.server.annotation.Get; import com.linecorp.armeria.server.annotation.Param; @@ -38,6 +43,8 @@ import com.linecorp.centraldogma.server.internal.api.auth.RequiresWritePermission; import com.linecorp.centraldogma.server.internal.storage.project.ProjectApiManager; import com.linecorp.centraldogma.server.mirror.Mirror; +import com.linecorp.centraldogma.server.mirror.MirrorResult; +import com.linecorp.centraldogma.server.mirror.MirroringServicePluginConfig; import com.linecorp.centraldogma.server.storage.project.Project; import com.linecorp.centraldogma.server.storage.repository.MetaRepository; @@ -52,9 +59,11 @@ public class MirroringServiceV1 extends AbstractService { // - Add Java APIs to the CentralDogma client private final ProjectApiManager projectApiManager; + private final File workDir; - public MirroringServiceV1(ProjectApiManager projectApiManager, CommandExecutor executor) { + public MirroringServiceV1(ProjectApiManager projectApiManager, CommandExecutor executor, File dataDir) { super(executor); + workDir = new File(dataDir, "_mirrors_manual"); this.projectApiManager = projectApiManager; } @@ -81,7 +90,6 @@ public CompletableFuture> listMirrors(@Param String projectName) @RequiresReadPermission(repository = Project.REPO_META) @Get("/projects/{projectName}/mirrors/{id}") public CompletableFuture getMirror(@Param String projectName, @Param String id) { - return metaRepo(projectName).mirror(id).thenApply(mirror -> { return convertToMirrorDto(projectName, mirror); }); @@ -125,11 +133,27 @@ private CompletableFuture createOrUpdate(String projectName, }); } + @Post("/projects/{projectName}/mirrors/{mirrorId}/run") + @Blocking + public MirrorResult runMirror(@Param String projectName, @Param String mirrorId) throws Exception { + final Mirror mirror = metaRepo(projectName).mirror(mirrorId).get(10, TimeUnit.SECONDS); + if (mirror.schedule() != null) { + throw new UnsupportedOperationException("The mirror is scheduled to run automatically."); + } + + return mirror.mirror(workDir, executor(), + // TODO(ikhoon): Use cfg.pluginConfigMap().get(configType()) + MirroringServicePluginConfig.INSTANCE.maxNumFilesPerMirror(), + MirroringServicePluginConfig.INSTANCE.maxNumBytesPerMirror()); + } + private static MirrorDto convertToMirrorDto(String projectName, Mirror mirror) { final URI remoteRepoUri = mirror.remoteRepoUri(); + final Cron schedule = mirror.schedule(); + final String scheduleStr = schedule != null ? schedule.asString() : null; return new MirrorDto(mirror.id(), mirror.enabled(), projectName, - mirror.schedule().asString(), + scheduleStr, mirror.direction().name(), mirror.localRepo().name(), mirror.localPath(), diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java index 155a8dbaa..6bdcdd428 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java @@ -41,6 +41,8 @@ import com.linecorp.centraldogma.server.credential.Credential; import com.linecorp.centraldogma.server.mirror.Mirror; import com.linecorp.centraldogma.server.mirror.MirrorDirection; +import com.linecorp.centraldogma.server.mirror.MirrorResult; +import com.linecorp.centraldogma.server.mirror.MirrorStatus; import com.linecorp.centraldogma.server.storage.repository.Repository; public abstract class AbstractMirror implements Mirror { @@ -51,7 +53,6 @@ public abstract class AbstractMirror implements Mirror { private final String id; private final boolean enabled; - private final Cron schedule; private final MirrorDirection direction; private final Credential credential; private final Repository localRepo; @@ -61,16 +62,18 @@ public abstract class AbstractMirror implements Mirror { private final String remoteBranch; @Nullable private final String gitignore; + @Nullable + private final Cron schedule; + @Nullable private final ExecutionTime executionTime; private final long jitterMillis; - protected AbstractMirror(String id, boolean enabled, Cron schedule, MirrorDirection direction, + protected AbstractMirror(String id, boolean enabled, @Nullable Cron schedule, MirrorDirection direction, Credential credential, Repository localRepo, String localPath, URI remoteRepoUri, String remotePath, String remoteBranch, @Nullable String gitignore) { this.id = requireNonNull(id, "id"); this.enabled = enabled; - this.schedule = requireNonNull(schedule, "schedule"); this.direction = requireNonNull(direction, "direction"); this.credential = requireNonNull(credential, "credential"); this.localRepo = requireNonNull(localRepo, "localRepo"); @@ -80,14 +83,21 @@ protected AbstractMirror(String id, boolean enabled, Cron schedule, MirrorDirect this.remoteBranch = requireNonNull(remoteBranch, "remoteBranch"); this.gitignore = gitignore; - executionTime = ExecutionTime.forCron(this.schedule); - - // Pre-calculate a constant jitter value up to 1 minute for a mirror. - // Use the properties' hash code so that the same properties result in the same jitter. - jitterMillis = Math.abs(Objects.hash(this.schedule.asString(), this.direction, - this.localRepo.parent().name(), this.localRepo.name(), - this.remoteRepoUri, this.remotePath, this.remoteBranch) / - (Integer.MAX_VALUE / 60000)); + if (schedule != null) { + this.schedule = requireNonNull(schedule, "schedule"); + executionTime = ExecutionTime.forCron(this.schedule); + + // Pre-calculate a constant jitter value up to 1 minute for a mirror. + // Use the properties' hash code so that the same properties result in the same jitter. + jitterMillis = Math.abs(Objects.hash(this.schedule.asString(), this.direction, + this.localRepo.parent().name(), this.localRepo.name(), + this.remoteRepoUri, this.remotePath, this.remoteBranch) / + (Integer.MAX_VALUE / 60000)); + } else { + this.schedule = null; + executionTime = null; + jitterMillis = -1; + } } @Override @@ -164,19 +174,21 @@ public final boolean enabled() { } @Override - public final void mirror(File workDir, CommandExecutor executor, int maxNumFiles, long maxNumBytes) { + public final MirrorResult mirror(File workDir, CommandExecutor executor, int maxNumFiles, + long maxNumBytes) { try { switch (direction()) { case LOCAL_TO_REMOTE: - mirrorLocalToRemote(workDir, maxNumFiles, maxNumBytes); - break; + return mirrorLocalToRemote(workDir, maxNumFiles, maxNumBytes); case REMOTE_TO_LOCAL: - mirrorRemoteToLocal(workDir, executor, maxNumFiles, maxNumBytes); - break; + return mirrorRemoteToLocal(workDir, executor, maxNumFiles, maxNumBytes); + default: + throw new Error("Should never reach here"); } } catch (InterruptedException e) { // Propagate the interruption. Thread.currentThread().interrupt(); + throw new MirrorException(e); } catch (MirrorException e) { throw e; } catch (Exception e) { @@ -184,12 +196,16 @@ public final void mirror(File workDir, CommandExecutor executor, int maxNumFiles } } - protected abstract void mirrorLocalToRemote( + protected abstract MirrorResult mirrorLocalToRemote( File workDir, int maxNumFiles, long maxNumBytes) throws Exception; - protected abstract void mirrorRemoteToLocal( + protected abstract MirrorResult mirrorRemoteToLocal( File workDir, CommandExecutor executor, int maxNumFiles, long maxNumBytes) throws Exception; + protected final MirrorResult newMirrorResult(MirrorStatus mirrorStatus, @Nullable String description) { + return new MirrorResult(id, localRepo.parent().name(), localRepo.name(), mirrorStatus, description); + } + @Override public String toString() { final ToStringHelper helper = MoreObjects.toStringHelper("") diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java index f0a84c7a5..f880cf17c 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java @@ -28,6 +28,7 @@ import com.linecorp.centraldogma.server.command.CommandExecutor; import com.linecorp.centraldogma.server.credential.Credential; import com.linecorp.centraldogma.server.mirror.MirrorDirection; +import com.linecorp.centraldogma.server.mirror.MirrorResult; import com.linecorp.centraldogma.server.storage.repository.Repository; public final class CentralDogmaMirror extends AbstractMirror { @@ -56,13 +57,14 @@ String remoteRepo() { } @Override - protected void mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes) throws Exception { + protected MirrorResult mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes) + throws Exception { throw new UnsupportedOperationException(); } @Override - protected void mirrorRemoteToLocal(File workDir, CommandExecutor executor, - int maxNumFiles, long maxNumBytes) throws Exception { + protected MirrorResult mirrorRemoteToLocal(File workDir, CommandExecutor executor, + int maxNumFiles, long maxNumBytes) throws Exception { throw new UnsupportedOperationException(); } } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringService.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringService.java index 6076ed77a..0190fe766 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringService.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringService.java @@ -185,7 +185,10 @@ private void schedulePendingMirrors() { logger.warn("Failed to load the mirror list from: {}", project.name(), e); return; } - mirrors.forEach(m -> { + for (Mirror m : mirrors) { + if (m.schedule() == null) { + continue; + } try { if (m.nextExecutionTime(currentLastExecutionTime).compareTo(now) < 0) { run(project, m); @@ -193,7 +196,7 @@ private void schedulePendingMirrors() { } catch (Exception e) { logger.warn("Unexpected exception while mirroring: {}", m, e); } - }); + } }); } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java index 58db07d63..dfc0c5e5e 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java @@ -292,10 +292,13 @@ private Command newCommand(Credential credential, Author author, S private static void validateMirror(MirrorDto mirror) { checkArgument(!Strings.isNullOrEmpty(mirror.id()), "Mirror ID is empty"); - final Cron schedule = MirrorConfig.CRON_PARSER.parse(mirror.schedule()); - final CronField secondField = schedule.retrieve(CronFieldName.SECOND); - checkArgument(!secondField.getExpression().asString().contains("*"), - "The second field of the schedule must be specified. (seconds: *, expected: 0-59)"); + final String scheduleString = mirror.schedule(); + if (scheduleString != null) { + final Cron schedule = MirrorConfig.CRON_PARSER.parse(scheduleString); + final CronField secondField = schedule.retrieve(CronFieldName.SECOND); + checkArgument(!secondField.getExpression().asString().contains("*"), + "The second field of the schedule must be specified. (seconds: *, expected: 0-59)"); + } } private static MirrorConfig converterToMirrorConfig(MirrorDto mirrorDto) { diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/MirrorConfig.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/MirrorConfig.java index a553dac74..58a7ef94e 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/MirrorConfig.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/MirrorConfig.java @@ -78,8 +78,8 @@ public final class MirrorConfig { private final URI remoteUri; @Nullable private final String gitignore; - @Nullable private final String credentialId; + @Nullable private final Cron schedule; @JsonCreator @@ -91,10 +91,14 @@ public MirrorConfig(@JsonProperty("id") String id, @JsonProperty("localPath") @Nullable String localPath, @JsonProperty(value = "remoteUri", required = true) URI remoteUri, @JsonProperty("gitignore") @Nullable Object gitignore, - @JsonProperty("credentialId") @Nullable String credentialId) { + @JsonProperty("credentialId") String credentialId) { this.id = requireNonNull(id, "id"); this.enabled = firstNonNull(enabled, true); - this.schedule = CRON_PARSER.parse(firstNonNull(schedule, DEFAULT_SCHEDULE)); + if (schedule != null) { + this.schedule = CRON_PARSER.parse(schedule); + } else { + this.schedule = null; + } this.direction = requireNonNull(direction, "direction"); this.localRepo = requireNonNull(localRepo, "localRepo"); this.localPath = firstNonNull(localPath, "/"); @@ -117,7 +121,7 @@ public MirrorConfig(@JsonProperty("id") String id, } else { this.gitignore = null; } - this.credentialId = credentialId; + this.credentialId = requireNonNull(credentialId, "credentialId"); } @Nullable @@ -195,9 +199,14 @@ public String credentialId() { return credentialId; } + @Nullable @JsonProperty("schedule") public String schedule() { - return schedule.asString(); + if (schedule != null) { + return schedule.asString(); + } else { + return null; + } } @Override diff --git a/server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java b/server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java index 4732a57b3..8c4e94cbf 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java @@ -40,7 +40,9 @@ public interface Mirror { /** * Returns the schedule for the mirroring task. + * {@code null} if the mirroring task is not scheduled. */ + @Nullable Cron schedule(); /** @@ -108,5 +110,5 @@ public interface Mirror { * @param maxNumBytes the maximum bytes allowed to the mirroring task. A {@link MirrorException} would be * raised if the total size of the files to be mirrored exceeds it. */ - void mirror(File workDir, CommandExecutor executor, int maxNumFiles, long maxNumBytes); + MirrorResult mirror(File workDir, CommandExecutor executor, int maxNumFiles, long maxNumBytes); } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorContext.java b/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorContext.java index a5ea678df..56dae0183 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorContext.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorContext.java @@ -35,6 +35,7 @@ public final class MirrorContext { private final String id; private final boolean enabled; + @Nullable private final Cron schedule; private final MirrorDirection direction; private final Credential credential; @@ -47,12 +48,12 @@ public final class MirrorContext { /** * Creates a new instance. */ - public MirrorContext(String id, boolean enabled, Cron schedule, MirrorDirection direction, + public MirrorContext(String id, boolean enabled, @Nullable Cron schedule, MirrorDirection direction, Credential credential, Repository localRepo, String localPath, URI remoteUri, @Nullable String gitignore) { this.id = requireNonNull(id, "id"); this.enabled = enabled; - this.schedule = requireNonNull(schedule, "schedule"); + this.schedule = schedule; this.direction = requireNonNull(direction, "direction"); this.credential = requireNonNull(credential, "credential"); this.localRepo = requireNonNull(localRepo, "localRepo"); @@ -77,7 +78,9 @@ public boolean enabled() { /** * Returns the cron schedule of this mirror. + * {@code null} if this mirror is not scheduled automatically. */ + @Nullable public Cron schedule() { return schedule; } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorResult.java b/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorResult.java new file mode 100644 index 000000000..7dedaa60d --- /dev/null +++ b/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorResult.java @@ -0,0 +1,121 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.centraldogma.server.mirror; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; + +/** + * The result of a mirroring operation. + */ +public final class MirrorResult { + + private final String mirrorId; + private final String projectName; + private final String repoName; + private final MirrorStatus mirrorStatus; + @Nullable + private final String description; + + /** + * Creates a new instance. + */ + public MirrorResult(String mirrorId, String projectName, String repoName, MirrorStatus mirrorStatus, + @Nullable String description) { + this.mirrorId = requireNonNull(mirrorId, "mirrorId"); + this.projectName = requireNonNull(projectName, "projectName"); + this.repoName = requireNonNull(repoName, "repoName"); + this.mirrorStatus = requireNonNull(mirrorStatus, "mirrorStatus"); + this.description = description; + } + + /** + * Returns the ID of the mirror. + */ + @JsonProperty("mirrorId") + public String mirrorId() { + return mirrorId; + } + + /** + * Returns the project name which {@link #mirrorId()} belongs to. + */ + @JsonProperty("projectName") + public String projectName() { + return projectName; + } + + /** + * Returns the repository name where the mirroring operation is performed. + */ + @JsonProperty("repoName") + public String repoName() { + return repoName; + } + + /** + * Returns the status of the mirroring operation. + */ + @JsonProperty("mirrorStatus") + public MirrorStatus mirrorStatus() { + return mirrorStatus; + } + + /** + * Returns the description of the mirroring operation. + */ + @Nullable + @JsonProperty("description") + public String description() { + return description; + } + + @Override + public boolean equals(Object o) { + if (this == o) {return true;} + if (!(o instanceof MirrorResult)) {return false;} + final MirrorResult that = (MirrorResult) o; + return mirrorId.equals(that.mirrorId) && + projectName.equals(that.projectName) && + repoName.equals(that.repoName) && + mirrorStatus == that.mirrorStatus && + Objects.equals(description, that.description); + } + + @Override + public int hashCode() { + return Objects.hash(mirrorId, projectName, repoName, mirrorStatus, description); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .omitNullValues() + .add("mirrorId", mirrorId) + .add("projectName", projectName) + .add("repoName", repoName) + .add("mirrorStatus", mirrorStatus) + .add("description", description) + .toString(); + } +} diff --git a/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorStatus.java b/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorStatus.java new file mode 100644 index 000000000..bbeafff6a --- /dev/null +++ b/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorStatus.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.centraldogma.server.mirror; + +/** + * The status of a mirroring operation. + */ +public enum MirrorStatus { + /** + * The mirroring was successful. + */ + SUCCESS, + /** + * The target repository was already up-to-date. + */ + UP_TO_DATE, +} diff --git a/webapp/src/dogma/common/components/Navbar.tsx b/webapp/src/dogma/common/components/Navbar.tsx index 7d81ce20a..ebe2a85f2 100644 --- a/webapp/src/dogma/common/components/Navbar.tsx +++ b/webapp/src/dogma/common/components/Navbar.tsx @@ -206,8 +206,8 @@ export const Navbar = () => { if (typeof window !== 'undefined') { Router.push( process.env.NEXT_PUBLIC_HOST - ? `${process.env.NEXT_PUBLIC_HOST}/link/auth/login/?return_to=${window.location.origin}` - : `/link/auth/login/`, + ? `${process.env.NEXT_PUBLIC_HOST}/link/auth/login?return_to=${window.location.origin}` + : `/link/auth/login`, ); } }} diff --git a/webapp/src/dogma/features/api/apiSlice.ts b/webapp/src/dogma/features/api/apiSlice.ts index 6e576c91d..49d3a3b56 100644 --- a/webapp/src/dogma/features/api/apiSlice.ts +++ b/webapp/src/dogma/features/api/apiSlice.ts @@ -30,6 +30,7 @@ import { AddUserPermissionDto } from 'dogma/features/repo/permissions/AddUserPer import { DeleteMemberDto } from 'dogma/features/project/settings/members/DeleteMemberDto'; import { MirrorDto } from 'dogma/features/project/settings/mirrors/MirrorDto'; import { CredentialDto } from 'dogma/features/project/settings/credentials/CredentialDto'; +import { MirrorResult } from '../mirror/MirrorResult'; export type ApiAction = { (arg: Arg): { unwrap: () => Promise }; @@ -346,6 +347,13 @@ export const apiSlice = createApi({ }), invalidatesTags: ['Metadata'], }), + runMirror: builder.mutation({ + query: ({ projectName, id }) => ({ + url: `/api/v1/projects/${projectName}/mirrors/${id}/run`, + method: 'POST', + }), + invalidatesTags: ['Metadata'], + }), getCredentials: builder.query({ query: (projectName) => `/api/v1/projects/${projectName}/credentials`, providesTags: ['Metadata'], @@ -422,6 +430,7 @@ export const { useGetMirrorQuery, useAddNewMirrorMutation, useUpdateMirrorMutation, + useRunMirrorMutation, // Credential useGetCredentialsQuery, useGetCredentialQuery, diff --git a/webapp/src/dogma/features/auth/Authorized.tsx b/webapp/src/dogma/features/auth/Authorized.tsx index 58283474e..355955f17 100644 --- a/webapp/src/dogma/features/auth/Authorized.tsx +++ b/webapp/src/dogma/features/auth/Authorized.tsx @@ -47,7 +47,7 @@ export const Authorized = (props: { children: ReactNode }) => { if (typeof window !== 'undefined') { router.push( process.env.NEXT_PUBLIC_HOST - ? `${process.env.NEXT_PUBLIC_HOST}/link/auth/login/?return_to=${window.location.origin}` + ? `${process.env.NEXT_PUBLIC_HOST}/link/auth/login?return_to=${window.location.origin}` : `/link/auth/login`, ); } diff --git a/webapp/src/dogma/features/mirror/MirrorResult.ts b/webapp/src/dogma/features/mirror/MirrorResult.ts new file mode 100644 index 000000000..6ae480c43 --- /dev/null +++ b/webapp/src/dogma/features/mirror/MirrorResult.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export type MirrorStatus = 'SUCCESS' | 'UP_TO_DATE'; + +export interface MirrorResult { + mirrorId: string; + projectName: string; + repoName: string; + mirrorStatus: MirrorStatus; + description: string; +} diff --git a/webapp/src/dogma/features/mirror/RunMirror.tsx b/webapp/src/dogma/features/mirror/RunMirror.tsx new file mode 100644 index 000000000..2072e94ce --- /dev/null +++ b/webapp/src/dogma/features/mirror/RunMirror.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { useRunMirrorMutation } from 'dogma/features/api/apiSlice'; +import { newNotification } from 'dogma/features/notification/notificationSlice'; +import { useAppDispatch } from 'dogma/hooks'; +import { SerializedError } from '@reduxjs/toolkit'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import ErrorMessageParser from 'dogma/features/services/ErrorMessageParser'; +import { MirrorResult } from './MirrorResult'; +import { MirrorDto } from '../project/settings/mirrors/MirrorDto'; +import { FaPlay } from 'react-icons/fa'; +import { IconButton } from '@chakra-ui/react'; + +export const RunMirror = ({ mirror }: { mirror: MirrorDto }) => { + const [runMirror, { isLoading }] = useRunMirrorMutation(); + const dispatch = useAppDispatch(); + const onClick = async () => { + try { + const response: any = await runMirror({ projectName: mirror.projectName, id: mirror.id }).unwrap(); + if ((response as { error: FetchBaseQueryError | SerializedError }).error) { + throw (response as { error: FetchBaseQueryError | SerializedError }).error; + } + const result: MirrorResult = response; + if (result.mirrorStatus === 'SUCCESS') { + dispatch( + newNotification(`Mirror ${mirror.id} is performed successfully`, result.description, 'success'), + ); + } else if (result.mirrorStatus === 'UP_TO_DATE') { + dispatch(newNotification(`No changes`, result.description, 'info')); + } + } catch (error) { + dispatch(newNotification(`Failed to run mirror ${mirror.id}`, ErrorMessageParser.parse(error), 'error')); + } + }; + + return ( + } + /> + ); +}; diff --git a/webapp/src/dogma/features/project/settings/mirrors/MirrorDto.ts b/webapp/src/dogma/features/project/settings/mirrors/MirrorDto.ts index b4fec4a96..cea47b704 100644 --- a/webapp/src/dogma/features/project/settings/mirrors/MirrorDto.ts +++ b/webapp/src/dogma/features/project/settings/mirrors/MirrorDto.ts @@ -1,7 +1,7 @@ export interface MirrorDto { id: string; projectName: string; - schedule: string; + schedule?: string; direction: 'REMOTE_TO_LOCAL' | 'LOCAL_TO_REMOTE'; localRepo: string; localPath: string; diff --git a/webapp/src/dogma/features/project/settings/mirrors/MirrorForm.tsx b/webapp/src/dogma/features/project/settings/mirrors/MirrorForm.tsx index de4e2b588..e1557d188 100644 --- a/webapp/src/dogma/features/project/settings/mirrors/MirrorForm.tsx +++ b/webapp/src/dogma/features/project/settings/mirrors/MirrorForm.tsx @@ -16,6 +16,8 @@ import { Controller, useForm, UseFormSetError } from 'react-hook-form'; import { + Alert, + AlertIcon, Button, Center, Divider, @@ -43,7 +45,7 @@ import { GoArrowBoth, GoArrowDown, GoArrowUp, GoKey, GoRepo } from 'react-icons/ import { Select } from 'chakra-react-select'; import { IoBanSharp } from 'react-icons/io5'; import { useGetCredentialsQuery, useGetReposQuery } from 'dogma/features/api/apiSlice'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import FieldErrorMessage from 'dogma/common/components/form/FieldErrorMessage'; import { RepoDto } from 'dogma/features/repo/RepoDto'; import { MirrorDto } from 'dogma/features/project/settings/mirrors/MirrorDto'; @@ -53,11 +55,7 @@ import { FiBox } from 'react-icons/fi'; interface MirrorFormProps { projectName: string; defaultValue: MirrorDto; - onSubmit: ( - credential: MirrorDto, - onSuccess: () => void, - setError: UseFormSetError, - ) => Promise; + onSubmit: (mirror: MirrorDto, onSuccess: () => void, setError: UseFormSetError) => Promise; isWaitingResponse: boolean; } @@ -73,21 +71,27 @@ const MIRROR_SCHEMES: OptionType[] = ['git+ssh', 'git+http', 'git+https'].map((s const INTERNAL_REPOS = new Set(['dogma', 'meta']); +type MirrorForm = MirrorDto & { + enableSchedule: boolean; +}; + const MirrorForm = ({ projectName, defaultValue, onSubmit, isWaitingResponse }: MirrorFormProps) => { const { register, handleSubmit, reset, - formState: { errors }, + formState: { errors, isDirty }, setError, setValue, control, - } = useForm(); + } = useForm(); const isNew = defaultValue.id === ''; const { data: repos } = useGetReposQuery(projectName); const { data: credentials } = useGetCredentialsQuery(projectName); + const [isScheduleEnabled, setScheduleEnabled] = useState(defaultValue.schedule != null); + const repoOptions: OptionType[] = (repos || []) .filter((repo: RepoDto) => !INTERNAL_REPOS.has(repo.name)) .map((repo: RepoDto) => ({ @@ -111,7 +115,14 @@ const MirrorForm = ({ projectName, defaultValue, onSubmit, isWaitingResponse }: setValue('credentialId', defaultValue.credentialId); setValue('direction', defaultValue.direction); } - }, [defaultValue, setValue, isNew]); + }, [ + isNew, + setValue, + defaultValue.localRepo, + defaultValue.remoteScheme, + defaultValue.credentialId, + defaultValue.direction, + ]); const defaultRemoteScheme: OptionType = defaultValue.remoteScheme ? { value: defaultValue.remoteScheme, label: defaultValue.remoteScheme } @@ -160,32 +171,59 @@ const MirrorForm = ({ projectName, defaultValue, onSubmit, isWaitingResponse }: - + + { + if (e.target.checked) { + setValue('schedule', defaultValue.schedule, { + shouldDirty: true, + }); + setScheduleEnabled(true); + } else { + setValue('schedule', null, { + shouldDirty: true, + }); + setScheduleEnabled(false); + } + }} + /> - + {isScheduleEnabled ? ( + + ) : ( + + + Scheduling is disabled. Mirroring can be triggered manually. + + )} {errors.schedule ? ( ) : ( - - - Quartz cron expression {' '} - - is used to describe when the mirroring task is supposed to be triggered. - + isScheduleEnabled && ( + + + Quartz cron expression {' '} + + is used to describe when the mirroring task is supposed to be triggered. + + ) )} @@ -417,6 +455,7 @@ const MirrorForm = ({ projectName, defaultValue, onSubmit, isWaitingResponse }: disabled type="submit" colorScheme="green" + isDisabled={!isDirty} isLoading={isWaitingResponse} loadingText="Updating" marginTop="10px" diff --git a/webapp/src/dogma/features/project/settings/mirrors/MirrorList.tsx b/webapp/src/dogma/features/project/settings/mirrors/MirrorList.tsx index ea417861d..e16df4f2a 100644 --- a/webapp/src/dogma/features/project/settings/mirrors/MirrorList.tsx +++ b/webapp/src/dogma/features/project/settings/mirrors/MirrorList.tsx @@ -6,6 +6,7 @@ import { Badge, Code, Link } from '@chakra-ui/react'; import { GoRepo } from 'react-icons/go'; import { LabelledIcon } from 'dogma/common/components/LabelledIcon'; import { MirrorDto } from 'dogma/features/project/settings/mirrors/MirrorDto'; +import { RunMirror } from '../../../mirror/RunMirror'; // eslint-disable-next-line @typescript-eslint/no-unused-vars export type MirrorListProps = { @@ -54,11 +55,19 @@ const MirrorList = ({ projectName }: MirrorListProps) header: 'Direction', }), columnHelper.accessor((row: MirrorDto) => row.schedule, { - cell: (info) => ( - - {info.getValue()} - - ), + cell: (info) => { + return ( + <> + {info.getValue() ? ( + + {info.getValue()} + + ) : ( + + )} + + ); + }, header: 'Schedule', }), columnHelper.accessor((row: MirrorDto) => row.enabled, {