Skip to content

Commit

Permalink
feat: support view diffs between post content versions
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed May 11, 2024
1 parent a629961 commit 7f91aeb
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package run.halo.app.content;

import static com.github.difflib.UnifiedDiffUtils.generateUnifiedDiff;
import static run.halo.app.content.PatchUtils.breakLine;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.isNull;

import com.github.difflib.DiffUtils;
import com.github.difflib.patch.Patch;
import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.OptimisticLockingFailureException;
Expand All @@ -26,6 +32,7 @@
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.Ref;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.exception.NotFoundException;

/**
* Abstract Service for {@link Snapshot}.
Expand Down Expand Up @@ -186,9 +193,57 @@ protected Snapshot determineRawAndContentPatch(Snapshot snapshotToUse,
return snapshotToUse;
}

/**
* Returns the unified diff content of the right compared to the left.
*/
protected Mono<String> generateContentDiffBy(String leftSnapshot, String rightSnapshot,
String baseSnapshot) {
if (StringUtils.isBlank(leftSnapshot) || StringUtils.isBlank(rightSnapshot)) {
return Mono.error(new IllegalArgumentException(
"The leftSnapshot and rightSnapshot must not be blank."));
}
if (StringUtils.isBlank(baseSnapshot)) {
return Mono.error(new IllegalArgumentException("The baseSnapshot must not be blank."));
}

var contentDiffDo = new ContentDiffDo();
var originalMono = getContent(leftSnapshot, baseSnapshot)
.switchIfEmpty(Mono.error(new NotFoundException("The leftSnapshot not found.")))
.doOnNext(contentWrapper -> contentDiffDo.setOriginal(contentWrapper.getContent()));
var revisedMono = getContent(rightSnapshot, baseSnapshot)
.switchIfEmpty(Mono.error(new NotFoundException("The rightSnapshot not found.")))
.doOnNext(contentWrapper -> contentDiffDo.setRevised(contentWrapper.getContent()));

return Mono.when(originalMono, revisedMono)
.then(Mono.defer(() -> generateContentUnifiedDiff(contentDiffDo.getOriginal(),
contentDiffDo.getRevised()))
);
}

static Mono<String> generateContentUnifiedDiff(String original, String revised) {
Assert.notNull(original, "The original must not be null.");
Assert.notNull(revised, "The revised must not be null.");
var originalLines = breakLine(original);
Patch<String> patch = DiffUtils.diff(originalLines, breakLine(revised));
var unifiedDiff = generateUnifiedDiff("left", "right",
originalLines, patch, 10);
if (unifiedDiff.size() < 3) {
return Mono.just("");
}
var result = String.join(PatchUtils.DELIMITER, unifiedDiff.subList(2, unifiedDiff.size()));
return Mono.just(result);
}

protected Mono<String> getContextUsername() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Principal::getName);
}

@Data
@Accessors(chain = true)
private static class ContentDiffDo {
private String original;
private String revised;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
* @since 2.0.0
*/
public class PatchUtils {
private static final String DELIMITER = "\n";
public static final String DELIMITER = "\n";
private static final Splitter lineSplitter = Splitter.on(DELIMITER);

public static Patch<String> create(String deltasJson) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,12 @@ public interface PostService {
Mono<Post> revertToSpecifiedSnapshot(String postName, String snapshotName);

Mono<ContentWrapper> deleteContent(String postName, String snapshotName);

/**
* <p>Returns the unified diff content of the right compared to the left.</p>
* <p>If the left snapshot is blank, the releaseSnapshot will be used as the left snapshot.</p>
* <p>If the right snapshot is blank, the headSnapshot will be used as the right snapshot.</p>
*/
Mono<String> generateContentDiff(String postName, String leftSnapshotName,
String rightSnapshotName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -355,6 +356,23 @@ public Mono<ContentWrapper> deleteContent(String postName, String snapshotName)
});
}

@Override
public Mono<String> generateContentDiff(String postName, String leftSnapshotName,
String rightSnapshotName) {
return client.get(Post.class, postName)
.flatMap(post -> {
String ensuredLeftSnapshotName = Optional.ofNullable(leftSnapshotName)
.filter(StringUtils::isNotBlank)
.orElse(post.getSpec().getReleaseSnapshot());

String ensuredRightSnapshotName = Optional.ofNullable(rightSnapshotName)
.filter(StringUtils::isNotBlank)
.orElse(post.getSpec().getHeadSnapshot());
return generateContentDiffBy(ensuredLeftSnapshotName, ensuredRightSnapshotName,
post.getSpec().getBaseSnapshot());
});
}

private Mono<Post> updatePostWithRetry(Post post, UnaryOperator<Post> func) {
return client.update(func.apply(post))
.onErrorResume(OptimisticLockingFailureException.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,28 @@ public RouterFunction<ServerResponse> endpoint() {
.response(responseBuilder()
.implementationArray(ListedSnapshotDto.class))
)
.GET("posts/{name}/diff-content", this::diffContent,
builder -> builder.operationId("diffPostContent")
.description("Generate diff content between two snapshots.")
.tag(tag)
.parameter(parameterBuilder().name("name")
.in(ParameterIn.PATH)
.required(true)
.implementation(String.class)
)
.parameter(parameterBuilder()
.name("leftSnapshot")
.in(ParameterIn.QUERY)
.required(true)
.implementation(String.class))
.parameter(parameterBuilder()
.name("rightSnapshot")
.in(ParameterIn.QUERY)
.required(true)
.implementation(String.class))
.response(responseBuilder()
.implementation(String.class))
)
.POST("posts", this::draftPost,
builder -> builder.operationId("DraftPost")
.description("Draft a post.")
Expand Down Expand Up @@ -238,6 +260,18 @@ public RouterFunction<ServerResponse> endpoint() {
.build();
}

private Mono<ServerResponse> diffContent(ServerRequest request) {
final var postName = request.pathVariable("name");
var leftSnapshotName = request.queryParam("leftSnapshot")
.orElse(null);
var rightSnapshotName = request.queryParam("rightSnapshot")
.orElse(null);
return client.get(Post.class, postName)
.flatMap(post -> postService.generateContentDiff(postName, leftSnapshotName,
rightSnapshotName))
.flatMap(diff -> ServerResponse.ok().bodyValue(diff));
}

private Mono<ServerResponse> deleteContent(ServerRequest request) {
final var postName = request.pathVariable("name");
final var snapshotName = request.queryParam("snapshotName").orElseThrow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ rules:
resources: [ "posts" ]
verbs: [ "get", "list" ]
- apiGroups: [ "api.console.halo.run" ]
resources: [ "posts", "posts/head-content", "posts/release-content", "posts/snapshot", "posts/content" ]
resources: [ "posts", "posts/head-content", "posts/release-content", "posts/snapshot", "posts/content", "posts/diff-content" ]
verbs: [ "get", "list" ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package run.halo.app.content;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

/**
* Tests for {@link AbstractContentService}.
*
* @author guqing
* @since 2.16.0
*/
class AbstractContentServiceTest {

@Test
void generateContentUnifiedDiff() {
String diff = AbstractContentService.generateContentUnifiedDiff("line1\nline2", "")
.block();
assertThat(diff).isEqualToIgnoringNewLines("""
@@ -1,2 +1,0 @@
-line1
-line2
""");
}
}

0 comments on commit 7f91aeb

Please sign in to comment.