dfee7b6196
GitOrigin-RevId: b578e69f18a543889ded9c57a8f0dffacdb103d8
3273 lines
133 KiB
Java
3273 lines
133 KiB
Java
/*
|
|
* Copyright (C) 2016 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.google.copybara;
|
|
|
|
import static com.google.common.truth.Truth.assertThat;
|
|
import static com.google.copybara.TransformWork.COPYBARA_CONTEXT_REFERENCE_LABEL;
|
|
import static com.google.copybara.WorkflowMode.CHANGE_REQUEST;
|
|
import static com.google.copybara.WorkflowMode.ITERATIVE;
|
|
import static com.google.copybara.WorkflowMode.SQUASH;
|
|
import static com.google.copybara.git.GitRepository.newBareRepo;
|
|
import static com.google.copybara.testing.DummyOrigin.HEAD;
|
|
import static com.google.copybara.testing.FileSubjects.assertThatPath;
|
|
import static com.google.copybara.testing.git.GitTestUtil.getGitEnv;
|
|
import static com.google.copybara.util.CommandRunner.DEFAULT_TIMEOUT;
|
|
import static com.google.copybara.util.DiffUtil.DiffFile.Operation.ADD;
|
|
import static com.google.copybara.util.DiffUtil.DiffFile.Operation.DELETE;
|
|
import static com.google.copybara.util.DiffUtil.DiffFile.Operation.MODIFIED;
|
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
|
import static org.junit.Assert.assertThrows;
|
|
import static org.junit.Assert.fail;
|
|
|
|
import com.google.common.base.Joiner;
|
|
import com.google.common.base.StandardSystemProperty;
|
|
import com.google.common.base.Throwables;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableMap;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import com.google.common.collect.Iterables;
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.collect.Maps;
|
|
import com.google.common.jimfs.Jimfs;
|
|
import com.google.copybara.DestinationEffect.Type;
|
|
import com.google.copybara.authoring.Author;
|
|
import com.google.copybara.authoring.AuthorParser;
|
|
import com.google.copybara.config.Config;
|
|
import com.google.copybara.config.MapConfigFile;
|
|
import com.google.copybara.config.Migration;
|
|
import com.google.copybara.exception.CannotResolveRevisionException;
|
|
import com.google.copybara.exception.ChangeRejectedException;
|
|
import com.google.copybara.exception.EmptyChangeException;
|
|
import com.google.copybara.exception.NotADestinationFileException;
|
|
import com.google.copybara.exception.RepoException;
|
|
import com.google.copybara.exception.ValidationException;
|
|
import com.google.copybara.exception.VoidOperationException;
|
|
import com.google.copybara.git.GitRepository;
|
|
import com.google.copybara.git.GitRepository.GitLogEntry;
|
|
import com.google.copybara.git.GitRevision;
|
|
import com.google.copybara.hg.HgRepository;
|
|
import com.google.copybara.monitor.EventMonitor.ChangeMigrationFinishedEvent;
|
|
import com.google.copybara.testing.DummyOrigin;
|
|
import com.google.copybara.testing.DummyRevision;
|
|
import com.google.copybara.testing.OptionsBuilder;
|
|
import com.google.copybara.testing.RecordsProcessCallDestination;
|
|
import com.google.copybara.testing.RecordsProcessCallDestination.ProcessedChange;
|
|
import com.google.copybara.testing.SkylarkTestExecutor;
|
|
import com.google.copybara.testing.TestingEventMonitor;
|
|
import com.google.copybara.testing.TransformWorks;
|
|
import com.google.copybara.testing.git.GitTestUtil;
|
|
import com.google.copybara.util.DiffUtil.DiffFile;
|
|
import com.google.copybara.util.FileUtil;
|
|
import com.google.copybara.util.FileUtil.CopySymlinkStrategy;
|
|
import com.google.copybara.util.Glob;
|
|
import com.google.copybara.util.console.Console;
|
|
import com.google.copybara.util.console.Message;
|
|
import com.google.copybara.util.console.Message.MessageType;
|
|
import com.google.copybara.util.console.testing.TestingConsole;
|
|
import java.io.IOException;
|
|
import java.nio.file.FileSystem;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.PathMatcher;
|
|
import java.nio.file.Paths;
|
|
import java.time.ZoneId;
|
|
import java.time.ZonedDateTime;
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.time.temporal.ChronoField;
|
|
import java.time.temporal.TemporalAccessor;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.StreamSupport;
|
|
import javax.annotation.Nullable;
|
|
import org.junit.Before;
|
|
import org.junit.Test;
|
|
import org.junit.runner.RunWith;
|
|
import org.junit.runners.JUnit4;
|
|
|
|
@RunWith(JUnit4.class)
|
|
public class WorkflowTest {
|
|
|
|
private static final String PREFIX = "TRANSFORMED";
|
|
private static final Author ORIGINAL_AUTHOR =
|
|
new Author("Foo Bar", "foo@bar.com");
|
|
private static final Author NOT_WHITELISTED_ORIGINAL_AUTHOR =
|
|
new Author("Secret Coder", "secret@coder.com");
|
|
private static final Author DEFAULT_AUTHOR = new Author("Copybara", "no-reply@google.com");
|
|
private static final String FORCED_MESSAGE = "Test forced message";
|
|
private static final Author FORCED_AUTHOR =
|
|
new Author("Forced Author", "<forcedauthor@google.com>");
|
|
|
|
private DummyOrigin origin;
|
|
private RecordsProcessCallDestination destination;
|
|
private OptionsBuilder options;
|
|
private String authoring;
|
|
|
|
private SkylarkTestExecutor skylark;
|
|
|
|
private ImmutableList<String> transformations;
|
|
private Path workdir;
|
|
private boolean includeReleaseNotes;
|
|
private String originFiles;
|
|
private String destinationFiles;
|
|
private TestingEventMonitor eventMonitor;
|
|
private TransformWork transformWork;
|
|
private boolean setRevId;
|
|
private boolean smartPrune;
|
|
private boolean migrateNoopChangesField;
|
|
private ImmutableList<String> extraWorkflowFields = ImmutableList.of();
|
|
|
|
@Before
|
|
public void setup() throws Exception {
|
|
options = new OptionsBuilder();
|
|
authoring = "authoring.overwrite('" + DEFAULT_AUTHOR + "')";
|
|
includeReleaseNotes = false;
|
|
workdir = Files.createTempDirectory("workdir");
|
|
Files.createDirectories(workdir);
|
|
origin = new DummyOrigin().setAuthor(ORIGINAL_AUTHOR);
|
|
originFiles = "glob(['**'], exclude = ['copy.bara.sky', 'excluded/**'])";
|
|
destinationFiles = "glob(['**'])";
|
|
destination = new RecordsProcessCallDestination();
|
|
transformations = ImmutableList.of(""
|
|
+ " core.replace(\n"
|
|
+ " before = '${linestart}${number}',\n"
|
|
+ " after = '${linestart}" + PREFIX + "${number}',\n"
|
|
+ " regex_groups = {\n"
|
|
+ " 'number' : '[0-9]+',\n"
|
|
+ " 'linestart' : '^',\n"
|
|
+ " },\n"
|
|
+ " multiline = True,"
|
|
+ " )");
|
|
TestingConsole console = new TestingConsole();
|
|
options.setConsole(console);
|
|
options.testingOptions.origin = origin;
|
|
options.testingOptions.destination = destination;
|
|
options.setForce(true); // Force by default unless we are testing the flag.
|
|
skylark = new SkylarkTestExecutor(options);
|
|
eventMonitor = new TestingEventMonitor();
|
|
options.general.withEventMonitor(eventMonitor);
|
|
transformWork = TransformWorks.of(workdir, "example", console);
|
|
setRevId = true;
|
|
smartPrune = false;
|
|
migrateNoopChangesField = false;
|
|
extraWorkflowFields = ImmutableList.of();
|
|
}
|
|
|
|
private TestingConsole console() {
|
|
return (TestingConsole) options.general.console();
|
|
}
|
|
|
|
private Workflow<?, ?> workflow() throws ValidationException, IOException {
|
|
origin.addSimpleChange(/*timestamp*/ 42);
|
|
return skylarkWorkflow("default", SQUASH);
|
|
}
|
|
|
|
private Workflow<?, ?> skylarkWorkflow(String name, WorkflowMode mode)
|
|
throws IOException, ValidationException {
|
|
List<String> transformations = Lists.newArrayList(this.transformations);
|
|
if (includeReleaseNotes) {
|
|
transformations.add("metadata.squash_notes()");
|
|
}
|
|
String config = ""
|
|
+ "core.workflow(\n"
|
|
+ " name = '" + name + "',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " origin_files = " + originFiles + ",\n"
|
|
+ " destination_files = " + destinationFiles + ",\n"
|
|
+ " transformations = " + transformations + ",\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " set_rev_id = " + (setRevId ? "True" : "False") + ",\n"
|
|
+ " smart_prune = " + (smartPrune ? "True" : "False") + ",\n"
|
|
+ " migrate_noop_changes = " + (migrateNoopChangesField ? "True" : "False") + ",\n"
|
|
+ " mode = '" + mode + "',\n"
|
|
+ (extraWorkflowFields.isEmpty()
|
|
? ""
|
|
: " " + Joiner.on(",\n ").join(extraWorkflowFields) + ",\n"
|
|
)
|
|
+ ")\n";
|
|
System.err.println(config);
|
|
return (Workflow<?, ?>) loadConfig(config).getMigration(name);
|
|
}
|
|
|
|
private Workflow<?, ?> iterativeWorkflow(String workflowName, @Nullable String previousRef)
|
|
throws ValidationException, IOException {
|
|
options.workflowOptions.lastRevision = previousRef;
|
|
options.general.withEventMonitor(eventMonitor);
|
|
options.general.setConsoleForTest(console());
|
|
return skylarkWorkflow(workflowName, WorkflowMode.ITERATIVE);
|
|
}
|
|
|
|
private Workflow<?, ?> iterativeWorkflow(@Nullable String previousRef)
|
|
throws ValidationException, IOException {
|
|
return iterativeWorkflow("default", previousRef);
|
|
}
|
|
|
|
private Workflow<?, ?> changeRequestWorkflow(@Nullable String baseline)
|
|
throws ValidationException, IOException {
|
|
options.workflowOptions.changeBaseline = baseline;
|
|
return skylarkWorkflow("default", WorkflowMode.CHANGE_REQUEST);
|
|
}
|
|
|
|
@Test
|
|
public void defaultNameIsDefault() throws Exception {
|
|
assertThat(workflow().getName()).isEqualTo("default");
|
|
}
|
|
|
|
@Test
|
|
public void toStringIncludesName() throws Exception {
|
|
assertThat(skylarkWorkflow("toStringIncludesName", SQUASH).toString())
|
|
.contains("toStringIncludesName");
|
|
}
|
|
|
|
@Test
|
|
public void squashWorkflowTestRecordContextReference() throws Exception {
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
transformations = ImmutableList.of();
|
|
Workflow<?, ?> workflow = workflow();
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
ProcessedChange change = Iterables.getOnlyElement(destination.processed);
|
|
assertThat(change.getRequestedRevision().contextReference()).isEqualTo("HEAD");
|
|
}
|
|
|
|
@Test
|
|
public void squashWorkflowPublishesEvents() throws Exception {
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
transformations = ImmutableList.of();
|
|
Workflow<?, ?> workflow = workflow();
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
assertThat(eventMonitor.changeMigrationStartedEventCount()).isEqualTo(1);
|
|
assertThat(eventMonitor.changeMigrationFinishedEventCount()).isEqualTo(1);
|
|
}
|
|
|
|
@Test
|
|
public void contextReferenceAsLabel() throws Exception {
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
transformations = ImmutableList.of(
|
|
"metadata.add_header('Import of ${" + COPYBARA_CONTEXT_REFERENCE_LABEL + "}\\n')",
|
|
"metadata.expose_label('" + COPYBARA_CONTEXT_REFERENCE_LABEL + "')"
|
|
);
|
|
Workflow<?, ?> workflow = workflow();
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
ProcessedChange change = Iterables.getOnlyElement(destination.processed);
|
|
assertThat(change.getChangesSummary()).isEqualTo(""
|
|
+ "Import of HEAD\n"
|
|
+ "\n"
|
|
+ "Project import generated by Copybara.\n"
|
|
+ "\n"
|
|
+ "COPYBARA_CONTEXT_REFERENCE=HEAD\n");
|
|
}
|
|
|
|
@Test
|
|
public void squashReadsLatestAffectedChangeInRoot() throws Exception {
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
transformations = ImmutableList.of();
|
|
Workflow<?, ?> workflow = workflow();
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
origin.addSimpleChange(/*timestamp*/ 2);
|
|
DummyRevision expected = origin.resolve("HEAD");
|
|
origin.addChange(/*timestamp*/ 3, Paths.get("not important"), "message",
|
|
/*matchesGlob=*/false);
|
|
|
|
options.setForce(false);
|
|
workflow = skylarkWorkflow("default", SQUASH);
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
ProcessedChange change = Iterables.getLast(destination.processed);
|
|
assertThat(change.getOriginRef().asString()).isEqualTo(expected.asString());
|
|
}
|
|
|
|
@Test
|
|
public void testDisableCheckout() throws Exception {
|
|
transformations = ImmutableList.of();
|
|
extraWorkflowFields = ImmutableList.of("checkout = False");
|
|
Workflow<?, ?> workflow = workflow();
|
|
DummyRevision head = origin.resolve("HEAD");
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
ProcessedChange change = Iterables.getLast(destination.processed);
|
|
assertThat(change.getOriginRef()).isEqualTo(head);
|
|
assertThatPath(workdir).containsNoMoreFiles();
|
|
}
|
|
|
|
@Test
|
|
public void testEmptyDescriptionForFolderDestination() throws Exception {
|
|
origin.singleFileChange(/*timestamp=*/44, "commit 1", "bar.txt", "1");
|
|
options
|
|
.setWorkdirToRealTempDir()
|
|
.setHomeDir(StandardSystemProperty.USER_HOME.value());
|
|
new SkylarkTestExecutor(options).loadConfig("core.workflow(\n"
|
|
+ " name = 'foo',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = folder.destination(),\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " transformations = [metadata.replace_message(''),],\n"
|
|
+ ")\n")
|
|
.getMigration("foo")
|
|
.run(workdir, ImmutableList.of());
|
|
}
|
|
|
|
@Test
|
|
public void testTestWorkflowWithDiffInOrigin() throws Exception {
|
|
GitRepository remote = GitRepository.newBareRepo(
|
|
Files.createTempDirectory("gitdir"), getGitEnv(), /*verbose=*/true,
|
|
DEFAULT_TIMEOUT, /*noVerify=*/ false).withWorkTree(workdir);
|
|
remote.init();
|
|
|
|
Files.write(workdir.resolve("foo.txt"), new byte[]{});
|
|
remote.add().files("foo.txt").run();
|
|
remote.simpleCommand("commit", "foo.txt", "-m", "message_a");
|
|
GitRevision lastRev = remote.resolveReference("master");
|
|
|
|
Files.write(workdir.resolve("bar.txt"), "change content".getBytes(UTF_8));
|
|
remote.add().files("bar.txt").run();
|
|
remote.simpleCommand("commit", "bar.txt", "-m", "message_s");
|
|
|
|
TestingConsole testingConsole = new TestingConsole().respondYes();
|
|
options.workflowOptions.lastRevision = lastRev.getSha1();
|
|
options
|
|
.setWorkdirToRealTempDir()
|
|
.setConsole(testingConsole)
|
|
.setHomeDir(StandardSystemProperty.USER_HOME.value());
|
|
|
|
Workflow<?, ?> workflow =
|
|
(Workflow<?, ?>) new SkylarkTestExecutor(options).loadConfig(
|
|
"core.workflow(\n"
|
|
+ " name = 'foo',\n"
|
|
+ " origin = git.origin(url='" + remote.getGitDir()+"'),\n"
|
|
+ " destination = folder.destination(),\n"
|
|
+ " mode = 'ITERATIVE',\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " transformations = [metadata.replace_message(''),],\n"
|
|
+ ")\n")
|
|
.getMigration("foo");
|
|
workflow.getWorkflowOptions().diffInOrigin = true;
|
|
workflow.run(workdir, ImmutableList.of("master"));
|
|
testingConsole.assertThat()
|
|
.onceInLog(MessageType.WARNING, "Change 1 of 1 \\(.*\\)\\: Continue to migrate with '"
|
|
+ workflow.getMode() + "'"+" to " + workflow.getDestination().getType()+ "\\?");
|
|
}
|
|
|
|
@Test
|
|
public void testTestWorkflowWithDiffInOriginAndRespondNo() throws Exception {
|
|
GitRepository remote = GitRepository.newBareRepo(
|
|
Files.createTempDirectory("gitdir"), getGitEnv(), /*verbose=*/true,
|
|
DEFAULT_TIMEOUT, /*noVerify=*/ false).withWorkTree(workdir);
|
|
remote.init();
|
|
|
|
Files.write(workdir.resolve("foo.txt"), new byte[]{});
|
|
remote.add().files("foo.txt").run();
|
|
remote.simpleCommand("commit", "foo.txt", "-m", "message_a");
|
|
GitRevision lastRev = remote.resolveReference("master");
|
|
|
|
Files.write(workdir.resolve("bar.txt"), "change content".getBytes(UTF_8));
|
|
remote.add().files("bar.txt").run();
|
|
remote.simpleCommand("commit", "bar.txt", "-m", "message_s");
|
|
|
|
TestingConsole testingConsole = new TestingConsole().respondNo();
|
|
options.workflowOptions.lastRevision = lastRev.getSha1();
|
|
options
|
|
.setWorkdirToRealTempDir()
|
|
.setConsole(testingConsole)
|
|
.setHomeDir(StandardSystemProperty.USER_HOME.value());
|
|
|
|
Workflow<?, ?> workflow =
|
|
(Workflow<?, ?>) new SkylarkTestExecutor(options).loadConfig(
|
|
"core.workflow(\n"
|
|
+ " name = 'foo',\n"
|
|
+ " origin = git.origin(url='" + remote.getGitDir()+"'),\n"
|
|
+ " destination = folder.destination(),\n"
|
|
+ " mode = 'ITERATIVE',\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " transformations = [metadata.replace_message(''),],\n"
|
|
+ ")\n")
|
|
.getMigration("foo");
|
|
workflow.getWorkflowOptions().diffInOrigin = true;
|
|
ChangeRejectedException e =
|
|
assertThrows(
|
|
ChangeRejectedException.class, () -> workflow.run(workdir, ImmutableList.of("master")));
|
|
assertThat(e.getMessage())
|
|
.contains("User aborted execution: did not confirm diff in origin changes.");
|
|
}
|
|
|
|
@Test
|
|
public void iterativeWorkflowTestRecordContextReference() throws Exception {
|
|
for (int timestamp = 0; timestamp < 10; timestamp++) {
|
|
origin.addSimpleChange(timestamp);
|
|
}
|
|
Workflow<?, ?> workflow = iterativeWorkflow("0");
|
|
|
|
// First change is migrated without context reference. Then we run again with
|
|
// context ("HEAD").
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
workflow = iterativeWorkflow(/*previousRef=*/ null);
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
for (ProcessedChange change : destination.processed) {
|
|
assertThat(change.getRequestedRevision().contextReference())
|
|
.isEqualTo(change.getOriginRef().asString().equals("1") ? null : "HEAD");
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void iterativeWorkflowWithSameOriginContext() throws Exception {
|
|
for (int timestamp = 0; timestamp < 10; timestamp++) {
|
|
origin.addSimpleChangeWithContextReference(timestamp, "HEAD");
|
|
}
|
|
Workflow<?, ?> workflow = iterativeWorkflow("0");
|
|
|
|
// First change is migrated without context reference. Then we run again with
|
|
// context ("HEAD").
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
workflow = iterativeWorkflow(/*previousRef=*/ null);
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
Set<String> identities = new HashSet<>();
|
|
for (ProcessedChange change : destination.processed) {
|
|
identities.add(change.getChangeIdentity());
|
|
}
|
|
// All change identities are different
|
|
assertThat(identities).hasSize(destination.processed.size());
|
|
}
|
|
|
|
@Test
|
|
public void iterativeWorkflowPublishesEvents() throws Exception {
|
|
for (int timestamp = 0; timestamp < 10; timestamp++) {
|
|
origin.addSimpleChange(timestamp);
|
|
}
|
|
|
|
Workflow<?, ?> workflow = iterativeWorkflow("0");
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
|
|
assertThat(eventMonitor.changeMigrationStartedEventCount()).isEqualTo(9);
|
|
assertThat(eventMonitor.changeMigrationFinishedEventCount()).isEqualTo(9);
|
|
}
|
|
|
|
@Test
|
|
public void iterativeWorkflowTestRecordMigrationKey() throws Exception {
|
|
for (int timestamp = 0; timestamp < 10; timestamp++) {
|
|
origin.addSimpleChange(timestamp);
|
|
}
|
|
String name = "notDefaultWorkflow";
|
|
Workflow<?, ?> workflow = iterativeWorkflow(name, "0");
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
workflow = iterativeWorkflow(name, null);
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
|
|
for (ProcessedChange change : destination.processed) {
|
|
assertThat(change.getWorkflowName()).isEqualTo(name);
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestWorkflowTestRecordContextReference() throws Exception {
|
|
origin
|
|
.addSimpleChange(0, "One Change\n" + destination.getLabelNameWhenOrigin() + "=42")
|
|
.addSimpleChange(1, "Second Change");
|
|
|
|
Workflow<?, ?> workflow = changeRequestWorkflow(null);
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
ProcessedChange change = destination.processed.get(0);
|
|
|
|
assertThat(change.getBaseline()).isEqualTo("42");
|
|
assertThat(change.getRequestedRevision().contextReference()).isEqualTo("HEAD");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestWorkflowPublishesEvents() throws Exception {
|
|
origin
|
|
.addSimpleChange(0, "One Change\n" + destination.getLabelNameWhenOrigin() + "=42")
|
|
.addSimpleChange(1, "Second Change");
|
|
|
|
Workflow<?, ?> workflow = changeRequestWorkflow(null);
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
|
|
assertThat(eventMonitor.changeMigrationStartedEventCount()).isEqualTo(1);
|
|
assertThat(eventMonitor.changeMigrationFinishedEventCount()).isEqualTo(1);
|
|
}
|
|
|
|
@Test
|
|
public void iterativeWorkflowTest_defaultAuthoring() throws Exception {
|
|
for (int timestamp = 0; timestamp < 61; timestamp++) {
|
|
origin.addSimpleChange(timestamp);
|
|
}
|
|
Workflow<?, ?> workflow = iterativeWorkflow(/*previousRef=*/ "42");
|
|
|
|
workflow.run(workdir, ImmutableList.of("50"));
|
|
assertThat(destination.processed).hasSize(8);
|
|
int nextChange = 43;
|
|
for (ProcessedChange change : destination.processed) {
|
|
assertThat(change.getChangesSummary()).isEqualTo(nextChange + " change");
|
|
String asString = Integer.toString(nextChange);
|
|
assertThat(change.getOriginRef().asString()).isEqualTo(asString);
|
|
assertThat(change.numFiles()).isEqualTo(1);
|
|
assertThat(change.getContent("file.txt")).isEqualTo(PREFIX + asString);
|
|
assertThat(change.getAuthor()).isEqualTo(DEFAULT_AUTHOR);
|
|
nextChange++;
|
|
}
|
|
|
|
workflow = iterativeWorkflow(null);
|
|
workflow.run(workdir, ImmutableList.of("60"));
|
|
assertThat(destination.processed).hasSize(18);
|
|
}
|
|
|
|
@Test
|
|
public void testIterativeModeWithLimit() throws Exception {
|
|
for (int timestamp = 0; timestamp < 51; timestamp++) {
|
|
origin.addSimpleChange(timestamp);
|
|
}
|
|
// First change so that iterative can find last imported revisions
|
|
iterativeWorkflow(/*previousRef=*/"40").run(workdir, ImmutableList.of("41"));
|
|
options.workflowOptions.iterativeLimitChanges = 1;
|
|
int numClsBefore = destination.processed.size();
|
|
for (int i = 42; i <= 50; i++) {
|
|
iterativeWorkflow(/*previousRef=*/null).run(workdir, ImmutableList.of("50"));
|
|
assertThat(destination.processed).hasSize(numClsBefore + 1);
|
|
numClsBefore++;
|
|
assertThat(Iterables.getLast(destination.processed).getChangesSummary())
|
|
.isEqualTo(i + " change");
|
|
}
|
|
|
|
// Check that we don't import anything else after we have migrated all pending changes.
|
|
assertThrows(
|
|
EmptyChangeException.class,
|
|
() -> iterativeWorkflow(/*previousRef=*/ null).run(workdir, ImmutableList.of()));
|
|
}
|
|
|
|
@Test
|
|
public void testIterativeModeProducesNoop() throws Exception {
|
|
assertThat(checkIterativeModeWithError(new EmptyChangeException("This was an empty change!")))
|
|
.hasMessageThat()
|
|
.isEqualTo("Iterative workflow produced no changes in the destination for resolved ref: 3");
|
|
console()
|
|
.assertThat()
|
|
.onceInLog(
|
|
MessageType.WARNING, "Migration of origin revision '2' resulted in an empty change.*")
|
|
.onceInLog(
|
|
MessageType.WARNING, "Migration of origin revision '3' resulted in an empty change.*");
|
|
}
|
|
|
|
@Test
|
|
public void testIterativeValidationException() throws Exception {
|
|
assertThat(checkIterativeModeWithError(new ValidationException("Your change is wrong!")))
|
|
.hasMessageThat()
|
|
.isEqualTo("Your change is wrong!");
|
|
console()
|
|
.assertThat()
|
|
.onceInLog(
|
|
MessageType.ERROR,
|
|
"Migration of origin revision '2' failed with error: Your change is wrong.*");
|
|
}
|
|
|
|
@Test
|
|
public void testIterativeRepoException() throws Exception {
|
|
assertThat(checkIterativeModeWithError(new RepoException("Your change is wrong!")))
|
|
.hasMessageThat()
|
|
.isEqualTo("Your change is wrong!");
|
|
console()
|
|
.assertThat()
|
|
.onceInLog(
|
|
MessageType.ERROR,
|
|
"Migration of origin revision '2' failed with error: Your change is wrong.*");
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private <T extends Exception> T checkIterativeModeWithError(T exception)
|
|
throws IOException, ValidationException {
|
|
for (int timestamp = 0; timestamp < 10; timestamp++) {
|
|
origin.addSimpleChange(timestamp);
|
|
}
|
|
// Override destination with one that always throws EmptyChangeException.
|
|
options.testingOptions.destination =
|
|
new RecordsProcessCallDestination() {
|
|
@Override
|
|
public Writer<Revision> newWriter(WriterContext writerContext) {
|
|
return new WriterImpl(writerContext.isDryRun()) {
|
|
@Override
|
|
public ImmutableList<DestinationEffect> write(
|
|
TransformResult transformResult, Glob destinationFiles, Console console)
|
|
throws ValidationException, RepoException {
|
|
assert exception != null;
|
|
Throwables.propagateIfPossible(
|
|
exception, ValidationException.class, RepoException.class);
|
|
throw new RuntimeException(exception);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
Workflow<?, ?> workflow = iterativeWorkflow(/*previousRef=*/"1");
|
|
|
|
try {
|
|
workflow.run(workdir, ImmutableList.of("3"));
|
|
fail();
|
|
} catch (Exception expected) {
|
|
assertThat(expected).isInstanceOf(expected.getClass());
|
|
return (T) expected;
|
|
}
|
|
return exception;
|
|
}
|
|
|
|
@Test
|
|
public void iterativeWorkflowTest_whitelistAuthoring() throws Exception {
|
|
origin
|
|
.addSimpleChange(0)
|
|
.setAuthor(ORIGINAL_AUTHOR)
|
|
.addSimpleChange(1)
|
|
.setAuthor(NOT_WHITELISTED_ORIGINAL_AUTHOR)
|
|
.addSimpleChange(2);
|
|
|
|
whiteListAuthoring();
|
|
|
|
Workflow<?, ?> workflow = iterativeWorkflow("0");
|
|
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
assertThat(destination.processed).hasSize(2);
|
|
|
|
assertThat(destination.processed.get(0).getAuthor()).isEqualTo(ORIGINAL_AUTHOR);
|
|
assertThat(destination.processed.get(1).getAuthor()).isEqualTo(DEFAULT_AUTHOR);
|
|
}
|
|
|
|
@Test
|
|
public void testDefaultAuthorFlag() throws Exception {
|
|
origin
|
|
.addSimpleChange(0)
|
|
.setAuthor(ORIGINAL_AUTHOR)
|
|
.addSimpleChange(1)
|
|
.setAuthor(NOT_WHITELISTED_ORIGINAL_AUTHOR)
|
|
.addSimpleChange(2);
|
|
|
|
options.workflowOptions.defaultAuthor = "From Flag <fromflag@google.com>";
|
|
whiteListAuthoring();
|
|
|
|
Workflow<?, ?> workflow = iterativeWorkflow("0");
|
|
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
assertThat(destination.processed).hasSize(2);
|
|
|
|
assertThat(destination.processed.get(0).getAuthor()).isEqualTo(ORIGINAL_AUTHOR);
|
|
assertThat(destination.processed.get(1).getAuthor()).isEqualTo(
|
|
AuthorParser.parse("From Flag <fromflag@google.com>"));
|
|
}
|
|
|
|
@Test
|
|
public void testDescription() throws Exception {
|
|
extraWorkflowFields = ImmutableList.of("description = \"Do foo with bar\"");
|
|
assertThat(workflow().getDescription()).isEqualTo("Do foo with bar");
|
|
}
|
|
|
|
@Test
|
|
public void testForcedChangeMessageAndAuthorFlags_squash() throws Exception {
|
|
options.workflowOptions.forcedChangeMessage = FORCED_MESSAGE;
|
|
options.workflowOptions.forcedAuthor = FORCED_AUTHOR;
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
options.workflowOptions.lastRevision = resolveHead();
|
|
origin.addSimpleChange(/*timestamp*/ 2);
|
|
origin.addSimpleChange(/*timestamp*/ 3);
|
|
|
|
Workflow<?, ?> workflow = workflow();
|
|
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
assertThat(destination.processed).hasSize(1);
|
|
ProcessedChange change = Iterables.getOnlyElement(destination.processed);
|
|
assertThat(change.getChangesSummary()).isEqualTo(FORCED_MESSAGE);
|
|
assertThat(change.getAuthor()).isEqualTo(FORCED_AUTHOR);
|
|
}
|
|
|
|
@Test
|
|
public void testSquashCustomLabel() throws Exception {
|
|
origin.addSimpleChange(/*timestamp*/ 0);
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
options.workflowOptions.lastRevision = resolveHead();
|
|
origin.addSimpleChange(/*timestamp*/ 2);
|
|
origin.addSimpleChange(/*timestamp*/ 3);
|
|
|
|
extraWorkflowFields = ImmutableList.of("experimental_custom_rev_id = \"CUSTOM_REV_ID\"");
|
|
|
|
Workflow<?, ?> workflow = skylarkWorkflow("default", SQUASH);
|
|
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
assertThat(destination.processed).hasSize(1);
|
|
ProcessedChange change = Iterables.getOnlyElement(destination.processed);
|
|
assertThat(change.getRevIdLabel()).isEqualTo("CUSTOM_REV_ID");
|
|
assertThat(change.getOriginRef().asString()).isEqualTo("3");
|
|
|
|
options.workflowOptions.lastRevision = null;
|
|
|
|
workflow = skylarkWorkflow("default", SQUASH);
|
|
|
|
assertThat(
|
|
Iterables.getOnlyElement(
|
|
workflow.getInfo().migrationReferences()).getLastMigrated().asString()).isEqualTo("3");
|
|
|
|
origin.addSimpleChange(/*timestamp*/ 4);
|
|
origin.addSimpleChange(/*timestamp*/ 5);
|
|
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
ProcessedChange last = Iterables.getLast(destination.processed);
|
|
assertThat(last.getOriginChanges()).hasSize(2);
|
|
assertThat(last.getOriginChanges().get(0).getRevision().asString()).isEqualTo("5");
|
|
assertThat(last.getOriginChanges().get(1).getRevision().asString()).isEqualTo("4");
|
|
}
|
|
|
|
@Test
|
|
public void testForcedChangeMessageAndAuthorFlags_iterative() throws Exception {
|
|
origin
|
|
.addSimpleChange(0)
|
|
.addSimpleChange(1)
|
|
.addSimpleChange(2);
|
|
|
|
options.workflowOptions.forcedChangeMessage = FORCED_MESSAGE;
|
|
options.workflowOptions.forcedAuthor = FORCED_AUTHOR;
|
|
whiteListAuthoring();
|
|
|
|
Workflow<?, ?> workflow = iterativeWorkflow("0");
|
|
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
assertThat(destination.processed).hasSize(2);
|
|
|
|
assertThat(destination.processed.get(0).getChangesSummary()).isEqualTo(FORCED_MESSAGE);
|
|
assertThat(destination.processed.get(0).getAuthor()).isEqualTo(FORCED_AUTHOR);
|
|
assertThat(destination.processed.get(1).getChangesSummary()).isEqualTo(FORCED_MESSAGE);
|
|
assertThat(destination.processed.get(1).getAuthor()).isEqualTo(FORCED_AUTHOR);
|
|
}
|
|
|
|
@Test
|
|
public void testForcedChangeMessageAndAuthorFlags_changeRequest() throws Exception {
|
|
origin
|
|
.addSimpleChange(0, "One Change\n" + destination.getLabelNameWhenOrigin() + "=42")
|
|
.addSimpleChange(1, "Second Change");
|
|
|
|
options.workflowOptions.forcedChangeMessage = FORCED_MESSAGE;
|
|
options.workflowOptions.forcedAuthor = FORCED_AUTHOR;
|
|
|
|
Workflow<?, ?> workflow = changeRequestWorkflow("0");
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
assertThat(destination.processed).hasSize(1);
|
|
|
|
assertThat(destination.processed.get(0).getChangesSummary()).isEqualTo(FORCED_MESSAGE);
|
|
assertThat(destination.processed.get(0).getAuthor()).isEqualTo(FORCED_AUTHOR);
|
|
}
|
|
|
|
private void whiteListAuthoring() {
|
|
authoring = ""
|
|
+ "authoring.whitelisted(\n"
|
|
+ " default = '" + DEFAULT_AUTHOR + "',\n"
|
|
+ " whitelist = ['" + ORIGINAL_AUTHOR.getEmail() + "'],\n"
|
|
+ ")";
|
|
}
|
|
|
|
@Test
|
|
public void iterativeWorkflowTest_passThruAuthoring() throws Exception {
|
|
origin
|
|
.addSimpleChange(0)
|
|
.setAuthor(ORIGINAL_AUTHOR)
|
|
.addSimpleChange(1)
|
|
.setAuthor(NOT_WHITELISTED_ORIGINAL_AUTHOR)
|
|
.addSimpleChange(2);
|
|
|
|
passThruAuthoring();
|
|
|
|
iterativeWorkflow("0").run(workdir, ImmutableList.of(HEAD));
|
|
|
|
assertThat(destination.processed).hasSize(2);
|
|
|
|
assertThat(destination.processed.get(0).getAuthor()).isEqualTo(ORIGINAL_AUTHOR);
|
|
assertThat(destination.processed.get(1).getAuthor()).isEqualTo(NOT_WHITELISTED_ORIGINAL_AUTHOR);
|
|
}
|
|
|
|
private void passThruAuthoring() {
|
|
authoring = "authoring.pass_thru('" + DEFAULT_AUTHOR + "')";
|
|
}
|
|
|
|
@Test
|
|
public void iterativeWorkflowConfirmationHandlingTest() throws Exception {
|
|
for (int timestamp = 0; timestamp < 10; timestamp++) {
|
|
origin.addSimpleChange(timestamp);
|
|
}
|
|
|
|
console()
|
|
.respondYes()
|
|
.respondNo();
|
|
RecordsProcessCallDestination programmableDestination = new RecordsProcessCallDestination(
|
|
ImmutableList.of(
|
|
ImmutableList.of(), ImmutableList.of("some error"), ImmutableList.of("Another error")
|
|
)
|
|
);
|
|
|
|
options.testingOptions.destination = programmableDestination;
|
|
|
|
Workflow<?, ?> workflow = iterativeWorkflow(/*previousRef=*/"2");
|
|
|
|
ChangeRejectedException expected =
|
|
assertThrows(
|
|
ChangeRejectedException.class, () -> workflow.run(workdir, ImmutableList.of("9")));
|
|
assertThat(expected.getMessage())
|
|
.contains("Iterative workflow aborted by user after: Change 3 of 7 (5)");
|
|
assertThat(programmableDestination.processed).hasSize(3);
|
|
}
|
|
|
|
@Test
|
|
public void iterativeWorkflowNoPreviousRef() throws Exception {
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
Workflow<?, ?> workflow = iterativeWorkflow(/*previousRef=*/null);
|
|
CannotResolveRevisionException thrown =
|
|
assertThrows(
|
|
CannotResolveRevisionException.class,
|
|
() -> workflow.run(workdir, ImmutableList.of("0")));
|
|
assertThat(thrown)
|
|
.hasMessageThat()
|
|
.contains("Previous revision label DummyOrigin-RevId could not be found");
|
|
}
|
|
|
|
@Test
|
|
public void iterativeWorkflowEmptyChanges() throws Exception {
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
Workflow<?, ?> workflow = iterativeWorkflow(/*previousRef=*/"0");
|
|
EmptyChangeException thrown =
|
|
assertThrows(
|
|
EmptyChangeException.class, () -> workflow.run(workdir, ImmutableList.of("0")));
|
|
assertThat(thrown).hasMessageThat().contains("No new changes to import for resolved ref: 0");
|
|
}
|
|
|
|
@Test
|
|
public void iterativeSkipCommits() throws Exception {
|
|
origin.singleFileChange(0, "one", "file.txt", "a");
|
|
origin.singleFileChange(1, "two", "file.txt", "b");
|
|
origin.singleFileChange(2, "three", "file.txt", "b");
|
|
origin.singleFileChange(3, "four", "file.txt", "c");
|
|
transformations = ImmutableList.of();
|
|
destination.failOnEmptyChange = true;
|
|
Workflow<?, ?> workflow = iterativeWorkflow(/*previousRef=*/"0");
|
|
workflow.run(workdir, ImmutableList.of("3"));
|
|
assertThat(destination.processed.get(1).getContent("file.txt")).isEqualTo("c");
|
|
}
|
|
|
|
@Test
|
|
public void iterativeOnlyRunForMatchingOriginFiles() throws Exception {
|
|
checkItereativeOnlyRUnForMatchingOriginFiles("one", "three");
|
|
}
|
|
|
|
@Test
|
|
public void iterativeOnlyRunForMatchingOriginFiles_importNoopFlag() throws Exception {
|
|
options.workflowOptions.migrateNoopChanges = true;
|
|
checkItereativeOnlyRUnForMatchingOriginFiles("one", "two", "three", "four");
|
|
}
|
|
|
|
@Test
|
|
public void iterativeOnlyRunForMatchingOriginFiles_importNoopField() throws Exception {
|
|
migrateNoopChangesField = true;
|
|
checkItereativeOnlyRUnForMatchingOriginFiles("one", "two", "three", "four");
|
|
}
|
|
|
|
private void checkItereativeOnlyRUnForMatchingOriginFiles(String... changes)
|
|
throws IOException, ValidationException, RepoException {
|
|
origin.singleFileChange(0, "base", "file.txt", "a");
|
|
origin.singleFileChange(1, "one", "file.txt", "b");
|
|
origin.singleFileChange(2, "two", "excluded/two", "b");
|
|
origin.singleFileChange(3, "three", "copy.bara.sky", "");
|
|
origin.singleFileChange(4, "four", "copy.bara.sky", "");
|
|
transformations = ImmutableList.of();
|
|
Workflow<?, ?> workflow = iterativeWorkflow(/*previousRef=*/"0");
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
|
|
assertThat(destination.processed).hasSize(changes.length);
|
|
for (int i = 0; i < changes.length; i++) {
|
|
assertThat(destination.processed.get(i).getChangesSummary()).contains(changes[i]);
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void iterativeWithGroup() throws Exception {
|
|
transformations = ImmutableList.of();
|
|
origin.singleFileChange(0, "base1", "file.txt", "a");
|
|
origin.singleFileChange(1, "base2", "file.txt", "b");
|
|
iterativeWorkflow(/*previousRef=*/"0").run(workdir, ImmutableList.of("1"));
|
|
origin.singleFileChange(2, "pending1", "file.txt", "c");
|
|
origin.addRevisionToGroup(origin.resolve("HEAD"), "pending1");
|
|
origin.singleFileChange(3, "pending2", "file.txt", "d");
|
|
origin.addRevisionToGroup(origin.resolve("HEAD"), "pending1");
|
|
origin.singleFileChange(4, "other_pending", "file.txt", "f");
|
|
origin.addRevisionToGroup(origin.resolve("HEAD"), "pending2");
|
|
origin.singleFileChange(5, "pending3", "file.txt", "e");
|
|
origin.addRevisionToGroup(origin.resolve("HEAD"), "pending1");
|
|
iterativeWorkflow(/*previousRef=*/null).run(workdir, ImmutableList.of("3"));
|
|
|
|
assertThat(Lists.transform(destination.processed, input -> input.getOriginRef().asString()))
|
|
.isEqualTo(Lists.newArrayList("1", "2", "3"));
|
|
|
|
// Mark last two changes as pending.
|
|
destination.processed.get(1).pending = true;
|
|
destination.processed.get(2).pending = true;
|
|
|
|
iterativeWorkflow(/*previousRef=*/null).run(workdir, ImmutableList.of("5"));
|
|
|
|
// We migrate everything from pending1
|
|
assertThat(Lists.transform(destination.processed, input -> input.getOriginRef().asString()))
|
|
.isEqualTo(Lists.newArrayList("1", "2", "3", "2", "3", "5"));
|
|
}
|
|
|
|
@Test
|
|
public void testSquashFlagOverridesIterative() throws Exception {
|
|
origin.singleFileChange(0, "base", "file.txt", "a");
|
|
origin.singleFileChange(1, "one", "file.txt", "b");
|
|
origin.singleFileChange(2, "two", "excluded/two", "b");
|
|
origin.singleFileChange(3, "three", "copy.bara.sky", "");
|
|
origin.singleFileChange(4, "four", "copy.bara.sky", "");
|
|
transformations = ImmutableList.of();
|
|
|
|
// Run with --squash
|
|
options.general.squash = true;
|
|
Workflow<?, ?> workflow = iterativeWorkflow(/*previousRef=*/"0");
|
|
workflow.run(workdir, ImmutableList.of("2"));
|
|
assertThat(destination.processed).hasSize(1);
|
|
ProcessedChange change = Iterables.getOnlyElement(destination.processed);
|
|
assertThat(change.getChangesSummary()).contains("Project import generated by Copybara.\n");
|
|
|
|
|
|
// Regular run afterwards
|
|
workflow = iterativeWorkflow(/*previousRef=*/ null);
|
|
options.general.squash = false;
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
assertThat(destination.processed).hasSize(2);
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestWithGroup() throws Exception {
|
|
transformations = ImmutableList.of("metadata.squash_notes()");
|
|
origin.singleFileChange(0, "base1", "file.txt", "a");
|
|
origin.singleFileChange(1, "base2\n\n" + destination.getLabelNameWhenOrigin() + ": 1\n",
|
|
"file.txt", "b");
|
|
origin.singleFileChange(2, "pending1", "file.txt", "c");
|
|
origin.addRevisionToGroup(origin.resolve("HEAD"), "pending1");
|
|
skylarkWorkflow("default", CHANGE_REQUEST).run(workdir, ImmutableList.of("2"));
|
|
origin.singleFileChange(3, "base3\n\n" + destination.getLabelNameWhenOrigin() + ": 3\n",
|
|
"file.txt", "d");
|
|
|
|
assertThat(destination.processed).hasSize(1);
|
|
assertThat(destination.processed.get(0).getBaseline()).isEqualTo("1");
|
|
|
|
origin.singleFileChange(4, "pending2", "file.txt", "c");
|
|
origin.addRevisionToGroup(origin.resolve("HEAD"), "pending1");
|
|
skylarkWorkflow("default", CHANGE_REQUEST).run(workdir, ImmutableList.of("4"));
|
|
|
|
assertThat(destination.processed).hasSize(2);
|
|
assertThat(destination.processed.get(1).getBaseline()).isEqualTo("3");
|
|
}
|
|
|
|
@Test
|
|
public void squashWithGroup() throws Exception {
|
|
transformations = ImmutableList.of("metadata.squash_notes()");
|
|
origin.singleFileChange(0, "base1", "file.txt", "a");
|
|
origin.singleFileChange(1, "base2", "file.txt", "b");
|
|
options.workflowOptions.lastRevision = "0";
|
|
skylarkWorkflow("default", SQUASH).run(workdir, ImmutableList.of("1"));
|
|
origin.singleFileChange(2, "pending1", "file.txt", "c");
|
|
origin.addRevisionToGroup(origin.resolve("HEAD"), "pending1");
|
|
origin.singleFileChange(3, "pending2", "file.txt", "d");
|
|
origin.addRevisionToGroup(origin.resolve("HEAD"), "pending1");
|
|
origin.singleFileChange(4, "other_pending", "file.txt", "f");
|
|
origin.addRevisionToGroup(origin.resolve("HEAD"), "pending2");
|
|
origin.singleFileChange(5, "pending3", "file.txt", "e");
|
|
origin.addRevisionToGroup(origin.resolve("HEAD"), "pending1");
|
|
options.workflowOptions.lastRevision = null;
|
|
skylarkWorkflow("default", SQUASH).run(workdir, ImmutableList.of("3"));
|
|
|
|
assertThat(Iterables.getLast(destination.processed).getChangesSummary()).isEqualTo(
|
|
"Copybara import of the project:\n"
|
|
+ "\n"
|
|
+ " - 3 pending2 by Copybara <no-reply@google.com>\n"
|
|
+ " - 2 pending1 by Copybara <no-reply@google.com>\n");
|
|
assertThat(Lists.transform(destination.processed, input -> input.getOriginRef().asString()))
|
|
.isEqualTo(Lists.newArrayList("1", "3"));
|
|
|
|
// Mark last change as pending.
|
|
Iterables.getLast(destination.processed).pending = true;
|
|
|
|
skylarkWorkflow("default", SQUASH).run(workdir, ImmutableList.of("5"));
|
|
|
|
assertThat(Iterables.getLast(destination.processed).getChangesSummary()).isEqualTo(
|
|
"Copybara import of the project:\n"
|
|
+ "\n"
|
|
+ " - 5 pending3 by Copybara <no-reply@google.com>\n"
|
|
+ " - 3 pending2 by Copybara <no-reply@google.com>\n"
|
|
+ " - 2 pending1 by Copybara <no-reply@google.com>\n");
|
|
// We migrate everything from pending1
|
|
assertThat(Lists.transform(destination.processed, input -> input.getOriginRef().asString()))
|
|
.isEqualTo(Lists.newArrayList("1", "3", "5"));
|
|
}
|
|
|
|
@Test
|
|
public void emptyTransformList() throws Exception {
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
transformations = ImmutableList.of();
|
|
Workflow<?, ?> workflow = workflow();
|
|
workflow.run(workdir, ImmutableList.of("0"));
|
|
ProcessedChange change = Iterables.getOnlyElement(destination.processed);
|
|
assertThat(change.getContent("file.txt")).isEqualTo("0");
|
|
}
|
|
|
|
@Test
|
|
public void processIsCalledWithCurrentTimeIfTimestampNotInOrigin() throws Exception {
|
|
Workflow<?, ?> workflow = workflow();
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
|
|
ZonedDateTime timestamp = destination.processed.get(0).getTimestamp();
|
|
assertThat(timestamp.toInstant().getEpochSecond()).isEqualTo(42);
|
|
}
|
|
|
|
@Test
|
|
public void processIsCalledWithCorrectWorkdir() throws Exception {
|
|
Workflow<?, ?> workflow = workflow();
|
|
String head = resolveHead();
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
assertThat(Files.readAllLines(workdir.resolve("checkout/file.txt"), UTF_8))
|
|
.contains(PREFIX + head);
|
|
}
|
|
|
|
@Test
|
|
public void sendsOriginTimestampToDest() throws Exception {
|
|
Workflow<?, ?> workflow = workflow();
|
|
origin.addSimpleChange(/*timestamp*/ 42918273);
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
assertThat(destination.processed).hasSize(1);
|
|
assertThat(destination.processed.get(0).getTimestamp().toInstant().getEpochSecond())
|
|
.isEqualTo(42918273);
|
|
}
|
|
|
|
@Test
|
|
public void usesDefaultAuthorForSquash() throws Exception {
|
|
// Squash always sets the default author for the commit but not in the release notes
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
options.workflowOptions.lastRevision = resolveHead();
|
|
origin.addSimpleChange(/*timestamp*/ 2);
|
|
origin.addSimpleChange(/*timestamp*/ 3);
|
|
includeReleaseNotes = true;
|
|
|
|
Workflow<?, ?> workflow = workflow();
|
|
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
assertThat(destination.processed).hasSize(1);
|
|
ProcessedChange change = Iterables.getOnlyElement(destination.processed);
|
|
assertThat(change.getChangesSummary()).contains(DEFAULT_AUTHOR.toString());
|
|
assertThat(change.getAuthor()).isEqualTo(DEFAULT_AUTHOR);
|
|
}
|
|
|
|
@Test
|
|
public void migrationIdentityConstant() throws Exception {
|
|
// Squash always sets the default author for the commit but not in the release notes
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
String before = workflow().getMigrationIdentity(origin.resolve(HEAD), transformWork);
|
|
origin.addSimpleChange(/*timestamp*/ 2);
|
|
origin.addSimpleChange(/*timestamp*/ 3);
|
|
String after = workflow().getMigrationIdentity(origin.resolve(HEAD), transformWork);
|
|
|
|
// If we use 'HEAD' as reference it is constant
|
|
assertThat(before).isEqualTo(after);
|
|
|
|
String nonConstant = workflow().getMigrationIdentity(origin.resolve("3"), transformWork);
|
|
|
|
// But if we use direct reference (3 instead of 'HEAD') it changes since we cannot
|
|
// find the context reference.
|
|
assertThat(after).isNotEqualTo(nonConstant);
|
|
}
|
|
|
|
@Test
|
|
public void migrationIdentityWithUser() throws Exception {
|
|
// Squash always sets the default author for the commit but not in the release notes
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
String withUser = workflow().getMigrationIdentity(origin.resolve(HEAD), transformWork);
|
|
|
|
options.workflowOptions.workflowIdentityUser = StandardSystemProperty.USER_NAME.value();
|
|
|
|
assertThat(withUser).isEqualTo(workflow().getMigrationIdentity(origin.resolve(HEAD),
|
|
transformWork));
|
|
|
|
options.workflowOptions.workflowIdentityUser = "TEST";
|
|
|
|
String withOtherUser = workflow().getMigrationIdentity(origin.resolve(HEAD), transformWork);
|
|
|
|
assertThat(withOtherUser).isNotEqualTo(withUser);
|
|
}
|
|
|
|
@Test
|
|
public void testSquashAlreadyMigrated() throws Exception {
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
String oldRef = resolveHead();
|
|
origin.addSimpleChange(/*timestamp*/ 2);
|
|
origin.addSimpleChange(/*timestamp*/ 3);
|
|
includeReleaseNotes = true;
|
|
|
|
options.setForce(true);
|
|
skylarkWorkflow("default", SQUASH).run(workdir, ImmutableList.of(HEAD));
|
|
options.setForce(false); // Disable force so that we get an error
|
|
EmptyChangeException thrown =
|
|
assertThrows(
|
|
EmptyChangeException.class,
|
|
() -> skylarkWorkflow("default", SQUASH).run(workdir, ImmutableList.of(oldRef)));
|
|
assertThat(thrown).hasMessageThat().contains("'0' has been already migrated");
|
|
}
|
|
|
|
@Test
|
|
public void testSquashAlreadyMigratedSameChange() throws Exception {
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
skylarkWorkflow("default", SQUASH).run(workdir, ImmutableList.of(HEAD));
|
|
options.setForce(false); // Disable force so that we get an error
|
|
EmptyChangeException thrown =
|
|
assertThrows(
|
|
EmptyChangeException.class,
|
|
() -> skylarkWorkflow("default", SQUASH).run(workdir, ImmutableList.of(HEAD)));
|
|
assertThat(thrown).hasMessageThat().contains("'0' has been already migrated");
|
|
}
|
|
|
|
@Test
|
|
public void testSquashLastRevDoesntExist() throws Exception {
|
|
options.setForce(false); // Disable force so that we get an error
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
options.workflowOptions.lastRevision = "42";
|
|
|
|
Workflow<?, ?> workflow = workflow();
|
|
|
|
ValidationException thrown =
|
|
assertThrows(
|
|
ValidationException.class, () -> workflow.run(workdir, ImmutableList.of(HEAD)));
|
|
assertThat(thrown)
|
|
.hasMessageThat()
|
|
.contains(
|
|
"Cannot find last imported revision."
|
|
+ " Use --force if you really want to proceed with the migration");
|
|
}
|
|
|
|
@Test
|
|
public void testSquashAlreadyMigratedWithForce() throws Exception {
|
|
origin.addSimpleChange(/*timestamp*/ 1);
|
|
String oldRef = resolveHead();
|
|
origin.addSimpleChange(/*timestamp*/ 2);
|
|
origin.addSimpleChange(/*timestamp*/ 3);
|
|
includeReleaseNotes = true;
|
|
|
|
options.setForce(true);
|
|
workflow().run(workdir, ImmutableList.of(HEAD));
|
|
WriterContext writerContext = new WriterContext("piper_to_github", "TEST", false,
|
|
new DummyRevision("test"), Glob.ALL_FILES.roots());
|
|
assertThat(
|
|
destination
|
|
.newWriter(writerContext)
|
|
.getDestinationStatus(Glob.ALL_FILES, origin.getLabelName())
|
|
.getBaseline())
|
|
.isEqualTo("3");
|
|
workflow().run(workdir, ImmutableList.of(oldRef));
|
|
assertThat(
|
|
destination
|
|
.newWriter(writerContext)
|
|
.getDestinationStatus(Glob.ALL_FILES, origin.getLabelName())
|
|
.getBaseline())
|
|
.isEqualTo("0");
|
|
}
|
|
|
|
@Test
|
|
public void runsTransformations() throws Exception {
|
|
workflow().run(workdir, ImmutableList.of(HEAD));
|
|
assertThat(destination.processed).hasSize(1);
|
|
assertThat(destination.processed.get(0).numFiles()).isEqualTo(1);
|
|
assertThat(destination.processed.get(0).getContent("file.txt")).isEqualTo(PREFIX + "0");
|
|
}
|
|
|
|
@Test
|
|
public void invalidExcludedOriginPath() throws Exception {
|
|
prepareOriginExcludes("a");
|
|
String outsideFolder = "../../file";
|
|
Path file = workdir.resolve(outsideFolder);
|
|
Files.createDirectories(file.getParent());
|
|
Files.write(file, new byte[]{});
|
|
|
|
originFiles = "glob(['" + outsideFolder + "'])";
|
|
|
|
ValidationException e =
|
|
assertThrows(
|
|
ValidationException.class, () -> workflow().run(workdir, ImmutableList.of(HEAD)));
|
|
console()
|
|
.assertThat()
|
|
.onceInLog(MessageType.ERROR, "(\n|.)*path has unexpected [.] or [.][.] components(\n|.)*");
|
|
assertThatPath(workdir).containsFiles(outsideFolder);
|
|
}
|
|
|
|
@Test
|
|
public void invalidExcludedOriginGlob() throws Exception {
|
|
prepareOriginExcludes("a");
|
|
originFiles = "glob(['{'])";
|
|
|
|
ValidationException e =
|
|
assertThrows(
|
|
ValidationException.class, () -> workflow().run(workdir, ImmutableList.of(HEAD)));
|
|
console()
|
|
.assertThat()
|
|
.onceInLog(
|
|
MessageType.ERROR, "(\n|.)*Cannot create a glob from: include='\\[\\{\\]' (\n|.)*");
|
|
}
|
|
|
|
@Test
|
|
public void excludedOriginPathDoesntExcludeDirectories() throws Exception {
|
|
// Ignore transforms that have no effect
|
|
options.workflowOptions.ignoreNoop = true;
|
|
|
|
originFiles = "glob(['**'], exclude = ['folder'])";
|
|
Workflow<?, ?> workflow = workflow();
|
|
prepareOriginExcludes("a");
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
assertThatPath(workdir.resolve("checkout"))
|
|
.containsFiles("folder/file.txt", "folder2/file.txt");
|
|
}
|
|
|
|
@Test
|
|
public void excludedOriginPathRecursive() throws Exception {
|
|
originFiles = "glob(['**'], exclude = ['folder/**'])";
|
|
transformations = ImmutableList.of();
|
|
Workflow<?, ?> workflow = workflow();
|
|
prepareOriginExcludes("a");
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
|
|
assertThatPath(workdir.resolve("checkout"))
|
|
.containsFiles("folder", "folder2")
|
|
.containsNoFiles(
|
|
"folder/file.txt", "folder/subfolder/file.txt", "folder/subfolder/file.java");
|
|
}
|
|
|
|
@Test
|
|
public void testNoopGroup() throws Exception {
|
|
options.workflowOptions.ignoreNoop = false;
|
|
transformations = ImmutableList.of(""
|
|
+ " core.transform("
|
|
+ " transformations = ["
|
|
+ " core.replace("
|
|
+ " before = 'foo',"
|
|
+ " after = 'bar',"
|
|
+ " )"
|
|
+ " ],"
|
|
+ " ignore_noop = True,"
|
|
+ " )"
|
|
);
|
|
workflow().run(workdir, ImmutableList.of(HEAD));
|
|
console().assertThat().onceInLog(MessageType.WARNING,
|
|
".*NOOP: Transformation.*");
|
|
}
|
|
|
|
@Test
|
|
public void testNoopGroupDefault() throws Exception {
|
|
options.workflowOptions.ignoreNoop = false;
|
|
transformations = ImmutableList.of(""
|
|
+ " core.transform("
|
|
+ " transformations = ["
|
|
+ " core.replace("
|
|
+ " before = 'foo',"
|
|
+ " after = 'bar',"
|
|
+ " )"
|
|
+ " ],"
|
|
+ " )"
|
|
);
|
|
VoidOperationException e =
|
|
assertThrows(
|
|
VoidOperationException.class, () -> workflow().run(workdir, ImmutableList.of(HEAD)));
|
|
assertThat(e.getMessage().contains("Use --ignore-noop if you want to ignore this error"))
|
|
.isTrue();
|
|
}
|
|
|
|
@Test
|
|
public void excludedOriginRecursiveByType() throws Exception {
|
|
originFiles = "glob(['**'], exclude = ['folder/**/*.java'])";
|
|
transformations = ImmutableList.of();
|
|
Workflow<?, ?> workflow = workflow();
|
|
prepareOriginExcludes("a");
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
|
|
assertThatPath(workdir.resolve("checkout"))
|
|
.containsFiles("folder", "folder2", "folder/subfolder", "folder/subfolder/file.txt")
|
|
.containsNoFiles("folder/subfolder/file.java");
|
|
}
|
|
|
|
@Test
|
|
public void skipNotesForExcludedFiles() throws Exception {
|
|
originFiles = "glob(['**'], exclude = ['foo'])";
|
|
transformations = ImmutableList.of("metadata.squash_notes()");
|
|
FileSystem fileSystem = Jimfs.newFileSystem();
|
|
Path one = Files.createDirectories(fileSystem.getPath("one"));
|
|
Path two = Files.createDirectories(fileSystem.getPath("two"));
|
|
Path three = Files.createDirectories(fileSystem.getPath("three"));
|
|
Path four = Files.createDirectories(fileSystem.getPath("four"));
|
|
|
|
touchFile(one, "foo", "");
|
|
touchFile(one, "bar", "");
|
|
origin.addChange(1, one, "foo and bar", /*matchesGlob=*/true);
|
|
|
|
touchFile(two, "foo", "a");
|
|
touchFile(two, "bar", "");
|
|
origin.addChange(2, two, "only foo", /*matchesGlob=*/true);
|
|
|
|
touchFile(three, "foo", "a");
|
|
touchFile(three, "bar", "a");
|
|
origin.addChange(3, three, "only bar", /*matchesGlob=*/true);
|
|
|
|
DummyRevision bar = origin.resolve(HEAD);
|
|
|
|
touchFile(four, "foo", "b");
|
|
touchFile(four, "bar", "a");
|
|
origin.addChange(4, four, "foo again", /*matchesGlob=*/true);
|
|
|
|
skylarkWorkflow("default", SQUASH).run(workdir, ImmutableList.of(HEAD));
|
|
|
|
// We skip changes that only touch foo.
|
|
assertThat(Iterables.getLast(destination.processed).getChangesSummary())
|
|
.isEqualTo("Copybara import of the project:\n"
|
|
+ "\n"
|
|
+ " - 2 only bar by Copybara <no-reply@google.com>\n"
|
|
+ " - 0 foo and bar by Copybara <no-reply@google.com>\n");
|
|
// We use as reference for the destination label the last change that affects
|
|
// origin_files.
|
|
assertThat(Iterables.getLast(destination.processed).getOriginRef().asString())
|
|
.isEqualTo(bar.asString());
|
|
}
|
|
|
|
@Test
|
|
public void originFilesNothingRemovedNoopNothingInLog() throws Exception {
|
|
originFiles = "glob(['**'], exclude = ['I_dont_exist'])";
|
|
transformations = ImmutableList.of();
|
|
Workflow<?, ?> workflow = workflow();
|
|
prepareOriginExcludes("a");
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
console().assertThat()
|
|
.timesInLog(0, MessageType.INFO, "Removed .* files from workdir");
|
|
}
|
|
|
|
@Test
|
|
public void excludeOriginPathIterative() throws Exception {
|
|
originFiles = "glob(['**'], exclude = ['folder/**/*.java'])";
|
|
transformations = ImmutableList.of();
|
|
prepareOriginExcludes("a");
|
|
Workflow<?, ?> workflow = iterativeWorkflow(resolveHead());
|
|
prepareOriginExcludes("b");
|
|
prepareOriginExcludes("c");
|
|
prepareOriginExcludes("d");
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
for (ProcessedChange processedChange : destination.processed) {
|
|
for (String path : ImmutableList.of("folder/file.txt",
|
|
"folder2/file.txt",
|
|
"folder2/subfolder/file.java",
|
|
"folder/subfolder/file.txt")) {
|
|
assertThat(processedChange.filePresent(path)).isTrue();
|
|
}
|
|
assertThat(processedChange.filePresent("folder/subfolder/file.java")).isFalse();
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void testOriginExcludesToString() throws Exception {
|
|
originFiles = "glob(['**'], exclude = ['foo/**/bar.htm'])";
|
|
String string = workflow().toString();
|
|
assertThat(string).contains("foo/**/bar.htm");
|
|
}
|
|
|
|
@Test
|
|
public void testDestinationExcludesToString() throws Exception {
|
|
destinationFiles = "glob(['**'], exclude = ['foo/**/bar.htm'])";
|
|
String string = workflow().toString();
|
|
assertThat(string).contains("foo/**/bar.htm");
|
|
}
|
|
|
|
@Test
|
|
public void testDestinationFilesPassedToDestination_iterative() throws Exception {
|
|
destinationFiles = "glob(['**'], exclude = ['foo', 'bar/**'])";
|
|
origin.addSimpleChange(/*timestamp*/ 42);
|
|
Workflow<?, ?> workflow = iterativeWorkflow(resolveHead());
|
|
origin.addSimpleChange(/*timestamp*/ 4242);
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
|
|
assertThat(destination.processed).hasSize(1);
|
|
|
|
PathMatcher matcher = destination.processed.get(0).getDestinationFiles()
|
|
.relativeTo(workdir);
|
|
assertThat(matcher.matches(workdir.resolve("foo"))).isFalse();
|
|
assertThat(matcher.matches(workdir.resolve("foo/indir"))).isTrue();
|
|
assertThat(matcher.matches(workdir.resolve("bar/indir"))).isFalse();
|
|
}
|
|
|
|
private String resolveHead() throws RepoException, CannotResolveRevisionException {
|
|
return origin.resolve(HEAD).asString();
|
|
}
|
|
|
|
@Test
|
|
public void testDestinationFilesPassedToDestination_squash() throws Exception {
|
|
destinationFiles = "glob(['**'], exclude = ['foo', 'bar/**'])";
|
|
workflow().run(workdir, ImmutableList.of(HEAD));
|
|
|
|
assertThat(destination.processed).hasSize(1);
|
|
|
|
PathMatcher matcher = destination.processed.get(0).getDestinationFiles()
|
|
.relativeTo(workdir);
|
|
assertThat(matcher.matches(workdir.resolve("foo"))).isFalse();
|
|
assertThat(matcher.matches(workdir.resolve("foo/indir"))).isTrue();
|
|
assertThat(matcher.matches(workdir.resolve("bar/indir"))).isFalse();
|
|
}
|
|
|
|
@Test
|
|
public void invalidLastRevFlagGivesClearError() throws Exception {
|
|
origin.addSimpleChange(/*timestamp*/ 42);
|
|
|
|
Workflow<?, ?> workflow = iterativeWorkflow("deadbeef");
|
|
CannotResolveRevisionException thrown =
|
|
assertThrows(
|
|
CannotResolveRevisionException.class,
|
|
() -> workflow.run(workdir, ImmutableList.of(HEAD)));
|
|
assertThat(thrown)
|
|
.hasMessageThat()
|
|
.contains(
|
|
"Could not resolve --last-rev flag. Please make sure it exists in the origin:"
|
|
+ " deadbeef");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequest_defaultAuthoring() throws Exception {
|
|
origin
|
|
.addSimpleChange(0, "One Change\n" + destination.getLabelNameWhenOrigin() + "=42")
|
|
.addSimpleChange(1, "Second Change");
|
|
|
|
Workflow<?, ?> workflow = changeRequestWorkflow(null);
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
ProcessedChange change = destination.processed.get(0);
|
|
|
|
assertThat(change.getBaseline()).isEqualTo("42");
|
|
assertThat(change.getAuthor()).isEqualTo(DEFAULT_AUTHOR);
|
|
console().assertThat()
|
|
.onceInLog(MessageType.PROGRESS, ".*Checking that the transformations can be reverted");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequest_customRevIdLabel() throws Exception {
|
|
origin
|
|
.addSimpleChange(0, "One Change\n\nCUSTOM_REV_ID=42")
|
|
.addSimpleChange(1, "Second Change");
|
|
|
|
extraWorkflowFields = ImmutableList.of("experimental_custom_rev_id = \"CUSTOM_REV_ID\"");
|
|
setRevId = false;
|
|
Workflow<?, ?> workflow = changeRequestWorkflow(null);
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
ProcessedChange change = destination.processed.get(0);
|
|
|
|
assertThat(change.getBaseline()).isEqualTo("42");
|
|
assertThat(change.getAuthor()).isEqualTo(DEFAULT_AUTHOR);
|
|
console().assertThat()
|
|
.onceInLog(MessageType.PROGRESS, ".*Checking that the transformations can be reverted");
|
|
|
|
}
|
|
|
|
@Test
|
|
public void changeRequest_customRevIdLabelWithSetRevId() throws Exception {
|
|
extraWorkflowFields = ImmutableList.of("experimental_custom_rev_id = \"CUSTOM_REV_ID\"");
|
|
setRevId = true;
|
|
ValidationException e =
|
|
assertThrows(ValidationException.class, () -> changeRequestWorkflow(null));
|
|
assertThat(e.getMessage())
|
|
.containsMatch(
|
|
"experimental_custom_rev_id is not allowed to be used in CHANGE_REQUEST mode if "
|
|
+ "set_rev_id is set to true. experimental_custom_rev_id is used");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequest_sot() throws Exception {
|
|
origin
|
|
.addSimpleChange(0, "Base Change")
|
|
.addSimpleChange(1, "First Change")
|
|
.addSimpleChange(2, "Second Change")
|
|
.addSimpleChange(3, "Third Change");
|
|
|
|
Workflow<?, ?> workflow = iterativeWorkflow("0");
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
ProcessedChange change = destination.processed.get(2);
|
|
|
|
assertThat(change.getBaseline()).isNull();
|
|
assertThat(change.getChangesSummary()).isEqualTo("Third Change");
|
|
|
|
Workflow<?, ?> w = skylarkWorkflow("default", WorkflowMode.CHANGE_REQUEST_FROM_SOT);
|
|
|
|
w.run(workdir, ImmutableList.of("2"));
|
|
|
|
change = destination.processed.get(destination.processed.size() - 1);
|
|
assertThat(change.getChangesSummary()).isEqualTo("Second Change");
|
|
assertThat(change.getBaseline()).isEqualTo("1");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequest_sot_ahead_sot() throws Exception {
|
|
checkChangeRequest_sot_ahead_sot();
|
|
}
|
|
|
|
@Test
|
|
public void changeRequest_sot_ahead_sot_retries() throws Exception {
|
|
options.workflowOptions.changeRequestFromSotRetry = Lists.newArrayList(1, 1, 1, 1, 1);
|
|
checkChangeRequest_sot_ahead_sot();
|
|
console().assertThat().timesInLog(5, MessageType.WARNING,
|
|
".*Couldn't find a change in the destination.*Retrying in.*");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequest_sot_no_origin_files_match() throws Exception {
|
|
options.workflowOptions.changeRequestFromSotLimit = 1;
|
|
origin
|
|
.addSimpleChange(0, "Base Change")
|
|
.addSimpleChange(1, "First Change")
|
|
.addSimpleChange(2, "Second Change");
|
|
|
|
Workflow<?, ?> workflow = iterativeWorkflow("0");
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
ProcessedChange change = destination.processed.get(0);
|
|
|
|
assertThat(change.getBaseline()).isNull();
|
|
assertThat(change.getChangesSummary()).isEqualTo("First Change");
|
|
|
|
origin.singleFileChange(3, "pending", "I_dont_exist/file.txt", "content");
|
|
originFiles = "glob(['I_dont_exist/**'])";
|
|
Workflow<?, ?> w = skylarkWorkflow("default", WorkflowMode.CHANGE_REQUEST_FROM_SOT);
|
|
|
|
ValidationException e =
|
|
assertThrows(ValidationException.class, () -> w.run(workdir, ImmutableList.of("3")));
|
|
assertThat(e)
|
|
.hasMessageThat()
|
|
.contains(
|
|
"Couldn't find any parent change for 3"
|
|
+ " and origin_files = glob(include = [\"I_dont_exist/**\"])");
|
|
}
|
|
|
|
/**
|
|
* Regression test that checks that we reuse the same writer in dry-run mode for multiple
|
|
* invocations inside the same migration so that state is kept.
|
|
*/
|
|
@Test
|
|
public void testDryRunWithLocalGitPath() throws Exception {
|
|
Path originPath = Files.createTempDirectory("origin");
|
|
Path destinationPath = Files.createTempDirectory("destination");
|
|
GitRepository origin = GitRepository.newRepo(/*verbose*/ true, originPath, getGitEnv()).init();
|
|
GitRepository destination = GitRepository.newBareRepo(destinationPath, getGitEnv(),
|
|
/*verbose=*/true, DEFAULT_TIMEOUT, /*noVerify=*/ false).init();
|
|
|
|
String config = "core.workflow("
|
|
+ " name = 'default',\n"
|
|
+ " origin = git.origin(\n"
|
|
+ " url = 'file://" + origin.getWorkTree() + "',\n"
|
|
+ " ref = 'master',\n"
|
|
+ " ),\n"
|
|
+ " destination = git.destination("
|
|
+ " url = 'file://" + destination.getGitDir() + "',\n"
|
|
+ " ),\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " mode = 'SQUASH',\n"
|
|
+ ")\n";
|
|
|
|
addGitFile(originPath, origin, "foo.txt", "not important");
|
|
commit(origin, "baseline\n\nOrigin-Label: 1234567");
|
|
|
|
options.setWorkdirToRealTempDir();
|
|
// Pass custom HOME directory so that we run an hermetic test and we
|
|
// can add custom configuration to $HOME/.gitconfig.
|
|
options.setEnvironment(GitTestUtil.getGitEnv().getEnvironment());
|
|
options.setHomeDir(Files.createTempDirectory("home").toString());
|
|
options.gitDestination.committerName = "Foo";
|
|
options.gitDestination.committerEmail = "foo@foo.com";
|
|
options.workflowOptions.initHistory = true;
|
|
|
|
loadConfig(config).getMigration("default")
|
|
.run(Files.createTempDirectory("workdir"), ImmutableList.of());
|
|
|
|
// Now run again with force and no changes so that it uses the default migrator (The affected
|
|
// path
|
|
|
|
options.gitDestination.localRepoPath = Files.createTempDirectory("temp").toString();
|
|
options.workflowOptions.initHistory = false;
|
|
options.general.dryRunMode = true;
|
|
options.setForce(true);
|
|
|
|
EmptyChangeException e =
|
|
assertThrows(
|
|
EmptyChangeException.class,
|
|
() ->
|
|
loadConfig(config)
|
|
.getMigration("default")
|
|
.run(Files.createTempDirectory("workdir"), ImmutableList.of()));
|
|
assertThat(e)
|
|
.hasMessageThat()
|
|
.contains("Migration of the revision resulted in an empty change");
|
|
assertThat(e)
|
|
.hasMessageThat()
|
|
.contains(destination.parseRef("HEAD"));
|
|
}
|
|
|
|
private void checkChangeRequest_sot_ahead_sot()
|
|
throws IOException, ValidationException, RepoException {
|
|
options.workflowOptions.changeRequestFromSotLimit = 1;
|
|
origin
|
|
.addSimpleChange(0, "Base Change")
|
|
.addSimpleChange(1, "First Change")
|
|
.addSimpleChange(2, "Second Change")
|
|
.addSimpleChange(3, "Third Change");
|
|
|
|
Workflow<?, ?> workflow = iterativeWorkflow("0");
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
ProcessedChange change = destination.processed.get(0);
|
|
|
|
assertThat(change.getBaseline()).isNull();
|
|
assertThat(change.getChangesSummary()).isEqualTo("First Change");
|
|
|
|
Workflow<?, ?> w = skylarkWorkflow("default", WorkflowMode.CHANGE_REQUEST_FROM_SOT);
|
|
|
|
try {
|
|
w.run(workdir, ImmutableList.of("3"));
|
|
fail();
|
|
} catch (ValidationException e) {
|
|
assertThat(e).hasMessageThat().contains("Make sure to sync the submitted changes");
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void changeRequest_sot_old_baseline() throws Exception {
|
|
options.workflowOptions.changeRequestFromSotLimit = 2;
|
|
origin
|
|
.addSimpleChange(0, "Base Change")
|
|
.addSimpleChange(1, "First Change")
|
|
.addSimpleChange(2, "Second Change")
|
|
.addSimpleChange(3, "Third Change");
|
|
|
|
Workflow<?, ?> workflow = iterativeWorkflow("0");
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
ProcessedChange change = destination.processed.get(0);
|
|
|
|
assertThat(change.getBaseline()).isNull();
|
|
assertThat(change.getChangesSummary()).isEqualTo("First Change");
|
|
|
|
Workflow<?, ?> w = skylarkWorkflow("default", WorkflowMode.CHANGE_REQUEST_FROM_SOT);
|
|
|
|
w.run(workdir, ImmutableList.of("3"));
|
|
change = destination.processed.get(destination.processed.size() - 1);
|
|
assertThat(change.getChangesSummary()).isEqualTo("Third Change");
|
|
assertThat(change.getBaseline()).isEqualTo("1");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequest_passThruAuthoring() throws Exception {
|
|
origin
|
|
.addSimpleChange(0, "One Change\n" + destination.getLabelNameWhenOrigin() + "=42")
|
|
.addSimpleChange(1, "Second Change");
|
|
|
|
passThruAuthoring();
|
|
Workflow<?, ?> workflow = changeRequestWorkflow(null);
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
|
|
assertThat(destination.processed.get(0).getAuthor()).isEqualTo(ORIGINAL_AUTHOR);
|
|
}
|
|
|
|
@Test
|
|
public void changeRequest_whitelistAuthoring() throws Exception {
|
|
origin
|
|
.setAuthor(NOT_WHITELISTED_ORIGINAL_AUTHOR)
|
|
.addSimpleChange(0, "One Change\n" + destination.getLabelNameWhenOrigin() + "=42")
|
|
.addSimpleChange(1, "Second Change");
|
|
|
|
whiteListAuthoring();
|
|
|
|
changeRequestWorkflow(null).run(workdir, ImmutableList.of("1"));
|
|
|
|
assertThat(destination.processed.get(0).getAuthor()).isEqualTo(DEFAULT_AUTHOR);
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestManualBaseline() throws Exception {
|
|
origin
|
|
.addSimpleChange(0, "One Change\n" + destination.getLabelNameWhenOrigin() + "=42")
|
|
.addSimpleChange(1, "Second Change");
|
|
Workflow<?, ?> workflow = changeRequestWorkflow("24");
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
assertThat(destination.processed.get(0).getBaseline()).isEqualTo("24");
|
|
console().assertThat()
|
|
.onceInLog(MessageType.PROGRESS, ".*Checking that the transformations can be reverted");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestChanges() throws Exception {
|
|
origin
|
|
.addSimpleChange(0, "One Change\n" + destination.getLabelNameWhenOrigin() + "=42")
|
|
.addSimpleChange(1, "Second Change")
|
|
.addSimpleChange(2, "Third Change");
|
|
includeReleaseNotes = true;
|
|
Workflow<?, ?> workflow = skylarkWorkflow("default", WorkflowMode.CHANGE_REQUEST);
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
assertThat(destination.processed).hasSize(1);
|
|
assertThat(destination.processed.get(0).getBaseline()).isEqualTo("42");
|
|
assertThat(destination.processed.get(0).getChangesSummary()).isEqualTo(""
|
|
+ "Copybara import of the project:\n"
|
|
+ "\n"
|
|
+ " - 2 Third Change by Copybara <no-reply@google.com>\n"
|
|
+ " - 1 Second Change by Copybara <no-reply@google.com>\n");
|
|
console().assertThat()
|
|
.onceInLog(MessageType.PROGRESS, ".*Checking that the transformations can be reverted");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestSetRevIdDisabled() throws Exception {
|
|
origin
|
|
.addSimpleChange(0, "One Change\n" + destination.getLabelNameWhenOrigin() + "=42")
|
|
.addSimpleChange(1, "Second Change")
|
|
.addSimpleChange(2, "Third Change");
|
|
setRevId = false;
|
|
Workflow<?, ?> workflow = skylarkWorkflow("default", WorkflowMode.CHANGE_REQUEST);
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
assertThat(destination.processed).hasSize(1);
|
|
assertThat(destination.processed.get(0).getBaseline()).isEqualTo("42");
|
|
assertThat(destination.processed.get(0).isSetRevId()).isFalse();
|
|
console().assertThat()
|
|
.onceInLog(MessageType.PROGRESS, ".*Checking that the transformations can be reverted");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestChanges_LastChange() throws Exception {
|
|
origin
|
|
.addSimpleChange(0, "One Change\n" + destination.getLabelNameWhenOrigin() + "=42")
|
|
.addSimpleChange(1, "Second Change")
|
|
.addSimpleChange(2, "Third Change");
|
|
transformations = ImmutableList.of("metadata.use_last_change()");
|
|
Workflow<?, ?> workflow = skylarkWorkflow("default", WorkflowMode.CHANGE_REQUEST);
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
assertThat(destination.processed).hasSize(1);
|
|
assertThat(destination.processed.get(0).getBaseline()).isEqualTo("42");
|
|
assertThat(destination.processed.get(0).getChangesSummary()).isEqualTo("Third Change");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestSmartPrune() throws Exception {
|
|
smartPrune = true;
|
|
ImmutableList<DiffFile> diffFiles = checkChangeRequestSmartPrune();
|
|
ImmutableMap<String, DiffFile> byName = Maps.uniqueIndex(diffFiles, DiffFile::getName);
|
|
assertThat(byName.size()).isEqualTo(3);
|
|
assertThat(byName.get("folder/deleted.txt").getOperation()).isEqualTo(DELETE);
|
|
assertThat(byName.get("folder/modified.txt").getOperation()).isEqualTo(MODIFIED);
|
|
assertThat(byName.get("folder/added.txt").getOperation()).isEqualTo(ADD);
|
|
assertThat(byName.get("folder/unmodified.txt")).isNull();
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestSmartPrune_disabledFlag() throws Exception {
|
|
smartPrune = true;
|
|
// This flag wins
|
|
options.workflowOptions.noSmartPrune = true;
|
|
ImmutableList<DiffFile> diffFiles = checkChangeRequestSmartPrune();
|
|
assertThat(diffFiles).isNull();
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestSmartPrune_disabled() throws Exception {
|
|
ImmutableList<DiffFile> diffFiles = checkChangeRequestSmartPrune();
|
|
assertThat(diffFiles).isNull();
|
|
}
|
|
|
|
@Test
|
|
public void smartPruneForDifferentWorkflowMode() throws Exception {
|
|
smartPrune = true;
|
|
ValidationException e =
|
|
assertThrows(
|
|
ValidationException.class, () -> skylarkWorkflow("default", WorkflowMode.SQUASH));
|
|
console()
|
|
.assertThat()
|
|
.onceInLog(
|
|
MessageType.ERROR,
|
|
".*'smart_prune = True' is only supported for CHANGE_REQUEST mode.*");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestSmartPrune_manualBaseline() throws Exception {
|
|
smartPrune = true;
|
|
// For now we don't support smart_prune with the flag since the flag refers to the destination
|
|
// baseline and for smart_prune we need the origin revision. We might revisit this if it
|
|
// if requested by users
|
|
options.workflowOptions.changeBaseline = "42";
|
|
ValidationException e =
|
|
assertThrows(ValidationException.class, () -> checkChangeRequestSmartPrune());
|
|
assertThat(e)
|
|
.hasMessageThat()
|
|
.contains("smart_prune is not compatible with --change-request-parent");
|
|
}
|
|
|
|
private ImmutableList<DiffFile> checkChangeRequestSmartPrune()
|
|
throws IOException, ValidationException, RepoException {
|
|
FileSystem fileSystem = Jimfs.newFileSystem();
|
|
Path base1 = Files.createTempDirectory(fileSystem.getPath("/"), "base");
|
|
|
|
writeFile(base1, "excluded/file.txt", "EXCLUDED FOO: Shouldn't be seen");
|
|
writeFile(base1, "folder/deleted.txt", "");
|
|
writeFile(base1, "folder/unmodified.txt", "");
|
|
writeFile(base1, "folder/modified.txt", "foo");
|
|
origin.addChange(0, base1,
|
|
String.format("One Change\n\n%s=42", destination.getLabelNameWhenOrigin()),
|
|
/*matchesGlob=*/ true);
|
|
Path base2 = Files.createTempDirectory(fileSystem.getPath("/"), "base");
|
|
FileUtil.copyFilesRecursively(base1, base2, CopySymlinkStrategy.FAIL_OUTSIDE_SYMLINKS);
|
|
|
|
Files.delete(base2.resolve("folder/deleted.txt"));
|
|
origin.addChange(1, base2, "change 1", /*matchesGlob=*/ true);
|
|
Path base3 = Files.createTempDirectory(fileSystem.getPath("/"), "base");
|
|
FileUtil.copyFilesRecursively(base2, base3, CopySymlinkStrategy.FAIL_OUTSIDE_SYMLINKS);
|
|
|
|
writeFile(base3, "excluded/file.txt", "EXCLUDED bAR: Shouldn't be seen");
|
|
writeFile(base3, "folder/modified.txt", "bar");
|
|
writeFile(base3, "folder/added.txt", "only_in_change");
|
|
origin.addChange(2, base3, "change 2", /*matchesGlob=*/ true);
|
|
|
|
options.workflowOptions.ignoreNoop = false;
|
|
transformations = ImmutableList.of(""
|
|
+ " core.replace(\n"
|
|
+ " before = 'only_in_change',\n"
|
|
+ " after = 'foo',\n"
|
|
+ " )",
|
|
" core.verify_match('EXCLUDED', verify_no_match=True)");
|
|
Workflow<?, ?> workflow = skylarkWorkflow("default", WorkflowMode.CHANGE_REQUEST);
|
|
workflow.run(workdir, ImmutableList.of("HEAD"));
|
|
assertThat(destination.processed).hasSize(1);
|
|
assertThat(destination.processed.get(0).getBaseline()).isEqualTo("42");
|
|
return destination.processed.get(0).getAffectedFilesForSmartPrune();
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestEmptyChanges() throws Exception {
|
|
Path originPath = Files.createTempDirectory("origin");
|
|
GitRepository origin = GitRepository.newRepo(/*verbose*/ true, originPath, getGitEnv()).init();
|
|
options.setOutputRootToTmpDir();
|
|
String config = "core.workflow("
|
|
+ " name = 'default',"
|
|
+ " origin = git.origin( url = 'file://" + origin.getWorkTree() + "', ref = 'master'),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " authoring = " + authoring + ","
|
|
+ " origin_files = glob(['included/**']),"
|
|
+ " mode = '" + WorkflowMode.CHANGE_REQUEST + "',"
|
|
+ ")\n";
|
|
|
|
Migration workflow = loadConfig(config).getMigration("default");
|
|
|
|
Files.createDirectory(originPath.resolve("included"));
|
|
Files.write(originPath.resolve("included/foo.txt"), "a".getBytes(UTF_8));
|
|
origin.add().files("included/foo.txt").run();
|
|
origin.commit(
|
|
"Foo <foo@bara.com>",
|
|
ZonedDateTime.now(ZoneId.systemDefault()),
|
|
"the baseline\n\n" + destination.getLabelNameWhenOrigin() + "=42");
|
|
|
|
Files.createDirectory(originPath.resolve("excluded"));
|
|
Files.write(originPath.resolve("excluded/foo.txt"), "a".getBytes(UTF_8));
|
|
origin.add().files("excluded/foo.txt").run();
|
|
origin.commit("Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), "head change");
|
|
|
|
EmptyChangeException thrown =
|
|
assertThrows(EmptyChangeException.class, () -> workflow.run(workdir, ImmutableList.of()));
|
|
assertThat(thrown)
|
|
.hasMessageThat()
|
|
.contains(
|
|
"doesn't include any change for origin_files = glob(include = [\"included/**\"])");
|
|
}
|
|
|
|
@Test
|
|
public void reversibleCheckSymlinkError() throws Exception {
|
|
Path someRoot = Files.createTempDirectory("someRoot");
|
|
Path originPath = someRoot.resolve("origin");
|
|
Files.createDirectories(originPath);
|
|
|
|
GitRepository origin = GitRepository.newRepo(/*verbose*/ true, originPath, getGitEnv()).init();
|
|
options.setOutputRootToTmpDir();
|
|
String config = "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " origin = git.origin( url = 'file://" + origin.getWorkTree() + "', ref = 'master'),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " origin_files = glob(['included/**']),\n"
|
|
+ " reversible_check = True,\n"
|
|
+ " mode = '" + WorkflowMode.SQUASH + "',\n"
|
|
+ ")\n";
|
|
|
|
|
|
Migration workflow = loadConfig(config).getMigration("default");
|
|
|
|
Path included = originPath.resolve("included");
|
|
Files.createDirectory(included);
|
|
Files.write(originPath.resolve("included/foo.txt"), "a".getBytes(UTF_8));
|
|
|
|
Path fileOutsideCheckout = someRoot.resolve("file_outside_checkout");
|
|
Files.write(fileOutsideCheckout, "THE CONTENT".getBytes(UTF_8));
|
|
Files.createSymbolicLink(included.resolve("symlink"), included.relativize(fileOutsideCheckout));
|
|
|
|
origin.add().files("included/foo.txt").run();
|
|
origin.add().files("included/symlink").run();
|
|
origin.commit("Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), "A commit");
|
|
|
|
ValidationException expected =
|
|
assertThrows(ValidationException.class, () -> workflow.run(workdir, ImmutableList.of()));
|
|
assertThat(expected.getMessage())
|
|
.matches(
|
|
""
|
|
+ "Failed to perform reversible check of transformations due to symlink '.*' that "
|
|
+ "points outside the checkout dir. Consider removing this symlink from your "
|
|
+ "origin_files or, alternatively, set reversible_check = False in your "
|
|
+ "workflow.");
|
|
}
|
|
|
|
@Test
|
|
public void testGitDescribeVersionSemanticsForFilteredChanges_squash() throws Exception {
|
|
runGitDescribeVersionSemanticsForFilteredChanges("SQUASH");
|
|
|
|
assertThat(destination.processed).hasSize(1);
|
|
// GIT_DESCRIBE_REQUESTED_VERSION points to the resolved, head revision
|
|
// GIT_CHANGE_DESCRIBE_REVISION points to the most up-to-date change that is being migrated
|
|
assertThat(destination.processed.get(0).getChangesSummary()).matches(
|
|
"Resolved revision is 0.1-3-g.* and change revision is 0.1-2-g(.|\n)*"
|
|
);
|
|
}
|
|
|
|
@Test
|
|
public void testGitDescribeVersionSemanticsForFilteredChanges_iterative() throws Exception {
|
|
runGitDescribeVersionSemanticsForFilteredChanges("ITERATIVE");
|
|
|
|
assertThat(destination.processed).hasSize(2);
|
|
|
|
// GIT_DESCRIBE_REQUESTED_VERSION points to the resolved, head revision
|
|
// GIT_CHANGE_DESCRIBE_REVISION points to the current change that is being migrated
|
|
assertThat(destination.processed.get(0).getChangesSummary()).matches(
|
|
"Resolved revision is 0.1-3-g.* and change revision is 0.1-1-g(.|\n)*"
|
|
);
|
|
assertThat(destination.processed.get(1).getChangesSummary()).matches(
|
|
"Resolved revision is 0.1-3-g.* and change revision is 0.1-2-g(.|\n)*"
|
|
);
|
|
}
|
|
|
|
private void runGitDescribeVersionSemanticsForFilteredChanges(String mode)
|
|
throws IOException, RepoException, ValidationException {
|
|
Path someRoot = Files.createTempDirectory("someRoot");
|
|
Path originPath = someRoot.resolve("origin");
|
|
Files.createDirectories(originPath);
|
|
|
|
GitRepository origin = GitRepository.newRepo(/*verbose*/ true, originPath, getGitEnv()).init();
|
|
options.setOutputRootToTmpDir();
|
|
|
|
String config = "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " origin = git.origin( url = 'file://" + origin.getWorkTree() + "', ref = 'master'),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " origin_files = glob(['included/**']),\n"
|
|
+ " mode = '" + mode + "',\n"
|
|
+ " transformations = ["
|
|
+ "metadata.add_header('Resolved revision is ${GIT_DESCRIBE_REQUESTED_VERSION}"
|
|
+ " and change revision is ${GIT_DESCRIBE_CHANGE_VERSION}')]"
|
|
+ ")\n";
|
|
|
|
|
|
GitTestUtil.writeFile(originPath, "initial.txt", "initial");
|
|
origin.add().files("initial.txt").run();
|
|
origin.commit("Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), "Initial");
|
|
origin.simpleCommand("tag", "-m", "this is a tag!", "0.1");
|
|
|
|
options.setLastRevision(origin.parseRef("HEAD"));
|
|
Migration workflow = loadConfig(config).getMigration("default");
|
|
|
|
GitTestUtil.writeFile(originPath, "included/foo.txt", "a");
|
|
origin.add().files("included/foo.txt").run();
|
|
origin.commit("Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), "one");
|
|
|
|
GitTestUtil.writeFile(originPath, "included/foo.txt", "b");
|
|
origin.add().files("included/foo.txt").run();
|
|
origin.commit("Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), "two");
|
|
|
|
GitTestUtil.writeFile(originPath, "excluded/foo.txt", "c");
|
|
origin.add().files("excluded/foo.txt").run();
|
|
origin.commit("Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), "three");
|
|
|
|
workflow.run(workdir, ImmutableList.of());
|
|
}
|
|
|
|
@Test
|
|
public void customIdentityTest() throws Exception {
|
|
options.workflowOptions.initHistory = true;
|
|
Config config = loadConfig(""
|
|
+ "core.workflow(\n"
|
|
+ " name = 'one',\n"
|
|
+ " authoring = " + authoring + "\n,"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = [metadata.expose_label('some_label', 'new_label')],\n"
|
|
+ " change_identity = '${copybara_config_path}foo${label:new_label}',\n"
|
|
+ " mode = 'ITERATIVE',\n"
|
|
+ ")\n\n"
|
|
+ "core.workflow(\n"
|
|
+ " name = 'two',\n"
|
|
+ " authoring = " + authoring + "\n,"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " change_identity = '${copybara_config_path}foo${label:some_label}',\n"
|
|
+ " mode = 'ITERATIVE',\n"
|
|
+ ")\n\n"
|
|
+ "core.workflow(\n"
|
|
+ " name = 'three',\n"
|
|
+ " authoring = " + authoring + "\n,"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " change_identity = '${copybara_config_path}foo${label:not_found}',\n"
|
|
+ " mode = 'ITERATIVE',\n"
|
|
+ ")\n\n"
|
|
+ "");
|
|
|
|
origin.addSimpleChange(1,"change\n\nsome_label=a");
|
|
origin.addSimpleChange(2,"change\n\nsome_label=b");
|
|
origin.addSimpleChange(3,"change\n\nsome_label=c");
|
|
|
|
config.getMigration("one").run(workdir, ImmutableList.of());
|
|
|
|
assertThat(destination.processed).hasSize(3);
|
|
ImmutableList<String> oneResult = destination.processed.stream().map(
|
|
ProcessedChange::getChangeIdentity).collect(ImmutableList.toImmutableList());
|
|
|
|
// Different identities
|
|
assertThat(ImmutableSet.copyOf(oneResult)).hasSize(3);
|
|
|
|
destination.processed.clear();
|
|
|
|
config.getMigration("two").run(workdir, ImmutableList.of());
|
|
|
|
ImmutableList<String> twoResult = destination.processed.stream().map(
|
|
ProcessedChange::getChangeIdentity).collect(ImmutableList.toImmutableList());
|
|
|
|
assertThat(oneResult).isEqualTo(twoResult);
|
|
|
|
destination.processed.clear();
|
|
config.getMigration("three").run(workdir, ImmutableList.of());
|
|
|
|
ImmutableList<String> threeResult = destination.processed.stream().map(
|
|
ProcessedChange::getChangeIdentity).collect(ImmutableList.toImmutableList());
|
|
|
|
assertThat(oneResult).isNotEqualTo(threeResult);
|
|
}
|
|
@Test
|
|
public void customIdentity_customPathIncluded() throws Exception {
|
|
options.workflowOptions.initHistory = true;
|
|
byte[] cfgContent = (""
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " authoring = " + authoring + "\n,"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " change_identity = '${copybara_config_path}foo${label:some_label}',\n"
|
|
+ " mode = 'ITERATIVE',\n"
|
|
+ ")\n\n"
|
|
+ "").getBytes();
|
|
Config config1 = skylark.loadConfig(
|
|
new MapConfigFile(ImmutableMap.of("foo/copy.bara.sky", cfgContent), "foo/copy.bara.sky"));
|
|
|
|
Config config2 = skylark.loadConfig(
|
|
new MapConfigFile(ImmutableMap.of("bar/copy.bara.sky", cfgContent), "bar/copy.bara.sky"));
|
|
|
|
origin.addSimpleChange(1,"change\n\nsome_label=a");
|
|
|
|
config1.getMigration("default").run(workdir, ImmutableList.of());
|
|
ImmutableList<String> oneResult = destination.processed.stream().map(
|
|
ProcessedChange::getChangeIdentity).collect(ImmutableList.toImmutableList());
|
|
destination.processed.clear();
|
|
config2.getMigration("default").run(workdir, ImmutableList.of());
|
|
ImmutableList<String> twoResult = destination.processed.stream().map(
|
|
ProcessedChange::getChangeIdentity).collect(ImmutableList.toImmutableList());
|
|
|
|
assertThat(oneResult).isNotEqualTo(twoResult);
|
|
}
|
|
|
|
@Test
|
|
public void changeRequest_findParentBaseline() throws Exception {
|
|
origin
|
|
.addSimpleChange(0, "One Change\n" + destination.getLabelNameWhenOrigin() + "=42")
|
|
.addSimpleChange(1, "Last Change\n" + destination.getLabelNameWhenOrigin() + "=BADBAD");
|
|
Workflow<?, ?> workflow = changeRequestWorkflow(null);
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
assertThat(destination.processed.get(0).getBaseline()).isEqualTo("42");
|
|
}
|
|
|
|
@Test
|
|
public void testNullAuthoring() throws Exception {
|
|
ValidationException e =
|
|
assertThrows(
|
|
ValidationException.class,
|
|
() ->
|
|
loadConfig(
|
|
""
|
|
+ "core.workflow(\n"
|
|
+ " name = 'foo',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ ")\n"));
|
|
console()
|
|
.assertThat()
|
|
.onceInLog(MessageType.ERROR, ".*missing 1 required named argument: authoring.*");
|
|
}
|
|
|
|
private Config loadConfig(String content) throws IOException, ValidationException {
|
|
return skylark.loadConfig(
|
|
new MapConfigFile(ImmutableMap.of("copy.bara.sky", content.getBytes()), "copy.bara.sky"));
|
|
}
|
|
|
|
@Test
|
|
public void testNullOrigin() throws Exception {
|
|
ValidationException e =
|
|
assertThrows(
|
|
ValidationException.class,
|
|
() ->
|
|
loadConfig(
|
|
""
|
|
+ "core.workflow(\n"
|
|
+ " name = 'foo',\n"
|
|
+ " authoring = "
|
|
+ authoring
|
|
+ "\n,"
|
|
+ " destination = testing.destination(),\n"
|
|
+ ")\n"));
|
|
for (Message message : console().getMessages()) {
|
|
System.err.println(message);
|
|
}
|
|
console()
|
|
.assertThat()
|
|
.onceInLog(MessageType.ERROR, ".*missing 1 required named argument: origin.*");
|
|
}
|
|
|
|
@Test
|
|
public void testMessageTransformerForSquash() throws Exception {
|
|
runWorkflowForMessageTransform(SQUASH, /*thirdTransform=*/null);
|
|
ProcessedChange change = Iterables.getOnlyElement(destination.processed);
|
|
assertThat(change.getChangesSummary())
|
|
.isEqualTo(""
|
|
+ "CHANGE: third commit (2) by Foo Baz\n"
|
|
+ "CHANGE: second commit (1) by Foo Bar\n"
|
|
+ "\n"
|
|
+ "BAR = foo\n");
|
|
assertThat(change.getAuthor().toString()).isEqualTo("Someone <someone@somewhere.com>");
|
|
}
|
|
|
|
@Test
|
|
public void testMessageTransformerForIterative() throws Exception {
|
|
runWorkflowForMessageTransform(WorkflowMode.ITERATIVE, /*thirdTransform=*/null);
|
|
ProcessedChange secondCommit = destination.processed.get(0);
|
|
assertThat(secondCommit.getChangesSummary())
|
|
.isEqualTo(""
|
|
+ "CHANGE: second commit (1) by Foo Bar\n"
|
|
+ "\n"
|
|
+ "BAR = foo\n");
|
|
assertThat(secondCommit.getAuthor().toString()).isEqualTo("Someone <someone@somewhere.com>");
|
|
ProcessedChange thirdCommit = destination.processed.get(1);
|
|
assertThat(thirdCommit.getChangesSummary())
|
|
.isEqualTo(""
|
|
+ "CHANGE: third commit (2) by Foo Baz\n"
|
|
+ "\n"
|
|
+ "BAR = foo\n");
|
|
assertThat(thirdCommit.getAuthor().toString()).isEqualTo("Someone <someone@somewhere.com>");
|
|
}
|
|
|
|
@Test
|
|
public void testMessageTransformerForIterativeWithMigrated() throws Exception {
|
|
runWorkflowForMessageTransform(WorkflowMode.ITERATIVE, ""
|
|
+ "def third(ctx):\n"
|
|
+ " msg = ''\n"
|
|
+ " for c in ctx.changes.migrated:\n"
|
|
+ " msg+='PREV: %s (%s) by %s\\n' % (c.message, c.ref, c.author.name)\n"
|
|
+ " ctx.set_message(ctx.message + '\\nPREVIOUS CHANGES:\\n' + msg)\n");
|
|
ProcessedChange secondCommit = destination.processed.get(0);
|
|
assertThat(secondCommit.getChangesSummary())
|
|
.isEqualTo(""
|
|
+ "CHANGE: second commit (1) by Foo Bar\n"
|
|
+ "\n"
|
|
+ "BAR = foo\n"
|
|
+ "\n"
|
|
+ "PREVIOUS CHANGES:\n");
|
|
assertThat(secondCommit.getAuthor().toString()).isEqualTo("Someone <someone@somewhere.com>");
|
|
ProcessedChange thirdCommit = destination.processed.get(1);
|
|
assertThat(thirdCommit.getChangesSummary())
|
|
.isEqualTo(""
|
|
+ "CHANGE: third commit (2) by Foo Baz\n"
|
|
+ "\n"
|
|
+ "BAR = foo\n"
|
|
+ "\n"
|
|
+ "PREVIOUS CHANGES:\n"
|
|
+ "PREV: second commit (1) by Foo Bar\n");
|
|
assertThat(thirdCommit.getAuthor().toString()).isEqualTo("Someone <someone@somewhere.com>");
|
|
}
|
|
|
|
@Test
|
|
public void testDateTimeOffset() throws Exception {
|
|
runWorkflowForMessageTransform(WorkflowMode.ITERATIVE, ""
|
|
+ "def third(ctx):\n"
|
|
+ " msg = ''\n"
|
|
+ " for c in ctx.changes.current:\n"
|
|
+ " msg += c.date_time_iso_offset + '\\n'\n"
|
|
+ " ctx.set_message(msg)\n");
|
|
ProcessedChange change = destination.processed.get(1);
|
|
|
|
TemporalAccessor actual = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(
|
|
change.getChangesSummary().trim());
|
|
|
|
// Refers to the same time
|
|
assertThat(actual.getLong(ChronoField.INSTANT_SECONDS))
|
|
.isEqualTo(change.getTimestamp().getLong(ChronoField.INSTANT_SECONDS));
|
|
|
|
// With the same time-zone
|
|
assertThat(actual.getLong(ChronoField.OFFSET_SECONDS))
|
|
.isEqualTo(change.getTimestamp().getLong(ChronoField.OFFSET_SECONDS));
|
|
}
|
|
|
|
@Test
|
|
public void testMessageTransformerForChangeRequest() throws Exception {
|
|
options.workflowOptions.changeBaseline = "1";
|
|
runWorkflowForMessageTransform(WorkflowMode.CHANGE_REQUEST, /*thirdTransform=*/null);
|
|
ProcessedChange change = Iterables.getOnlyElement(destination.processed);
|
|
assertThat(change.getChangesSummary())
|
|
.isEqualTo(""
|
|
+ "CHANGE: third commit (2) by Foo Baz\n"
|
|
+ "\n"
|
|
+ "BAR = foo\n");
|
|
assertThat(change.getAuthor().toString()).isEqualTo("Someone <someone@somewhere.com>");
|
|
}
|
|
|
|
private void runWorkflowForMessageTransform(WorkflowMode mode, @Nullable String thirdTransform)
|
|
throws IOException, RepoException, ValidationException {
|
|
origin.addSimpleChange(0, "first commit")
|
|
.setAuthor(new Author("Foo Bar", "foo@bar.com"))
|
|
.addSimpleChange(1, "second commit")
|
|
.setAuthor(new Author("Foo Baz", "foo@baz.com"))
|
|
.addSimpleChange(2000000000, "third commit");
|
|
|
|
options.workflowOptions.lastRevision = "0";
|
|
passThruAuthoring();
|
|
|
|
Config config = loadConfig(""
|
|
+ "def first(ctx):\n"
|
|
+ " msg =''\n"
|
|
+ " for c in ctx.changes.current:\n"
|
|
+ " msg+='CHANGE: %s (%s) by %s\\n' % (c.message, c.ref, c.author.name)\n"
|
|
+ " ctx.set_message(msg)\n"
|
|
+ "def second(ctx):\n"
|
|
+ " ctx.set_message(ctx.message +'\\nBAR = foo\\n')\n"
|
|
+ " ctx.set_author(new_author('Someone <someone@somewhere.com>'))\n"
|
|
+ "\n"
|
|
+ (thirdTransform == null ? "" : thirdTransform)
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " authoring = " + authoring + "\n,"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " mode = '" + mode + "',\n"
|
|
+ " transformations = [\n"
|
|
+ " first, second" + (thirdTransform == null ? "" : ", third") + "]\n"
|
|
+ ")\n");
|
|
config.getMigration("default").run(workdir, ImmutableList.of("2"));
|
|
}
|
|
|
|
@Test
|
|
public void testNullDestination() throws Exception {
|
|
ValidationException e =
|
|
assertThrows(
|
|
ValidationException.class,
|
|
() ->
|
|
loadConfig(
|
|
""
|
|
+ "core.workflow(\n"
|
|
+ " name = 'foo',\n"
|
|
+ " authoring = "
|
|
+ authoring
|
|
+ "\n,"
|
|
+ " origin = testing.origin(),\n"
|
|
+ ")\n"));
|
|
console()
|
|
.assertThat()
|
|
.onceInLog(MessageType.ERROR, ".*missing 1 required named argument: destination.*");
|
|
}
|
|
|
|
@Test
|
|
public void testNoNestedSequenceProgressMessage() throws Exception {
|
|
Transformation transformation = ((Workflow<?, ?>) loadConfig(""
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " authoring = " + authoring + "\n,"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = ["
|
|
+ " core.transform("
|
|
+ " ["
|
|
+ " core.transform("
|
|
+ " ["
|
|
+ " core.move('foo', 'bar'),"
|
|
+ " core.move('bar', 'foo')"
|
|
+ " ],"
|
|
+ " reversal = [],"
|
|
+ " )"
|
|
+ " ],"
|
|
+ " reversal = []"
|
|
+ " )\n"
|
|
+ " ],"
|
|
+ ")\n").getMigration("default")).getTransformation();
|
|
|
|
Files.write(workdir.resolve("foo"), new byte[0]);
|
|
transformation.transform(TransformWorks.of(workdir, "message", console()));
|
|
|
|
// Check that we don't nest sequence progress messages
|
|
console().assertThat().onceInLog(MessageType.PROGRESS, "^\\[ 1/2\\] Transform Moving foo");
|
|
console().assertThat().onceInLog(MessageType.PROGRESS, "^\\[ 2/2\\] Transform Moving bar");
|
|
}
|
|
|
|
@Test
|
|
public void testFailWithNoopFunc() throws Exception {
|
|
Transformation transformation = ((Workflow<?, ?>) loadConfig(""
|
|
+ "def fail_test(ctx):\n"
|
|
+ " core.fail_with_noop('Hello, this is empty!')\n"
|
|
+ ""
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " authoring = " + authoring + "\n,"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = [fail_test],"
|
|
+ ")\n").getMigration("default")).getTransformation();
|
|
|
|
EmptyChangeException e =
|
|
assertThrows(
|
|
EmptyChangeException.class,
|
|
() -> transformation.transform(TransformWorks.of(workdir, "message", console())));
|
|
assertThat(e).hasMessageThat().contains("Hello, this is empty!");
|
|
}
|
|
|
|
@Test
|
|
public void testWorkflowDefinedInParentConfig() throws Exception {
|
|
Workflow<?, ?> wf = ((Workflow<?, ?>) skylark.loadConfig(
|
|
new MapConfigFile(
|
|
ImmutableMap.of(
|
|
"foo.bara.sky", (""
|
|
+ "def foo_wf(name):\n"
|
|
+ " core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " authoring = " + authoring + "\n,"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ ")\n"
|
|
+ "").getBytes(UTF_8),
|
|
"copy.bara.sky", (""
|
|
+ "load('foo', 'foo_wf')\n"
|
|
+ "foo_wf('default')\n"
|
|
+ "").getBytes(UTF_8)), "copy.bara.sky")).getMigration("default"));
|
|
|
|
assertThat(wf.getName()).isEqualTo("default");
|
|
}
|
|
|
|
@Test
|
|
public void testNonFreezeParentDynamicFunction() throws Exception {
|
|
origin.singleFileChange(0, "one commit", "foo.txt", "1");
|
|
Workflow<?, ?> wf = ((Workflow<?, ?>) skylark.loadConfig(
|
|
new MapConfigFile(
|
|
ImmutableMap.of(
|
|
"foo.bara.sky", (""
|
|
+ "def _dynamic_foo(ctx):\n"
|
|
+ " for f in ctx.run(glob(['**'])):\n"
|
|
+ " if f.attr.size > 10:\n"
|
|
+ " ctx.console.info('Hello! this is function!')\n"
|
|
+ "\n"
|
|
+ "def dynamic_foo():\n"
|
|
+ " return [_dynamic_foo]\n"
|
|
+ "").getBytes(UTF_8),
|
|
"copy.bara.sky", (""
|
|
+ "load('foo', 'dynamic_foo')\n"
|
|
+ "transformations = dynamic_foo()\n"
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " authoring = " + authoring + "\n,"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = transformations,\n"
|
|
+ ")\n"
|
|
+ "").getBytes(UTF_8)), "copy.bara.sky")).getMigration("default"));
|
|
|
|
wf.run(workdir, ImmutableList.of("HEAD"));
|
|
}
|
|
|
|
@Test
|
|
public void nonReversibleButCheckReverseSet() throws Exception {
|
|
origin
|
|
.singleFileChange(0, "one commit", "foo.txt", "1")
|
|
.singleFileChange(1, "one commit", "test.txt", "1\nTRANSFORMED42");
|
|
Workflow<?, ?> workflow = changeRequestWorkflow("0");
|
|
ValidationException e =
|
|
assertThrows(ValidationException.class, () -> workflow.run(workdir, ImmutableList.of("1")));
|
|
assertThat(e).hasMessageThat().isEqualTo("Workflow 'default' is not reversible");
|
|
console()
|
|
.assertThat()
|
|
.onceInLog(MessageType.PROGRESS, "Checking that the transformations can be reverted");
|
|
}
|
|
|
|
@Test
|
|
public void testReverseMetadata() throws IOException, ValidationException, RepoException {
|
|
origin.singleFileChange(0, "one commit", "foo.txt", "1");
|
|
|
|
String config = ""
|
|
+ "def forward(ctx):\n"
|
|
+ " ctx.set_message('modified message')\n"
|
|
+ "def reverse(ctx):\n"
|
|
+ " if ctx.message != 'modified message':\n"
|
|
+ " ctx.console.error('Expecting \"modified message\": '+ ctx.message)\n"
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = [core.transform([forward], reversal=[reverse])],\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " reversible_check = True,\n"
|
|
+ ")\n";
|
|
loadConfig(config).getMigration("default").run(workdir, ImmutableList.of());
|
|
}
|
|
|
|
@Test
|
|
public void testSkylarkTransformParams() throws Exception {
|
|
origin.singleFileChange(0, "one commit", "foo.txt", "1");
|
|
|
|
String config = ""
|
|
+ "def _test_impl(ctx):\n"
|
|
+ " ctx.set_message("
|
|
+ " ctx.message + ctx.params['name'] + str(ctx.params['number']) + '\\n')\n"
|
|
+ "\n"
|
|
+ "def test(name, number = 2):\n"
|
|
+ " return core.dynamic_transform(impl = _test_impl,\n"
|
|
+ " params = { 'name': name, 'number': number})\n"
|
|
+ "\n"
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = [\n"
|
|
+ " metadata.replace_message(''),\n"
|
|
+ " test('example'),\n"
|
|
+ " test('other', 42),\n"
|
|
+ " ],\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ ")\n";
|
|
loadConfig(config).getMigration("default").run(workdir, ImmutableList.of());
|
|
assertThat(Iterables.getOnlyElement(destination.processed).getChangesSummary())
|
|
.isEqualTo("example2\n"
|
|
+ "other42\n");
|
|
|
|
}
|
|
|
|
@Test
|
|
public void testOnFinishHook() throws Exception {
|
|
origin.singleFileChange(0, "one commit", "foo.txt", "1");
|
|
|
|
String config = ""
|
|
+ "def _test_impl(ctx):\n"
|
|
+ " for effect in ctx.effects:\n"
|
|
+ " ctx.origin.message(effect.origin_refs[0].ref + ' created ' "
|
|
+ "+ effect.destination_ref.id + ' ' + ctx.params['name'] + str(ctx.params['number']))\n"
|
|
+ " ctx.destination.message(effect.origin_refs[0].ref + ' created ' "
|
|
+ "+ effect.destination_ref.id + ' ' + ctx.params['name'] + str(ctx.params['number']))\n"
|
|
+ "\n"
|
|
+ "def other(ctx):\n"
|
|
+ " ctx.origin.message('constant')\n"
|
|
+ " ctx.destination.message('constant')\n"
|
|
+ "\n"
|
|
+ "def test(name, number = 2):\n"
|
|
+ " return core.dynamic_feedback(impl = _test_impl,\n"
|
|
+ " params = { 'name': name, 'number': number})\n"
|
|
+ "\n"
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = [],\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " after_migration = [test('example'), test('other', 42), other]"
|
|
+ ")\n";
|
|
loadConfig(config).getMigration("default").run(workdir, ImmutableList.of());
|
|
assertThat(origin.getEndpoint().getMessages())
|
|
.containsExactly("0 created destination/1 example2",
|
|
"0 created destination/1 other42",
|
|
"constant");
|
|
assertThat(destination.getEndpoint().getMessages())
|
|
.containsExactly("0 created destination/1 example2",
|
|
"0 created destination/1 other42",
|
|
"constant");
|
|
}
|
|
|
|
@Test
|
|
public void testAfterAllMigrations1() throws Exception {
|
|
checkAfterAllMigrations("1", ITERATIVE);
|
|
assertThat(destination.getEndpoint().messages).containsExactly(
|
|
"after [\"CREATED destination/1\"]",
|
|
"after_all [\"CREATED destination/1\"]");
|
|
}
|
|
|
|
@Test
|
|
public void testAfterAllMigrations2() throws Exception {
|
|
checkAfterAllMigrations("3", ITERATIVE);
|
|
assertThat(destination.getEndpoint().messages).containsExactly(
|
|
"after [\"CREATED destination/1\"]",
|
|
"after [\"CREATED destination/2\"]",
|
|
"after [\"CREATED destination/3\"]",
|
|
"after_all [\"CREATED destination/1\","
|
|
+ " \"CREATED destination/2\","
|
|
+ " \"CREATED destination/3\"]");
|
|
}
|
|
|
|
@Test
|
|
public void testAfterAllMigrations3() throws Exception {
|
|
checkAfterAllMigrations("3", SQUASH);
|
|
assertThat(destination.getEndpoint().messages).containsExactly(
|
|
"after [\"CREATED destination/1\"]",
|
|
"after_all [\"CREATED destination/1\"]");
|
|
}
|
|
|
|
private void checkAfterAllMigrations(String rev, WorkflowMode mode) throws Exception {
|
|
origin.singleFileChange(0, "base commit", "foo.txt", "0");
|
|
DummyRevision base = origin.resolve("HEAD");
|
|
origin.singleFileChange(1, "first commit", "foo.txt", "1");
|
|
origin.singleFileChange(2, "second commit", "foo.txt", "2");
|
|
origin.singleFileChange(3, "third commit", "foo.txt", "3");
|
|
|
|
String config = ""
|
|
+ "def after_all(ctx):\n"
|
|
+ " ctx.destination.message('after_all '"
|
|
+ " + str([e.type + ' ' + e.destination_ref.id for e in ctx.effects]))\n"
|
|
+ "def after(ctx):\n"
|
|
+ " ctx.destination.message('after '"
|
|
+ " + str([e.type + ' ' + e.destination_ref.id for e in ctx.effects]))\n"
|
|
+ "\n"
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = [],\n"
|
|
+ " mode = \"" + mode.name() + "\",\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " after_migration = [after],\n"
|
|
+ " after_workflow = [after_all]"
|
|
+ ")\n";
|
|
options.setLastRevision(base.asString());
|
|
loadConfig(config).getMigration("default").run(workdir, ImmutableList.of(rev));
|
|
}
|
|
|
|
@Test
|
|
public void testOnFinishHookCreatesEffects() throws Exception {
|
|
origin.singleFileChange(0, "one commit", "foo.txt", "1");
|
|
|
|
String config = ""
|
|
+ "def test(ctx):\n"
|
|
+ " origin_refs = [ctx.origin.new_origin_ref('1111')]\n"
|
|
+ " dest_ref = ctx.destination.new_destination_ref(ref = '9999', type = 'some_type')\n"
|
|
+ " ctx.record_effect('New effect', origin_refs, dest_ref)\n"
|
|
+ "\n"
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = [],\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " after_migration = [test]"
|
|
+ ")\n";
|
|
loadConfig(config).getMigration("default").run(workdir, ImmutableList.of());
|
|
|
|
assertThat(eventMonitor.changeMigrationFinishedEventCount()).isEqualTo(1);
|
|
ChangeMigrationFinishedEvent event =
|
|
Iterables.getOnlyElement(eventMonitor.changeMigrationFinishedEvents);
|
|
assertThat(event.getDestinationEffects()).hasSize(2);
|
|
DestinationEffect firstEffect = event.getDestinationEffects().get(0);
|
|
assertThat(firstEffect.getSummary()).isEqualTo("Change created");
|
|
assertThat(firstEffect.getOriginRefs().get(0).getRef()).isEqualTo("0");
|
|
assertThat(firstEffect.getDestinationRef().getId()).isEqualTo("destination/1");
|
|
DestinationEffect secondEffect = event.getDestinationEffects().get(1);
|
|
assertThat(secondEffect.getSummary()).isEqualTo("New effect");
|
|
assertThat(secondEffect.getType()).isEqualTo(Type.UPDATED);
|
|
assertThat(secondEffect.getOriginRefs().get(0).getRef()).isEqualTo("1111");
|
|
assertThat(secondEffect.getDestinationRef().getId()).isEqualTo("9999");
|
|
}
|
|
|
|
// Validates that the hook is executed when the workflow throws ValidationException, and that
|
|
// the correct effect is populated
|
|
@Test
|
|
public void testOnFinishHook_Error() throws Exception {
|
|
options.testingOptions.destination = new RecordsProcessCallDestination() {
|
|
@Override
|
|
public Writer<Revision> newWriter(WriterContext writerContext) {
|
|
return new RecordsProcessCallDestination.WriterImpl(false) {
|
|
@Override
|
|
public ImmutableList<DestinationEffect> write(TransformResult transformResult,
|
|
Glob destinationFiles, Console console) throws ValidationException {
|
|
throw new ValidationException("Validation exception!");
|
|
}
|
|
};
|
|
}
|
|
};
|
|
verifyHookForException(ValidationException.class, Type.ERROR, "Validation exception!");
|
|
}
|
|
|
|
// Validates that the hook is executed when the workflow throws an exception != VE, and that
|
|
// the correct effect is populated
|
|
@Test
|
|
public void testOnFinishHook_TemporaryError() throws Exception {
|
|
options.testingOptions.destination = new RecordsProcessCallDestination() {
|
|
@Override
|
|
public Writer<Revision> newWriter(WriterContext writerContext) {
|
|
return new RecordsProcessCallDestination.WriterImpl(false) {
|
|
@Override
|
|
public ImmutableList<DestinationEffect> write(TransformResult transformResult,
|
|
Glob destinationFiles, Console console) throws RepoException {
|
|
throw new RepoException("Repo exception!");
|
|
}
|
|
};
|
|
}
|
|
};
|
|
verifyHookForException(RepoException.class, Type.TEMPORARY_ERROR, "Repo exception!");
|
|
}
|
|
|
|
private <T extends Exception> void verifyHookForException(Class<T> expectedExceptionType,
|
|
Type expectedEffectType, String expectedErrorMsg)
|
|
throws IOException, ValidationException, RepoException {
|
|
origin.singleFileChange(0, "one commit", "foo.txt", "1");
|
|
|
|
String config = ""
|
|
+ "def test(ctx):\n"
|
|
+ " origin_refs = [ctx.origin.new_origin_ref('1111')]\n"
|
|
+ " dest_ref = ctx.destination.new_destination_ref(ref = '9999', type = 'some_type')\n"
|
|
+ " ctx.record_effect('New effect', origin_refs, dest_ref)\n"
|
|
+ "\n"
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = [],\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " after_migration = [test]"
|
|
+ ")\n";
|
|
try {
|
|
loadConfig(config).getMigration("default").run(workdir, ImmutableList.of());
|
|
fail();
|
|
} catch (Exception expected) {
|
|
if (!expectedExceptionType.isInstance(expected)) {
|
|
throw expected;
|
|
}
|
|
assertThat(expected).hasMessageThat().contains(expectedErrorMsg);
|
|
}
|
|
|
|
assertThat(eventMonitor.changeMigrationFinishedEventCount()).isEqualTo(1);
|
|
ChangeMigrationFinishedEvent event =
|
|
Iterables.getOnlyElement(eventMonitor.changeMigrationFinishedEvents);
|
|
assertThat(event.getDestinationEffects()).hasSize(2);
|
|
DestinationEffect firstEffect = event.getDestinationEffects().get(0);
|
|
assertThat(firstEffect.getType()).isEqualTo(expectedEffectType);
|
|
assertThat(firstEffect.getSummary()).isEqualTo("Errors happened during the migration");
|
|
assertThat(firstEffect.getErrors()).contains(expectedErrorMsg);
|
|
// Effect from the hook is also created
|
|
DestinationEffect secondEffect = event.getDestinationEffects().get(1);
|
|
assertThat(secondEffect.getSummary()).isEqualTo("New effect");
|
|
assertThat(secondEffect.getType()).isEqualTo(Type.UPDATED);
|
|
assertThat(secondEffect.getOriginRefs().get(0).getRef()).isEqualTo("1111");
|
|
assertThat(secondEffect.getDestinationRef().getId()).isEqualTo("9999");
|
|
}
|
|
|
|
@Test
|
|
public void testOnFinishHookDoesNotReturnResult() throws Exception {
|
|
origin.singleFileChange(0, "one commit", "foo.txt", "1");
|
|
|
|
String config = ""
|
|
+ "def _test_impl(ctx):\n"
|
|
+ " return 'foo'\n"
|
|
+ "\n"
|
|
+ "def test(name, number = 2):\n"
|
|
+ " return core.dynamic_feedback(impl = _test_impl,\n"
|
|
+ " params = { 'name': name, 'number': number})\n"
|
|
+ "\n"
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = [],\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " after_migration = [test('example'), test('other', 42), other]"
|
|
+ ")\n";
|
|
ValidationException expected =
|
|
assertThrows(
|
|
ValidationException.class,
|
|
() -> loadConfig(config).getMigration("default").run(workdir, ImmutableList.of()));
|
|
assertThat(expected.getMessage()).contains("Error loading config file");
|
|
}
|
|
|
|
@Test
|
|
public void testDryRun() throws Exception {
|
|
options.general.dryRunMode = true;
|
|
checkDryRun(ImmutableList.of());
|
|
}
|
|
|
|
@Test
|
|
public void testDryRun_false() throws Exception {
|
|
options.general.dryRunMode = false;
|
|
ValidationException ignored =
|
|
assertThrows(ValidationException.class, () -> checkDryRun(ImmutableList.of()));
|
|
assertThat(ignored)
|
|
.hasMessageThat()
|
|
.contains("Error while executing the skylark transformation other");
|
|
}
|
|
|
|
@Test
|
|
public void testDryRun_no_changes() throws Exception {
|
|
options.general.dryRunMode = true;
|
|
options.setForce(true);
|
|
options.setLastRevision("0");
|
|
checkDryRun(ImmutableList.of("0"));
|
|
}
|
|
|
|
private void checkDryRun(ImmutableList<String> refs)
|
|
throws IOException, RepoException, ValidationException {
|
|
origin.singleFileChange(0, "one commit", "foo.txt", "1");
|
|
String config = ""
|
|
+ "def other(ctx):\n"
|
|
+ " a = 3 + 'cannot add int to string!'\n"
|
|
+ "\n"
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = [],\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " after_migration = [other]"
|
|
+ ")\n";
|
|
|
|
try {
|
|
loadConfig(config).getMigration("default").run(workdir, refs);
|
|
} finally {
|
|
assertThat(destination.processed.get(0).isDryRun()).isEqualTo(options.general.dryRunMode);
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void testNonReversibleInsideGit() throws IOException, ValidationException, RepoException {
|
|
origin.singleFileChange(0, "one commit", "foo.txt", "foo\nbar\n");
|
|
|
|
GitRepository.newRepo(/*verbose*/ true, workdir, getGitEnv()).init();
|
|
Path subdir = Files.createDirectory(workdir.resolve("subdir"));
|
|
String config = ""
|
|
+ "core.workflow(\n"
|
|
+ " name = 'default',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " transformations = [core.replace('foo', 'bar')],\n"
|
|
+ " authoring = " + authoring + ",\n"
|
|
+ " reversible_check = True,\n"
|
|
+ ")\n";
|
|
Migration workflow = loadConfig(config).getMigration("default");
|
|
ValidationException e =
|
|
assertThrows(ValidationException.class, () -> workflow.run(subdir, ImmutableList.of()));
|
|
assertThat(e)
|
|
.hasMessageThat()
|
|
.contains("Cannot use 'reversible_check = True' because Copybara temporary directory");
|
|
}
|
|
|
|
@Test
|
|
public void errorWritingFileThatDoesNotMatchDestinationFiles() throws Exception {
|
|
destinationFiles = "glob(['foo*'], exclude = ['foo42'])";
|
|
transformations = ImmutableList.of();
|
|
|
|
origin.singleFileChange(/*timestamp=*/44, "one commit", "bar.txt", "1");
|
|
Workflow<?, ?> workflow = skylarkWorkflow("default", SQUASH);
|
|
|
|
NotADestinationFileException thrown =
|
|
assertThrows(
|
|
NotADestinationFileException.class,
|
|
() -> workflow.run(workdir, ImmutableList.of(HEAD)));
|
|
assertThat(thrown).hasMessageThat().contains("[bar.txt]");
|
|
}
|
|
|
|
@Test
|
|
public void errorWritingMultipleFilesThatDoNotMatchDestinationFiles() throws Exception {
|
|
// 'foo42' is like a file that is expected to be in the destination but not
|
|
// originating from the origin repo (e.g. a metadata file specific to one repo type).
|
|
// In this example, though, the file is copied from the origin, hence an error.
|
|
destinationFiles = "glob(['foo*'], exclude = ['foo42'])";
|
|
transformations = ImmutableList.of();
|
|
|
|
Path originDir = Files.createTempDirectory("change1");
|
|
Files.write(originDir.resolve("bar"), new byte[] {});
|
|
Files.write(originDir.resolve("foo_included"), new byte[] {});
|
|
Files.write(originDir.resolve("foo42"), new byte[] {});
|
|
origin.addChange(/*timestamp=*/42, originDir, "change1", /*matchesGlob=*/true);
|
|
Workflow<?, ?> workflow = skylarkWorkflow("default", SQUASH);
|
|
|
|
NotADestinationFileException thrown =
|
|
assertThrows(
|
|
NotADestinationFileException.class,
|
|
() -> workflow.run(workdir, ImmutableList.of(HEAD)));
|
|
assertThat(thrown).hasMessageThat().contains("[bar, foo42]");
|
|
}
|
|
|
|
@Test
|
|
public void checkForNonMatchingDestinationFilesAfterTransformations() throws Exception {
|
|
destinationFiles = "glob(['foo*'])";
|
|
options.workflowOptions.ignoreNoop = true;
|
|
transformations = ImmutableList.of("core.move('bar.txt', 'foo53')");
|
|
|
|
origin.singleFileChange(/*timestamp=*/44, "commit 1", "bar.txt", "1");
|
|
origin.singleFileChange(/*timestamp=*/45, "commit 2", "foo42", "1");
|
|
Workflow<?, ?> workflow = skylarkWorkflow("default", SQUASH);
|
|
|
|
workflow.run(workdir, ImmutableList.of(HEAD));
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestWithFolderDestinationError()
|
|
throws IOException, ValidationException, RepoException {
|
|
origin.singleFileChange(/*timestamp=*/44, "commit 1", "bar.txt", "1");
|
|
|
|
ValidationException thrown =
|
|
assertThrows(
|
|
ValidationException.class,
|
|
() ->
|
|
loadConfig(
|
|
"core.workflow(\n"
|
|
+ " name = 'foo',\n"
|
|
+ " origin = testing.origin(),\n"
|
|
+ " destination = folder.destination(),\n"
|
|
+ " authoring = "
|
|
+ authoring
|
|
+ ",\n"
|
|
+ " mode = 'CHANGE_REQUEST',\n"
|
|
+ ")\n")
|
|
.getMigration("foo")
|
|
.run(workdir, ImmutableList.of()));
|
|
assertThat(thrown)
|
|
.hasMessageThat()
|
|
.contains(
|
|
"'CHANGE_REQUEST' is incompatible with destinations that don't support" + " history");
|
|
}
|
|
|
|
@Test
|
|
public void checkLastRevStatus_squash() throws Exception {
|
|
checkLastRevStatus(WorkflowMode.SQUASH);
|
|
}
|
|
|
|
@Test
|
|
public void checkLastRevStatus_iterative() throws Exception {
|
|
checkLastRevStatus(WorkflowMode.ITERATIVE);
|
|
}
|
|
|
|
@Test
|
|
public void checkLastRevStatus_change_request() throws Exception {
|
|
ValidationException e =
|
|
assertThrows(
|
|
ValidationException.class, () -> checkLastRevStatus(WorkflowMode.CHANGE_REQUEST));
|
|
assertThat(e.getMessage())
|
|
.isEqualTo("--check-last-rev-state is not compatible with CHANGE_REQUEST");
|
|
}
|
|
|
|
@Test
|
|
@SuppressWarnings("unchecked")
|
|
public void givenLastRevFlagInfoCommandUsesIt() throws Exception {
|
|
Path originPath = Files.createTempDirectory("origin");
|
|
Path destinationPath = Files.createTempDirectory("destination");
|
|
GitRepository origin = GitRepository.newRepo(/*verbose*/ true, originPath, getGitEnv()).init();
|
|
GitRepository destination =
|
|
GitRepository.newRepo(/*verbose*/ true, destinationPath, getGitEnv()).init();
|
|
|
|
String config = "core.workflow("
|
|
+ " name = '" + "default" + "',"
|
|
+ " origin = git.origin("
|
|
+ " url = 'file://" + origin.getWorkTree() + "',\n"
|
|
+ " ref = 'master',\n"
|
|
+ " ),\n"
|
|
+ " destination = git.destination("
|
|
+ " url = 'file://" + destination.getWorkTree() + "',\n"
|
|
+ " ),\n"
|
|
+ " authoring = " + authoring + ","
|
|
+ " mode = '" + WorkflowMode.ITERATIVE + "',"
|
|
+ ")\n";
|
|
|
|
Files.write(originPath.resolve("foo.txt"), "not important".getBytes(UTF_8));
|
|
origin.add().files("foo.txt").run();
|
|
origin.commit("Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), "not important");
|
|
String firstCommit = origin.parseRef("HEAD");
|
|
|
|
Files.write(destinationPath.resolve("foo.txt"), "not important".getBytes(UTF_8));
|
|
destination.add().files("foo.txt").run();
|
|
destination.commit(
|
|
"Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), "not important");
|
|
|
|
Files.write(originPath.resolve("foo.txt"), "foo".getBytes(UTF_8));
|
|
origin.add().files("foo.txt").run();
|
|
origin.commit("Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), "change1");
|
|
|
|
options.setWorkdirToRealTempDir();
|
|
// Pass custom HOME directory so that we run an hermetic test and we
|
|
// can add custom configuration to $HOME/.gitconfig.
|
|
options.setEnvironment(GitTestUtil.getGitEnv().getEnvironment());
|
|
options.setHomeDir(Files.createTempDirectory("home").toString());
|
|
options.gitDestination.committerName = "Foo";
|
|
options.gitDestination.committerEmail = "foo@foo.com";
|
|
options.workflowOptions.checkLastRevState = true;
|
|
options.setLastRevision(firstCommit);
|
|
|
|
Info<Revision> info = (Info<Revision>) loadConfig(config).getMigration("default").getInfo();
|
|
verifyInfo(info, "change1\n");
|
|
assertThat(info.originDescription().get("url"))
|
|
.containsExactly("file://" + origin.getWorkTree());
|
|
assertThat(info.destinationDescription().get("url"))
|
|
.containsExactly("file://" + destination.getWorkTree());
|
|
}
|
|
|
|
|
|
@Test
|
|
public void testHgOriginNoFlags() throws Exception {
|
|
Path originPath = Files.createTempDirectory("origin");
|
|
HgRepository origin = new HgRepository(originPath, true, DEFAULT_TIMEOUT).init();
|
|
|
|
Path destinationPath = Files.createTempDirectory("destination");
|
|
GitRepository destRepo = GitRepository
|
|
.newBareRepo(destinationPath, getGitEnv(), true, DEFAULT_TIMEOUT, /*noVerify=*/ false)
|
|
.init();
|
|
|
|
String config = "core.workflow("
|
|
+ " name = 'default',"
|
|
+ " origin = hg.origin( url = 'file://" + origin.getHgDir() + "', ref = 'default'),\n"
|
|
+ " destination = git.destination( url = 'file://" + destRepo.getGitDir() + "'),\n"
|
|
+ " authoring = " + authoring + ","
|
|
+ " mode = '" + WorkflowMode.ITERATIVE + "',"
|
|
+ ")\n";
|
|
|
|
Files.write(originPath.resolve("foo.txt"), "testing foo".getBytes(UTF_8));
|
|
origin.hg(originPath, "add", "foo.txt");
|
|
origin.hg(originPath, "commit", "-m", "add foo");
|
|
|
|
options.gitDestination.committerName = "Foo";
|
|
options.gitDestination.committerEmail = "foo@foo.com";
|
|
options.setWorkdirToRealTempDir();
|
|
options.setHomeDir(Files.createTempDirectory("home").toString());
|
|
options.workflowOptions.initHistory = true;
|
|
Migration workflow = loadConfig(config).getMigration("default");
|
|
workflow.run(workdir, ImmutableList.of());
|
|
|
|
ImmutableList<GitLogEntry> destCommits = destRepo.log("HEAD").run();
|
|
assertThat(destCommits).hasSize(1);
|
|
assertThat(destCommits.get(0).getBody()).contains("add foo");
|
|
|
|
Files.write(originPath.resolve("bar.txt"), "testing bar".getBytes(UTF_8));
|
|
origin.hg(originPath, "add", "bar.txt");
|
|
origin.hg(originPath, "commit", "-m", "add bar");
|
|
|
|
options.workflowOptions.initHistory = false;
|
|
workflow.run(workdir, ImmutableList.of());
|
|
|
|
destCommits = destRepo.log("HEAD").run();
|
|
assertThat(destCommits).hasSize(2);
|
|
assertThat(destCommits.get(0).getBody()).contains("add bar");
|
|
assertThat(destCommits.get(1).getBody()).contains("add foo");
|
|
}
|
|
|
|
@Test
|
|
@SuppressWarnings("unchecked")
|
|
public void testInfoSkipsChangesThatDontAffectOriginPaths() throws Exception {
|
|
originFiles = "glob(['**'], exclude = ['folder/**'])";
|
|
transformations = ImmutableList.of();
|
|
|
|
Workflow<?, ?> workflow = iterativeWorkflow(/*previousRef*/ "0");
|
|
|
|
origin.singleFileChange(0, "change1", "file.txt", "aaa");
|
|
origin.singleFileChange(1, "change2", "file.txt", "bbb");
|
|
origin.singleFileChange(2, "change3", "folder/foo.txt", "bar");
|
|
origin.singleFileChange(3, "change4", "file.txt", "ccc");
|
|
|
|
workflow.run(workdir, ImmutableList.of("1"));
|
|
|
|
workflow = iterativeWorkflow(/*previousRef*/ null);
|
|
|
|
verifyInfo((Info<Revision>) workflow.getInfo(), "change4");
|
|
|
|
workflow.run(workdir, ImmutableList.of("3"));
|
|
|
|
// Empty list of changes
|
|
verifyInfo((Info<Revision>) workflow.getInfo());
|
|
}
|
|
|
|
@Test
|
|
public void iterativeInitHistory() throws Exception {
|
|
transformations = ImmutableList.of();
|
|
origin.singleFileChange(0, "change 1", "file.txt", "a");
|
|
origin.singleFileChange(1, "change 2", "file.txt", "b");
|
|
origin.singleFileChange(2, "change 3", "file.txt", "c");
|
|
origin.singleFileChange(3, "change 4", "file.txt", "d");
|
|
origin.singleFileChange(4, "change 5", "file.txt", "e");
|
|
options.workflowOptions.initHistory = true;
|
|
skylarkWorkflow("default", ITERATIVE).run(workdir, ImmutableList.of("3"));
|
|
assertThat(Lists.transform(destination.processed, input -> input.getOriginRef().asString()))
|
|
.isEqualTo(Lists.newArrayList("0", "1", "2", "3"));
|
|
}
|
|
|
|
@Test
|
|
public void iterativeInitHistoryIgnored() throws Exception {
|
|
options.workflowOptions.initHistory = true;
|
|
|
|
transformations = ImmutableList.of();
|
|
origin.singleFileChange(0, "change 1", "file.txt", "a");
|
|
origin.singleFileChange(1, "change 2", "file.txt", "b");
|
|
origin.singleFileChange(2, "change 3", "file.txt", "c");
|
|
origin.singleFileChange(3, "change 4", "file.txt", "d");
|
|
|
|
skylarkWorkflow("default", ITERATIVE).run(workdir, ImmutableList.of("3"));
|
|
assertThat(Lists.transform(destination.processed, input -> input.getOriginRef().asString()))
|
|
.isEqualTo(Lists.newArrayList("0", "1", "2", "3"));
|
|
|
|
origin.singleFileChange(4, "change 5", "file.txt", "e");
|
|
origin.singleFileChange(5, "change 6", "file.txt", "f");
|
|
|
|
skylarkWorkflow("default", ITERATIVE).run(workdir, ImmutableList.of("5"));
|
|
assertThat(Lists.transform(destination.processed, input -> input.getOriginRef().asString()))
|
|
.isEqualTo(Lists.newArrayList("0", "1", "2", "3", "4", "5"));
|
|
console().assertThat().onceInLog(MessageType.WARNING,
|
|
".*Ignoring --init-history because a previous imported revision '3' was found in "
|
|
+ "the destination.*");
|
|
}
|
|
|
|
@Test
|
|
public void squashInitHistory() throws Exception {
|
|
transformations = ImmutableList.of();
|
|
origin.singleFileChange(0, "change 1", "file.txt", "a");
|
|
origin.singleFileChange(1, "change 2", "file.txt", "b");
|
|
origin.singleFileChange(2, "change 3", "file.txt", "c");
|
|
origin.singleFileChange(3, "change 4", "file.txt", "d");
|
|
origin.singleFileChange(4, "change 5", "file.txt", "e");
|
|
options.workflowOptions.initHistory = true;
|
|
skylarkWorkflow("default", SQUASH).run(workdir, ImmutableList.of("3"));
|
|
assertThat(Lists.transform(destination.processed, input -> input.getOriginRef().asString()))
|
|
.isEqualTo(Lists.newArrayList("3"));
|
|
}
|
|
|
|
@Test
|
|
public void testSquashEmptyChangeWithForceTrue() throws Exception {
|
|
originFiles = "glob(['foo/**'], exclude = ['copy.bara.sky', 'excluded/**'])";
|
|
transformations = ImmutableList.of();
|
|
origin.singleFileChange(0, "change 1", "bar/file.txt", "a");
|
|
options.workflowOptions.initHistory = true;
|
|
skylarkWorkflow("default", SQUASH).run(workdir, ImmutableList.of("0"));
|
|
console().assertThat()
|
|
.onceInLog(MessageType.WARNING,
|
|
"No changes up to 0 match any origin_files. Migrating anyway because of --force");
|
|
|
|
}
|
|
|
|
@Test
|
|
public void testSquashEmptyChangeWithForceFalse() throws Exception {
|
|
originFiles = "glob(['foo/**'], exclude = ['copy.bara.sky', 'excluded/**'])";
|
|
transformations = ImmutableList.of();
|
|
options.workflowOptions.initHistory = true;
|
|
origin.singleFileChange(0, "change 1", "bar/file.txt", "a");
|
|
options.setForce(false);
|
|
EmptyChangeException e =
|
|
assertThrows(
|
|
EmptyChangeException.class,
|
|
() -> skylarkWorkflow("default", SQUASH).run(workdir, ImmutableList.of("0")));
|
|
assertThat(e)
|
|
.hasMessageThat()
|
|
.contains(
|
|
"No changes up to 0 match any origin_files. "
|
|
+ "Use --force if you really want to run the migration anyway.");
|
|
}
|
|
|
|
@Test
|
|
public void changeRequestInitHistory() throws Exception {
|
|
options.workflowOptions.initHistory = true;
|
|
options.general.withEventMonitor(eventMonitor);
|
|
options.general.setConsoleForTest(console());
|
|
ValidationException e =
|
|
assertThrows(
|
|
ValidationException.class,
|
|
() ->
|
|
skylarkWorkflow("default", WorkflowMode.CHANGE_REQUEST)
|
|
.run(workdir, ImmutableList.of("")));
|
|
assertThat(e.getMessage()).isEqualTo("--init-history is not compatible with CHANGE_REQUEST");
|
|
}
|
|
|
|
/**
|
|
* Regression that test that when using git.origin with first_parent = False, if the first parent
|
|
* of a merge is already imported and the merge is a no-op, we detect the import as
|
|
* EmptyChangeException instead of detecting the non-first parent as the change to import
|
|
*/
|
|
@Test
|
|
public void testFirstParentAlreadyImportedInNoFirstParent() throws Exception {
|
|
Path originPath = Files.createTempDirectory("origin");
|
|
GitRepository origin = GitRepository.newRepo(/*verbose*/ true, originPath, getGitEnv()).init();
|
|
options.setOutputRootToTmpDir();
|
|
options.setForce(false);
|
|
options.workflowOptions.initHistory = true;
|
|
|
|
String config = "core.workflow("
|
|
+ " name = 'default',"
|
|
+ " origin = git.origin( url = 'file://" + origin.getWorkTree() + "',\n"
|
|
+ " ref = 'master',\n"
|
|
+ " first_parent = False),\n"
|
|
+ " destination = testing.destination(),\n"
|
|
+ " authoring = " + authoring + ","
|
|
+ " origin_files = glob(['included/**']),"
|
|
+ " mode = '" + WorkflowMode.SQUASH + "',"
|
|
+ ")\n";
|
|
|
|
Migration workflow = loadConfig(config).getMigration("default");
|
|
|
|
addGitFile(originPath, origin, "included/foo.txt", "");
|
|
GitRevision lastRev = commit(origin, "last_rev");
|
|
workflow.run(workdir, ImmutableList.of());
|
|
assertThat(destination.processed.get(0).getOriginRef()).isEqualTo(lastRev);
|
|
|
|
origin.simpleCommand("checkout", "-b", "feature");
|
|
addGitFile(originPath, origin, "included/foo.txt", "SHOULD NOT BE IN MASTER");
|
|
commit(origin, "feature commit");
|
|
origin.simpleCommand("revert", "HEAD");
|
|
addGitFile(originPath, origin, "excluded/bar.txt", "don't migrate!");
|
|
commit(origin, "excluded commit");
|
|
origin.simpleCommand("checkout", "master");
|
|
origin.simpleCommand("merge", "-s", "ours", "feature");
|
|
EmptyChangeException e =
|
|
assertThrows(EmptyChangeException.class, () -> workflow.run(workdir, ImmutableList.of()));
|
|
assertThat(e)
|
|
.hasMessageThat()
|
|
.matches("No changes from " + lastRev.getSha1() + " up to .* match any origin_files.*");
|
|
}
|
|
|
|
private GitRevision commit(GitRepository origin, String msg)
|
|
throws RepoException, ValidationException {
|
|
origin.commit("Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), msg);
|
|
return origin.resolveReference("HEAD");
|
|
}
|
|
|
|
private void addGitFile(Path originPath, GitRepository origin, String path, String content)
|
|
throws IOException, RepoException {
|
|
Files.createDirectories(originPath.resolve(path).getParent());
|
|
Files.write(originPath.resolve(path), content.getBytes(UTF_8));
|
|
origin.add().files(path).run();
|
|
}
|
|
|
|
private void checkLastRevStatus(WorkflowMode mode)
|
|
throws IOException, RepoException, ValidationException {
|
|
Path originPath = Files.createTempDirectory("origin");
|
|
Path destinationWorkdir = Files.createTempDirectory("destination_workdir");
|
|
GitRepository origin = GitRepository.newRepo(/*verbose*/ true, originPath, getGitEnv()).init();
|
|
GitRepository destinationBare =
|
|
newBareRepo(
|
|
Files.createTempDirectory("destination"),
|
|
getGitEnv(),
|
|
/*verbose=*/ true,
|
|
DEFAULT_TIMEOUT,
|
|
/*noVerify=*/ false);
|
|
destinationBare.init();
|
|
GitRepository destination = destinationBare.withWorkTree(destinationWorkdir);
|
|
|
|
String config = "core.workflow("
|
|
+ " name = '" + "default" + "',"
|
|
+ " origin = git.origin( url = 'file://" + origin.getWorkTree() + "'),\n"
|
|
+ " destination = git.destination( url = 'file://" + destinationBare.getGitDir() + "'),"
|
|
+ " authoring = " + authoring + ","
|
|
+ " mode = '" + mode + "',"
|
|
+ ")\n";
|
|
|
|
Files.write(originPath.resolve("foo.txt"), "not important".getBytes(UTF_8));
|
|
origin.add().files("foo.txt").run();
|
|
origin.commit("Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), "not important");
|
|
String firstCommit = origin.parseRef("HEAD");
|
|
|
|
Files.write(originPath.resolve("foo.txt"), "foo".getBytes(UTF_8));
|
|
origin.add().files("foo.txt").run();
|
|
origin.commit("Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), "change1");
|
|
|
|
options.setWorkdirToRealTempDir();
|
|
// Pass custom HOME directory so that we run an hermetic test and we
|
|
// can add custom configuration to $HOME/.gitconfig.
|
|
options.setEnvironment(GitTestUtil.getGitEnv().getEnvironment());
|
|
options.setHomeDir(Files.createTempDirectory("home").toString());
|
|
options.gitDestination.committerName = "Foo";
|
|
options.gitDestination.committerEmail = "foo@foo.com";
|
|
options.workflowOptions.checkLastRevState = true;
|
|
options.setLastRevision(firstCommit);
|
|
|
|
loadConfig(config).getMigration("default").run(workdir, ImmutableList.of("HEAD"));
|
|
|
|
// Modify destination last commit
|
|
Files.write(destinationWorkdir.resolve("foo.txt"), "foo_changed".getBytes(UTF_8));
|
|
destination.add().files("foo.txt").run();
|
|
destination.simpleCommand("commit", "--amend", "-a", "-C", "HEAD");
|
|
|
|
Files.write(originPath.resolve("foo.txt"), "foo_origin_changed".getBytes(UTF_8));
|
|
origin.add().files("foo.txt").run();
|
|
origin.commit("Foo <foo@bara.com>", ZonedDateTime.now(ZoneId.systemDefault()), "change2");
|
|
|
|
options.setForce(false);
|
|
options.setLastRevision(null);
|
|
ValidationException thrown =
|
|
assertThrows(
|
|
ValidationException.class,
|
|
() ->
|
|
loadConfig(config).getMigration("default").run(workdir, ImmutableList.of("HEAD")));
|
|
assertThat(thrown)
|
|
.hasMessageThat()
|
|
.contains(
|
|
"didn't result in an empty change. This means that the result change of"
|
|
+ " that migration was modified ouside of Copybara");
|
|
}
|
|
|
|
private void prepareOriginExcludes(String content) throws IOException {
|
|
FileSystem fileSystem = Jimfs.newFileSystem();
|
|
Path base = fileSystem.getPath("excludesTest");
|
|
Path folder = workdir.resolve("folder");
|
|
Files.createDirectories(folder);
|
|
touchFile(base, "folder/file.txt", content);
|
|
touchFile(base, "folder/subfolder/file.txt", content);
|
|
touchFile(base, "folder/subfolder/file.java", content);
|
|
touchFile(base, "folder2/file.txt", content);
|
|
touchFile(base, "folder2/subfolder/file.txt", content);
|
|
touchFile(base, "folder2/subfolder/file.java", content);
|
|
origin.addChange(1, base, "excludes", /*matchesGlob=*/true);
|
|
}
|
|
|
|
private void touchFile(Path base, String path, String content) throws IOException {
|
|
Files.createDirectories(base.resolve(path).getParent());
|
|
Files.write(base.resolve(path), content.getBytes(UTF_8));
|
|
}
|
|
|
|
private Path writeFile(Path base, String path, String content) throws IOException {
|
|
Files.createDirectories(base.resolve(path).getParent());
|
|
return Files.write(base.resolve(path), content.getBytes(UTF_8));
|
|
}
|
|
|
|
private void verifyInfo(Info<Revision> info, String... expectedChanges) {
|
|
List<String> commitMessages =
|
|
StreamSupport.stream(info.migrationReferences().spliterator(), false)
|
|
.flatMap(
|
|
revisionMigrationReference ->
|
|
revisionMigrationReference.getAvailableToMigrate().stream())
|
|
.map(Change::getMessage)
|
|
.collect(Collectors.toList());
|
|
assertThat(commitMessages).containsExactly((Object[]) expectedChanges);
|
|
}
|
|
|
|
@Test
|
|
public void testInvalidMigrationName() {
|
|
skylark.evalFails(
|
|
""
|
|
+ "core.workflow(\n"
|
|
+ " name = 'foo| bad;name',\n"
|
|
+ " origin = folder.origin(),\n"
|
|
+ " destination = folder.destination(),\n"
|
|
+ " authoring = authoring.overwrite('Foo <foo@example.com>')\n"
|
|
+ ")\n",
|
|
".*Migration name 'foo[|] bad;name' doesn't conform to expected pattern.*");
|
|
}
|
|
}
|