Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add git diff mode to CLI #1724

Merged
merged 1 commit into from
Apr 7, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions smithy-cli/build.gradle
Original file line number Diff line number Diff line change
@@ -229,6 +229,11 @@ task integ(type: Test) {
testClassesDirs = sourceSets["it"].output.classesDirs
classpath = sourceSets["it"].runtimeClasspath
maxParallelForks = Runtime.getRuntime().availableProcessors() / 2

testLogging {
events = ["passed", "skipped", "failed"]
exceptionFormat = "full"
}
}

// Runtime images need to be created before integration tests can run.
Original file line number Diff line number Diff line change
@@ -17,17 +17,19 @@

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.utils.IoUtils;
import software.amazon.smithy.utils.ListUtils;

public class DiffCommandTest {
@@ -38,7 +40,7 @@ public void passingDiffEventsExitZero() {
writeFile(a, "$version: \"2.0\"\nnamespace example\nstring A\n");

RunResult result = IntegUtils.run(dir, ListUtils.of("diff", "--old", a.toString(), "--new", a.toString()));
assertThat(result.getExitCode(), equalTo(0));
assertThat("Not 0: output [" + result.getOutput() + ']', result.getExitCode(), is(0));
});
}

@@ -49,7 +51,7 @@ public void showsLabelForOldModelEvents() {
writeFile(a, "$version: \"2.0\"\nnamespace example\n@aaaaaa\nstring A\n");

RunResult result = IntegUtils.run(dir, ListUtils.of("diff", "--old", a.toString(), "--new", a.toString()));
assertThat(result.getExitCode(), equalTo(1));
assertThat("Not 1: output [" + result.getOutput() + ']', result.getExitCode(), is(1));
assertThat(result.getOutput(), containsString("── OLD ERROR ──"));
});
}
@@ -64,7 +66,7 @@ public void showsLabelForNewModelEvents() {
writeFile(b, "$version: \"2.0\"\nnamespace example\n@aaaaaa\nstring A\n");

RunResult result = IntegUtils.run(dir, ListUtils.of("diff", "--old", a.toString(), "--new", b.toString()));
assertThat(result.getExitCode(), equalTo(1));
assertThat("Not 1: output [" + result.getOutput() + ']', result.getExitCode(), is(1));
assertThat(result.getOutput(), containsString("── NEW ERROR ──"));
});
}
@@ -83,7 +85,7 @@ public void showsLabelForDiffEvents() {
"--old", a.toString(),
"--new", b.toString(),
"--severity", "NOTE")); // Note that this is required since the default severity is WARNING.
assertThat(result.getExitCode(), equalTo(0));
assertThat("Not 0: output [" + result.getOutput() + ']', result.getExitCode(), is(0));
assertThat(result.getOutput(), containsString("── DIFF NOTE ──"));
});
}
@@ -110,15 +112,15 @@ public void doesNotUseImportsOrSourcesWithArbitraryMode() {
"-c", dir.resolve("smithy-build.json").toString(),
"--old", file.toString(),
"--new", file.toString()));
assertThat(result.getExitCode(), equalTo(0));
assertThat("Not 0: output [" + result.getOutput() + ']', result.getExitCode(), is(0));
});
}

@Test
public void requiresOldAndNewForArbitraryMode() {
RunResult result = IntegUtils.run(Paths.get("."), ListUtils.of("diff"));

assertThat(result.getExitCode(), is(1));
assertThat("Not 1: output [" + result.getOutput() + ']', result.getExitCode(), is(1));
assertThat(result.getOutput(), containsString("Missing required --old argument"));
}

@@ -129,7 +131,7 @@ public void doesNotAllowNewWithProjectMode() {
"--new", "x",
"--old", "y"));

assertThat(result.getExitCode(), is(1));
assertThat("Not 1: output [" + result.getOutput() + ']', result.getExitCode(), is(1));
assertThat(result.getOutput(), containsString("--new cannot be used with this diff mode"));
}

@@ -143,8 +145,9 @@ public void projectModeUsesConfigOfOldModel() {
"project",
"--old",
outer.toString()));

assertThat("Not 1: output [" + result.getOutput() + ']', result.getExitCode(), is(1));
assertThat(result.getOutput(), containsString("ChangedShapeType"));
assertThat(result.getExitCode(), equalTo(1));
});
});
}
@@ -159,7 +162,151 @@ public void projectModeCanDiffAgainstSingleFile() {
"project",
"--old",
dir.resolve("model").resolve("main.smithy").toString()));
assertThat(result.getExitCode(), equalTo(0));

