Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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 @@ -218,6 +218,13 @@ public CompletableFuture<Map<String, EntryType>> listFiles(String projectName, S
e -> EntryConverter.convertEntryType(e.getType()))));
}

@Override
public CompletableFuture<Map<String, Entry<?>>> listFiles(String projectName, String repositoryName,
Revision revision, PathPattern pathPattern,
int includeLastFileRevision) {
throw new UnsupportedOperationException("Use ArmeriaCentralDogma instead.");
}

@Override
public <T> CompletableFuture<Entry<T>> getFile(String projectName, String repositoryName,
Revision revision, Query<T> query) {
Expand Down Expand Up @@ -291,6 +298,13 @@ public CompletableFuture<Map<String, Entry<?>>> getFiles(String projectName, Str
});
}

@Override
public CompletableFuture<Map<String, Entry<?>>> getFiles(String projectName, String repositoryName,
Revision revision, PathPattern pathPattern,
int includeLastFileRevision) {
throw new UnsupportedOperationException("Use ArmeriaCentralDogma instead.");
}

@Override
public <T> CompletableFuture<MergedEntry<T>> mergeFiles(String projectName, String repositoryName,
Revision revision, MergeQuery<T> mergeQuery) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,29 +408,68 @@ private static Revision normalizeRevision(AggregatedHttpResponse res) {
@Override
public CompletableFuture<Map<String, EntryType>> listFiles(String projectName, String repositoryName,
Revision revision, PathPattern pathPattern) {
return listFiles0(projectName, repositoryName, revision, pathPattern, -1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) What do you think of setting the default value as 1 for both client/server side since it seems like 1 represents "don't search the history"?

Personally, I think it makes more sense that:

  1. 0 represents "check backwards history for 0 revisions"
  2. validate on includeLastFileRevision<0

The current implementation seems fine though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 represents "check backwards history for 0 revisions"

If so, should 0 and 1 return the same result?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I think it makes more sense that:
0 represents "check backwards history for 0 revisions"
validate on includeLastFileRevision<0

I meant it feels more intuitive to me that the parameter represents the number of revisions to go backwards. (i.e. Revision#backward(int)).

I think the current implementation is fine as long as 1) the default value is well defined with validation 2) and the javadocs are clear. It might be helpful if the parameter name better represents the meaning of "check x number of revision including the head revision" as well.

ArmeriaCentralDogma::listFiles);
}

@Override
public CompletableFuture<Map<String, Entry<?>>> listFiles(String projectName, String repositoryName,
Revision revision, PathPattern pathPattern,
int includeLastFileRevision) {
return listFiles0(projectName, repositoryName, revision, pathPattern, includeLastFileRevision,
ArmeriaCentralDogma::listFilesWithRevision);
}

private static Map<String, EntryType> listFiles(AggregatedHttpResponse res) {
switch (res.status().code()) {
case 200:
final ImmutableMap.Builder<String, EntryType> builder = ImmutableMap.builder();
final JsonNode node = toJson(res, JsonNodeType.ARRAY);
node.forEach(e -> builder.put(
getField(e, "path").asText(),
EntryType.valueOf(getField(e, "type").asText())));
return builder.build();
case 204:
return ImmutableMap.of();
}

return handleErrorResponse(res);
}

private <T> CompletableFuture<T> listFiles0(String projectName, String repositoryName,
Revision revision, PathPattern pathPattern,
int includeLastFileRevision,
Function<AggregatedHttpResponse, T> responseConverter) {
validateProjectAndRepositoryName(projectName, repositoryName);
requireNonNull(revision, "revision");
requireNonNull(pathPattern, "pathPattern");
try {
final StringBuilder path = pathBuilder(projectName, repositoryName);
path.append("/list").append(pathPattern.encoded()).append("?revision=").append(revision.major());
if (includeLastFileRevision >= 1) {
path.append("&includeLastFileRevision=").append(includeLastFileRevision);
}

return client.execute(headers(HttpMethod.GET, path.toString()))
.aggregate()
.thenApply(ArmeriaCentralDogma::listFiles);
.thenApply(responseConverter);
} catch (Exception e) {
return exceptionallyCompletedFuture(e);
}
}

