From 87081e00676d009f856f112d58a77a78256ecebb Mon Sep 17 00:00:00 2001 From: Shinpei Hayashi Date: Mon, 23 Mar 2026 00:06:03 +0900 Subject: [PATCH 1/9] Introduce intermediate layer: BlobEntry --- .../ac/titech/c/se/stein/app/Anonymize.java | 4 +- .../c/se/stein/app/blob/ConvertBlob.java | 9 +- .../ac/titech/c/se/stein/app/blob/Cregit.java | 3 +- .../c/se/stein/app/blob/FilterBlob.java | 4 +- .../titech/c/se/stein/app/blob/Historage.java | 3 +- .../c/se/stein/app/blob/HistorageViaJDT.java | 3 +- .../titech/c/se/stein/app/blob/Tokenize.java | 4 +- .../c/se/stein/app/blob/TokenizeViaJDT.java | 4 +- .../c/se/stein/app/blob/Untokenize.java | 4 +- .../ac/titech/c/se/stein/entry/BlobEntry.java | 124 +++++++++++++++ .../ac/titech/c/se/stein/entry/HotEntry.java | 142 ++---------------- .../c/se/stein/rewriter/BlobTranslator.java | 12 +- .../se/stein/rewriter/RepositoryRewriter.java | 3 +- .../c/se/stein/app/blob/ConvertBlobTest.java | 9 +- .../stein/app/blob/HistorageViaJDTTest.java | 5 +- .../c/se/stein/entry/AnyHotEntryTest.java | 4 +- .../titech/c/se/stein/entry/HotEntryTest.java | 12 +- .../se/stein/rewriter/BlobTranslatorTest.java | 7 +- 18 files changed, 189 insertions(+), 167 deletions(-) create mode 100644 src/main/java/jp/ac/titech/c/se/stein/entry/BlobEntry.java diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/Anonymize.java b/src/main/java/jp/ac/titech/c/se/stein/app/Anonymize.java index 467fecb..d474b93 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/Anonymize.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/Anonymize.java @@ -5,7 +5,7 @@ import jp.ac.titech.c.se.stein.entry.Entry; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; -import jp.ac.titech.c.se.stein.entry.HotEntry; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.util.HashUtils; import lombok.ToString; import lombok.extern.slf4j.Slf4j; @@ -105,7 +105,7 @@ public String rewriteMessage(final String message, final Context c) { } @Override - public AnyHotEntry rewriteBlobEntry(HotEntry entry, final Context c) { + public AnyHotEntry rewriteBlobEntry(BlobEntry entry, final Context c) { if (isBlobContentEnabled) { entry = entry.update(entry.getId().name()); } diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlob.java b/src/main/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlob.java index 3b6a3f9..24e0328 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlob.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlob.java @@ -3,6 +3,7 @@ import jp.ac.titech.c.se.stein.core.Context; import jp.ac.titech.c.se.stein.core.Try; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.entry.HotEntry; import jp.ac.titech.c.se.stein.rewriter.BlobTranslator; import jp.ac.titech.c.se.stein.rewriter.NameFilter; @@ -77,7 +78,7 @@ protected String[] makeCommand(final String inputFile) { } @Override - public AnyHotEntry rewriteBlobEntry(final HotEntry entry, final Context c) { + public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { if (!filter.accept(entry)) { return entry; } @@ -92,7 +93,7 @@ public AnyHotEntry rewriteBlobEntry(final HotEntry entry, final Context c) { } } - protected HotEntry processCommandlineFilter(final HotEntry entry, final Context c) { + protected BlobEntry processCommandlineFilter(final BlobEntry entry, final Context c) { try { final Process proc = new ProcessBuilder() .command(makeCommand(entry.getName())) @@ -131,7 +132,7 @@ protected HotEntry processCommandlineFilter(final HotEntry entry, final Context } } - protected AnyHotEntry processCommandline(final HotEntry entry, final Context c) { + protected AnyHotEntry processCommandline(final BlobEntry entry, final Context c) { try (final TemporaryFile tmp = TemporaryFile.directoryOf("_stein")) { // write input final Path inputPath = tmp.getPath().resolve(entry.getName()); @@ -163,7 +164,7 @@ protected AnyHotEntry processCommandline(final HotEntry entry, final Context c) } } - protected HotEntry processEndpoint(final HotEntry entry, final Context c) { + protected BlobEntry processEndpoint(final BlobEntry entry, final Context c) { try { final HttpURLConnection conn = (HttpURLConnection) options.endpoint.openConnection(); conn.setRequestMethod("POST"); diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/blob/Cregit.java b/src/main/java/jp/ac/titech/c/se/stein/app/blob/Cregit.java index 0c258bd..5decb18 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/blob/Cregit.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/blob/Cregit.java @@ -2,6 +2,7 @@ import jp.ac.titech.c.se.stein.core.Context; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.entry.HotEntry; import jp.ac.titech.c.se.stein.rewriter.BlobTranslator; import jp.ac.titech.c.se.stein.rewriter.NameFilter; @@ -81,7 +82,7 @@ protected void setLanguage(final String language) { protected String language; @Override - public AnyHotEntry rewriteBlobEntry(HotEntry entry, Context c) { + public AnyHotEntry rewriteBlobEntry(BlobEntry entry, Context c) { if (!filter.accept(entry)) { return entry; } diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/blob/FilterBlob.java b/src/main/java/jp/ac/titech/c/se/stein/app/blob/FilterBlob.java index 179e4f1..4da0153 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/blob/FilterBlob.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/blob/FilterBlob.java @@ -1,7 +1,7 @@ package jp.ac.titech.c.se.stein.app.blob; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; -import jp.ac.titech.c.se.stein.entry.HotEntry; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.rewriter.BlobTranslator; import jp.ac.titech.c.se.stein.rewriter.NameFilter; import lombok.ToString; @@ -30,7 +30,7 @@ public class FilterBlob implements BlobTranslator { protected long maxSize = -1L; @Override - public AnyHotEntry rewriteBlobEntry(final HotEntry entry, final Context c) { + public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { // name if (nameFilter.getPatterns() != null) { if (!nameFilter.accept(entry)) { diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/blob/Historage.java b/src/main/java/jp/ac/titech/c/se/stein/app/blob/Historage.java index c4da302..d22b497 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/blob/Historage.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/blob/Historage.java @@ -4,6 +4,7 @@ import com.google.gson.reflect.TypeToken; import jp.ac.titech.c.se.stein.core.*; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.entry.HotEntry; import jp.ac.titech.c.se.stein.rewriter.BlobTranslator; import jp.ac.titech.c.se.stein.rewriter.NameFilter; @@ -59,7 +60,7 @@ public class Historage implements BlobTranslator { protected Set moduleKinds; @Override - public AnyHotEntry rewriteBlobEntry(final HotEntry entry, final Context c) { + public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { if (!filter.accept(entry)) { return entry; } diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/blob/HistorageViaJDT.java b/src/main/java/jp/ac/titech/c/se/stein/app/blob/HistorageViaJDT.java index b77df83..1f862b1 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/blob/HistorageViaJDT.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/blob/HistorageViaJDT.java @@ -9,6 +9,7 @@ import jp.ac.titech.c.se.stein.entry.AnyHotEntry; import jp.ac.titech.c.se.stein.core.SourceText; import jp.ac.titech.c.se.stein.core.SourceText.Fragment; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.entry.HotEntry; import jp.ac.titech.c.se.stein.rewriter.BlobTranslator; import jp.ac.titech.c.se.stein.rewriter.NameFilter; @@ -86,7 +87,7 @@ public class HistorageViaJDT implements BlobTranslator { protected boolean parsable = false; @Override - public AnyHotEntry rewriteBlobEntry(final HotEntry entry, final Context c) { + public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { if (!JAVA.accept(entry)) { return entry; } diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/blob/Tokenize.java b/src/main/java/jp/ac/titech/c/se/stein/app/blob/Tokenize.java index b32ab6a..1de8689 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/blob/Tokenize.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/blob/Tokenize.java @@ -3,7 +3,7 @@ import jp.ac.titech.c.se.stein.core.Context; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; import jp.ac.titech.c.se.stein.core.SourceText; -import jp.ac.titech.c.se.stein.entry.HotEntry; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.rewriter.BlobTranslator; import lombok.ToString; import picocli.CommandLine.Command; @@ -30,7 +30,7 @@ public class Tokenize implements BlobTranslator { )); @Override - public AnyHotEntry rewriteBlobEntry(final HotEntry entry, final Context c) { + public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { final String text = SourceText.of(entry.getBlob()).getContent(); return entry.update(encode(text)); } diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/blob/TokenizeViaJDT.java b/src/main/java/jp/ac/titech/c/se/stein/app/blob/TokenizeViaJDT.java index 84a3820..34c52ba 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/blob/TokenizeViaJDT.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/blob/TokenizeViaJDT.java @@ -4,7 +4,7 @@ import jp.ac.titech.c.se.stein.entry.AnyHotEntry; import jp.ac.titech.c.se.stein.core.SourceText; -import jp.ac.titech.c.se.stein.entry.HotEntry; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.rewriter.BlobTranslator; import lombok.ToString; import lombok.extern.slf4j.Slf4j; @@ -27,7 +27,7 @@ @Command(name = "@tokenize-jdt", description = "Encode Java source files to linetoken format via JDT") public class TokenizeViaJDT implements BlobTranslator { @Override - public AnyHotEntry rewriteBlobEntry(final HotEntry entry, final Context c) { + public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { if (!HistorageViaJDT.JAVA.accept(entry)) { return entry; } diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/blob/Untokenize.java b/src/main/java/jp/ac/titech/c/se/stein/app/blob/Untokenize.java index b1c91c5..4d087b3 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/blob/Untokenize.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/blob/Untokenize.java @@ -3,7 +3,7 @@ import jp.ac.titech.c.se.stein.core.Context; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; import jp.ac.titech.c.se.stein.core.SourceText; -import jp.ac.titech.c.se.stein.entry.HotEntry; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.rewriter.BlobTranslator; import jp.ac.titech.c.se.stein.rewriter.NameFilter; import lombok.ToString; @@ -23,7 +23,7 @@ public class Untokenize implements BlobTranslator { @Mixin protected final NameFilter filter = new NameFilter(); @Override - public AnyHotEntry rewriteBlobEntry(final HotEntry entry, final Context c) { + public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { if (!filter.accept(entry)) { return entry; } diff --git a/src/main/java/jp/ac/titech/c/se/stein/entry/BlobEntry.java b/src/main/java/jp/ac/titech/c/se/stein/entry/BlobEntry.java new file mode 100644 index 0000000..879d067 --- /dev/null +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/BlobEntry.java @@ -0,0 +1,124 @@ +package jp.ac.titech.c.se.stein.entry; + +import jp.ac.titech.c.se.stein.core.Context; +import jp.ac.titech.c.se.stein.core.RepositoryAccess; +import jp.ac.titech.c.se.stein.util.HashUtils; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jgit.lib.ObjectId; + +import java.nio.charset.StandardCharsets; + +/** + * A Hot entry representing a blob (file content). + * + * @see SourceBlob + * @see NewBlob + */ +public abstract class BlobEntry extends HotEntry { + public abstract byte[] getBlob(); + + public abstract long getBlobSize(); + + @Override + public Entry fold(RepositoryAccess target, Context c) { + return Entry.of(getMode(), getName(), target.writeBlob(getBlob(), c), getDirectory()); + } + + /** + * Returns a new {@link NewBlob} with the given name, keeping the blob content unchanged. + */ + public NewBlob rename(final String newName) { + return new NewBlob(getMode(), newName, getBlob(), getDirectory()); + } + + /** + * Returns a new {@link NewBlob} with the given blob content, keeping the name unchanged. + */ + public NewBlob update(final byte[] newBlob) { + return new NewBlob(getMode(), getName(), newBlob, getDirectory()); + } + + /** + * String variant of {@link #update(byte[])}. + */ + public NewBlob update(final String newContent) { + return update(newContent.getBytes(StandardCharsets.UTF_8)); + } + + /** + * A Hot entry backed by an existing blob in a repository. + * The blob content is lazily loaded on the first call to {@link #getBlob()}. + */ + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + public static class SourceBlob extends BlobEntry { + @Delegate(types = SingleEntry.class) + private final Entry entry; + + private final RepositoryAccess source; + + private byte[] blob; + + @Override + public byte[] getBlob() { + if (blob == null) { + blob = source.readBlob(entry.id); + } + return blob; + } + + @Override + public long getBlobSize() { + return blob != null ? blob.length : source.getBlobSize(entry.id); + } + + @Override + public String toString() { + return String.format("%s [hot(%s):%o]", getPath(), getId().name(), getMode()); + } + } + + /** + * A Hot entry holding new or transformed blob data directly. + */ + @Slf4j + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + public static class NewBlob extends BlobEntry { + @Getter + private final int mode; + + @Getter + private final String name; + + @Getter + private final byte[] blob; + + @Getter + private final String directory; + + @Override + public long getBlobSize() { + return blob.length; + } + + /** + * Computes and returns the SHA-1 hash of the blob data. + * Since a {@link NewBlob} has no pre-existing object ID, this requires + * hash computation on every call and logs a warning, as it is typically + * not intended in normal usage. + */ + @Override + public ObjectId getId() { + log.warn("Getting Object ID for NewBlob requires hash computation"); + return HashUtils.idFor(blob); + } + + @Override + public String toString() { + return String.format("%s [new(%d):%o]", getPath(), getBlobSize(), getMode()); + } + } +} diff --git a/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java b/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java index 9396a37..3d6fcd8 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java @@ -2,63 +2,47 @@ import jp.ac.titech.c.se.stein.core.Context; import jp.ac.titech.c.se.stein.core.RepositoryAccess; -import jp.ac.titech.c.se.stein.util.HashUtils; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.experimental.Delegate; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.ObjectInserter; -import java.nio.charset.StandardCharsets; import java.util.stream.Stream; /** - * A Hot (data-bearing) single tree entry that holds or lazily loads the actual blob content. + * A Hot (data-bearing) single tree entry. * *

This is the abstract base on the Hot side of the entry hierarchy, implementing both * {@link AnyHotEntry} (as a singleton collection) and {@link SingleEntry}.

* + * @see BlobEntry * @see Entry - * @see AnyHotEntry - * @see SourceBlob - * @see NewBlob */ public abstract class HotEntry implements AnyHotEntry, SingleEntry { /** - * Creates a {@link SourceBlob} that lazily reads blob content from the given source. + * Creates a {@link BlobEntry.SourceBlob} that lazily reads blob content from the given source. */ - public static SourceBlob of(Entry e, RepositoryAccess source) { - return new SourceBlob(e, source); + public static BlobEntry.SourceBlob of(Entry e, RepositoryAccess source) { + return new BlobEntry.SourceBlob(e, source); } /** - * Creates a {@link NewBlob} by replacing the blob content of an existing entry. + * Creates a {@link BlobEntry.NewBlob} by replacing the blob content of an existing entry. */ - public static NewBlob of(Entry e, byte[] updatedBlob) { - return new NewBlob(e.getMode(), e.getName(), updatedBlob, e.getDirectory()); + public static BlobEntry.NewBlob of(Entry e, byte[] updatedBlob) { + return new BlobEntry.NewBlob(e.getMode(), e.getName(), updatedBlob, e.getDirectory()); } /** - * Creates a {@link NewBlob} with the given properties. + * Creates a {@link BlobEntry.NewBlob} with the given properties. */ - public static NewBlob of(int mode, String name, byte[] blob) { - return new NewBlob(mode, name, blob, null); + public static BlobEntry.NewBlob of(int mode, String name, byte[] blob) { + return new BlobEntry.NewBlob(mode, name, blob, null); } /** - * Creates a {@link NewBlob} with the given properties. + * Creates a {@link BlobEntry.NewBlob} with the given properties. */ - public static NewBlob of(int mode, String name, byte[] blob, String directory) { - return new NewBlob(mode, name, blob, directory); + public static BlobEntry.NewBlob of(int mode, String name, byte[] blob, String directory) { + return new BlobEntry.NewBlob(mode, name, blob, directory); } - public abstract byte[] getBlob(); - - public abstract long getBlobSize(); - @Override public Stream stream() { return Stream.of(this); @@ -70,101 +54,5 @@ public int size() { } @Override - public Entry fold(RepositoryAccess target, Context c) { - return Entry.of(getMode(), getName(), target.writeBlob(getBlob(), c), getDirectory()); - } - - /** - * Returns a new {@link NewBlob} with the given name, keeping the blob content unchanged. - */ - public NewBlob rename(final String newName) { - return of(getMode(), newName, getBlob(), getDirectory()); - } - - /** - * Returns a new {@link NewBlob} with the given blob content, keeping the name unchanged. - */ - public NewBlob update(final byte[] newBlob) { - return of(getMode(), getName(), newBlob, getDirectory()); - } - - /** - * String variant of {@link #update(byte[])}. - */ - public NewBlob update(final String newContent) { - return update(newContent.getBytes(StandardCharsets.UTF_8)); - } - - /** - * A Hot entry backed by an existing blob in a repository. - * The blob content is lazily loaded on the first call to {@link #getBlob()}. - */ - @RequiredArgsConstructor(access = AccessLevel.PACKAGE) - public static class SourceBlob extends HotEntry { - @Delegate(types = SingleEntry.class) - private final Entry entry; - - private final RepositoryAccess source; - - private byte[] blob; - - @Override - public byte[] getBlob() { - if (blob == null) { - blob = source.readBlob(entry.id); - } - return blob; - } - - @Override - public long getBlobSize() { - return blob != null ? blob.length : source.getBlobSize(entry.id); - } - - @Override - public String toString() { - return String.format("%s [hot(%s):%o]", getPath(), getId().name(), getMode()); - } - } - - /** - * A Hot entry holding new or transformed blob data directly. - */ - @Slf4j - @RequiredArgsConstructor(access = AccessLevel.PACKAGE) - public static class NewBlob extends HotEntry { - @Getter - private final int mode; - - @Getter - private final String name; - - @Getter - private final byte[] blob; - - @Getter - private final String directory; - - @Override - public long getBlobSize() { - return blob.length; - } - - /** - * Computes and returns the SHA-1 hash of the blob data. - * Since a {@link NewBlob} has no pre-existing object ID, this requires - * hash computation on every call and logs a warning, as it is typically - * not intended in normal usage. - */ - @Override - public ObjectId getId() { - log.warn("Getting Object ID for NewBlob requires hash computation"); - return HashUtils.idFor(blob); - } - - @Override - public String toString() { - return String.format("%s [new(%d):%o]", getPath(), getBlobSize(), getMode()); - } - } + public abstract Entry fold(RepositoryAccess target, Context c); } diff --git a/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java b/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java index be2e6db..cc70e0b 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java +++ b/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java @@ -2,6 +2,7 @@ import jp.ac.titech.c.se.stein.core.*; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.entry.HotEntry; import lombok.Getter; import lombok.ToString; @@ -15,7 +16,7 @@ public interface BlobTranslator extends RewriterCommand { default void setUp(final Context c) {} - AnyHotEntry rewriteBlobEntry(final HotEntry entry, final Context c); + AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c); /** * Creates a {@link BlobTranslator} from a String-to-String function. @@ -38,7 +39,7 @@ public Single(BlobTranslator translator) { } @Override - public AnyHotEntry rewriteBlobEntry(final HotEntry entry, final Context c) { + public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { return translator.rewriteBlobEntry(entry, c); } } @@ -63,10 +64,11 @@ public void setUp(final Context c) { } @Override - public AnyHotEntry rewriteBlobEntry(final HotEntry entry, final Context c) { - Stream stream = Stream.of(entry); + public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { + Stream stream = Stream.of(entry); for (BlobTranslator translator : translators) { - stream = stream.flatMap(e -> translator.rewriteBlobEntry(e, c).stream()); + // TODO: if e is not BlobEntry? + stream = stream.flatMap(e -> translator.rewriteBlobEntry((BlobEntry) e, c).stream()); } return AnyHotEntry.set(stream.collect(Collectors.toList())); } diff --git a/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java b/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java index 565e9ab..25d93ac 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java +++ b/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java @@ -10,6 +10,7 @@ import java.util.stream.StreamSupport; import jp.ac.titech.c.se.stein.core.*; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.entry.Entry; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; import jp.ac.titech.c.se.stein.entry.HotEntry; @@ -368,7 +369,7 @@ protected AnyColdEntry rewriteEntry(final Entry entry, final Context c) { } } - protected AnyHotEntry rewriteBlobEntry(HotEntry entry, Context c) { + protected AnyHotEntry rewriteBlobEntry(BlobEntry entry, Context c) { return entry; } diff --git a/src/test/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlobTest.java b/src/test/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlobTest.java index 82bf094..7a69517 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlobTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlobTest.java @@ -3,6 +3,7 @@ import com.sun.net.httpserver.HttpServer; import jp.ac.titech.c.se.stein.core.Context; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.entry.HotEntry; import jp.ac.titech.c.se.stein.util.ProcessRunner; import org.eclipse.jgit.lib.FileMode; @@ -51,11 +52,11 @@ public void testFilterMode() { convert.requiresShell = true; convert.isFilter = true; - final HotEntry entry = HotEntry.of(BLOB_MODE, "hello.txt", "hello".getBytes(StandardCharsets.UTF_8)); + final BlobEntry entry = HotEntry.of(BLOB_MODE, "hello.txt", "hello".getBytes(StandardCharsets.UTF_8)); final AnyHotEntry result = convert.rewriteBlobEntry(entry, C); assertEquals(1, result.size()); - assertEquals("HELLO", new String(result.stream().findFirst().orElseThrow().getBlob(), StandardCharsets.UTF_8)); + assertEquals("HELLO", new String(((BlobEntry) result.stream().findFirst().orElseThrow()).getBlob(), StandardCharsets.UTF_8)); } @Test @@ -78,11 +79,11 @@ public void testEndpointMode() throws Exception { convert.options = new ConvertBlob.ConvertOptions(); convert.options.endpoint = new URL("http://127.0.0.1:" + port + "/convert"); - final HotEntry entry = HotEntry.of(BLOB_MODE, "hello.txt", "hello".getBytes(StandardCharsets.UTF_8)); + final BlobEntry entry = HotEntry.of(BLOB_MODE, "hello.txt", "hello".getBytes(StandardCharsets.UTF_8)); final AnyHotEntry result = convert.rewriteBlobEntry(entry, C); assertEquals(1, result.size()); - assertEquals("HELLO", new String(result.stream().findFirst().orElseThrow().getBlob(), StandardCharsets.UTF_8)); + assertEquals("HELLO", new String(((BlobEntry) result.stream().findFirst().orElseThrow()).getBlob(), StandardCharsets.UTF_8)); } finally { server.stop(0); } diff --git a/src/test/java/jp/ac/titech/c/se/stein/app/blob/HistorageViaJDTTest.java b/src/test/java/jp/ac/titech/c/se/stein/app/blob/HistorageViaJDTTest.java index d77b438..7c3f6f8 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/app/blob/HistorageViaJDTTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/app/blob/HistorageViaJDTTest.java @@ -5,6 +5,7 @@ import jp.ac.titech.c.se.stein.core.SourceText; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; import jp.ac.titech.c.se.stein.entry.Entry; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.entry.HotEntry; import jp.ac.titech.c.se.stein.core.RepositoryAccess; import jp.ac.titech.c.se.stein.testing.TestRepo; @@ -166,7 +167,7 @@ public void testDigestParameters() { @Test public void testNonJavaFilePassedThrough() { - HotEntry entry = HotEntry.of(BLOB_MODE, "README.md", "# Hello".getBytes(StandardCharsets.UTF_8)); + BlobEntry entry = HotEntry.of(BLOB_MODE, "README.md", "# Hello".getBytes(StandardCharsets.UTF_8)); HistorageViaJDT historage = new HistorageViaJDT(); AnyHotEntry result = historage.rewriteBlobEntry(entry, Context.init()); assertEquals(1, result.size()); @@ -177,7 +178,7 @@ public void testNonJavaFilePassedThrough() { public void testRequiresOriginals() { HistorageViaJDT historage = new HistorageViaJDT(); historage.requiresOriginals = false; - HotEntry entry = HotEntry.of(BLOB_MODE, "Hello.java", sampleSource.getBytes(StandardCharsets.UTF_8)); + BlobEntry entry = HotEntry.of(BLOB_MODE, "Hello.java", sampleSource.getBytes(StandardCharsets.UTF_8)); AnyHotEntry result = historage.rewriteBlobEntry(entry, Context.init()); // original should NOT be included diff --git a/src/test/java/jp/ac/titech/c/se/stein/entry/AnyHotEntryTest.java b/src/test/java/jp/ac/titech/c/se/stein/entry/AnyHotEntryTest.java index 2e61dc7..34d205b 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/entry/AnyHotEntryTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/entry/AnyHotEntryTest.java @@ -15,8 +15,8 @@ public class AnyHotEntryTest { static final byte[] HELLO = "hello".getBytes(StandardCharsets.UTF_8); static final byte[] WORLD = "world".getBytes(StandardCharsets.UTF_8); - final HotEntry.NewBlob h1 = HotEntry.of(BLOB_MODE, "hello.txt", HELLO); - final HotEntry.NewBlob h2 = HotEntry.of(BLOB_MODE, "world.txt", WORLD); + final BlobEntry h1 = HotEntry.of(BLOB_MODE, "hello.txt", HELLO); + final BlobEntry h2 = HotEntry.of(BLOB_MODE, "world.txt", WORLD); @Test public void testEmpty() { diff --git a/src/test/java/jp/ac/titech/c/se/stein/entry/HotEntryTest.java b/src/test/java/jp/ac/titech/c/se/stein/entry/HotEntryTest.java index 125fe21..670df36 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/entry/HotEntryTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/entry/HotEntryTest.java @@ -14,7 +14,7 @@ public class HotEntryTest { static final int BLOB_MODE = FileMode.REGULAR_FILE.getBits(); static final byte[] HELLO = "hello".getBytes(StandardCharsets.UTF_8); - final HotEntry.NewBlob nb = HotEntry.of(BLOB_MODE, "hello", HELLO); + final BlobEntry nb = HotEntry.of(BLOB_MODE, "hello", HELLO); @Test public void testFactories() { @@ -23,7 +23,7 @@ public void testFactories() { assertArrayEquals(HELLO, nb.getBlob()); assertNull(nb.getDirectory()); - final HotEntry withDir = HotEntry.of(BLOB_MODE, "hello", HELLO, "src"); + final BlobEntry withDir = HotEntry.of(BLOB_MODE, "hello", HELLO, "src"); assertEquals("src", withDir.getDirectory()); final Entry entry = Entry.of(BLOB_MODE, "hello", ObjectId.zeroId()); @@ -38,7 +38,7 @@ public void testBlobSize() { @Test public void testSingleEntryMethods() { - final HotEntry withDir = HotEntry.of(BLOB_MODE, "hello", HELLO, "src"); + final BlobEntry withDir = HotEntry.of(BLOB_MODE, "hello", HELLO, "src"); assertEquals("src/hello", withDir.getPath()); assertTrue(withDir.isBlob()); assertEquals(SingleEntry.Type.BLOB, withDir.getType()); @@ -52,8 +52,8 @@ public void testStream() { @Test public void testRename() { - final HotEntry withDir = HotEntry.of(BLOB_MODE, "hello", HELLO, "dir"); - final HotEntry renamed = withDir.rename("world"); + final BlobEntry withDir = HotEntry.of(BLOB_MODE, "hello", HELLO, "dir"); + final BlobEntry renamed = withDir.rename("world"); assertNotSame(withDir, renamed); assertEquals("world", renamed.getName()); assertEquals("hello", withDir.getName()); @@ -65,7 +65,7 @@ public void testRename() { public void testUpdate() { final byte[] newData = "world".getBytes(StandardCharsets.UTF_8); - final HotEntry updated = nb.update(newData); + final BlobEntry updated = nb.update(newData); assertNotSame(nb, updated); assertArrayEquals(newData, updated.getBlob()); assertEquals("hello", updated.getName()); diff --git a/src/test/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslatorTest.java b/src/test/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslatorTest.java index b85301d..7beb447 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslatorTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslatorTest.java @@ -5,6 +5,7 @@ import jp.ac.titech.c.se.stein.core.Context; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; import jp.ac.titech.c.se.stein.entry.Entry; +import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.entry.HotEntry; import jp.ac.titech.c.se.stein.core.RepositoryAccess; import jp.ac.titech.c.se.stein.testing.TestRepo; @@ -23,12 +24,12 @@ public class BlobTranslatorTest { static final int BLOB_MODE = FileMode.REGULAR_FILE.getBits(); static final Context CTX = Context.init(); - HotEntry blob(String name, String content) { + BlobEntry blob(String name, String content) { return HotEntry.of(BLOB_MODE, name, content.getBytes(StandardCharsets.UTF_8)); } - String content(HotEntry entry) { - return new String(entry.getBlob(), StandardCharsets.UTF_8); + String content(AnyHotEntry entry) { + return new String(((BlobEntry) entry.stream().findFirst().orElseThrow()).getBlob(), StandardCharsets.UTF_8); } @Test From 8d2ce0903681c42c7f9a19474b05c7d77bcbf952 Mon Sep 17 00:00:00 2001 From: Shinpei Hayashi Date: Mon, 23 Mar 2026 10:41:14 +0900 Subject: [PATCH 2/9] TreeEntry: hot entries for tree objects --- .../titech/c/se/stein/entry/AnyHotEntry.java | 2 + .../ac/titech/c/se/stein/entry/HotEntry.java | 26 +++- .../ac/titech/c/se/stein/entry/TreeEntry.java | 143 ++++++++++++++++++ .../c/se/stein/rewriter/BlobTranslator.java | 38 ++++- .../se/stein/rewriter/RepositoryRewriter.java | 58 ++++--- .../titech/c/se/stein/entry/HotEntryTest.java | 2 +- 6 files changed, 222 insertions(+), 47 deletions(-) create mode 100644 src/main/java/jp/ac/titech/c/se/stein/entry/TreeEntry.java diff --git a/src/main/java/jp/ac/titech/c/se/stein/entry/AnyHotEntry.java b/src/main/java/jp/ac/titech/c/se/stein/entry/AnyHotEntry.java index d79b3fb..b9b46b0 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/entry/AnyHotEntry.java +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/AnyHotEntry.java @@ -3,6 +3,7 @@ import jp.ac.titech.c.se.stein.core.Context; import jp.ac.titech.c.se.stein.core.RepositoryAccess; import lombok.Getter; +import org.eclipse.jgit.lib.ObjectId; import java.util.ArrayList; import java.util.Arrays; @@ -98,6 +99,7 @@ public int size() { public AnyColdEntry fold(RepositoryAccess target, Context c) { return AnyColdEntry.set(stream() .map(e -> e.fold(target, c)) + .filter(e -> !e.getId().equals(ObjectId.zeroId())) .collect(Collectors.toList())) .pack(); } diff --git a/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java b/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java index 3d6fcd8..1081bf6 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java @@ -12,37 +12,47 @@ * {@link AnyHotEntry} (as a singleton collection) and {@link SingleEntry}.

* * @see BlobEntry + * @see TreeEntry * @see Entry */ public abstract class HotEntry implements AnyHotEntry, SingleEntry { /** - * Creates a {@link BlobEntry.SourceBlob} that lazily reads blob content from the given source. + * Creates a {@link BlobEntry} that lazily reads blob content from the given source. */ - public static BlobEntry.SourceBlob of(Entry e, RepositoryAccess source) { + public static BlobEntry of(Entry e, RepositoryAccess source) { return new BlobEntry.SourceBlob(e, source); } /** - * Creates a {@link BlobEntry.NewBlob} by replacing the blob content of an existing entry. + * Creates a {@link BlobEntry} by replacing the blob content of an existing entry. */ - public static BlobEntry.NewBlob of(Entry e, byte[] updatedBlob) { + public static BlobEntry of(Entry e, byte[] updatedBlob) { return new BlobEntry.NewBlob(e.getMode(), e.getName(), updatedBlob, e.getDirectory()); } /** - * Creates a {@link BlobEntry.NewBlob} with the given properties. + * Creates a {@link BlobEntry} with the given properties. */ - public static BlobEntry.NewBlob of(int mode, String name, byte[] blob) { + public static BlobEntry of(int mode, String name, byte[] blob) { return new BlobEntry.NewBlob(mode, name, blob, null); } /** - * Creates a {@link BlobEntry.NewBlob} with the given properties. + * Creates a {@link BlobEntry} with the given properties. */ - public static BlobEntry.NewBlob of(int mode, String name, byte[] blob, String directory) { + public static BlobEntry of(int mode, String name, byte[] blob, String directory) { return new BlobEntry.NewBlob(mode, name, blob, directory); } + /** + * Creates a {@link TreeEntry} that lazily reads tree contents from the given source. + * + * @param directory the directory path to set on child entries, or {@code null} + */ + public static TreeEntry ofTree(Entry e, RepositoryAccess source, String directory) { + return new TreeEntry.SourceTree(e, source, directory); + } + @Override public Stream stream() { return Stream.of(this); diff --git a/src/main/java/jp/ac/titech/c/se/stein/entry/TreeEntry.java b/src/main/java/jp/ac/titech/c/se/stein/entry/TreeEntry.java new file mode 100644 index 0000000..ee096ef --- /dev/null +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/TreeEntry.java @@ -0,0 +1,143 @@ +package jp.ac.titech.c.se.stein.entry; + +import jp.ac.titech.c.se.stein.core.Context; +import jp.ac.titech.c.se.stein.core.RepositoryAccess; +import lombok.Getter; +import lombok.experimental.Delegate; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A Hot entry representing a tree (directory). + * + * @see SourceTree + * @see NewTree + */ +public abstract class TreeEntry extends HotEntry { + @Override + public int getMode() { + return FileMode.TREE.getBits(); + } + + /** + * Returns the children as cold entries. + */ + public abstract List getEntries(); + + /** + * Returns the children as hot entries. + */ + public abstract List getHotEntries(); + + /** + * A Hot tree entry backed by an existing tree in a repository. + * The tree contents are lazily loaded on the first call to {@link #getEntries()}. + */ + public static class SourceTree extends TreeEntry { + @Delegate(types = SingleEntry.class) + private final Entry entry; + + private final RepositoryAccess source; + + private final String directory; + + private List entries; + + SourceTree(Entry entry, RepositoryAccess source, String directory) { + this.entry = entry; + this.source = source; + this.directory = directory; + } + + @Override + public List getEntries() { + if (entries == null) { + entries = source.readTree(entry.id, directory); + } + return entries; + } + + @Override + public List getHotEntries() { + return getEntries().stream().map(e -> { + if (e.isTree()) { + return HotEntry.ofTree(e, source, directory != null ? directory + "/" + e.getName() : null); + } else { + return HotEntry.of(e, source); + } + }).collect(Collectors.toList()); + } + + @Override + public Entry fold(RepositoryAccess target, Context c) { + return entry; + } + + @Override + public String toString() { + return String.format("%s [source-tree:%o]", getPath(), getMode()); + } + } + + /** + * A new tree node holding child entries in memory. + * + *

BlobTranslators can return a NewTree to produce subdirectory structures. + * On {@link #fold}, children are recursively folded and an empty tree + * (all children produce zero IDs) collapses to a zero-ID entry.

+ */ + public static class NewTree extends TreeEntry { + @Getter + private final String name; + @Getter + private final List hotEntries; + + public NewTree(String name, List hotEntries) { + this.name = name; + this.hotEntries = hotEntries; + } + + public NewTree(String name, HotEntry... hotEntries) { + this(name, new ArrayList<>(Arrays.asList(hotEntries))); + } + + @Override + public List getEntries() { + throw new UnsupportedOperationException("NewTree has no object ID"); + } + + @Override + public ObjectId getId() { + throw new UnsupportedOperationException("NewTree has no object ID"); + } + + @Override + public String getDirectory() { + return null; + } + + @Override + public Entry fold(RepositoryAccess target, Context c) { + final List entries = new ArrayList<>(); + for (HotEntry child : hotEntries) { + final Entry folded = child.fold(target, c); + if (!folded.getId().equals(ObjectId.zeroId())) { + entries.add(folded); + } + } + return Entry.of(FileMode.TREE.getBits(), name, + entries.isEmpty() ? ObjectId.zeroId() : target.writeTree(entries, c)); + } + + @Override + public String toString() { + return name + "/" + hotEntries; + } + } +} diff --git a/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java b/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java index cc70e0b..02ee150 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java +++ b/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java @@ -4,14 +4,14 @@ import jp.ac.titech.c.se.stein.entry.AnyHotEntry; import jp.ac.titech.c.se.stein.entry.BlobEntry; import jp.ac.titech.c.se.stein.entry.HotEntry; +import jp.ac.titech.c.se.stein.entry.TreeEntry; import lombok.Getter; import lombok.ToString; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.List; import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; public interface BlobTranslator extends RewriterCommand { default void setUp(final Context c) {} @@ -65,12 +65,36 @@ public void setUp(final Context c) { @Override public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { - Stream stream = Stream.of(entry); - for (BlobTranslator translator : translators) { - // TODO: if e is not BlobEntry? - stream = stream.flatMap(e -> translator.rewriteBlobEntry((BlobEntry) e, c).stream()); + return apply(entry, c, 0); + } + + private AnyHotEntry apply(AnyHotEntry input, Context c, int from) { + if (from >= translators.length) { + return input; + } + if (input instanceof TreeEntry.NewTree) { + final TreeEntry.NewTree tree = (TreeEntry.NewTree) input; + final List newChildren = new ArrayList<>(); + for (HotEntry child : tree.getHotEntries()) { + collect(apply(child, c, from), newChildren); + } + return new TreeEntry.NewTree(tree.getName(), newChildren); + } + if (input.size() != 1) { + final List results = new ArrayList<>(); + input.stream().forEach(e -> + collect(apply(translators[from].rewriteBlobEntry((BlobEntry) e, c), c, from + 1), results)); + return AnyHotEntry.set(results); + } + return apply(translators[from].rewriteBlobEntry((BlobEntry) input.stream().findFirst().get(), c), c, from + 1); + } + + private static void collect(AnyHotEntry result, List out) { + if (result instanceof HotEntry) { + out.add((HotEntry) result); + } else { + result.stream().forEach(out::add); } - return AnyHotEntry.set(stream.collect(Collectors.toList())); } } } diff --git a/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java b/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java index 25d93ac..00de179 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java +++ b/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java @@ -10,10 +10,7 @@ import java.util.stream.StreamSupport; import jp.ac.titech.c.se.stein.core.*; -import jp.ac.titech.c.se.stein.entry.BlobEntry; -import jp.ac.titech.c.se.stein.entry.Entry; -import jp.ac.titech.c.se.stein.entry.AnyHotEntry; -import jp.ac.titech.c.se.stein.entry.HotEntry; +import jp.ac.titech.c.se.stein.entry.*; import jp.ac.titech.c.se.stein.jgit.RevWalk; import lombok.Setter; import org.eclipse.jgit.lib.Constants; @@ -30,7 +27,6 @@ import jp.ac.titech.c.se.stein.Application.Config; import jp.ac.titech.c.se.stein.core.Context.Key; -import jp.ac.titech.c.se.stein.entry.AnyColdEntry; /** * The core rewriting engine that copies a Git repository while transforming its contents. @@ -346,7 +342,7 @@ protected AnyColdEntry getEntry(final Entry entry, final Context c) { } /** - * Rewrites a tree entry. + * Rewrites an entry by dispatching to the appropriate type. */ protected AnyColdEntry rewriteEntry(final Entry entry, final Context c) { final Context uc = c.with(Key.entry, entry); @@ -356,7 +352,9 @@ protected AnyColdEntry rewriteEntry(final Entry entry, final Context c) { log.debug("Rewrite blob: {} -> {} {}", entry, newBlob, c); return newBlob; case TREE: - final AnyColdEntry newTree = rewriteTreeEntry(entry, uc); + final String path = entry.isRoot() ? "" : c.getPath() + "/" + entry.name; + final String dir = isPathSensitive ? path : null; + final AnyColdEntry newTree = rewriteTreeEntry(HotEntry.ofTree(entry, source, dir), uc.with(Key.path, path)); log.debug("Rewrite tree: {} -> {} {}", entry, newTree, c); return newTree; case LINK: @@ -373,40 +371,38 @@ protected AnyHotEntry rewriteBlobEntry(BlobEntry entry, Context c) { return entry; } - protected AnyColdEntry rewriteTreeEntry(Entry entry, Context c) { - final ObjectId newId = rewriteTree(entry.id, c); - final String newName = rewriteName(entry.name, c); - return newId == ZERO ? AnyColdEntry.empty() : Entry.of(entry.mode, newName, newId, entry.directory); - } - - protected AnyColdEntry rewriteLinkEntry(Entry entry, Context c) { - final ObjectId newId = rewriteLink(entry.id, c); - final String newName = rewriteName(entry.name, c); - return newId == ZERO ? AnyColdEntry.empty() : Entry.of(entry.mode, newName, newId, entry.directory); + /** + * Rewrites a tree entry. Loads children from the source, rewrites each with caching, + * and writes the resulting tree to the target. + */ + protected AnyColdEntry rewriteTreeEntry(TreeEntry entry, Context c) { + final ObjectId newId = rewriteTree(entry, c); + final String newName = rewriteName(entry.getName(), c); + return newId == ZERO ? AnyColdEntry.empty() : Entry.of(entry.getMode(), newName, newId, entry.getDirectory()); } /** - * Rewrites a tree object. + * Rewrites a tree object by processing its children. */ - protected ObjectId rewriteTree(final ObjectId treeId, final Context c) { - final Entry entry = c.getEntry(); - final String path = entry.isRoot() ? "" : c.getPath() + "/" + entry.name; - final Context uc = c.with(Key.path, path); - - final String dir = isPathSensitive ? path : null; - + protected ObjectId rewriteTree(final TreeEntry entry, final Context c) { final List entries = new ArrayList<>(); - for (final Entry e : source.readTree(treeId, dir)) { - final AnyColdEntry rewritten = getEntry(e, uc); - rewritten.stream().forEach(entries::add); + for (final Entry e : entry.getEntries()) { + final AnyColdEntry rewritten = getEntry(e, c); + rewritten.stream().filter(r -> !r.getId().equals(ZERO)).forEach(entries::add); } - final ObjectId newId = entries.isEmpty() ? ZERO : target.writeTree(entries, uc); - if (log.isDebugEnabled() && !newId.equals(treeId)) { - log.debug("Rewrite tree: {} -> {} {}", treeId.name(), newId.name(), c); + final ObjectId newId = entries.isEmpty() ? ZERO : target.writeTree(entries, c); + if (log.isDebugEnabled() && !newId.equals(entry.getId())) { + log.debug("Rewrite tree: {} -> {} {}", entry.getId().name(), newId.name(), c); } return newId; } + protected AnyColdEntry rewriteLinkEntry(Entry entry, Context c) { + final ObjectId newId = rewriteLink(entry.id, c); + final String newName = rewriteName(entry.name, c); + return newId == ZERO ? AnyColdEntry.empty() : Entry.of(entry.mode, newName, newId, entry.directory); + } + /** * Rewrites a commit link. */ diff --git a/src/test/java/jp/ac/titech/c/se/stein/entry/HotEntryTest.java b/src/test/java/jp/ac/titech/c/se/stein/entry/HotEntryTest.java index 670df36..4714651 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/entry/HotEntryTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/entry/HotEntryTest.java @@ -27,7 +27,7 @@ public void testFactories() { assertEquals("src", withDir.getDirectory()); final Entry entry = Entry.of(BLOB_MODE, "hello", ObjectId.zeroId()); - final HotEntry fromEntry = HotEntry.of(entry, "world".getBytes(StandardCharsets.UTF_8)); + final BlobEntry fromEntry = HotEntry.of(entry, "world".getBytes(StandardCharsets.UTF_8)); assertEquals("hello", fromEntry.getName()); } From 8cc34b6ded5abc1b10f699073a5acd9b8f84bded Mon Sep 17 00:00:00 2001 From: Shinpei Hayashi Date: Mon, 23 Mar 2026 11:54:27 +0900 Subject: [PATCH 3/9] Refine composition process --- .../ac/titech/c/se/stein/entry/HotEntry.java | 15 +++++++ .../ac/titech/c/se/stein/entry/TreeEntry.java | 14 ++++++ .../c/se/stein/rewriter/BlobTranslator.java | 45 ++++++++----------- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java b/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java index 1081bf6..070d02d 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java @@ -3,6 +3,7 @@ import jp.ac.titech.c.se.stein.core.Context; import jp.ac.titech.c.se.stein.core.RepositoryAccess; +import java.util.List; import java.util.stream.Stream; /** @@ -53,6 +54,20 @@ public static TreeEntry ofTree(Entry e, RepositoryAccess source, String director return new TreeEntry.SourceTree(e, source, directory); } + /** + * Creates a {@link TreeEntry.NewTree} with the given name and children. + */ + public static TreeEntry.NewTree ofTree(String name, List children) { + return new TreeEntry.NewTree(name, children); + } + + /** + * Creates a {@link TreeEntry.NewTree} with the given name and children. + */ + public static TreeEntry.NewTree ofTree(String name, HotEntry... children) { + return new TreeEntry.NewTree(name, children); + } + @Override public Stream stream() { return Stream.of(this); diff --git a/src/main/java/jp/ac/titech/c/se/stein/entry/TreeEntry.java b/src/main/java/jp/ac/titech/c/se/stein/entry/TreeEntry.java index ee096ef..fb267fd 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/entry/TreeEntry.java +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/TreeEntry.java @@ -25,6 +25,20 @@ public int getMode() { return FileMode.TREE.getBits(); } + /** + * Returns a new {@link NewTree} with the given name, keeping the children unchanged. + */ + public NewTree rename(String newName) { + return new NewTree(newName, getHotEntries()); + } + + /** + * Returns a new {@link NewTree} with the given children, keeping the name unchanged. + */ + public NewTree update(List newChildren) { + return new NewTree(getName(), newChildren); + } + /** * Returns the children as cold entries. */ diff --git a/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java b/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java index 02ee150..7510cbe 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java +++ b/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java @@ -9,8 +9,8 @@ import lombok.ToString; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import java.util.function.Function; public interface BlobTranslator extends RewriterCommand { @@ -65,36 +65,27 @@ public void setUp(final Context c) { @Override public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { - return apply(entry, c, 0); + return apply(entry, List.of(translators), c); } - private AnyHotEntry apply(AnyHotEntry input, Context c, int from) { - if (from >= translators.length) { - return input; + private AnyHotEntry apply(AnyHotEntry input, List rest, Context c) { + if (input instanceof BlobEntry) { + final BlobTranslator head = rest.get(0); + final List tail = rest.subList(1, rest.size()); + final AnyHotEntry result = head.rewriteBlobEntry((BlobEntry) input, c); + return tail.isEmpty() ? result : apply(result, tail, c); } - if (input instanceof TreeEntry.NewTree) { - final TreeEntry.NewTree tree = (TreeEntry.NewTree) input; - final List newChildren = new ArrayList<>(); - for (HotEntry child : tree.getHotEntries()) { - collect(apply(child, c, from), newChildren); - } - return new TreeEntry.NewTree(tree.getName(), newChildren); - } - if (input.size() != 1) { - final List results = new ArrayList<>(); - input.stream().forEach(e -> - collect(apply(translators[from].rewriteBlobEntry((BlobEntry) e, c), c, from + 1), results)); - return AnyHotEntry.set(results); - } - return apply(translators[from].rewriteBlobEntry((BlobEntry) input.stream().findFirst().get(), c), c, from + 1); - } - - private static void collect(AnyHotEntry result, List out) { - if (result instanceof HotEntry) { - out.add((HotEntry) result); - } else { - result.stream().forEach(out::add); + if (input instanceof TreeEntry) { + final TreeEntry tree = (TreeEntry) input; + final List newChildren = tree.getHotEntries().stream() + .flatMap(e -> apply(e, rest, c).stream()) + .collect(Collectors.toList()); + return tree.update(newChildren); } + // Set/Empty: apply to each element + return AnyHotEntry.set(input.stream() + .flatMap(e -> apply(e, rest, c).stream()) + .collect(Collectors.toList())); } } } From 154023713bbe42a0beaaed6fdcd9913ff0d007be Mon Sep 17 00:00:00 2001 From: Shinpei Hayashi Date: Mon, 23 Mar 2026 12:03:33 +0900 Subject: [PATCH 4/9] refactor: add shortcut methods (asBlob/asTree) --- .../jp/ac/titech/c/se/stein/entry/AnyHotEntry.java | 14 ++++++++++++++ .../c/se/stein/app/blob/ConvertBlobTest.java | 4 ++-- .../c/se/stein/rewriter/BlobTranslatorTest.java | 8 ++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/java/jp/ac/titech/c/se/stein/entry/AnyHotEntry.java b/src/main/java/jp/ac/titech/c/se/stein/entry/AnyHotEntry.java index b9b46b0..1993497 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/entry/AnyHotEntry.java +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/AnyHotEntry.java @@ -30,6 +30,20 @@ public interface AnyHotEntry { */ int size(); + /** + * Returns the first entry as a {@link BlobEntry}. + */ + default BlobEntry asBlob() { + return (BlobEntry) stream().findFirst().orElseThrow(); + } + + /** + * Returns the first entry as a {@link TreeEntry}. + */ + default TreeEntry asTree() { + return (TreeEntry) stream().findFirst().orElseThrow(); + } + /** * Converts this Hot entry to a Cold entry by writing blob data to the target repository. */ diff --git a/src/test/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlobTest.java b/src/test/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlobTest.java index 7a69517..f1b9811 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlobTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlobTest.java @@ -56,7 +56,7 @@ public void testFilterMode() { final AnyHotEntry result = convert.rewriteBlobEntry(entry, C); assertEquals(1, result.size()); - assertEquals("HELLO", new String(((BlobEntry) result.stream().findFirst().orElseThrow()).getBlob(), StandardCharsets.UTF_8)); + assertEquals("HELLO", new String(result.asBlob().getBlob(), StandardCharsets.UTF_8)); } @Test @@ -83,7 +83,7 @@ public void testEndpointMode() throws Exception { final AnyHotEntry result = convert.rewriteBlobEntry(entry, C); assertEquals(1, result.size()); - assertEquals("HELLO", new String(((BlobEntry) result.stream().findFirst().orElseThrow()).getBlob(), StandardCharsets.UTF_8)); + assertEquals("HELLO", new String(result.asBlob().getBlob(), StandardCharsets.UTF_8)); } finally { server.stop(0); } diff --git a/src/test/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslatorTest.java b/src/test/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslatorTest.java index 7beb447..4595e62 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslatorTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslatorTest.java @@ -29,14 +29,14 @@ BlobEntry blob(String name, String content) { } String content(AnyHotEntry entry) { - return new String(((BlobEntry) entry.stream().findFirst().orElseThrow()).getBlob(), StandardCharsets.UTF_8); + return new String(entry.asBlob().getBlob(), StandardCharsets.UTF_8); } @Test public void testOf() { final BlobTranslator upper = BlobTranslator.of(String::toUpperCase); final AnyHotEntry result = upper.rewriteBlobEntry(blob("f.txt", "hello"), CTX); - assertEquals("HELLO", content(result.stream().findFirst().orElseThrow())); + assertEquals("HELLO", content(result)); } @Test @@ -45,7 +45,7 @@ public void testSingleCompositeSingle() { BlobTranslator.of(String::toUpperCase)); final AnyHotEntry result = translator.rewriteBlobEntry(blob("f.txt", "hello"), CTX); assertEquals(1, result.size()); - assertEquals("HELLO", content(result.stream().findFirst().orElseThrow())); + assertEquals("HELLO", content(result)); } @Test @@ -55,7 +55,7 @@ public void testCompositeMultiple() { BlobTranslator.of(String::toUpperCase)); final AnyHotEntry result = translator.rewriteBlobEntry(blob("f.txt", "hello"), CTX); assertEquals(1, result.size()); - assertEquals("PREFIX:HELLO", content(result.stream().findFirst().orElseThrow())); + assertEquals("PREFIX:HELLO", content(result)); } @Test From 062c5b9a8e92f31a73d3c07f79960427fd76b534 Mon Sep 17 00:00:00 2001 From: Shinpei Hayashi Date: Mon, 23 Mar 2026 12:19:10 +0900 Subject: [PATCH 5/9] refactor: add shortcut methods (ofBlob) --- .../ac/titech/c/se/stein/entry/BlobEntry.java | 7 +++ .../ac/titech/c/se/stein/entry/HotEntry.java | 23 +++++++++ .../c/se/stein/rewriter/BlobTranslator.java | 2 +- .../c/se/stein/app/blob/ConvertBlobTest.java | 9 ++-- .../stein/app/blob/HistorageViaJDTTest.java | 5 +- .../c/se/stein/entry/AnyHotEntryTest.java | 4 +- .../se/stein/rewriter/BlobTranslatorTest.java | 48 +++++++------------ 7 files changed, 57 insertions(+), 41 deletions(-) diff --git a/src/main/java/jp/ac/titech/c/se/stein/entry/BlobEntry.java b/src/main/java/jp/ac/titech/c/se/stein/entry/BlobEntry.java index 879d067..cbc00ca 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/entry/BlobEntry.java +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/BlobEntry.java @@ -21,6 +21,13 @@ public abstract class BlobEntry extends HotEntry { public abstract byte[] getBlob(); + /** + * Returns the blob content as a UTF-8 string. + */ + public String getContent() { + return new String(getBlob(), StandardCharsets.UTF_8); + } + public abstract long getBlobSize(); @Override diff --git a/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java b/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java index 070d02d..21689c7 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/HotEntry.java @@ -2,7 +2,9 @@ import jp.ac.titech.c.se.stein.core.Context; import jp.ac.titech.c.se.stein.core.RepositoryAccess; +import org.eclipse.jgit.lib.FileMode; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Stream; @@ -45,6 +47,27 @@ public static BlobEntry of(int mode, String name, byte[] blob, String directory) return new BlobEntry.NewBlob(mode, name, blob, directory); } + /** + * Creates a {@link BlobEntry} with the given UTF-8 string content. + */ + public static BlobEntry of(int mode, String name, String content) { + return new BlobEntry.NewBlob(mode, name, content.getBytes(StandardCharsets.UTF_8), null); + } + + /** + * Creates a regular-file {@link BlobEntry} with the given byte content. + */ + public static BlobEntry ofBlob(String name, byte[] blob) { + return new BlobEntry.NewBlob(FileMode.REGULAR_FILE.getBits(), name, blob, null); + } + + /** + * Creates a regular-file {@link BlobEntry} with the given UTF-8 string content. + */ + public static BlobEntry ofBlob(String name, String content) { + return new BlobEntry.NewBlob(FileMode.REGULAR_FILE.getBits(), name, content.getBytes(StandardCharsets.UTF_8), null); + } + /** * Creates a {@link TreeEntry} that lazily reads tree contents from the given source. * diff --git a/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java b/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java index 7510cbe..ed995f6 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java +++ b/src/main/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslator.java @@ -22,7 +22,7 @@ default void setUp(final Context c) {} * Creates a {@link BlobTranslator} from a String-to-String function. */ static BlobTranslator of(Function f) { - return (entry, c) -> entry.update(f.apply(new String(entry.getBlob(), StandardCharsets.UTF_8))); + return (entry, c) -> entry.update(f.apply(entry.getContent())); } default RepositoryRewriter toRewriter() { diff --git a/src/test/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlobTest.java b/src/test/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlobTest.java index f1b9811..f481438 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlobTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/app/blob/ConvertBlobTest.java @@ -17,7 +17,6 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; public class ConvertBlobTest { - static final int BLOB_MODE = FileMode.REGULAR_FILE.getBits(); static final Context C = Context.init(); @Test @@ -52,11 +51,11 @@ public void testFilterMode() { convert.requiresShell = true; convert.isFilter = true; - final BlobEntry entry = HotEntry.of(BLOB_MODE, "hello.txt", "hello".getBytes(StandardCharsets.UTF_8)); + final BlobEntry entry = HotEntry.ofBlob("hello.txt", "hello"); final AnyHotEntry result = convert.rewriteBlobEntry(entry, C); assertEquals(1, result.size()); - assertEquals("HELLO", new String(result.asBlob().getBlob(), StandardCharsets.UTF_8)); + assertEquals("HELLO", result.asBlob().getContent()); } @Test @@ -79,11 +78,11 @@ public void testEndpointMode() throws Exception { convert.options = new ConvertBlob.ConvertOptions(); convert.options.endpoint = new URL("http://127.0.0.1:" + port + "/convert"); - final BlobEntry entry = HotEntry.of(BLOB_MODE, "hello.txt", "hello".getBytes(StandardCharsets.UTF_8)); + final BlobEntry entry = HotEntry.ofBlob("hello.txt", "hello"); final AnyHotEntry result = convert.rewriteBlobEntry(entry, C); assertEquals(1, result.size()); - assertEquals("HELLO", new String(result.asBlob().getBlob(), StandardCharsets.UTF_8)); + assertEquals("HELLO", result.asBlob().getContent()); } finally { server.stop(0); } diff --git a/src/test/java/jp/ac/titech/c/se/stein/app/blob/HistorageViaJDTTest.java b/src/test/java/jp/ac/titech/c/se/stein/app/blob/HistorageViaJDTTest.java index 7c3f6f8..63e72ac 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/app/blob/HistorageViaJDTTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/app/blob/HistorageViaJDTTest.java @@ -25,7 +25,6 @@ import static org.junit.jupiter.api.Assertions.*; public class HistorageViaJDTTest { - static final int BLOB_MODE = FileMode.REGULAR_FILE.getBits(); static String sampleSource; static RepositoryAccess source, result; @@ -167,7 +166,7 @@ public void testDigestParameters() { @Test public void testNonJavaFilePassedThrough() { - BlobEntry entry = HotEntry.of(BLOB_MODE, "README.md", "# Hello".getBytes(StandardCharsets.UTF_8)); + BlobEntry entry = HotEntry.ofBlob("README.md", "# Hello"); HistorageViaJDT historage = new HistorageViaJDT(); AnyHotEntry result = historage.rewriteBlobEntry(entry, Context.init()); assertEquals(1, result.size()); @@ -178,7 +177,7 @@ public void testNonJavaFilePassedThrough() { public void testRequiresOriginals() { HistorageViaJDT historage = new HistorageViaJDT(); historage.requiresOriginals = false; - BlobEntry entry = HotEntry.of(BLOB_MODE, "Hello.java", sampleSource.getBytes(StandardCharsets.UTF_8)); + BlobEntry entry = HotEntry.ofBlob("Hello.java", sampleSource); AnyHotEntry result = historage.rewriteBlobEntry(entry, Context.init()); // original should NOT be included diff --git a/src/test/java/jp/ac/titech/c/se/stein/entry/AnyHotEntryTest.java b/src/test/java/jp/ac/titech/c/se/stein/entry/AnyHotEntryTest.java index 34d205b..d438d87 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/entry/AnyHotEntryTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/entry/AnyHotEntryTest.java @@ -15,8 +15,8 @@ public class AnyHotEntryTest { static final byte[] HELLO = "hello".getBytes(StandardCharsets.UTF_8); static final byte[] WORLD = "world".getBytes(StandardCharsets.UTF_8); - final BlobEntry h1 = HotEntry.of(BLOB_MODE, "hello.txt", HELLO); - final BlobEntry h2 = HotEntry.of(BLOB_MODE, "world.txt", WORLD); + final BlobEntry h1 = HotEntry.ofBlob("hello.txt", HELLO); + final BlobEntry h2 = HotEntry.ofBlob("world.txt", WORLD); @Test public void testEmpty() { diff --git a/src/test/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslatorTest.java b/src/test/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslatorTest.java index 4595e62..d6ad813 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslatorTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/rewriter/BlobTranslatorTest.java @@ -4,8 +4,8 @@ import jp.ac.titech.c.se.stein.app.blob.TokenizeViaJDT; import jp.ac.titech.c.se.stein.core.Context; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; -import jp.ac.titech.c.se.stein.entry.Entry; import jp.ac.titech.c.se.stein.entry.BlobEntry; +import jp.ac.titech.c.se.stein.entry.Entry; import jp.ac.titech.c.se.stein.entry.HotEntry; import jp.ac.titech.c.se.stein.core.RepositoryAccess; import jp.ac.titech.c.se.stein.testing.TestRepo; @@ -14,38 +14,28 @@ import org.junit.jupiter.api.Test; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; public class BlobTranslatorTest { - static final int BLOB_MODE = FileMode.REGULAR_FILE.getBits(); static final Context CTX = Context.init(); - BlobEntry blob(String name, String content) { - return HotEntry.of(BLOB_MODE, name, content.getBytes(StandardCharsets.UTF_8)); - } - - String content(AnyHotEntry entry) { - return new String(entry.asBlob().getBlob(), StandardCharsets.UTF_8); - } - @Test public void testOf() { final BlobTranslator upper = BlobTranslator.of(String::toUpperCase); - final AnyHotEntry result = upper.rewriteBlobEntry(blob("f.txt", "hello"), CTX); - assertEquals("HELLO", content(result)); + final AnyHotEntry result = upper.rewriteBlobEntry(HotEntry.ofBlob("f.txt", "hello"), CTX); + assertEquals("HELLO", result.asBlob().getContent()); } @Test public void testSingleCompositeSingle() { final RepositoryRewriter translator = new BlobTranslator.Composite( BlobTranslator.of(String::toUpperCase)); - final AnyHotEntry result = translator.rewriteBlobEntry(blob("f.txt", "hello"), CTX); + final AnyHotEntry result = translator.rewriteBlobEntry(HotEntry.ofBlob("f.txt", "hello"), CTX); assertEquals(1, result.size()); - assertEquals("HELLO", content(result)); + assertEquals("HELLO", result.asBlob().getContent()); } @Test @@ -53,9 +43,9 @@ public void testCompositeMultiple() { final RepositoryRewriter translator = new BlobTranslator.Composite( BlobTranslator.of(s -> "PREFIX:" + s), BlobTranslator.of(String::toUpperCase)); - final AnyHotEntry result = translator.rewriteBlobEntry(blob("f.txt", "hello"), CTX); + final AnyHotEntry result = translator.rewriteBlobEntry(HotEntry.ofBlob("f.txt", "hello"), CTX); assertEquals(1, result.size()); - assertEquals("PREFIX:HELLO", content(result)); + assertEquals("PREFIX:HELLO", result.asBlob().getContent()); } @Test @@ -63,11 +53,10 @@ public void testSplit() { final BlobTranslator splitter = (entry, c) -> { final AnyHotEntry.Set set = AnyHotEntry.set(); set.add(entry.rename("a_" + entry.getName())); - set.add(HotEntry.of(entry.getMode(), "b_" + entry.getName(), - (content(entry) + "_copy").getBytes(StandardCharsets.UTF_8))); + set.add(HotEntry.of(entry.getMode(), "b_" + entry.getName(), entry.getContent() + "_copy")); return set; }; - final AnyHotEntry result = splitter.rewriteBlobEntry(blob("f.txt", "data"), CTX); + final AnyHotEntry result = splitter.rewriteBlobEntry(HotEntry.ofBlob("f.txt", "data"), CTX); final List entries = result.stream().collect(Collectors.toList()); assertEquals(2, entries.size()); assertEquals("a_f.txt", entries.get(0).getName()); @@ -78,8 +67,8 @@ public void testSplit() { public void testFilter() { final BlobTranslator filter = (entry, c) -> entry.getName().endsWith(".bak") ? AnyHotEntry.empty() : entry; - assertEquals(0, filter.rewriteBlobEntry(blob("test.bak", "backup"), CTX).size()); - assertEquals(1, filter.rewriteBlobEntry(blob("test.txt", "keep"), CTX).size()); + assertEquals(0, filter.rewriteBlobEntry(HotEntry.ofBlob("test.bak", "backup"), CTX).size()); + assertEquals(1, filter.rewriteBlobEntry(HotEntry.ofBlob("test.txt", "keep"), CTX).size()); } @Test @@ -93,11 +82,11 @@ public void testSplitThenTransform() { final RepositoryRewriter translator = new BlobTranslator.Composite( splitter, BlobTranslator.of(String::toUpperCase)); final List entries = translator - .rewriteBlobEntry(blob("f.txt", "hello"), CTX) + .rewriteBlobEntry(HotEntry.ofBlob("f.txt", "hello"), CTX) .stream().collect(Collectors.toList()); assertEquals(2, entries.size()); - assertEquals("HELLO", content(entries.get(0))); - assertEquals("HELLO", content(entries.get(1))); + assertEquals("HELLO", entries.get(0).asBlob().getContent()); + assertEquals("HELLO", entries.get(1).asBlob().getContent()); } @Test @@ -106,9 +95,9 @@ public void testFinerGit() throws IOException { final RepositoryRewriter composite = new BlobTranslator.Composite(new HistorageViaJDT(), new TokenizeViaJDT()); - try (RepositoryAccess compositeResult = TestRepo.rewrite(source,composite); - RepositoryAccess step1 = TestRepo.rewrite(source,new HistorageViaJDT()); - RepositoryAccess sequentialResult = TestRepo.rewrite(step1,new TokenizeViaJDT())) { + try (RepositoryAccess compositeResult = TestRepo.rewrite(source, composite); + RepositoryAccess step1 = TestRepo.rewrite(source, new HistorageViaJDT()); + RepositoryAccess sequentialResult = TestRepo.rewrite(step1, new TokenizeViaJDT())) { final RevCommit compositeHead = compositeResult.getHead("refs/heads/main"); final RevCommit sequentialHead = sequentialResult.getHead("refs/heads/main"); @@ -121,7 +110,7 @@ public void testFinerGit() throws IOException { sequentialFiles.stream().map(Entry::getName).sorted().collect(Collectors.toList())); for (Entry ce : compositeFiles) { - Entry se = sequentialFiles.stream() + final Entry se = sequentialFiles.stream() .filter(e -> e.getName().equals(ce.getName())) .findFirst().orElseThrow(); assertEquals(ce.getId(), se.getId(), "blob mismatch for " + ce.getName()); @@ -129,5 +118,4 @@ public void testFinerGit() throws IOException { } } } - } From 7f11018ac9a58db563a051c4b8ed4f9d1428fffc Mon Sep 17 00:00:00 2001 From: Shinpei Hayashi Date: Mon, 23 Mar 2026 14:35:18 +0900 Subject: [PATCH 6/9] merge rewrite{Tree,Link} and rewrite{Tree,Link}Entry --- .../se/stein/rewriter/RepositoryRewriter.java | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java b/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java index 00de179..a528f1f 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java +++ b/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java @@ -376,38 +376,22 @@ protected AnyHotEntry rewriteBlobEntry(BlobEntry entry, Context c) { * and writes the resulting tree to the target. */ protected AnyColdEntry rewriteTreeEntry(TreeEntry entry, Context c) { - final ObjectId newId = rewriteTree(entry, c); - final String newName = rewriteName(entry.getName(), c); - return newId == ZERO ? AnyColdEntry.empty() : Entry.of(entry.getMode(), newName, newId, entry.getDirectory()); - } - - /** - * Rewrites a tree object by processing its children. - */ - protected ObjectId rewriteTree(final TreeEntry entry, final Context c) { final List entries = new ArrayList<>(); for (final Entry e : entry.getEntries()) { final AnyColdEntry rewritten = getEntry(e, c); rewritten.stream().filter(r -> !r.getId().equals(ZERO)).forEach(entries::add); } + final String newName = rewriteName(entry.getName(), c); final ObjectId newId = entries.isEmpty() ? ZERO : target.writeTree(entries, c); if (log.isDebugEnabled() && !newId.equals(entry.getId())) { log.debug("Rewrite tree: {} -> {} {}", entry.getId().name(), newId.name(), c); } - return newId; + return newId == ZERO ? AnyColdEntry.empty() : Entry.of(entry.getMode(), newName, newId, entry.getDirectory()); } protected AnyColdEntry rewriteLinkEntry(Entry entry, Context c) { - final ObjectId newId = rewriteLink(entry.id, c); final String newName = rewriteName(entry.name, c); - return newId == ZERO ? AnyColdEntry.empty() : Entry.of(entry.mode, newName, newId, entry.directory); - } - - /** - * Rewrites a commit link. - */ - protected ObjectId rewriteLink(final ObjectId commitId, @SuppressWarnings("unused") final Context c) { - return commitId; + return Entry.of(entry.mode, newName, entry.id, entry.directory); } /** From 91d61884072881567f5e3504e6c65f265541d2de Mon Sep 17 00:00:00 2001 From: Shinpei Hayashi Date: Mon, 23 Mar 2026 14:42:13 +0900 Subject: [PATCH 7/9] Remove rewriteName API --- .../java/jp/ac/titech/c/se/stein/app/Anonymize.java | 13 ++++++++----- .../c/se/stein/rewriter/RepositoryRewriter.java | 13 ++----------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/Anonymize.java b/src/main/java/jp/ac/titech/c/se/stein/app/Anonymize.java index d474b93..c90d8d2 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/Anonymize.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/Anonymize.java @@ -5,7 +5,9 @@ import jp.ac.titech.c.se.stein.entry.Entry; import jp.ac.titech.c.se.stein.entry.AnyHotEntry; +import jp.ac.titech.c.se.stein.entry.AnyColdEntry; import jp.ac.titech.c.se.stein.entry.BlobEntry; +import jp.ac.titech.c.se.stein.entry.TreeEntry; import jp.ac.titech.c.se.stein.util.HashUtils; import lombok.ToString; import lombok.extern.slf4j.Slf4j; @@ -116,12 +118,13 @@ public AnyHotEntry rewriteBlobEntry(BlobEntry entry, final Context c) { } @Override - public String rewriteName(final String name, final Context c) { - final Entry entry = c.getEntry(); - if (entry.isTree()) { - return isTreeNameEnabled ? treeNameMap.convert(name) : name; + protected AnyColdEntry rewriteTreeEntry(TreeEntry entry, Context c) { + final AnyColdEntry result = super.rewriteTreeEntry(entry, c); + if (isTreeNameEnabled && result instanceof Entry) { + final Entry e = (Entry) result; + return Entry.of(e.mode, treeNameMap.convert(e.name), e.id, e.directory); } - return name; + return result; } @Override diff --git a/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java b/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java index a528f1f..a681b3e 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java +++ b/src/main/java/jp/ac/titech/c/se/stein/rewriter/RepositoryRewriter.java @@ -381,24 +381,15 @@ protected AnyColdEntry rewriteTreeEntry(TreeEntry entry, Context c) { final AnyColdEntry rewritten = getEntry(e, c); rewritten.stream().filter(r -> !r.getId().equals(ZERO)).forEach(entries::add); } - final String newName = rewriteName(entry.getName(), c); final ObjectId newId = entries.isEmpty() ? ZERO : target.writeTree(entries, c); if (log.isDebugEnabled() && !newId.equals(entry.getId())) { log.debug("Rewrite tree: {} -> {} {}", entry.getId().name(), newId.name(), c); } - return newId == ZERO ? AnyColdEntry.empty() : Entry.of(entry.getMode(), newName, newId, entry.getDirectory()); + return newId == ZERO ? AnyColdEntry.empty() : Entry.of(entry.getMode(), entry.getName(), newId, entry.getDirectory()); } protected AnyColdEntry rewriteLinkEntry(Entry entry, Context c) { - final String newName = rewriteName(entry.name, c); - return Entry.of(entry.mode, newName, entry.id, entry.directory); - } - - /** - * Rewrites the name of a tree entry. - */ - protected String rewriteName(final String name, final Context c) { - return name; + return entry; } /** From 2ff5507d2934eac1e151dc219d7f684797873290 Mon Sep 17 00:00:00 2001 From: Shinpei Hayashi Date: Mon, 23 Mar 2026 14:54:30 +0900 Subject: [PATCH 8/9] New option: --link in Anonymize --- README.md | 3 ++- .../jp/ac/titech/c/se/stein/app/Anonymize.java | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c4b533f..66d1d5e 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ public class MyTranslator implements BlobTranslator { - `--stream-size-limit={,K,M,G}`: increase the stream size limit. - `--no-notes`: Stop noting the source commit ID to the commits in the target repository. - `--no-pack`: Stop packing objects after transformation finished. -- `--alternates`: Share source objects via Git alternates to skip writing unchanged objects. Significantly speeds up transformations where many objects are unchanged. The target repository will depend on the source's object store until repacked (`git repack -a -d`). +- `--alternates`: Share source objects via Git alternates to skip writing unchanged objects, which speeds up transformations where many objects are unchanged. The target repository will depend on the source's object store until repacked. - `--no-composite`: Stop composing multiple blob translators. - `--extra-attributes`: Allow opportunity to rewrite the encoding and the signature fields in commits. - `--cache=,...`: Specify the object types for caching (`commit`, `blob`, `tree`. See [Incremental transformation](#incremental-transformation) for the details). Default: none. `commit` is recommended. @@ -266,6 +266,7 @@ Anonymizes filenames, blob content, commit messages, branch/tag names, and autho Options: - `--all`: Enable all anonymization options. - `--tree`: Anonymize directory names. +- `--link`: Anonymize link names. - `--blob`: Anonymize file names. - `--content`: Anonymize file contents. - `--message`: Anonymize commit/tag messages. diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/Anonymize.java b/src/main/java/jp/ac/titech/c/se/stein/app/Anonymize.java index c90d8d2..ad906ae 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/Anonymize.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/Anonymize.java @@ -30,6 +30,9 @@ public class Anonymize extends RepositoryRewriter { @Option(names = "--tree", description = "anonymize tree name") protected boolean isTreeNameEnabled; + @Option(names = "--link", description = "anonymize link name") + protected boolean isLinkNameEnabled; + @Option(names = "--blob", description = "anonymize blob name") protected boolean isBlobNameEnabled; @@ -55,6 +58,7 @@ public class Anonymize extends RepositoryRewriter { @Option(names = "--all", description = "anonymize all") protected void setAllEnabled(boolean isEnabled) { isTreeNameEnabled = isEnabled; + isLinkNameEnabled = isEnabled; isBlobNameEnabled = isEnabled; isBlobContentEnabled = isEnabled; isMessageEnabled = isEnabled; @@ -93,6 +97,8 @@ public String convert(final String name) { private final NameMap treeNameMap = new NameMap("directory", "t"); + private final NameMap linkNameMap = new NameMap("link", "l"); + private final NameMap blobNameMap = new NameMap("file", "f"); private final NameMap branchNameMap = new NameMap("branch", "b"); @@ -127,6 +133,14 @@ protected AnyColdEntry rewriteTreeEntry(TreeEntry entry, Context c) { return result; } + @Override + protected AnyColdEntry rewriteLinkEntry(Entry entry, Context c) { + if (isLinkNameEnabled) { + return Entry.of(entry.mode, linkNameMap.convert(entry.name), entry.id, entry.directory); + } + return entry; + } + @Override public PersonIdent rewritePerson(final PersonIdent person, final Context c) { if (person == null) { From 7129b256698b4905812c1365e52b8f393b7d8a2a Mon Sep 17 00:00:00 2001 From: Shinpei Hayashi Date: Mon, 23 Mar 2026 15:03:24 +0900 Subject: [PATCH 9/9] refactor: rename method (pack -> normalize) --- .../jp/ac/titech/c/se/stein/entry/AnyColdEntry.java | 6 +++--- .../jp/ac/titech/c/se/stein/entry/AnyHotEntry.java | 2 +- .../ac/titech/c/se/stein/entry/AnyColdEntryTest.java | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/jp/ac/titech/c/se/stein/entry/AnyColdEntry.java b/src/main/java/jp/ac/titech/c/se/stein/entry/AnyColdEntry.java index 58558ab..4bde10a 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/entry/AnyColdEntry.java +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/AnyColdEntry.java @@ -33,7 +33,7 @@ public interface AnyColdEntry extends Serializable { * Normalizes this entry. A {@link Set} of size 0 becomes {@link Empty}, * size 1 is unwrapped to its sole {@link Entry}, and others remain as-is. */ - default AnyColdEntry pack() { + default AnyColdEntry normalize() { return this; } @@ -67,7 +67,7 @@ static Empty empty() { /** * A collection of multiple {@link Entry} instances. - * Use {@link #pack()} to normalize after construction. + * Use {@link #normalize()} to normalize after construction. */ @NoArgsConstructor @EqualsAndHashCode @@ -101,7 +101,7 @@ public String toString() { } @Override - public AnyColdEntry pack() { + public AnyColdEntry normalize() { switch (size()) { case 0: return empty(); diff --git a/src/main/java/jp/ac/titech/c/se/stein/entry/AnyHotEntry.java b/src/main/java/jp/ac/titech/c/se/stein/entry/AnyHotEntry.java index 1993497..1c31a59 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/entry/AnyHotEntry.java +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/AnyHotEntry.java @@ -115,7 +115,7 @@ public AnyColdEntry fold(RepositoryAccess target, Context c) { .map(e -> e.fold(target, c)) .filter(e -> !e.getId().equals(ObjectId.zeroId())) .collect(Collectors.toList())) - .pack(); + .normalize(); } @Override diff --git a/src/test/java/jp/ac/titech/c/se/stein/entry/AnyColdEntryTest.java b/src/test/java/jp/ac/titech/c/se/stein/entry/AnyColdEntryTest.java index d1f0128..56984b6 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/entry/AnyColdEntryTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/entry/AnyColdEntryTest.java @@ -22,7 +22,7 @@ public void testEmpty() { final AnyColdEntry.Empty empty = AnyColdEntry.empty(); assertEquals(0, empty.size()); assertEquals(0, empty.stream().count()); - assertSame(empty, empty.pack()); + assertSame(empty, empty.normalize()); assertEquals("[]", empty.toString()); final AnyColdEntry.Empty empty2 = AnyColdEntry.empty(); @@ -51,17 +51,17 @@ public void testSet() { @Test public void testPack() { // empty set packs to Empty - assertInstanceOf(AnyColdEntry.Empty.class, AnyColdEntry.set().pack()); + assertInstanceOf(AnyColdEntry.Empty.class, AnyColdEntry.set().normalize()); // singleton set packs to the sole Entry - assertSame(e1, AnyColdEntry.set(e1).pack()); + assertSame(e1, AnyColdEntry.set(e1).normalize()); // multiple set packs to itself final AnyColdEntry.Set multiple = AnyColdEntry.set(e1, e2); - assertSame(multiple, multiple.pack()); + assertSame(multiple, multiple.normalize()); // Entry packs to itself - assertSame(e1, e1.pack()); + assertSame(e1, e1.normalize()); } @Test