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 467fecb..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 @@ -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.HotEntry; +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; @@ -28,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; @@ -53,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; @@ -91,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"); @@ -105,7 +113,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()); } @@ -116,12 +124,21 @@ public AnyHotEntry rewriteBlobEntry(HotEntry 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 + 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 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/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 d79b3fb..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 @@ -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; @@ -29,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. */ @@ -98,8 +113,9 @@ 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(); + .normalize(); } @Override 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..cbc00ca --- /dev/null +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/BlobEntry.java @@ -0,0 +1,131 @@ +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(); + + /** + * Returns the blob content as a UTF-8 string. + */ + public String getContent() { + return new String(getBlob(), StandardCharsets.UTF_8); + } + + 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..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,169 +2,105 @@ 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 org.eclipse.jgit.lib.FileMode; import java.nio.charset.StandardCharsets; +import java.util.List; 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 TreeEntry * @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} 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 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} 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 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} with the given properties. */ - public static NewBlob of(int mode, String name, byte[] blob) { - return new NewBlob(mode, name, blob, null); + public static BlobEntry 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} 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 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); - } - - @Override - public int size() { - return 1; - } - - @Override - public Entry fold(RepositoryAccess target, Context c) { - return Entry.of(getMode(), getName(), target.writeBlob(getBlob(), c), getDirectory()); + /** + * 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); } /** - * Returns a new {@link NewBlob} with the given name, keeping the blob content unchanged. + * Creates a regular-file {@link BlobEntry} with the given byte content. */ - public NewBlob rename(final String newName) { - return of(getMode(), newName, getBlob(), getDirectory()); + public static BlobEntry ofBlob(String name, byte[] blob) { + return new BlobEntry.NewBlob(FileMode.REGULAR_FILE.getBits(), name, blob, null); } /** - * Returns a new {@link NewBlob} with the given blob content, keeping the name unchanged. + * Creates a regular-file {@link BlobEntry} with the given UTF-8 string content. */ - public NewBlob update(final byte[] newBlob) { - return of(getMode(), getName(), newBlob, getDirectory()); + public static BlobEntry ofBlob(String name, String content) { + return new BlobEntry.NewBlob(FileMode.REGULAR_FILE.getBits(), name, content.getBytes(StandardCharsets.UTF_8), null); } /** - * String variant of {@link #update(byte[])}. + * 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 NewBlob update(final String newContent) { - return update(newContent.getBytes(StandardCharsets.UTF_8)); + public static TreeEntry ofTree(Entry e, RepositoryAccess source, String directory) { + return new TreeEntry.SourceTree(e, source, directory); } /** - * A Hot entry backed by an existing blob in a repository. - * The blob content is lazily loaded on the first call to {@link #getBlob()}. + * Creates a {@link TreeEntry.NewTree} with the given name and children. */ - @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()); - } + public static TreeEntry.NewTree ofTree(String name, List children) { + return new TreeEntry.NewTree(name, children); } /** - * A Hot entry holding new or transformed blob data directly. + * Creates a {@link TreeEntry.NewTree} with the given name and children. */ - @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; - } + public static TreeEntry.NewTree ofTree(String name, HotEntry... children) { + return new TreeEntry.NewTree(name, children); + } - /** - * 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 Stream stream() { + return Stream.of(this); + } - @Override - public String toString() { - return String.format("%s [new(%d):%o]", getPath(), getBlobSize(), getMode()); - } + @Override + public int size() { + return 1; } + + @Override + public abstract Entry fold(RepositoryAccess target, Context c); } 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..fb267fd --- /dev/null +++ b/src/main/java/jp/ac/titech/c/se/stein/entry/TreeEntry.java @@ -0,0 +1,157 @@ +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 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. + */ + 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 be2e6db..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 @@ -2,26 +2,27 @@ 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.entry.TreeEntry; import lombok.Getter; import lombok.ToString; import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.function.Function; 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. */ 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() { @@ -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,12 +64,28 @@ public void setUp(final Context c) { } @Override - public AnyHotEntry rewriteBlobEntry(final HotEntry entry, final Context c) { - Stream stream = Stream.of(entry); - for (BlobTranslator translator : translators) { - stream = stream.flatMap(e -> translator.rewriteBlobEntry(e, c).stream()); + public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { + return apply(entry, List.of(translators), c); + } + + 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) { + 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); } - return AnyHotEntry.set(stream.collect(Collectors.toList())); + // Set/Empty: apply to each element + return AnyHotEntry.set(input.stream() + .flatMap(e -> apply(e, rest, c).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..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 @@ -10,9 +10,7 @@ import java.util.stream.StreamSupport; import jp.ac.titech.c.se.stein.core.*; -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; @@ -29,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. @@ -345,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); @@ -355,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: @@ -368,56 +367,29 @@ 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; } - 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 object. + * Rewrites a tree entry. Loads children from the source, rewrites each with caching, + * and writes the resulting tree to the target. */ - 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 AnyColdEntry rewriteTreeEntry(TreeEntry entry, 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; + return newId == ZERO ? AnyColdEntry.empty() : Entry.of(entry.getMode(), entry.getName(), newId, entry.getDirectory()); } - /** - * Rewrites a commit link. - */ - protected ObjectId rewriteLink(final ObjectId commitId, @SuppressWarnings("unused") final Context c) { - return commitId; - } - - /** - * Rewrites the name of a tree entry. - */ - protected String rewriteName(final String name, final Context c) { - return name; + protected AnyColdEntry rewriteLinkEntry(Entry 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..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 @@ -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; @@ -16,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 @@ -51,11 +51,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.ofBlob("hello.txt", "hello"); 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", result.asBlob().getContent()); } @Test @@ -78,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 HotEntry 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.stream().findFirst().orElseThrow().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 d77b438..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 @@ -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; @@ -24,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; @@ -166,7 +166,7 @@ public void testDigestParameters() { @Test public void testNonJavaFilePassedThrough() { - HotEntry 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()); @@ -177,7 +177,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.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/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 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..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 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.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/entry/HotEntryTest.java b/src/test/java/jp/ac/titech/c/se/stein/entry/HotEntryTest.java index 125fe21..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 @@ -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,11 +23,11 @@ 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()); - 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()); } @@ -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..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,6 +4,7 @@ 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.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; @@ -13,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(); - HotEntry 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); - } - @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())); + 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.stream().findFirst().orElseThrow())); + assertEquals("HELLO", result.asBlob().getContent()); } @Test @@ -52,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.stream().findFirst().orElseThrow())); + assertEquals("PREFIX:HELLO", result.asBlob().getContent()); } @Test @@ -62,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()); @@ -77,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 @@ -92,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 @@ -105,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"); @@ -120,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()); @@ -128,5 +118,4 @@ public void testFinerGit() throws IOException { } } } - }