Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: branch merge #804

Merged
merged 1 commit into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading