diff --git a/src/main/java/jp/ac/titech/c/se/stein/Application.java b/src/main/java/jp/ac/titech/c/se/stein/Application.java index 75fc33b..55c0515 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/Application.java +++ b/src/main/java/jp/ac/titech/c/se/stein/Application.java @@ -9,7 +9,6 @@ import java.util.concurrent.Callable; import java.util.stream.Collectors; -import jp.ac.titech.c.se.stein.rewriter.BlobTranslator; import jp.ac.titech.c.se.stein.app.Identity; import jp.ac.titech.c.se.stein.rewriter.RewriterCommand; import jp.ac.titech.c.se.stein.util.SettableHelpCommand; @@ -292,7 +291,7 @@ public int execute(final ParseResult parseResult) throws ExecutionException, Par .collect(Collectors.toList()); if (conf.useComposite) { log.debug("Optimizing rewriters..."); - commands = optimizeRewriters(commands); + commands = RewriterCommand.optimize(commands); } this.rewriters.addAll(prepareRewriters(commands)); @@ -307,35 +306,6 @@ public List prepareRewriters(final List com return commands.stream().map(RewriterCommand::toRewriter).collect(Collectors.toList()); } - public List optimizeRewriters(final List commands) { - final List result = new ArrayList<>(); - final List pending = new ArrayList<>(); - - for (final RewriterCommand cmd : commands) { - if (cmd instanceof BlobTranslator t) { - pending.add(t); - } else { - flushPendingTranslators(pending, result); - result.add(cmd); - } - } - flushPendingTranslators(pending, result); - return result; - } - - private void flushPendingTranslators(final List pending, final List result) { - if (pending.isEmpty()) { - return; - } - if (pending.size() >= 2) { - log.info("Compose {} blob translators: {}", pending.size(), pending); - result.add(new BlobTranslator.Composite(pending)); - } else { - result.add(new BlobTranslator.Single(pending.get(0))); - } - pending.clear(); - } - /** * Add all the command classes found in the given package as subcommands to the given commandline. */ 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 a113e01..a07d251 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 @@ -14,6 +14,7 @@ import org.eclipse.jgit.lib.PersonIdent; import jp.ac.titech.c.se.stein.core.Context; +import jp.ac.titech.c.se.stein.rewriter.EntryResolver; import jp.ac.titech.c.se.stein.rewriter.RepositoryRewriter; import picocli.CommandLine.Command; import picocli.CommandLine.Option; @@ -124,8 +125,8 @@ public AnyHotEntry rewriteBlobEntry(BlobEntry entry, final Context c) { } @Override - protected AnyColdEntry rewriteTreeEntry(TreeEntry entry, Context c) { - final AnyColdEntry result = super.rewriteTreeEntry(entry, c); + protected AnyColdEntry rewriteTreeEntry(TreeEntry entry, EntryResolver resolver, Context c) { + final AnyColdEntry result = super.rewriteTreeEntry(entry, resolver, c); if (isTreeNameEnabled && result instanceof Entry e) { return Entry.of(e.mode, treeNameMap.convert(e.name), e.id, e.directory); } diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/commit/NoteCommit.java b/src/main/java/jp/ac/titech/c/se/stein/app/commit/NoteCommit.java index c7fe217..1e3c649 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/commit/NoteCommit.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/commit/NoteCommit.java @@ -1,6 +1,8 @@ package jp.ac.titech.c.se.stein.app.commit; 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.rewriter.CommitTranslator; import jp.ac.titech.c.se.stein.rewriter.RepositoryRewriter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; @@ -11,25 +13,31 @@ import java.nio.charset.StandardCharsets; /** - * Prepends the original commit ID (from Git notes) to each commit message. - * If no note exists, the zero ID is used instead. + * Prepends the original commit ID to each commit message. + * If the source has notes (chained transformation), the original ID is read from the note. + * Otherwise, the current commit ID itself is used as the original. */ @Slf4j @ToString @Command(name = "@note-commit", description = "Note original commit id on each commit message") -public class NoteCommit extends RepositoryRewriter { +public class NoteCommit implements CommitTranslator { @Option(names = "--length", paramLabel = "", description = "length of SHA1 hash (default: ${DEFAULT-VALUE})") protected int length = 40; @Override public String rewriteCommitMessage(final String message, final Context c) { + final ObjectId originalId = resolveOriginalId(c); + return originalId.name().substring(0, length) + " " + message; + } + + private ObjectId resolveOriginalId(final Context c) { final ObjectId current = c.getRev().getId(); - final byte[] note = source.readNote(source.getDefaultNotes(), current); + final RepositoryAccess source = c.getRewriter().getSource(); + final byte[] note = source.readNote(source.getNotes(RepositoryRewriter.R_NOTES_ORIG), current); if (note != null && note.length == 40) { - // use the commit note for the original commit id - return new String(note, StandardCharsets.UTF_8).substring(0, length) + " " + message; - } - // no note or no valid note: use the zero id - return RepositoryRewriter.ZERO.name().substring(0, length) + " " + message; + return ObjectId.fromString(new String(note, StandardCharsets.UTF_8)); + } else { + return current; + } } } diff --git a/src/main/java/jp/ac/titech/c/se/stein/app/commit/SvnMetadata.java b/src/main/java/jp/ac/titech/c/se/stein/app/commit/SvnMetadata.java index c7a18bc..7ae17fa 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/app/commit/SvnMetadata.java +++ b/src/main/java/jp/ac/titech/c/se/stein/app/commit/SvnMetadata.java @@ -16,7 +16,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import jp.ac.titech.c.se.stein.core.Context; -import jp.ac.titech.c.se.stein.rewriter.RepositoryRewriter; +import jp.ac.titech.c.se.stein.rewriter.CommitTranslator; import jp.ac.titech.c.se.stein.core.Try; import picocli.CommandLine.Command; import picocli.CommandLine.Option; @@ -29,7 +29,7 @@ @Slf4j @ToString @Command(name = "@svn-metadata", description = "Attach metadata obtained from svn2git") -public class SvnMetadata extends RepositoryRewriter { +public class SvnMetadata implements CommitTranslator { @Option(names = "--svn-mapping", paramLabel = "", description = "svn mapping", required = true) protected Path svnMappingFile; @@ -41,12 +41,12 @@ public class SvnMetadata extends RepositoryRewriter { protected Map mapping; @Override - protected void setUp(final Context c) { + public void setUp(final Context c) { mapping = Try.io(() -> collectCommitMapping(svnMappingFile, objectMappingFile)); } @Override - protected String rewriteCommitMessage(final String message, final Context c) { + public String rewriteCommitMessage(final String message, final Context c) { final RevCommit commit = c.getCommit(); final Integer svnId = mapping.get(commit.getId()); if (svnId != null) { diff --git a/src/main/java/jp/ac/titech/c/se/stein/core/Context.java b/src/main/java/jp/ac/titech/c/se/stein/core/Context.java index 86388b9..8622fd4 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/core/Context.java +++ b/src/main/java/jp/ac/titech/c/se/stein/core/Context.java @@ -10,6 +10,7 @@ import java.util.stream.Stream; import jp.ac.titech.c.se.stein.Application; +import jp.ac.titech.c.se.stein.rewriter.RepositoryRewriter; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.RevCommit; @@ -31,7 +32,7 @@ public class Context implements Map { * The keys that can be stored in a context. */ public enum Key { - commit, path, entry, rev, tag, ref, conf, inserter; + commit, path, entry, rev, tag, ref, conf, inserter, rewriter; public static final Key[] ALL = Key.values(); public static final int SIZE = ALL.length; @@ -230,4 +231,11 @@ public Application.Config getConfig() { public ObjectInserter getInserter() { return (ObjectInserter) get(Key.inserter); } + + /** + * Returns the rewriter, or {@code null} if not set. + */ + public RepositoryRewriter getRewriter() { + return (RepositoryRewriter) get(Key.rewriter); + } } diff --git a/src/main/java/jp/ac/titech/c/se/stein/core/RepositoryAccess.java b/src/main/java/jp/ac/titech/c/se/stein/core/RepositoryAccess.java index a568cd1..6d81c12 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/core/RepositoryAccess.java +++ b/src/main/java/jp/ac/titech/c/se/stein/core/RepositoryAccess.java @@ -35,8 +35,7 @@ public class RepositoryAccess implements AutoCloseable { public final Repository repo; - @Getter - protected final NoteMap defaultNotes; + private final Map notesCache = new HashMap<>(); protected boolean isDryRunning = false; @@ -51,7 +50,6 @@ public void setDryRunning(final boolean isDryRunning) { public RepositoryAccess(final Repository repo) { this.repo = repo; - this.defaultNotes = readNotes(); } @Override @@ -388,7 +386,6 @@ public void writeNotes(final NoteMap notes, final String ref, final Context writ final ObjectId commit = writeCommit(NO_PARENTS, treeId, ident, ident, message, writingContext); applyRefUpdate(new RefEntry(ref, commit)); - } /** @@ -403,10 +400,17 @@ public void forEachNote(final NoteMap notes, final BiConsumer } /** - * Reads notes from the default notes ref ({@code refs/notes/commits}). + * Returns the notes for the default ref ({@code refs/notes/commits}), reading lazily. + */ + public NoteMap getDefaultNotes() { + return getNotes(Constants.R_NOTES_COMMITS); + } + + /** + * Returns the notes for the specified ref, reading lazily and caching the result. */ - public NoteMap readNotes() { - return readNotes(Constants.R_NOTES_COMMITS); + public NoteMap getNotes(final String noteRef) { + return notesCache.computeIfAbsent(noteRef, this::readNotes); } /** 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 bea0e9c..b72deb5 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 @@ -24,6 +24,14 @@ static BlobTranslator of(Function f) { return (entry, c) -> entry.update(f.apply(entry.getContent())); } + static BlobTranslator composite(BlobTranslator... translators) { + return new Composite(translators); + } + + static BlobTranslator composite(List translators) { + return new Composite(translators); + } + default RepositoryRewriter toRewriter() { return new Single(this); } @@ -44,7 +52,7 @@ public AnyHotEntry rewriteBlobEntry(final BlobEntry entry, final Context c) { } @ToString - class Composite extends RepositoryRewriter { + class Composite implements BlobTranslator { BlobTranslator[] translators; public Composite(BlobTranslator... translators) { diff --git a/src/main/java/jp/ac/titech/c/se/stein/rewriter/CommitTranslator.java b/src/main/java/jp/ac/titech/c/se/stein/rewriter/CommitTranslator.java new file mode 100644 index 0000000..3558bb3 --- /dev/null +++ b/src/main/java/jp/ac/titech/c/se/stein/rewriter/CommitTranslator.java @@ -0,0 +1,147 @@ +package jp.ac.titech.c.se.stein.rewriter; + +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.entry.TreeEntry; +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.Delegate; +import org.eclipse.jgit.lib.PersonIdent; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * A commit-level translator that can rewrite commit metadata and/or entry content. + * + *

Operates at the commit level: message, author, committer, and tree entries. + * Wrapped in a {@link Single} to run inside a {@link RepositoryRewriter}.

+ */ +public interface CommitTranslator extends RewriterCommand { + default void setUp(Context c) {} + default String rewriteCommitMessage(String message, Context c) { return message; } + default PersonIdent rewriteAuthor(PersonIdent author, Context c) { return author; } + default PersonIdent rewriteCommitter(PersonIdent committer, Context c) { return committer; } + default AnyHotEntry rewriteBlobEntry(BlobEntry entry, Context c) { return entry; } + + /** + * Lifts a {@link BlobTranslator} into a {@link CommitTranslator} that only rewrites blob entries. + */ + static CommitTranslator fromBlob(BlobTranslator translator) { + return new CommitTranslator() { + @Override + public void setUp(Context c) { + translator.setUp(c); + } + + @Override + public AnyHotEntry rewriteBlobEntry(BlobEntry entry, Context c) { + return translator.rewriteBlobEntry(entry, c); + } + + @Override + public String toString() { + return translator.toString(); + } + }; + } + + static CommitTranslator composite(CommitTranslator... translators) { + return new Composite(translators); + } + + static CommitTranslator composite(List translators) { + return new Composite(translators); + } + + @Override + default RepositoryRewriter toRewriter() { + return new Single(this); + } + + @ToString + class Single extends RepositoryRewriter { + @Getter + @Delegate + private final CommitTranslator translator; + + public Single(CommitTranslator translator) { + this.translator = translator; + } + + @Override + public void setUp(Context c) { + translator.setUp(c); + } + } + + @ToString + class Composite implements CommitTranslator { + private final CommitTranslator[] translators; + + public Composite(CommitTranslator... translators) { + this.translators = translators; + } + + public Composite(List translators) { + this(translators.toArray(new CommitTranslator[0])); + } + + @Override + public void setUp(Context c) { + for (CommitTranslator translator : translators) { + translator.setUp(c); + } + } + + @Override + public String rewriteCommitMessage(String message, Context c) { + for (CommitTranslator translator : translators) { + message = translator.rewriteCommitMessage(message, c); + } + return message; + } + + @Override + public PersonIdent rewriteAuthor(PersonIdent author, Context c) { + for (CommitTranslator translator : translators) { + author = translator.rewriteAuthor(author, c); + } + return author; + } + + @Override + public PersonIdent rewriteCommitter(PersonIdent committer, Context c) { + for (CommitTranslator translator : translators) { + committer = translator.rewriteCommitter(committer, c); + } + return committer; + } + + // Same logic as BlobTranslator.Composite.apply + @Override + public AnyHotEntry rewriteBlobEntry(BlobEntry entry, Context c) { + return apply(entry, List.of(translators), c); + } + + private AnyHotEntry apply(AnyHotEntry input, List rest, Context c) { + if (input instanceof BlobEntry blob) { + final CommitTranslator head = rest.get(0); + final List tail = rest.subList(1, rest.size()); + final AnyHotEntry result = head.rewriteBlobEntry(blob, c); + return tail.isEmpty() ? result : apply(result, tail, c); + } + if (input instanceof TreeEntry tree) { + final List newChildren = tree.getHotEntries().stream() + .flatMap(e -> apply(e, rest, c).stream()) + .collect(Collectors.toList()); + return tree.update(newChildren); + } + 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/EntryResolver.java b/src/main/java/jp/ac/titech/c/se/stein/rewriter/EntryResolver.java new file mode 100644 index 0000000..08c773d --- /dev/null +++ b/src/main/java/jp/ac/titech/c/se/stein/rewriter/EntryResolver.java @@ -0,0 +1,14 @@ +package jp.ac.titech.c.se.stein.rewriter; + +import jp.ac.titech.c.se.stein.core.Context; +import jp.ac.titech.c.se.stein.entry.AnyColdEntry; +import jp.ac.titech.c.se.stein.entry.Entry; + +/** + * Resolves an entry to its rewritten form, using caching to avoid redundant transformations. + * This is the interface through which translators access child entries during tree rewriting. + */ +@FunctionalInterface +public interface EntryResolver { + AnyColdEntry resolve(Entry entry, Context c); +} 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 fc58eb0..2647def 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 @@ -15,6 +15,7 @@ import jp.ac.titech.c.se.stein.core.cache.*; import jp.ac.titech.c.se.stein.entry.*; import jp.ac.titech.c.se.stein.jgit.RevWalk; +import lombok.Getter; import lombok.Setter; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; @@ -95,6 +96,7 @@ private static Map createEntryMapping(long memoryBudget) { */ public static final String R_NOTES_ORIG = "refs/notes/git-stein-orig"; + @Getter protected RepositoryAccess source, target; /** @@ -155,18 +157,19 @@ public void initialize(final Repository sourceRepo, final Repository targetRepo) } public void rewrite(final Context c) { - setUp(c); - try (final RevWalk walk = prepareRevisionWalk(c)) { + final Context uc = c.with(Key.rewriter, this); + setUp(uc); + try (final RevWalk walk = prepareRevisionWalk(uc)) { if (config.nthreads >= 2) { - rewriteRootTrees(walk, c); + rewriteRootTrees(walk, uc); Try.io(walk::memoReset); } - rewriteCommits(walk, c); - updateRefs(c); + rewriteCommits(walk, uc); + updateRefs(uc); if (config.isAddingNotes) { - prevNotes.write(R_NOTES_PREV, c); + prevNotes.write(R_NOTES_PREV, uc); if (isChained) { - origNotes.write(R_NOTES_ORIG, c); + origNotes.write(R_NOTES_ORIG, uc); } else { // Single transformation: orig = prev, share the same ref target.applyRefUpdate(new RefEntry(R_NOTES_ORIG, target.getRef(R_NOTES_PREV).getObjectId())); @@ -174,7 +177,7 @@ public void rewrite(final Context c) { // Default notes = orig (for git log display) target.applyRefUpdate(new RefEntry(Constants.R_NOTES_COMMITS, target.getRef(R_NOTES_ORIG).getObjectId())); } else { - target.writeNotes(target.getDefaultNotes(), c); + target.writeNotes(target.getDefaultNotes(), uc); } } finally { final long blobHit = blobCacheHits.get(), blobMiss = blobCacheMisses.get(); @@ -191,7 +194,7 @@ public void rewrite(final Context c) { if (entryCache != null) { entryCache.close(); } - cleanUp(c); + cleanUp(uc); } } @@ -370,7 +373,7 @@ protected ObjectId rewriteRootTree(final ObjectId treeId, final Context c) { // A root tree is represented as a special entry whose name is "/" final Entry root = Entry.of(FileMode.TREE.getBits(), "", treeId, isPathSensitive ? "" : null); - final AnyColdEntry newRoot = getEntry(root, c); + final AnyColdEntry newRoot = entryResolver.resolve(root, c); final ObjectId newId = newRoot instanceof AnyColdEntry.Empty ? target.writeTree(Collections.emptyList(), c) : ((Entry) newRoot).id; log.debug("Rewrite root tree: {} -> {} {}", treeId.name(), newId.name(), c); @@ -379,9 +382,10 @@ protected ObjectId rewriteRootTree(final ObjectId treeId, final Context c) { } /** - * Obtains tree entries from a tree entry. + * The entry resolver that provides cached entry resolution. + * Translators use this to resolve child entries during tree rewriting. */ - protected AnyColdEntry getEntry(final Entry entry, final Context c) { + protected final EntryResolver entryResolver = (entry, c) -> { // computeIfAbsent is unsuitable because this may be invoked recursively final AnyColdEntry cached = entryMapping.get(entry); if (cached != null) { @@ -392,7 +396,7 @@ protected AnyColdEntry getEntry(final Entry entry, final Context c) { final AnyColdEntry result = rewriteEntry(entry, c); entryMapping.put(entry, result); return result; - } + }; /** * Rewrites an entry by dispatching to the appropriate type. @@ -404,7 +408,7 @@ protected AnyColdEntry rewriteEntry(final Entry entry, final Context c) { case tree -> { final String path = entry.isRoot() ? "" : c.getPath() + "/" + entry.name; final String dir = isPathSensitive ? path : null; - yield rewriteTreeEntry(HotEntry.ofTree(entry, source, dir), uc.with(Key.path, path)); + yield rewriteTreeEntry(HotEntry.ofTree(entry, source, dir), entryResolver, uc.with(Key.path, path)); } case link -> rewriteLinkEntry(entry, uc); }; @@ -420,10 +424,10 @@ protected AnyHotEntry rewriteBlobEntry(BlobEntry entry, Context c) { * 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) { + protected AnyColdEntry rewriteTreeEntry(TreeEntry entry, EntryResolver resolver, Context c) { final List entries = new ArrayList<>(); for (final Entry e : entry.getEntries()) { - final AnyColdEntry rewritten = getEntry(e, c); + final AnyColdEntry rewritten = resolver.resolve(e, c); rewritten.stream().filter(r -> !r.getId().equals(ZERO)).forEach(entries::add); } final ObjectId newId = entries.isEmpty() ? ZERO : target.writeTree(entries, c); diff --git a/src/main/java/jp/ac/titech/c/se/stein/rewriter/RewriterCommand.java b/src/main/java/jp/ac/titech/c/se/stein/rewriter/RewriterCommand.java index f89057e..4fca383 100644 --- a/src/main/java/jp/ac/titech/c/se/stein/rewriter/RewriterCommand.java +++ b/src/main/java/jp/ac/titech/c/se/stein/rewriter/RewriterCommand.java @@ -1,5 +1,82 @@ package jp.ac.titech.c.se.stein.rewriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + public interface RewriterCommand { + Logger log = LoggerFactory.getLogger(RewriterCommand.class); + RepositoryRewriter toRewriter(); + + /** + * Optimizes a list of commands by composing consecutive translators. + * Phase 1: compose consecutive BlobTranslators. + * Phase 2: compose consecutive CommitTranslators and BlobTranslators (lifting BlobTranslators). + */ + static List optimize(final List commands) { + return composeCommitTranslators(composeBlobTranslators(commands)); + } + + private static List composeBlobTranslators(final List commands) { + final List result = new ArrayList<>(); + final List pending = new ArrayList<>(); + for (final RewriterCommand cmd : commands) { + if (cmd instanceof BlobTranslator t) { + pending.add(t); + } else { + flushBlobs(pending, result); + result.add(cmd); + } + } + flushBlobs(pending, result); + return result; + } + + private static void flushBlobs(final List pending, final List result) { + if (pending.isEmpty()) { + return; + } + if (pending.size() >= 2) { + log.info("Compose {} blob translators: {}", pending.size(), pending); + result.add(BlobTranslator.composite(pending)); + } else { + result.add(pending.get(0)); + } + pending.clear(); + } + + private static List composeCommitTranslators(final List commands) { + final List result = new ArrayList<>(); + final List pending = new ArrayList<>(); + for (final RewriterCommand cmd : commands) { + if (cmd instanceof CommitTranslator || cmd instanceof BlobTranslator) { + pending.add(cmd); + } else { + flushCommits(pending, result); + result.add(cmd); + } + } + flushCommits(pending, result); + return result; + } + + private static void flushCommits(final List pending, final List result) { + if (pending.isEmpty()) { + return; + } + if (pending.size() >= 2) { + final List lifted = pending.stream() + .map(c -> c instanceof BlobTranslator t ? CommitTranslator.fromBlob(t) : (CommitTranslator) c) + .collect(Collectors.toList()); + log.info("Compose {} commit translators: {}", lifted.size(), lifted); + result.add(CommitTranslator.composite(lifted)); + } else { + result.addAll(pending); + } + pending.clear(); + } } diff --git a/src/test/java/jp/ac/titech/c/se/stein/app/commit/NoteCommitTest.java b/src/test/java/jp/ac/titech/c/se/stein/app/commit/NoteCommitTest.java index 83e1ffd..8ee339f 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/app/commit/NoteCommitTest.java +++ b/src/test/java/jp/ac/titech/c/se/stein/app/commit/NoteCommitTest.java @@ -4,7 +4,6 @@ import jp.ac.titech.c.se.stein.app.blob.TokenizeViaJDT; import jp.ac.titech.c.se.stein.core.RepositoryAccess; import jp.ac.titech.c.se.stein.testing.TestRepo; -import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.RevCommit; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -30,18 +29,19 @@ static void tearDown() { @Test public void testNoTransform() { - // NoteCommit directly on source: no prior notes → zero id prefix - try (RepositoryAccess result = TestRepo.rewrite(source,new NoteCommit())) { + // NoteCommit directly on source: no prior notes → commit's own id prefix + final List sourceCommits = source.collectCommits("refs/heads/main"); + + try (RepositoryAccess result = TestRepo.rewrite(source, new NoteCommit())) { final List commits = result.collectCommits("refs/heads/main"); assertEquals(3, commits.size()); - final String zeroId = ObjectId.zeroId().name(); - for (RevCommit commit : commits) { - assertTrue(commit.getFullMessage().startsWith(zeroId + " ")); + for (int i = 0; i < 3; i++) { + final String msg = commits.get(i).getFullMessage(); + final String expectedPrefix = sourceCommits.get(i).getId().name(); + assertTrue(msg.startsWith(expectedPrefix + " "), + "Expected source id " + expectedPrefix + " in: " + msg); } - assertEquals(zeroId + " initial", commits.get(0).getFullMessage()); - assertEquals(zeroId + " add features", commits.get(1).getFullMessage()); - assertEquals(zeroId + " modern syntax", commits.get(2).getFullMessage()); } } 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 5e772ef..bac1468 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,8 +29,7 @@ public void testOf() { @Test public void testSingleCompositeSingle() { - final RepositoryRewriter translator = new BlobTranslator.Composite( - BlobTranslator.of(String::toUpperCase)); + final BlobTranslator translator = BlobTranslator.composite(BlobTranslator.of(String::toUpperCase)); final AnyHotEntry result = translator.rewriteBlobEntry(HotEntry.ofBlob("f.txt", "hello"), CTX); assertEquals(1, result.size()); assertEquals("HELLO", result.asBlob().getContent()); @@ -38,7 +37,7 @@ public void testSingleCompositeSingle() { @Test public void testCompositeMultiple() { - final RepositoryRewriter translator = new BlobTranslator.Composite( + final BlobTranslator translator = BlobTranslator.composite( BlobTranslator.of(s -> "PREFIX:" + s), BlobTranslator.of(String::toUpperCase)); final AnyHotEntry result = translator.rewriteBlobEntry(HotEntry.ofBlob("f.txt", "hello"), CTX); @@ -77,8 +76,7 @@ public void testSplitThenTransform() { set.add(entry.rename("copy_" + entry.getName())); return set; }; - final RepositoryRewriter translator = new BlobTranslator.Composite( - splitter, BlobTranslator.of(String::toUpperCase)); + final BlobTranslator translator = BlobTranslator.composite(splitter, BlobTranslator.of(String::toUpperCase)); final List entries = translator .rewriteBlobEntry(HotEntry.ofBlob("f.txt", "hello"), CTX) .stream().collect(Collectors.toList()); @@ -90,8 +88,7 @@ public void testSplitThenTransform() { @Test public void testFinerGit() throws IOException { try (RepositoryAccess source = TestRepo.createSample()) { - final RepositoryRewriter composite = - new BlobTranslator.Composite(new HistorageViaJDT(), new TokenizeViaJDT()); + final BlobTranslator composite = BlobTranslator.composite(new HistorageViaJDT(), new TokenizeViaJDT()); try (RepositoryAccess compositeResult = TestRepo.rewrite(source, composite); RepositoryAccess step1 = TestRepo.rewrite(source, new HistorageViaJDT()); diff --git a/src/test/java/jp/ac/titech/c/se/stein/rewriter/CommitTranslatorTest.java b/src/test/java/jp/ac/titech/c/se/stein/rewriter/CommitTranslatorTest.java new file mode 100644 index 0000000..a696d39 --- /dev/null +++ b/src/test/java/jp/ac/titech/c/se/stein/rewriter/CommitTranslatorTest.java @@ -0,0 +1,165 @@ +package jp.ac.titech.c.se.stein.rewriter; + +import jp.ac.titech.c.se.stein.app.blob.TokenizeViaJDT; +import jp.ac.titech.c.se.stein.app.commit.NoteCommit; +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.entry.AnyHotEntry; +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.testing.TestRepo; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +public class CommitTranslatorTest { + static final Context CTX = Context.init(); + + static RepositoryAccess source; + + @BeforeAll + static void setUp() throws IOException { + source = TestRepo.createSample(); + } + + @AfterAll + static void tearDown() { + source.close(); + } + + @Test + public void testMessageRewrite() { + final CommitTranslator t = new CommitTranslator() { + @Override + public String rewriteCommitMessage(String message, Context c) { + return "[tag] " + message; + } + }; + assertEquals("[tag] hello", t.rewriteCommitMessage("hello", CTX)); + } + + @Test + public void testCompositeMessage() { + final CommitTranslator a = new CommitTranslator() { + @Override + public String rewriteCommitMessage(String message, Context c) { + return "A:" + message; + } + }; + final CommitTranslator b = new CommitTranslator() { + @Override + public String rewriteCommitMessage(String message, Context c) { + return "B:" + message; + } + }; + final CommitTranslator composite = CommitTranslator.composite(a, b); + assertEquals("B:A:hello", composite.rewriteCommitMessage("hello", CTX)); + } + + @Test + public void testCompositeAuthor() { + final CommitTranslator renamer = new CommitTranslator() { + @Override + public PersonIdent rewriteAuthor(PersonIdent author, Context c) { + return new PersonIdent("Replaced", author.getEmailAddress()); + } + }; + final CommitTranslator tagger = new CommitTranslator() { + @Override + public PersonIdent rewriteAuthor(PersonIdent author, Context c) { + return new PersonIdent(author.getName() + " [bot]", author.getEmailAddress()); + } + }; + final CommitTranslator composite = CommitTranslator.composite(renamer, tagger); + final PersonIdent original = new PersonIdent("Alice", "alice@example.com"); + final PersonIdent result = composite.rewriteAuthor(original, CTX); + assertEquals("Replaced [bot]", result.getName()); + assertEquals("alice@example.com", result.getEmailAddress()); + } + + @Test + public void testFromBlob() { + final BlobTranslator upper = BlobTranslator.of(String::toUpperCase); + final CommitTranslator lifted = CommitTranslator.fromBlob(upper); + final AnyHotEntry result = lifted.rewriteBlobEntry(HotEntry.ofBlob("f.txt", "hello"), CTX); + assertEquals("HELLO", result.asBlob().getContent()); + // message is identity + assertEquals("msg", lifted.rewriteCommitMessage("msg", CTX)); + } + + @Test + public void testCompositeBlobRewrite() { + final CommitTranslator composite = CommitTranslator.composite( + CommitTranslator.fromBlob(BlobTranslator.of(s -> s + "!")), + CommitTranslator.fromBlob(BlobTranslator.of(String::toUpperCase))); + final AnyHotEntry result = composite.rewriteBlobEntry(HotEntry.ofBlob("f.txt", "hello"), CTX); + assertEquals("HELLO!", result.asBlob().getContent()); + } + + @Test + public void testCompositeMessageAndBlob() { + final CommitTranslator msgRewriter = new CommitTranslator() { + @Override + public String rewriteCommitMessage(String message, Context c) { + return "[processed] " + message; + } + }; + final CommitTranslator composite = CommitTranslator.composite( + CommitTranslator.fromBlob(BlobTranslator.of(String::toUpperCase)), + msgRewriter); + assertEquals("[processed] hello", composite.rewriteCommitMessage("hello", CTX)); + assertEquals("HELLO", composite.rewriteBlobEntry(HotEntry.ofBlob("f.txt", "hello"), CTX).asBlob().getContent()); + } + + @Test + public void testCompositeWithBlobOnRepo() throws IOException { + // Tokenize (BlobTranslator) + NoteCommit (CommitTranslator) composed vs sequential + final List sourceCommits = source.collectCommits("refs/heads/main"); + + final CommitTranslator composite = CommitTranslator.composite( + CommitTranslator.fromBlob(new TokenizeViaJDT()), + new NoteCommit()); + + try (RepositoryAccess compositeResult = TestRepo.rewrite(source, composite); + RepositoryAccess step1 = TestRepo.rewrite(source, new TokenizeViaJDT()); + RepositoryAccess sequentialResult = TestRepo.rewrite(step1, new NoteCommit())) { + + final List compositeCommits = compositeResult.collectCommits("refs/heads/main"); + final List sequentialCommits = sequentialResult.collectCommits("refs/heads/main"); + assertEquals(compositeCommits.size(), sequentialCommits.size()); + + // Both should have original commit IDs in the message + for (int i = 0; i < compositeCommits.size(); i++) { + final String compositeMsg = compositeCommits.get(i).getFullMessage(); + final String expectedPrefix = sourceCommits.get(i).getId().name(); + assertTrue(compositeMsg.startsWith(expectedPrefix + " "), + "Expected original id " + expectedPrefix + " in: " + compositeMsg); + } + + // Tree content should match + final RevCommit compositeHead = compositeResult.getHead("refs/heads/main"); + final RevCommit sequentialHead = sequentialResult.getHead("refs/heads/main"); + final List compositeFiles = compositeResult.flattenTree(compositeHead.getTree().getId()); + final List sequentialFiles = sequentialResult.flattenTree(sequentialHead.getTree().getId()); + + assertEquals( + compositeFiles.stream().map(Entry::getName).sorted().collect(Collectors.toList()), + sequentialFiles.stream().map(Entry::getName).sorted().collect(Collectors.toList())); + + for (Entry ce : compositeFiles) { + final Entry se = sequentialFiles.stream() + .filter(e -> e.getName().equals(ce.getName())) + .findFirst().orElseThrow(); + assertEquals(ce.getId(), se.getId(), "blob mismatch for " + ce.getName()); + } + } + } +} diff --git a/src/test/java/jp/ac/titech/c/se/stein/testing/RewriteBenchmark.java b/src/test/java/jp/ac/titech/c/se/stein/testing/RewriteBenchmark.java index 1cc4539..ef43a58 100644 --- a/src/test/java/jp/ac/titech/c/se/stein/testing/RewriteBenchmark.java +++ b/src/test/java/jp/ac/titech/c/se/stein/testing/RewriteBenchmark.java @@ -56,7 +56,7 @@ public static void main(String[] args) throws Exception { results.add(benchmark("tokenize-jdt", sourceDir, () -> new TokenizeViaJDT().toRewriter(), alternates, cache)); results.add(benchmark("historage-jdt", sourceDir, () -> new HistorageViaJDT().toRewriter(), alternates, cache)); results.add(benchmark("historage+tokenize", sourceDir, - () -> new BlobTranslator.Composite(new HistorageViaJDT(), new TokenizeViaJDT()), alternates, cache)); + () -> BlobTranslator.composite(new HistorageViaJDT(), new TokenizeViaJDT()).toRewriter(), alternates, cache)); // summary System.out.println();