diff --git a/pom.xml b/pom.xml index f4512614..e61e388a 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,11 @@ org.eclipse.jgit 4.6.0.201612231935-r + + org.apache.maven + maven-model + 3.3.9 + @@ -43,6 +48,10 @@ org.eclipse.jgit org.eclipse.jgit + + org.apache.maven + maven-model + org.springframework.boot diff --git a/src/main/java/org/springframework/cloud/release/PomParser.java b/src/main/java/org/springframework/cloud/release/PomParser.java new file mode 100644 index 00000000..26072798 --- /dev/null +++ b/src/main/java/org/springframework/cloud/release/PomParser.java @@ -0,0 +1,73 @@ +package org.springframework.cloud.release; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.lang.invoke.MethodHandles; + +import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Parses the poms for a given project and populates versions. + * + * @author Marcin Grzejszczak + */ +class PomParser { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final String STARTER_POM = "spring-cloud-starter-parent/pom.xml"; + private static final String BOOT_STARTER_ARTIFACTID = "spring-boot-starter-parent"; + private static final String DEPENDENCIES_POM = "spring-cloud-dependencies/pom.xml"; + + private final File projectRootDir; + private final String bootPom; + private final PomReader pomReader = new PomReader(); + + PomParser(File projectRootDir) { + this(projectRootDir, STARTER_POM); + } + + PomParser(File projectRootDir, String bootPom) { + this.projectRootDir = projectRootDir; + this.bootPom = bootPom; + } + + String bootVersion() { + File bootPom = new File(this.projectRootDir, this.bootPom); + if (!bootPom.exists()) { + throw new IllegalStateException("Pom with boot version is not present"); + } + Model model = this.pomReader.readPom(bootPom); + String bootArtifactId = model.getParent().getArtifactId(); + if (log.isDebugEnabled()) { + log.debug("Boot artifact id is equal to [{}]", bootArtifactId); + } + if (!BOOT_STARTER_ARTIFACTID.equals(bootArtifactId)) { + throw new IllegalStateException("The pom doesn't have a boot version"); + } + String bootVersion = model.getParent().getVersion(); + if (log.isDebugEnabled()) { + log.debug("Boot version is equal to [{}]", bootVersion); + } + return bootVersion; + } +} + +class PomReader { + + Model readPom(File pom) { + try(Reader reader = new FileReader(pom)) { + MavenXpp3Reader xpp3Reader = new MavenXpp3Reader(); + return xpp3Reader.read(reader); + } + catch (XmlPullParserException | IOException e) { + throw new IllegalStateException("Failed to read file", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/cloud/release/ProjectCloner.java b/src/main/java/org/springframework/cloud/release/ProjectCloner.java deleted file mode 100644 index 98582a2e..00000000 --- a/src/main/java/org/springframework/cloud/release/ProjectCloner.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.springframework.cloud.release; - -import java.io.File; -import java.io.IOException; -import java.lang.invoke.MethodHandles; -import java.net.URI; - -import org.eclipse.jgit.api.CloneCommand; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.util.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author Marcin Grzejszczak - */ -class ProjectCloner { - - private static final Logger log = LoggerFactory - .getLogger(MethodHandles.lookup().lookupClass()); - - private final ProjectCloner.JGitFactory gitFactory; - - private final File basedir; - - ProjectCloner(File basedir) { - this.basedir = basedir; - this.gitFactory = new ProjectCloner.JGitFactory(); - } - - ProjectCloner(File basedir, ProjectCloner.JGitFactory factory) { - this.basedir = basedir; - this.gitFactory = factory; - } - - File cloneProject(URI projectUrl) { - try { - if (log.isDebugEnabled()) { - log.debug("Cloning repo from [{}] to [{}]", projectUrl, this.basedir); - } - Git git = cloneToBasedir(projectUrl, this.basedir); - if (git != null) { - git.close(); - } - File clonedRepo = git.getRepository().getDirectory(); - if (log.isDebugEnabled()) { - log.debug("Cloned repo to [{}]", clonedRepo); - } - return clonedRepo; - } - catch (Exception e) { - throw new IllegalStateException(e); - } - } - - private Git cloneToBasedir(URI projectUrl, File destinationFolder) - throws GitAPIException { - CloneCommand clone = this.gitFactory.getCloneCommandByCloneRepository() - .setURI(projectUrl.toString() + ".git").setDirectory(destinationFolder); - try { - return clone.call(); - } - catch (GitAPIException e) { - deleteBaseDirIfExists(); - throw e; - } - } - - private void deleteBaseDirIfExists() { - if (this.basedir.exists()) { - try { - FileUtils.delete(this.basedir, FileUtils.RECURSIVE); - } - catch (IOException e) { - throw new IllegalStateException("Failed to initialize base directory", e); - } - } - } - - /** - * Wraps the static method calls to {@link org.eclipse.jgit.api.Git} and - * {@link org.eclipse.jgit.api.CloneCommand} allowing for easier unit testing. - */ - static class JGitFactory { - CloneCommand getCloneCommandByCloneRepository() { - return Git.cloneRepository(); - } - } -} \ No newline at end of file diff --git a/src/main/java/org/springframework/cloud/release/ProjectRepo.java b/src/main/java/org/springframework/cloud/release/ProjectRepo.java new file mode 100644 index 00000000..a27d32a8 --- /dev/null +++ b/src/main/java/org/springframework/cloud/release/ProjectRepo.java @@ -0,0 +1,144 @@ +package org.springframework.cloud.release; + +import java.io.File; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.net.URI; + +import org.eclipse.jgit.api.CheckoutCommand; +import org.eclipse.jgit.api.CloneCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.util.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstraction over a Git repo. Can clonea repo from a given location + * and check its branch. + * + * @author Marcin Grzejszczak + */ +class ProjectRepo { + + private static final Logger log = LoggerFactory + .getLogger(MethodHandles.lookup().lookupClass()); + + private final ProjectRepo.JGitFactory gitFactory; + + private final File basedir; + + ProjectRepo(File basedir) { + this.basedir = basedir; + this.gitFactory = new ProjectRepo.JGitFactory(); + } + + ProjectRepo(File basedir, ProjectRepo.JGitFactory factory) { + this.basedir = basedir; + this.gitFactory = factory; + } + + /** + * Clones the project + * @param projectUri - URI of the project + * @return file where the project was cloned + */ + File cloneProject(URI projectUri) { + try { + if (log.isDebugEnabled()) { + log.debug("Cloning repo from [{}] to [{}]", projectUri, this.basedir); + } + Git git = cloneToBasedir(projectUri, this.basedir); + if (git != null) { + git.close(); + } + File clonedRepo = git.getRepository().getDirectory(); + if (log.isDebugEnabled()) { + log.debug("Cloned repo to [{}]", clonedRepo); + } + return clonedRepo; + } + catch (Exception e) { + throw new IllegalStateException("Exception occurred while cloning repo", e); + } + } + + /** + * Checks out a branch for a project + * @param project - a Git project + * @param branch - branch to check out + */ + void checkout(File project, String branch) { + try { + if (log.isDebugEnabled()) { + log.debug("Checking out branch [{}] for repo [{}] to [{}]", this.basedir, branch); + } + checkoutBranch(project, branch); + if (log.isDebugEnabled()) { + log.debug("Successfully checked out the branch [{}]", branch); + } + } + catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private Git cloneToBasedir(URI projectUrl, File destinationFolder) + throws GitAPIException { + CloneCommand command = this.gitFactory.getCloneCommandByCloneRepository() + .setURI(projectUrl.toString() + ".git").setDirectory(destinationFolder); + try { + return command.call(); + } + catch (GitAPIException e) { + deleteBaseDirIfExists(); + throw e; + } + } + + private Ref checkoutBranch(File projectDir, String branch) + throws GitAPIException { + Git git = this.gitFactory.open(projectDir); + CheckoutCommand command = git.checkout().setName(branch); + try { + return command.call(); + } + catch (GitAPIException e) { + deleteBaseDirIfExists(); + throw e; + } finally { + git.close(); + } + } + + private void deleteBaseDirIfExists() { + if (this.basedir.exists()) { + try { + FileUtils.delete(this.basedir, FileUtils.RECURSIVE); + } + catch (IOException e) { + throw new IllegalStateException("Failed to initialize base directory", e); + } + } + } + + /** + * Wraps the static method calls to {@link org.eclipse.jgit.api.Git} and + * {@link org.eclipse.jgit.api.CloneCommand} allowing for easier unit testing. + */ + static class JGitFactory { + CloneCommand getCloneCommandByCloneRepository() { + return Git.cloneRepository(); + } + + Git open(File file) { + try { + return Git.open(file); + } + catch (IOException e) { + throw new IllegalStateException(e); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/cloud/release/Versions.java b/src/main/java/org/springframework/cloud/release/Versions.java new file mode 100644 index 00000000..45ec55b0 --- /dev/null +++ b/src/main/java/org/springframework/cloud/release/Versions.java @@ -0,0 +1,16 @@ +package org.springframework.cloud.release; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents versions taken out from Spring Cloud Release pom + * + * @author Marcin Grzejszczak + */ +class Versions { + + String boot; + String build; + Map projects = new HashMap<>(); +} \ No newline at end of file diff --git a/src/test/java/org/springframework/cloud/release/PomParserTests.java b/src/test/java/org/springframework/cloud/release/PomParserTests.java new file mode 100644 index 00000000..cf175c8f --- /dev/null +++ b/src/test/java/org/springframework/cloud/release/PomParserTests.java @@ -0,0 +1,51 @@ +package org.springframework.cloud.release; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; + +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.assertj.core.api.BDDAssertions.thenThrownBy; + +/** + * @author Marcin Grzejszczak + */ +public class PomParserTests { + + File springCloudReleaseProject; + + @Before + public void setup() throws IOException, URISyntaxException { + this.springCloudReleaseProject = new File(ProjectClonerTests.class.getResource("/projects/spring-cloud-release").toURI()); + } + + @Test + public void should_throw_exception_when_boot_pom_is_missing() { + PomParser parser = new PomParser(new File(".")); + + thenThrownBy(parser::bootVersion) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Pom with boot version is not present"); + } + + @Test + public void should_throw_exception_when_boot_version_is_missing_in_pom() { + PomParser parser = new PomParser(this.springCloudReleaseProject, "pom.xml"); + + thenThrownBy(parser::bootVersion) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("The pom doesn't have a boot version"); + } + + @Test + public void should_populate_boot_version() { + PomParser parser = new PomParser(this.springCloudReleaseProject); + + String bootVersion = parser.bootVersion(); + + then(bootVersion).isEqualTo("1.5.1.BUILD-SNAPSHOT"); + } +} \ No newline at end of file diff --git a/src/test/java/org/springframework/cloud/release/ProjectClonerTests.java b/src/test/java/org/springframework/cloud/release/ProjectClonerTests.java index 7c64aa23..2a793227 100644 --- a/src/test/java/org/springframework/cloud/release/ProjectClonerTests.java +++ b/src/test/java/org/springframework/cloud/release/ProjectClonerTests.java @@ -3,6 +3,7 @@ package org.springframework.cloud.release; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.file.Files; import org.eclipse.jgit.api.CloneCommand; import org.junit.Before; @@ -10,7 +11,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.BDDAssertions.then; +import static org.assertj.core.api.BDDAssertions.thenThrownBy; /** * @author Marcin Grzejszczak @@ -20,38 +23,69 @@ public class ProjectClonerTests { @Rule public TemporaryFolder tmp = new TemporaryFolder(); File springCloudReleaseProject; File tmpFolder; - ProjectCloner projectCloner; + ProjectRepo projectRepo; @Before public void setup() throws IOException, URISyntaxException { this.tmpFolder = this.tmp.newFolder(); this.springCloudReleaseProject = new File(ProjectClonerTests.class.getResource("/projects/spring-cloud-release").toURI()); TestUtils.prepareLocalRepo(); - this.projectCloner = new ProjectCloner(this.tmpFolder); + this.projectRepo = new ProjectRepo(this.tmpFolder); } @Test public void should_clone_the_project_from_a_given_location() throws IOException { - this.projectCloner.cloneProject(this.springCloudReleaseProject.toURI()); + this.projectRepo.cloneProject(this.springCloudReleaseProject.toURI()); then(new File(this.tmpFolder, ".git")).exists(); } - @Test(expected = IllegalStateException.class) - public void should_throw_exception_when_there_is_no_repo() - throws IOException, URISyntaxException { - this.projectCloner.cloneProject(ProjectClonerTests.class.getResource("/projects/").toURI()); + @Test + public void should_throw_exception_when_there_is_no_repo() throws IOException, URISyntaxException { + thenThrownBy(() -> this.projectRepo.cloneProject(ProjectClonerTests.class.getResource("/projects/").toURI())) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Exception occurred while cloning repo"); } - @Test(expected = IllegalStateException.class) + @Test public void should_throw_an_exception_when_failed_to_initialize_the_repo() throws IOException { - new ProjectCloner(this.tmpFolder, new ExceptionThrowingJGitFactory()).cloneProject(this.springCloudReleaseProject.toURI()); + thenThrownBy(() -> new ProjectRepo(this.tmpFolder, new ExceptionThrowingJGitFactory()).cloneProject(this.springCloudReleaseProject.toURI())) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Exception occurred while cloning repo") + .hasCauseInstanceOf(CustomException.class); + } + + @Test + public void should_check_out_a_branch_on_cloned_repo() throws IOException { + File project = this.projectRepo.cloneProject(this.springCloudReleaseProject.toURI()); + this.projectRepo.checkout(project, "vCamden.SR3"); + + File pom = new File(this.tmpFolder, "pom.xml"); + then(pom).exists(); + then(Files.lines(pom.toPath()).anyMatch(s -> s.contains("Camden.SR3"))).isTrue(); + } + + @Test + public void should_throw_an_exception_when_checking_out_nonexisting_branch() throws IOException { + File project = this.projectRepo.cloneProject(this.springCloudReleaseProject.toURI()); + try { + this.projectRepo.checkout(project, "nonExistingBranch"); + fail("should throw an exception"); + } catch (IllegalStateException e) { + then(e).hasMessageContaining("Ref nonExistingBranch can not be resolved"); + } } } -class ExceptionThrowingJGitFactory extends ProjectCloner.JGitFactory { +class ExceptionThrowingJGitFactory extends ProjectRepo.JGitFactory { @Override CloneCommand getCloneCommandByCloneRepository() { - throw new RuntimeException("foo"); + throw new CustomException("foo"); + } +} + +class CustomException extends RuntimeException { + public CustomException(String message) { + super(message); } } \ No newline at end of file