Skip to content

Commit

Permalink
feat: branch merge
Browse files Browse the repository at this point in the history
  • Loading branch information
yevheniyJ committed May 18, 2024
1 parent 206ca40 commit 6ecb835
Show file tree
Hide file tree
Showing 16 changed files with 360 additions and 33 deletions.
48 changes: 24 additions & 24 deletions src/main/java/com/crowdin/cli/client/CrowdinClientCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,29 @@ abstract class CrowdinClientCore {
private static final long defaultMillisToRetry = 100;

private static final Map<BiPredicate<String, String>, RuntimeException> standardErrorHandlers =
new LinkedHashMap<BiPredicate<String, String>, RuntimeException>() {{
put((code, message) -> code.equals("401"),
new ExitCodeExceptionMapper.AuthorizationException(RESOURCE_BUNDLE.getString("error.response.401")));
put((code, message) -> code.equals("403") && message.contains("upgrade your subscription plan to upload more file formats"),
new ExitCodeExceptionMapper.ForbiddenException(RESOURCE_BUNDLE.getString("error.response.403_upgrade_subscription")));
put((code, message) -> code.equals("403"),
new ExitCodeExceptionMapper.ForbiddenException(RESOURCE_BUNDLE.getString("error.response.403")));
put((code, message) -> code.equals("404") && StringUtils.containsIgnoreCase(message, "Project Not Found"),
new ExitCodeExceptionMapper.NotFoundException(RESOURCE_BUNDLE.getString("error.response.404_project_not_found")));
put((code, message) -> code.equals("404") && StringUtils.containsIgnoreCase(message, "Organization Not Found"),
new ExitCodeExceptionMapper.NotFoundException(RESOURCE_BUNDLE.getString("error.response.404_organization_not_found")));
put((code, message) -> code.equals("429"),
new ExitCodeExceptionMapper.RateLimitException(RESOURCE_BUNDLE.getString("error.response.429")));
put((code, message) -> StringUtils.containsAny(message,
"PKIX path building failed",
"sun.security.provider.certpath.SunCertPathBuilderException",
"unable to find valid certification path to requested target"),
new RuntimeException(RESOURCE_BUNDLE.getString("error.response.certificate")));
put((code, message) -> message.equals("Name or service not known"),
new RuntimeException(RESOURCE_BUNDLE.getString("error.response.url_not_known")));
put((code, message) -> code.equals("<empty_code>") && message.equals("<empty_message>"),
new RuntimeException("Empty error message from server"));
}};
new LinkedHashMap<>() {{
put((code, message) -> code.equals("401"),
new ExitCodeExceptionMapper.AuthorizationException(RESOURCE_BUNDLE.getString("error.response.401")));
put((code, message) -> code.equals("403") && message.contains("upgrade your subscription plan to upload more file formats"),
new ExitCodeExceptionMapper.ForbiddenException(RESOURCE_BUNDLE.getString("error.response.403_upgrade_subscription")));
put((code, message) -> code.equals("403") && !message.contains("Merge is possible only into main branch"),
new ExitCodeExceptionMapper.ForbiddenException(RESOURCE_BUNDLE.getString("error.response.403")));
put((code, message) -> code.equals("404") && StringUtils.containsIgnoreCase(message, "Project Not Found"),
new ExitCodeExceptionMapper.NotFoundException(RESOURCE_BUNDLE.getString("error.response.404_project_not_found")));
put((code, message) -> code.equals("404") && StringUtils.containsIgnoreCase(message, "Organization Not Found"),
new ExitCodeExceptionMapper.NotFoundException(RESOURCE_BUNDLE.getString("error.response.404_organization_not_found")));
put((code, message) -> code.equals("429"),
new ExitCodeExceptionMapper.RateLimitException(RESOURCE_BUNDLE.getString("error.response.429")));
put((code, message) -> StringUtils.containsAny(message,
"PKIX path building failed",
"sun.security.provider.certpath.SunCertPathBuilderException",
"unable to find valid certification path to requested target"),
new RuntimeException(RESOURCE_BUNDLE.getString("error.response.certificate")));
put((code, message) -> message.equals("Name or service not known"),
new RuntimeException(RESOURCE_BUNDLE.getString("error.response.url_not_known")));
put((code, message) -> code.equals("<empty_code>") && message.equals("<empty_message>"),
new RuntimeException("Empty error message from server"));
}};

/**
* Util logic for downloading full lists.
Expand Down Expand Up @@ -122,7 +122,7 @@ protected static <T, R extends Exception> T executeRequest(Map<BiPredicate<Strin
}
}

private static <T, R extends Exception> void searchErrorHandler(Map<BiPredicate<String, String>, R> errorHandlers, String code, String message) throws R {
private static <R extends Exception> void searchErrorHandler(Map<BiPredicate<String, String>, R> errorHandlers, String code, String message) throws R {
for (Map.Entry<BiPredicate<String, String>, R> errorHandler : errorHandlers.entrySet()) {
if (errorHandler.getKey().test(code, message)) {
throw errorHandler.getValue();
Expand Down
27 changes: 23 additions & 4 deletions src/main/java/com/crowdin/cli/client/CrowdinProjectClient.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.crowdin.cli.client;

import com.crowdin.client.branches.model.BranchCloneStatus;
import com.crowdin.client.branches.model.CloneBranchRequest;
import com.crowdin.client.branches.model.ClonedBranch;
import com.crowdin.client.branches.model.*;
import com.crowdin.client.core.model.PatchRequest;
import com.crowdin.client.labels.model.AddLabelRequest;
import com.crowdin.client.labels.model.Label;
Expand Down Expand Up @@ -150,7 +148,7 @@ public Branch addBranch(AddBranchRequest request) {

@Override
public BranchCloneStatus cloneBranch(Long branchId, CloneBranchRequest request) throws ResponseException {
Map<BiPredicate<String, String>, ResponseException> errorHandlers = new LinkedHashMap<BiPredicate<String, String>, ResponseException>() {{
Map<BiPredicate<String, String>, ResponseException> errorHandlers = new LinkedHashMap<>() {{
put((code, message) -> StringUtils.containsAny(message, "Name must be unique"), new ExistsResponseException());
}};
return executeRequest(errorHandlers,
Expand All @@ -173,6 +171,27 @@ public ClonedBranch getClonedBranch(Long branchId, String cloneId) {
.getData());
}

@Override
public BranchMergeStatus mergeBranch(Long branchId, MergeBranchRequest request) {
return executeRequest(() -> this.client.getBranchesApi()
.mergeBranch(projectId, branchId, request)
.getData());
}

@Override
public BranchMergeStatus checkMergeBranchStatus(Long branchId, String mergeId) {
return executeRequest(() -> this.client.getBranchesApi()
.checkMergeBranchStatus(projectId, branchId, mergeId)
.getData());
}

@Override
public BranchMergeSummary getBranchMergeSummary(Long branchId, String mergeId) {
return executeRequest(() -> this.client.getBranchesApi()
.getMergeBranchSummary(this.projectId, branchId, mergeId)
.getData());
}

@Override
public void deleteBranch(Long branchId) {
executeRequest(() -> {
Expand Down
10 changes: 7 additions & 3 deletions src/main/java/com/crowdin/cli/client/ProjectClient.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.crowdin.cli.client;

import com.crowdin.client.branches.model.BranchCloneStatus;
import com.crowdin.client.branches.model.CloneBranchRequest;
import com.crowdin.client.branches.model.ClonedBranch;
import com.crowdin.client.branches.model.*;
import com.crowdin.client.core.model.PatchRequest;
import com.crowdin.client.labels.model.AddLabelRequest;
import com.crowdin.client.labels.model.Label;
Expand Down Expand Up @@ -41,6 +39,12 @@ default CrowdinProjectFull downloadFullProject() {

ClonedBranch getClonedBranch(Long branchId, String cloneId);

BranchMergeStatus mergeBranch(Long branchId, MergeBranchRequest request) throws ResponseException;

BranchMergeStatus checkMergeBranchStatus(Long branchId, String mergeId);

BranchMergeSummary getBranchMergeSummary(Long branchId, String mergeId);

void deleteBranch(Long branchId);

List<Branch> listBranches();
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/crowdin/cli/commands/Actions.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ NewAction<PropertiesWithFiles, ProjectClient> preTranslate(

NewAction<ProjectProperties, ProjectClient> branchClone(String source, String target, boolean noProgress, boolean plainView);

NewAction<ProjectProperties, ProjectClient> branchMerge(String source, String target, boolean dryrun, boolean deleteAfterMerge, boolean noProgress, boolean plainView);

NewAction<ProjectProperties, ProjectClient> branchDelete(String name);

NewAction<ProjectProperties, ClientScreenshot> screenshotList(Long stringId, boolean plainView);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.crowdin.cli.commands.actions;

import com.crowdin.cli.client.CrowdinProjectFull;
import com.crowdin.cli.client.ProjectClient;
import com.crowdin.cli.commands.NewAction;
import com.crowdin.cli.commands.Outputter;
import com.crowdin.cli.commands.picocli.ExitCodeExceptionMapper;
import com.crowdin.cli.properties.ProjectProperties;
import com.crowdin.cli.utils.console.ConsoleSpinner;
import com.crowdin.client.branches.model.BranchMergeStatus;
import com.crowdin.client.branches.model.BranchMergeSummary;
import com.crowdin.client.branches.model.MergeBranchRequest;
import com.crowdin.client.projectsgroups.model.Type;
import com.crowdin.client.sourcefiles.model.Branch;
import lombok.AllArgsConstructor;

import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.crowdin.cli.BaseCli.RESOURCE_BUNDLE;
import static com.crowdin.cli.utils.console.ExecutionStatus.OK;

@AllArgsConstructor
class BranchMergeAction implements NewAction<ProjectProperties, ProjectClient> {

private final String source;
private final String target;
private final boolean noProgress;
private final boolean plainView;
private final boolean dryrun;
private final boolean deleteAfterMerge;

@Override
public void act(Outputter out, ProjectProperties properties, ProjectClient client) {
CrowdinProjectFull project = ConsoleSpinner.execute(out, "message.spinner.fetching_project_info", "error.collect_project_info",
this.noProgress, plainView, client::downloadFullProject);

boolean isStringsBasedProject = Objects.equals(project.getType(), Type.STRINGS_BASED);
if (!isStringsBasedProject) {
throw new ExitCodeExceptionMapper.ValidationException(RESOURCE_BUNDLE.getString("error.string_based_only"));
}

Optional<Branch> sourceBranch = project.findBranchByName(source);
if (sourceBranch.isEmpty()) {
throw new ExitCodeExceptionMapper.NotFoundException(String.format(RESOURCE_BUNDLE.getString("error.branch_not_exists"), source));
}

Optional<Branch> targetBranch = project.findBranchByName(target);
if (targetBranch.isEmpty()) {
throw new ExitCodeExceptionMapper.NotFoundException(String.format(RESOURCE_BUNDLE.getString("error.branch_not_exists"), target));
}

MergeBranchRequest request = new MergeBranchRequest();
request.setSourceBranchId(sourceBranch.get().getId());
request.setDeleteAfterMerge(deleteAfterMerge);
request.setDryRun(dryrun);
BranchMergeSummary summary = mergeBranch(out, client, targetBranch.get().getId(), request);

String summaryStr = summary.getDetails().entrySet().stream()
.map(entry -> entry.getKey() + ": " + entry.getValue())
.collect(Collectors.joining(", "));

if (!plainView) {
out.println(OK.withIcon(String.format(RESOURCE_BUNDLE.getString("message.branch.merge"), source, target)));
out.println(String.format(RESOURCE_BUNDLE.getString("message.branch.merge_details"), summaryStr));
} else {
out.println(String.valueOf(summary.getTargetBranchId()));
}
}

private BranchMergeSummary mergeBranch(Outputter out, ProjectClient client, Long branchId, MergeBranchRequest request) {
return ConsoleSpinner.execute(
out,
"message.spinner.merging_branch",
"error.branch.merge",
this.noProgress,
false,
() -> {
BranchMergeStatus status = client.mergeBranch(branchId, request);

while (!status.getStatus().equalsIgnoreCase("finished")) {
ConsoleSpinner.update(
String.format(RESOURCE_BUNDLE.getString("message.spinner.merging_branch_percents"), status.getProgress()));
Thread.sleep(1000);

status = client.checkMergeBranchStatus(branchId, status.getIdentifier());

if (status.getStatus().equalsIgnoreCase("failed")) {
throw new RuntimeException(RESOURCE_BUNDLE.getString("error.branch.merge"));
}
}
ConsoleSpinner.update(String.format(RESOURCE_BUNDLE.getString("message.spinner.merging_branch_percents"), 100));
return client.getBranchMergeSummary(branchId, status.getIdentifier());
}
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ public NewAction<ProjectProperties, ProjectClient> branchClone(String source, St
return new BranchCloneAction(source, target, noProgress, plainView);
}

@Override
public NewAction<ProjectProperties, ProjectClient> branchMerge(String source, String target, boolean dryrun, boolean deleteAfterMerge, boolean noProgress, boolean plainView) {
return new BranchMergeAction(source, target, noProgress, plainView, dryrun, deleteAfterMerge);
}

@Override
public NewAction<ProjectProperties, ProjectClient> branchDelete(String name) {
return new BranchDeleteAction(name);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.crowdin.cli.commands.picocli;

import com.crowdin.cli.client.ProjectClient;
import com.crowdin.cli.commands.Actions;
import com.crowdin.cli.commands.NewAction;
import com.crowdin.cli.properties.ProjectProperties;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;

@Command(
sortOptions = false,
name = CommandNames.BRANCH_MERGE
)
class BranchMergeSubcommand extends ActCommandProject {

@Parameters(descriptionKey = "crowdin.branch.merge.source")
protected String source;

@Parameters(descriptionKey = "crowdin.branch.merge.target")
protected String target;

@CommandLine.Option(names = {"--dryrun"}, descriptionKey = "crowdin.branch.merge.dryrun", order = -2)
protected boolean dryrun;

@CommandLine.Option(names = {"--delete-after-merge"}, descriptionKey = "crowdin.branch.merge.delete-after-merge", order = -2)
protected boolean deleteAfterMerge;

@CommandLine.Option(names = {"--plain"}, descriptionKey = "crowdin.list.usage.plain")
protected boolean plainView;

@Override
protected NewAction<ProjectProperties, ProjectClient> getAction(Actions actions) {
return actions.branchMerge(source, target, dryrun, deleteAfterMerge, noProgress, plainView);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
BranchAddSubcommand.class,
BranchCloneSubcommand.class,
BranchDeleteSubcommand.class,
BranchListSubcommand.class
BranchListSubcommand.class,
BranchMergeSubcommand.class
}
)
class BranchSubcommand extends HelpCommand {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ public final class CommandNames {

public static final String STRING = "string";
public static final String STRING_EDIT = "edit";
public static final String STRING_COMMENT = "comment";

public static final String BRANCH = "branch";
public static final String BRANCH_CLONE = "clone";
public static final String BRANCH_MERGE = "merge";

public static final String GLOSSARY = "glossary";
public static final String GLOSSARY_UPLOAD = "upload";
Expand Down
13 changes: 13 additions & 0 deletions src/main/resources/messages/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,14 @@ crowdin.branch.clone.usage.customSynopsis=@|fg(green) crowdin branch clone|@ <so
crowdin.branch.clone.source=Source branch name
crowdin.branch.clone.target=Target branch name

# CROWDIN BRANCH MERGE COMMAND
crowdin.branch.merge.usage.description=Merge branches
crowdin.branch.merge.usage.customSynopsis=@|fg(green) crowdin branch merge|@ <source> <target> [CONFIG OPTIONS] [OPTIONS]
crowdin.branch.merge.source=Source branch name
crowdin.branch.merge.target=Target branch name
crowdin.branch.merge.dryrun=Simulate merging without making any real changes
crowdin.branch.merge.delete-after-merge=Whether to delete branch after merge

crowdin.glossary.upload.usage.description=Upload glossary to localization resources
crowdin.glossary.upload.usage.customSynopsis=@|fg(green) crowdin glossary upload|@ <file> [CONFIG OPTIONS] [OPTIONS]
crowdin.glossary.upload.file=File to upload
Expand Down Expand Up @@ -574,6 +582,7 @@ error.file.dest_required='dest' parameter required to specify source file path
error.file.type_required='--type' is required for '--parser-version' option

error.branch.clone=Failed to clone the branch
error.branch.merge=Failed to merge the branch

error.response.401=Couldn't authorize. Check your 'api_token'
error.response.403=You do not have permission to view/edit project with provided id
Expand Down Expand Up @@ -673,6 +682,8 @@ message.download_translations.preserve_hierarchy_warning=Because the 'preserve_h
message.language.list=@|yellow %s|@ @|green %s|@

message.branch.list=@|yellow #%d|@ @|green %s|@
message.branch.merge=@|green Merged branch '%s' into '%s'|@
message.branch.merge_details=\tMerge summary: %s

message.file.list=@|yellow #%d|@ @|green %s|@
message.file.list_verbose=@|yellow #%d|@ @|green %s|@ %s
Expand Down Expand Up @@ -778,7 +789,9 @@ message.spinner.uploading_strings=Uploading strings
message.spinner.uploading_strings_percents=Uploading strings (%d%%)
message.spinner.upload_strings_failed=Upload has failed
message.spinner.cloning_branch=Cloning branch
message.spinner.merging_branch=Merging branch
message.spinner.cloning_branch_percents=Cloning branch @|bold (%d%%)|@
message.spinner.merging_branch_percents=Merging branch @|bold (%d%%)|@

message.faq_link=Visit the @|cyan https://crowdin.github.io/crowdin-cli/faq|@ for more details
message.translations_build_unsuccessful=Didn't manage to build translations
Expand Down
Loading

0 comments on commit 6ecb835

Please sign in to comment.