Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,12 @@ default MergeReferenceBuilder fromRef(Reference fromRef) {
return fromRefName(fromRef.getName()).fromHash(fromRef.getHash());
}

/** Perform the merge operation. */
MergeResponse merge() throws NessieNotFoundException, NessieConflictException;

/**
* Perform the merge operation and allows to optionally inspect merge conflicts using the content
* state of the conflicting contents on the merge-base, merge-source and merge-target.
*/
MergeResponseInspector mergeInspect() throws NessieNotFoundException, NessieConflictException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright (C) 2023 Dremio
*
* Licensed 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
*
* http://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 org.projectnessie.client.api;

import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.immutables.value.Value;
import org.projectnessie.api.v2.params.Merge;
import org.projectnessie.error.NessieNotFoundException;
import org.projectnessie.model.Conflict;
import org.projectnessie.model.Conflict.ConflictType;
import org.projectnessie.model.Content;
import org.projectnessie.model.ContentKey;
import org.projectnessie.model.MergeBehavior;
import org.projectnessie.model.MergeKeyBehavior;
import org.projectnessie.model.MergeResponse;

/**
* Allows inspection of merge results, to resolve {@link ConflictType#VALUE_DIFFERS content related}
* merge {@link Conflict conflicts} indicated in the {@link #getResponse() merge response}.
*/
public interface MergeResponseInspector {
/** The merge request sent to Nessie. */
Merge getRequest();

/** The merge response received from Nessie. */
MergeResponse getResponse();

/**
* Provides details about the conflicts that happened during a merge operation.
*
* <p>The returned stream contains one {@link MergeConflictDetails element} for each conflict.
* Non-conflicting contents are not included.
*
* <p>Each {@link MergeConflictDetails conflict details object} allows callers to resolve
* conflicts based on that information, also known as "content aware merge".
*
* <p>Once conflicts have been either resolved, or alternatively specific keys declared to "use
* the {@link MergeBehavior#DROP left/from} or {@link MergeBehavior#FORCE right/target} side",
* another {@link MergeReferenceBuilder merge operation} cna be performed, providing a {@link
* MergeKeyBehavior#getResolvedContent() resolved content} for a content key.
*
* <p>Keep in mind that calling this function triggers API calls against nessie to retrieve the
* relevant content objects on the merge-base and and the content keys and content objects on the
* merge-from (source) and merge-target.
*/
Stream<MergeConflictDetails> collectMergeConflictDetails() throws NessieNotFoundException;

@Value.Immutable
interface MergeConflictDetails {
/** The content ID of the conflicting content. */
default String getContentId() {
return contentOnMergeBase().getId();
}

/** Key of the content on the {@link MergeResponse#getCommonAncestor() merge-base commit}. */
ContentKey keyOnMergeBase();

/** Key of the content on the {@link MergeReferenceBuilder#fromRef merge-from reference}. */
@Nullable
@jakarta.annotation.Nullable
ContentKey keyOnSource();

/**
* Key of the content on the {@link MergeResponse#getEffectiveTargetHash() merge-target
* reference}.
*/
@Nullable
@jakarta.annotation.Nullable
ContentKey keyOnTarget();

/** Content object on the {@link MergeResponse#getCommonAncestor() merge-base commit}. */
// TODO this can also be null, if the same key was added on source + target but is not present
// on merge-base.
Content contentOnMergeBase();

/**
* Content on the {@link MergeReferenceBuilder#fromRef merge-from reference}, or {@code null} if
* not present on the merge-from.
*/
@Nullable
@jakarta.annotation.Nullable
Content contentOnSource();

/**
* Content on the {@link MergeResponse#getEffectiveTargetHash() merge-target reference}, or
* {@code null} if not present on the merge-target.
*/
@Nullable
@jakarta.annotation.Nullable
Content contentOnTarget();

/**
* Contains {@link Conflict#conflictType() machine interpretable} and {@link Conflict#message()
* human.readable information} about the conflict.
*/
// TODO this can also be null, if the same key was added on source + target but is not present
// on merge-base.
Conflict conflict();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright (C) 2023 Dremio
*
* Licensed 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
*
* http://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 org.projectnessie.client.api.impl;

import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
import static org.projectnessie.client.api.impl.MapEntry.mapEntry;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.projectnessie.client.api.ImmutableMergeConflictDetails;
import org.projectnessie.client.api.MergeResponseInspector;
import org.projectnessie.error.NessieNotFoundException;
import org.projectnessie.model.Conflict;
import org.projectnessie.model.Content;
import org.projectnessie.model.ContentKey;
import org.projectnessie.model.Detached;
import org.projectnessie.model.DiffResponse;
import org.projectnessie.model.GetMultipleContentsResponse;
import org.projectnessie.model.MergeResponse;

/** Base class used for testing and production code, because testing does not use the Nessie API. */
public abstract class BaseMergeResponseInspector implements MergeResponseInspector {

@Override
public Stream<MergeConflictDetails> collectMergeConflictDetails() throws NessieNotFoundException {
// Note: The API exposes a `Stream` so we can optimize this implementation later to reduce
// runtime or heap pressure.
return mergeConflictDetails().stream();
}

protected List<MergeConflictDetails> mergeConflictDetails() throws NessieNotFoundException {
Map<ContentKey, Conflict> conflictMap = conflictMap();
Map<ContentKey, Content> mergeBaseContentByKey = mergeBaseContentByKey();
if (mergeBaseContentByKey.isEmpty()) {
return emptyList();
}

Map<String, List<DiffResponse.DiffEntry>> sourceDiff = sourceDiff();
Map<String, List<DiffResponse.DiffEntry>> targetDiff = targetDiff();

List<MergeConflictDetails> details = new ArrayList<>(mergeBaseContentByKey.size());

for (Map.Entry<ContentKey, Content> base : mergeBaseContentByKey.entrySet()) {
ContentKey baseKey = base.getKey();
Content baseContent = base.getValue();

Map.Entry<ContentKey, Content> source = keyAndContent(baseKey, baseContent, sourceDiff);
Map.Entry<ContentKey, Content> target = keyAndContent(baseKey, baseContent, targetDiff);

MergeConflictDetails detail =
ImmutableMergeConflictDetails.builder()
.conflict(conflictMap.get(baseKey))
.keyOnMergeBase(baseKey)
.keyOnSource(source.getKey())
.keyOnTarget(target.getKey())
.contentOnMergeBase(baseContent)
.contentOnSource(source.getValue())
.contentOnTarget(target.getValue())
.build();

details.add(detail);
}
return details;
}

protected Map<ContentKey, Conflict> conflictMap() {
return getResponse().getDetails().stream()
.map(MergeResponse.ContentKeyDetails::getConflict)
.filter(Objects::nonNull)
.collect(Collectors.toMap(Conflict::key, Function.identity()));
}

protected Set<String> mergeBaseContentIds() throws NessieNotFoundException {
return mergeBaseContents().stream()
.map(GetMultipleContentsResponse.ContentWithKey::getContent)
.map(Content::getId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}

protected Map<ContentKey, Content> mergeBaseContentByKey() throws NessieNotFoundException {
return mergeBaseContents().stream()
.collect(
Collectors.toMap(
GetMultipleContentsResponse.ContentWithKey::getKey,
GetMultipleContentsResponse.ContentWithKey::getContent));
}

protected Map<String, List<DiffResponse.DiffEntry>> sourceDiff() throws NessieNotFoundException {
// TODO it would help to have the effective from-hash in the response, in case the merge-request
// did not contain it. If we have that merge, we can use 'DETACHED' here.
String hash = getRequest().getFromHash();
String ref = hash != null ? Detached.REF_NAME : getRequest().getFromRefName();
return diffByContentId(ref, hash);
}

protected Map<String, List<DiffResponse.DiffEntry>> targetDiff() throws NessieNotFoundException {
return diffByContentId(Detached.REF_NAME, getResponse().getEffectiveTargetHash());
}

protected abstract List<GetMultipleContentsResponse.ContentWithKey> mergeBaseContents()
throws NessieNotFoundException;

protected abstract Map<String, List<DiffResponse.DiffEntry>> diffByContentId(
String ref, String hash) throws NessieNotFoundException;

static String contentIdFromDiffEntry(DiffResponse.DiffEntry e) {
Content from = e.getFrom();
return requireNonNull(from != null ? from.getId() : requireNonNull(e.getTo()).getId());
}

static Map.Entry<ContentKey, Content> keyAndContent(
ContentKey baseKey, Content baseContent, Map<String, List<DiffResponse.DiffEntry>> diff) {
List<DiffResponse.DiffEntry> diffs = diff.get(baseContent.getId());
if (diffs != null) {
int size = diffs.size();
DiffResponse.DiffEntry last = diffs.get(size - 1);
return mapEntry(last.getKey(), last.getTo());
}
return mapEntry(baseKey, baseContent);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2023 Dremio
*
* Licensed 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
*
* http://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 org.projectnessie.client.api.impl;

import java.util.Map;
import javax.annotation.Nullable;
import org.immutables.value.Value;

/**
* A helper that implements {@link Map.Entry}, because the code is built for Java 8 and Guava's not
* a dependency.
*/
@Value.Immutable
abstract class MapEntry<K, V> implements Map.Entry<K, V> {
private static final MapEntry<?, ?> EMPTY_ENTRY = (MapEntry<?, ?>) MapEntry.mapEntry(null, null);

@SuppressWarnings("unchecked")
static <K, V> Map.Entry<K, V> emptyEntry() {
return (Map.Entry<K, V>) EMPTY_ENTRY;
}

static <K, V> Map.Entry<K, V> mapEntry(K key, V value) {
return ImmutableMapEntry.of(key, value);
}

@Override
@Value.Parameter(order = 1)
@Nullable
public abstract K getKey();

@Override
@Value.Parameter(order = 2)
@Nullable
public abstract V getValue();

@Override
public V setValue(V value) {
throw new UnsupportedOperationException();
}
}
Loading