assertThat("Not 0: output [" + result.getOutput() + ']', result.getExitCode(), is(0));
});
}

@Test
public void gitDiffAgainstLastCommit() {
// Diff against itself (the only model file of the project), so there should be no differences.
IntegUtils.withProject("simple-config-sources", dir -> {
initRepo(dir);
commitChanges(dir);

RunResult result = runDiff(dir);

assertThat("Not 0: output [" + result.getOutput() + ']', result.getExitCode(), is(0));
});
}

private RunResult runDiff(Path dir) {
return runDiff(dir, null);
}

private RunResult runDiff(Path dir, String oldCommit) {
List<String> args = new ArrayList<>();
args.add("diff");
args.add("--mode");
args.add("git");
if (oldCommit != null) {
args.add("--old");
args.add(oldCommit);
}

return IntegUtils.run(dir, args);
}

@Test
public void canDiffAgainstLastCommitWithFailure() {
IntegUtils.withProject("simple-config-sources", dir -> {
initRepo(dir);
commitChanges(dir);

Path file = dir.resolve("model").resolve("main.smithy");
writeFile(file, "$version: \"2.0\"\nnamespace smithy.example\ninteger MyString\n");
RunResult result = IntegUtils.run(dir, ListUtils.of("diff", "--mode", "git"));

assertThat("Not 1: output [" + result.getOutput() + ']', result.getExitCode(), is(1));
assertThat(result.getOutput(), containsString("ChangedShapeType"));
});
}

private void initRepo(Path dir) {
run(ListUtils.of("git", "init"), dir);
run(ListUtils.of("git", "config", "user.email", "you@example.com"), dir);
run(ListUtils.of("git", "config", "user.name", "Your Name"), dir);
}

private void commitChanges(Path dir) {
run(ListUtils.of("git", "add", "-A"), dir);
run(ListUtils.of("git", "commit", "-m", "Foo"), dir);
}

private void run(List<String> args, Path root) {
StringBuilder output = new StringBuilder();
int result = IoUtils.runCommand(args, root, output, Collections.emptyMap());
if (result != 0) {
throw new RuntimeException("Error running command: " + args + ": " + output);
}
}

@Test
public void gitDiffAgainstLastCommitAfterClean() {
IntegUtils.withProject("simple-config-sources", dir -> {
initRepo(dir);
commitChanges(dir);
RunResult result = runDiff(dir);
assertThat("Not zero: output [" + result.getOutput() + ']', result.getExitCode(), is(0));

// This will remove the previously created worktree.
IntegUtils.run(dir, ListUtils.of("clean"));

// Running diff again ensures that we handle the case where there's a prunable worktree.
result = runDiff(dir);

assertThat("Not zero: output [" + result.getOutput() + ']', result.getExitCode(), is(0));
});
}

@Test
public void gitDiffEnsuresHeadUpdatesAsCommitsAreMade() {
IntegUtils.withProject("simple-config-sources", dir -> {
initRepo(dir);
commitChanges(dir);

// Run with HEAD (the default)
RunResult result = runDiff(dir);
assertThat("Not zero: output [" + result.getOutput() + ']', result.getExitCode(), is(0));

// Now make another commit, which updates HEAD and ensures the worktree updates too.
Path file = dir.resolve("model").resolve("main.smithy");
writeFile(file, "$version: \"2.0\"\nnamespace smithy.example\ninteger MyString\n");
commitChanges(dir);

// Run diff again, which won't fail because the last commit is the change that broke the model, meaning
// the current state of the model is valid.
result = runDiff(dir);

assertThat("Not zero: output [" + result.getOutput() + ']', result.getExitCode(), is(0));
});
}

@Test
public void gitDiffAgainstSpecificCommit() {
IntegUtils.withProject("simple-config-sources", dir -> {
initRepo(dir);
commitChanges(dir);

// Run with explicit HEAD
RunResult result = runDiff(dir, "HEAD");
assertThat("Not zero: output [" + result.getOutput() + ']', result.getExitCode(), is(0));

// Now make another commit, which updates HEAD and ensures the worktree updates too.
Path file = dir.resolve("model").resolve("main.smithy");
writeFile(file, "$version: \"2.0\"\nnamespace smithy.example\ninteger MyString\n");
commitChanges(dir);

// Run diff again, which won't fail because the last commit is the change that broke the model, meaning
// the current state of the model is valid.
result = runDiff(dir, "HEAD~1");

assertThat("Not 1: output [" + result.getOutput() + ']', result.getExitCode(), is(1));
assertThat(result.getOutput(), containsString("ChangedShapeType"));
});
}