private static Map<String, EntryType> listFiles(AggregatedHttpResponse res) {
private static Map<String, Entry<?>> listFilesWithRevision(AggregatedHttpResponse res) {
switch (res.status().code()) {
case 200:
final ImmutableMap.Builder<String, EntryType> builder = ImmutableMap.builder();
final ImmutableMap.Builder<String, Entry<?>> builder = ImmutableMap.builder();
final JsonNode node = toJson(res, JsonNodeType.ARRAY);
node.forEach(e -> builder.put(
getField(e, "path").asText(),
EntryType.valueOf(getField(e, "type").asText())));
for (JsonNode e : node) {
final Revision revision = new Revision(getField(e, "revision").asInt());
final String path = getField(e, "path").asText();
final EntryType type = EntryType.valueOf(getField(e, "type").asText());
final Entry<Object> entry = Entry.ofNullable(revision, path, type, null);
builder.put(path, entry);
}
return builder.build();
case 204:
return ImmutableMap.of();
Expand All @@ -455,25 +494,26 @@ public <T> CompletableFuture<Entry<T>> getFile(String projectName, String reposi

return client.execute(headers(HttpMethod.GET, path.toString()))
.aggregate()
.thenApply(res -> getFile(normRev, res, query));
.thenApply(res -> getFile(res, query));
});
} catch (Exception e) {
return exceptionallyCompletedFuture(e);
}
}

private static <T> Entry<T> getFile(Revision normRev, AggregatedHttpResponse res, Query<T> query) {
private static <T> Entry<T> getFile(AggregatedHttpResponse res, Query<T> query) {
if (res.status().code() == 200) {
final JsonNode node = toJson(res, JsonNodeType.OBJECT);
return toEntry(normRev, node, query.type());
return toEntry(node, query.type());
}

return handleErrorResponse(res);
}

@Override
public CompletableFuture<Map<String, Entry<?>>> getFiles(String projectName, String repositoryName,
Revision revision, PathPattern pathPattern) {
Revision revision, PathPattern pathPattern,
int includeLastFileRevision) {
validateProjectAndRepositoryName(projectName, repositoryName);
requireNonNull(revision, "revision");
requireNonNull(pathPattern, "pathPattern");
Expand All @@ -485,27 +525,30 @@ public CompletableFuture<Map<String, Entry<?>>> getFiles(String projectName, Str
.append(pathPattern.encoded())
.append("?revision=")
.append(normRev.major());
if (includeLastFileRevision >= 1) {
path.append("&includeLastFileRevision=").append(includeLastFileRevision);
}

return client.execute(headers(HttpMethod.GET, path.toString()))
.aggregate()
.thenApply(res -> getFiles(normRev, res));
.thenApply(res -> getFiles(res));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.thenApply(res -> getFiles(res));
.thenApply(ArmeriaCentralDogma::getFiles);

});
} catch (Exception e) {
return exceptionallyCompletedFuture(e);
}
}

private static Map<String, Entry<?>> getFiles(Revision normRev, AggregatedHttpResponse res) {
private static Map<String, Entry<?>> getFiles(AggregatedHttpResponse res) {
switch (res.status().code()) {
case 200:
final JsonNode node = toJson(res, null);
final ImmutableMap.Builder<String, Entry<?>> builder = ImmutableMap.builder();
if (node.isObject()) { // Single entry
final Entry<?> entry = toEntry(normRev, node, QueryType.IDENTITY);
final Entry<?> entry = toEntry(node, QueryType.IDENTITY);
builder.put(entry.path(), entry);
} else if (node.isArray()) { // Multiple entries
node.forEach(e -> {
final Entry<?> entry = toEntry(normRev, e, QueryType.IDENTITY);
final Entry<?> entry = toEntry(e, QueryType.IDENTITY);
builder.put(entry.path(), entry);
});
} else {
Expand Down Expand Up @@ -861,8 +904,7 @@ private static <T> Entry<T> watchFile(AggregatedHttpResponse res, QueryType quer
switch (res.status().code()) {
case 200: // OK
final JsonNode node = toJson(res, JsonNodeType.OBJECT);
final Revision revision = new Revision(getField(node, "revision").asInt());
return toEntry(revision, getField(node, "entry"), queryType);
return toEntry(getField(node, "entry"), queryType);
case 304: // Not Modified
return null;
}
Expand Down Expand Up @@ -1027,9 +1069,11 @@ private static String toString(AggregatedHttpResponse res) {
return res.content(charset);
}

private static <T> Entry<T> toEntry(Revision revision, JsonNode node, QueryType queryType) {
private static <T> Entry<T> toEntry(JsonNode node, QueryType queryType) {
final String entryPath = getField(node, "path").asText();
final EntryType receivedEntryType = EntryType.valueOf(getField(node, "type").asText());
final Revision revision = new Revision(getField(node, "revision").asInt());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client may talk to an older Central Dogma instance. Can you make it handle an old server response as well and add a test case for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like @ikhoon just moved this logic into this method:

@JsonProperty
public Revision revision() {
return revision;
}

final Revision revision = new Revision(getField(node, "revision").asInt());

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entry node always has the revision so it shouldn't be a problem?


switch (queryType) {
case IDENTITY_TEXT:
return entryAsText(revision, node, entryPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,26 @@ default CompletableFuture<Map<String, EntryType>> listFiles(String projectName,
* @return a {@link Map} of file path and type pairs
*/
CompletableFuture<Map<String, EntryType>> listFiles(String projectName, String repositoryName,
Revision revision, PathPattern pathPattern);
Revision revision, PathPattern pathPattern);

/**
* Retrieves the list of the files matched by the given {@link PathPattern}.
* This method is equivalent to calling:
* <pre>{@code
* CentralDogma dogma = ...
* // Find the last file revision from the last 100 revisions.
* int includeLastFileRevision = 100;
* dogma.forRepo(projectName, repositoryName)
* .files(pathPattern)
* .includeLastFileRevision(includeLastFileRevision)
* .list(revision);
* }</pre>
*
* @return a {@link Map} of file path and {@link Entry} pairs
*/
CompletableFuture<Map<String, Entry<?>>> listFiles(String projectName, String repositoryName,
Revision revision, PathPattern pathPattern,
int includeLastFileRevision);

/**
* Retrieves the file at the specified revision and path. This method is a shortcut of
Expand Down Expand Up @@ -220,8 +239,30 @@ default CompletableFuture<Map<String, Entry<?>>> getFiles(String projectName, St
*
* @return a {@link Map} of file path and {@link Entry} pairs
*/
default CompletableFuture<Map<String, Entry<?>>> getFiles(String projectName, String repositoryName,
Revision revision, PathPattern pathPattern) {
return getFiles(projectName, repositoryName, revision, pathPattern, -1);
}

/**
* Retrieves the files matched by the {@link PathPattern}.
* This method is equivalent to calling:
* <pre>{@code
* CentralDogma dogma = ...
* // Find the last file revision from the last 100 revisions.
* // If the last file revision is not found, the `Revision.INIT` revision is returned instead.
* int includeLastFileRevision = 100;
* dogma.forRepo(projectName, repositoryName)
* .file(pathPattern)
* .includeLastFileRevision(includeLastFileRevision)
* .get(revision);
* }</pre>
*
* @return a {@link Map} of file path and {@link Entry} pairs
*/
CompletableFuture<Map<String, Entry<?>>> getFiles(String projectName, String repositoryName,
Revision revision, PathPattern pathPattern);
Revision revision, PathPattern pathPattern,
int includeLastFileRevision);

/**
* Retrieves the merged entry of the specified {@link MergeSource}s at the specified revision.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.linecorp.centraldogma.client;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

import java.util.Map;
Expand All @@ -40,6 +41,25 @@ public final class FilesRequest {
this.pathPattern = pathPattern;
}

/**
* Includes the last revision of the file in the result. This option is disabled by default.
* If the last file revision is not found from the specified range of revisions, {@link Revision#INIT} is
* returned.
*
* <p>Note that this operation may be slow because it needs to search for the last file revisions from the
* revision history. So please use it carefully.
*
* @param includeLastFileRevision the maximum number of revisions to search for the last file revision.
* If the value is equal to 1, the head revision is
* included instead.
*/
public FilesWithRevisionRequest includeLastFileRevision(int includeLastFileRevision) {
checkArgument(includeLastFileRevision >= 1 && includeLastFileRevision <= 1000,
"includeLastFileRevision: %s (expected: 1 .. 1000)",
includeLastFileRevision);
return new FilesWithRevisionRequest(centralDogmaRepo, pathPattern, includeLastFileRevision);
}

/**
* Retrieves the list of the files matched by the given path pattern at the {@link Revision#HEAD}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2025 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.client;

import static java.util.Objects.requireNonNull;

import java.util.Map;
import java.util.concurrent.CompletableFuture;

import com.linecorp.centraldogma.common.Entry;
import com.linecorp.centraldogma.common.PathPattern;
import com.linecorp.centraldogma.common.Revision;

/**
* Prepares to send a {@link CentralDogma#getFiles(String, String, Revision, PathPattern, int)} or
* {@link CentralDogma#listFiles(String, String, Revision, PathPattern, int)} request to the
* Central Dogma repository.
*/
public final class FilesWithRevisionRequest {

private final CentralDogmaRepository centralDogmaRepo;
private final PathPattern pathPattern;
private final int includeLastFileRevision;

FilesWithRevisionRequest(CentralDogmaRepository centralDogmaRepo, PathPattern pathPattern,
int includeLastFileRevision) {
this.centralDogmaRepo = centralDogmaRepo;
this.pathPattern = pathPattern;
this.includeLastFileRevision = includeLastFileRevision;
}

/**
* Retrieves the list of the files matched by the given path pattern at the {@link Revision#HEAD}.
* Note that the returned {@link Entry} does not contain the content of the file.
*
* @return a {@link Map} of file path and {@link Entry} pairs.
*/
public CompletableFuture<Map<String, Entry<?>>> list() {
return list(Revision.HEAD);
}

/**
* Retrieves the list of the files matched by the given path pattern at the {@link Revision}.
* Note that the returned {@link Entry} does not contain the content of the file.
*
* @return a {@link Map} of file path and {@link Entry} pairs
*/
public CompletableFuture<Map<String, Entry<?>>> list(Revision revision) {
requireNonNull(revision, "revision");
return centralDogmaRepo.centralDogma().listFiles(centralDogmaRepo.projectName(),
centralDogmaRepo.repositoryName(),
revision, pathPattern, includeLastFileRevision);
}

/**
* Retrieves the files matched by the path pattern at the {@link Revision#HEAD}.
*
* @return a {@link Map} of file path and {@link Entry} pairs
*/
public CompletableFuture<Map<String, Entry<?>>> get() {
return get(Revision.HEAD);
}

/**
* Retrieves the files matched by the path pattern at the {@link Revision}.
*
* @return a {@link Map} of file path and {@link Entry} pairs
*/
public CompletableFuture<Map<String, Entry<?>>> get(Revision revision) {
requireNonNull(revision, "revision");
return centralDogmaRepo.centralDogma().getFiles(centralDogmaRepo.projectName(),
centralDogmaRepo.repositoryName(),
revision, pathPattern, includeLastFileRevision);
}
}
Loading
Loading