@Test
public void gitModeDoesNotAllowNewArgument() {
IntegUtils.withProject("simple-config-sources", dir -> {
initRepo(dir);
commitChanges(dir);

// Now that it's running in a git repo, it won't fail due to that and will validate that --new is invalid.
RunResult result = IntegUtils.run(dir, ListUtils.of("diff", "--mode", "git", "--new", "some-file.smithy"));

assertThat("Not 1: output [" + result.getOutput() + ']', result.getExitCode(), is(1));
assertThat(result.getOutput(), containsString("--new cannot be used with this diff mode"));
});
}
}
Original file line number Diff line number Diff line change
@@ -33,11 +33,15 @@
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import software.amazon.smithy.utils.IoUtils;
import software.amazon.smithy.utils.MapUtils;

public final class IntegUtils {

private static final Logger LOGGER = Logger.getLogger(IntegUtils.class.getName());

private IntegUtils() {}

public static void withProject(String projectName, Consumer<Path> consumer) {
@@ -97,7 +101,11 @@ static void withTempDir(String name, Consumer<Path> consumer) {
try {
consumer.accept(path);
} finally {
IoUtils.rmdir(path);
try {
IoUtils.rmdir(path);
} catch (Exception e) {
LOGGER.log(Level.INFO, "Unable to delete temp directory", e);
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
Original file line number Diff line number Diff line change
@@ -260,6 +260,12 @@ LineWrapper space() {
LineWrapper appendWithinLine(String text) {
if (column + text.length() > maxLength) {
newLine();
// If the text starts with a space, then eat the space since it isn't needed to separate words now.
if (text.startsWith(" ")) {
builder.append(text, 1, text.length());
column += text.length() - 1;
return this;
}
}

builder.append(text);
Original file line number Diff line number Diff line change
@@ -15,7 +15,11 @@

package software.amazon.smithy.cli.commands;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.function.Consumer;
import software.amazon.smithy.build.SmithyBuild;
import software.amazon.smithy.build.model.SmithyBuildConfig;
import software.amazon.smithy.cli.ArgumentReceiver;
import software.amazon.smithy.cli.HelpPrinter;

@@ -70,4 +74,21 @@ String output() {
void noPositionalArguments(boolean noPositionalArguments) {
this.noPositionalArguments = noPositionalArguments;
}

/**
* Resolves the correct build directory by looking at the --output argument, outputDirectory config setting,
* and finally the default build directory.
*
* @param config Config to check.
* @return Returns the resolved build directory.
*/
Path resolveOutput(SmithyBuildConfig config) {
if (output != null) {
return Paths.get(output);
} else {
return config.getOutputDirectory()
.map(Paths::get)
.orElseGet(SmithyBuild::getDefaultOutputDirectory);
}
}
}
Original file line number Diff line number Diff line change
@@ -17,14 +17,12 @@

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.logging.Logger;
import software.amazon.smithy.build.SmithyBuild;
import software.amazon.smithy.build.model.MavenConfig;
import software.amazon.smithy.build.model.MavenRepository;
import software.amazon.smithy.build.model.SmithyBuildConfig;
@@ -134,7 +132,9 @@ private List<Path> resolveDependencies(
DependencyResolver baseResolver = dependencyResolverFactory.create(smithyBuildConfig, env);
long lastModified = smithyBuildConfig.getLastModifiedInMillis();
DependencyResolver delegate = new FilterCliVersionResolver(SmithyCli.getVersion(), baseResolver);
DependencyResolver resolver = new FileCacheResolver(getCacheFile(buildOptions), lastModified, delegate);
DependencyResolver resolver = new FileCacheResolver(getCacheFile(buildOptions, smithyBuildConfig),
lastModified,
delegate);
addConfiguredMavenRepos(smithyBuildConfig, resolver);
maven.getDependencies().forEach(resolver::addDependency);
List<ResolvedArtifact> artifacts = resolver.resolve();
@@ -171,9 +171,7 @@ private static void addConfiguredMavenRepos(SmithyBuildConfig config, Dependency
}
}

private File getCacheFile(BuildOptions buildOptions) {
String output = buildOptions.output();
Path buildPath = output == null ? SmithyBuild.getDefaultOutputDirectory() : Paths.get(output);
return buildPath.resolve("classpath.json").toFile();
private File getCacheFile(BuildOptions buildOptions, SmithyBuildConfig config) {
return buildOptions.resolveOutput(config).resolve("classpath.json").toFile();
}
}
Loading