/* * Copyright 2017 ThoughtWorks, 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.thoughtworks.go.config.materials.git; import com.googlecode.junit.ext.JunitExtRunner; import com.thoughtworks.go.buildsession.BuildSession; import com.thoughtworks.go.buildsession.BuildSessionBasedTestCase; import com.thoughtworks.go.domain.JobResult; import com.thoughtworks.go.domain.materials.Modification; import com.thoughtworks.go.domain.materials.RevisionContext; import com.thoughtworks.go.domain.materials.git.GitCommand; import com.thoughtworks.go.domain.materials.git.GitMaterialUpdater; import com.thoughtworks.go.domain.materials.git.GitTestRepo; import com.thoughtworks.go.domain.materials.mercurial.StringRevision; import com.thoughtworks.go.helper.GitSubmoduleRepos; import com.thoughtworks.go.helper.TestRepo; import com.thoughtworks.go.matchers.RegexMatcher; import com.thoughtworks.go.util.command.CommandLine; import com.thoughtworks.go.util.command.InMemoryStreamConsumer; import org.apache.commons.io.FileUtils; import org.hamcrest.Matchers; import org.hamcrest.core.Is; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.thoughtworks.go.domain.materials.git.GitTestRepo.*; import static com.thoughtworks.go.matchers.FileExistsMatcher.exists; import static com.thoughtworks.go.util.command.ProcessOutputStreamConsumer.inMemoryConsumer; import static java.lang.String.format; import static junit.framework.TestCase.assertTrue; import static org.apache.commons.io.filefilter.FileFilterUtils.*; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNot.not; import static org.junit.Assert.assertThat; @RunWith(JunitExtRunner.class) public class GitMaterialUpdaterTest extends BuildSessionBasedTestCase { private static final String SUBMODULE = "submodule-1"; private File workingDir; @Before public void setup() throws Exception { workingDir = new File(sandbox, "working"); } @After public void teardown() throws Exception { TestRepo.internalTearDown(); } @Test public void shouldCreateBuildCommandUpdateToSpecificRevision() throws Exception { GitMaterial material = new GitMaterial(new GitTestRepo().projectRepositoryUrl(), true); File newFile = new File(workingDir, "second.txt"); updateTo(material, new RevisionContext(REVISION_1, REVISION_0, 2), JobResult.Passed); assertThat(console.output(), containsString("Start updating files at revision " + REVISION_1.getRevision())); assertThat(newFile.exists(), is(false)); console.clear(); updateTo(material, new RevisionContext(REVISION_2, REVISION_1, 2), JobResult.Passed); assertThat(console.output(), containsString("Start updating files at revision " + REVISION_2.getRevision())); assertThat(newFile.exists(), is(true)); } @Test public void shouldRemoveSubmoduleFolderFromWorkingDirWhenSubmoduleIsRemovedFromRepo() throws Exception { GitSubmoduleRepos submoduleRepos = new GitSubmoduleRepos(); submoduleRepos.addSubmodule(SUBMODULE, "sub1"); GitMaterial gitMaterial = new GitMaterial(submoduleRepos.mainRepo().getUrl(), true); StringRevision revision = new StringRevision("origin/master"); updateTo(gitMaterial, new RevisionContext(revision), JobResult.Passed); assertThat(new File(workingDir, "sub1"), exists()); submoduleRepos.removeSubmodule("sub1"); updateTo(gitMaterial, new RevisionContext(revision), JobResult.Passed); assertThat(new File(workingDir, "sub1"), not(exists())); } @Test public void shouldDeleteAndRecheckoutDirectoryWhenUrlChanges() throws Exception { updateTo(new GitMaterial(new GitTestRepo().projectRepositoryUrl(), true), new RevisionContext(new StringRevision("origin/master")), JobResult.Passed); File shouldBeRemoved = new File(workingDir, "shouldBeRemoved"); shouldBeRemoved.createNewFile(); assertThat(shouldBeRemoved.exists(), is(true)); String repositoryUrl = new GitTestRepo().projectRepositoryUrl(); GitMaterial material = new GitMaterial(repositoryUrl, true); updateTo(material, new RevisionContext(REVISION_4), JobResult.Passed); assertThat(localRepoFor(material).workingRepositoryUrl().forCommandline(), is(repositoryUrl)); assertThat(shouldBeRemoved.exists(), is(false)); } @Test public void shouldNotDeleteAndRecheckoutDirectoryWhenUrlSame() throws Exception { GitMaterial material = new GitMaterial(new GitTestRepo().projectRepositoryUrl(), true); updateTo(material, new RevisionContext(new StringRevision("origin/master")), JobResult.Passed); File shouldNotBeRemoved = new File(new File(workingDir, ".git"), "shouldNotBeRemoved"); FileUtils.writeStringToFile(shouldNotBeRemoved, "gundi"); assertThat(shouldNotBeRemoved.exists(), is(true)); updateTo(material, new RevisionContext(new StringRevision("origin/master")), JobResult.Passed); assertThat("Should not have deleted whole folder", shouldNotBeRemoved.exists(), is(true)); } /* This is to test the functionality of the private method isRepositoryChanged() */ @Test public void shouldNotDeleteAndRecheckoutDirectoryWhenBranchIsBlank() throws Exception { String repositoryUrl = new GitTestRepo().projectRepositoryUrl(); GitMaterial material = new GitMaterial(repositoryUrl, false); updateTo(material, new RevisionContext(new StringRevision("origin/master")), JobResult.Passed); File shouldNotBeRemoved = new File(new File(workingDir, ".git"), "shouldNotBeRemoved"); FileUtils.writeStringToFile(shouldNotBeRemoved, "Text file"); GitMaterial material1 = new GitMaterial(repositoryUrl, " "); updateTo(material1, new RevisionContext(new StringRevision("origin/master")), JobResult.Passed); assertThat("Should not have deleted whole folder", shouldNotBeRemoved.exists(), is(true)); } @Test public void shouldDeleteAndRecheckoutDirectoryWhenBranchChanges() throws Exception { GitTestRepo repoWithBranch = GitTestRepo.testRepoAtBranch(GIT_FOO_BRANCH_BUNDLE, "foo"); GitMaterial material = new GitMaterial(repoWithBranch.projectRepositoryUrl(), true); updateTo(material, new RevisionContext(new StringRevision("origin/master")), JobResult.Passed); InMemoryStreamConsumer output = inMemoryConsumer(); CommandLine.createCommandLine("git").withEncoding("UTF-8").withArg("branch").withWorkingDir(workingDir).run(output, ""); assertThat(output.getStdOut(), Is.is("* master")); GitMaterial material1 = new GitMaterial(repoWithBranch.projectRepositoryUrl(), "foo", null, true); updateTo(material1, new RevisionContext(new StringRevision("origin/foo")), JobResult.Passed); output = inMemoryConsumer(); CommandLine.createCommandLine("git").withEncoding("UTF-8").withArg("branch").withWorkingDir(workingDir).run(output, ""); assertThat(output.getStdOut(), Is.is("* foo")); } @Test public void shouldLogRepoInfoToConsoleOutWithoutFolder() throws Exception { String repositoryUrl = new GitTestRepo().projectRepositoryUrl(); GitMaterial material = new GitMaterial(repositoryUrl, false); updateTo(material, new RevisionContext(REVISION_1), JobResult.Passed); assertThat(console.output(), containsString( format("Start updating %s at revision %s from %s", "files", REVISION_1.getRevision(), repositoryUrl))); } @Test public void shouldConvertExistingRepoToFullRepoWhenShallowCloneIsOff() throws IOException { String repositoryUrl = new GitTestRepo().projectRepositoryUrl(); GitMaterial shallowMaterial = new GitMaterial(repositoryUrl, true); updateTo(shallowMaterial, new RevisionContext(REVISION_3), JobResult.Passed); assertThat(localRepoFor(shallowMaterial).isShallow(), is(true)); GitMaterial fullMaterial = new GitMaterial(repositoryUrl, false); updateTo(fullMaterial, new RevisionContext(REVISION_4), JobResult.Passed); assertThat(localRepoFor(fullMaterial).isShallow(), is(false)); } @Test public void shouldCleanDirtyFilesUponUpdate() throws IOException { String repositoryUrl = new GitTestRepo().projectRepositoryUrl(); GitMaterial material = new GitMaterial(repositoryUrl, true); updateTo(material, new RevisionContext(REVISION_4), JobResult.Passed); File shouldBeRemoved = new File(workingDir, "shouldBeRemoved"); assertTrue(shouldBeRemoved.createNewFile()); updateTo(material, new RevisionContext(REVISION_4), JobResult.Passed); assertThat(shouldBeRemoved.exists(), is(false)); } @Test public void cloneWithDeepWorkingDir() throws Exception { GitMaterial material = new GitMaterial(new GitTestRepo().projectRepositoryUrl(), "", "foo/bar/baz", true); updateTo(material, new RevisionContext(REVISION_4), JobResult.Passed); assertThat(new File(workingDir, "foo/bar/baz/build.xml").exists(), is(true)); } @Test public void failureCommandShouldNotLeakPasswordOnUrl() throws Exception { GitMaterial material = new GitMaterial("https://foo:foopassword@this.is.absolute.not.exists", true); updateTo(material, new RevisionContext(new StringRevision("origin/master")), JobResult.Failed); assertThat(console.output(), containsString("https://foo:******@this.is.absolute.not.exists/")); assertThat(console.output(), not(containsString("foopassword"))); } @Test public void shouldCleanUnversionedFilesInsideSubmodulesBeforeUpdating() throws Exception { GitSubmoduleRepos submoduleRepos = new GitSubmoduleRepos(); String submoduleDirectoryName = "local-submodule"; submoduleRepos.addSubmodule(SUBMODULE, submoduleDirectoryName); GitMaterial material = new GitMaterial(submoduleRepos.projectRepositoryUrl(), true); updateTo(material, new RevisionContext(new StringRevision("origin/HEAD")), JobResult.Passed); File unversionedFile = new File(new File(workingDir, submoduleDirectoryName), "unversioned_file.txt"); FileUtils.writeStringToFile(unversionedFile, "this is an unversioned file. lets see you deleting me.. come on.. I dare you!!!!"); updateTo(material, new RevisionContext(new StringRevision("origin/HEAD")), JobResult.Passed); assertThat(unversionedFile.exists(), Matchers.is(false)); } @Test public void shouldRemoveChangesToModifiedFilesInsideSubmodulesBeforeUpdating() throws Exception { GitSubmoduleRepos submoduleRepos = new GitSubmoduleRepos(); String submoduleDirectoryName = "local-submodule"; File remoteSubmoduleLocation = submoduleRepos.addSubmodule(SUBMODULE, submoduleDirectoryName); GitMaterial material = new GitMaterial(submoduleRepos.projectRepositoryUrl(), true); updateTo(material, new RevisionContext(new StringRevision("origin/HEAD")), JobResult.Passed); /* Simulate a local modification of file inside submodule, on agent side. */ File fileInSubmodule = allFilesIn(new File(workingDir, submoduleDirectoryName), "file-").get(0); FileUtils.writeStringToFile(fileInSubmodule, "Some other new content."); /* Commit a change to the file on the repo. */ List<Modification> modifications = submoduleRepos.modifyOneFileInSubmoduleAndUpdateMainRepo( remoteSubmoduleLocation, submoduleDirectoryName, fileInSubmodule.getName(), "NEW CONTENT OF FILE"); updateTo(material, new RevisionContext(new StringRevision(modifications.get(0).getRevision())), JobResult.Passed); assertThat(FileUtils.readFileToString(fileInSubmodule), Matchers.is("NEW CONTENT OF FILE")); } @Test public void shouldAllowSubmoduleUrlstoChange() throws Exception { GitSubmoduleRepos submoduleRepos = new GitSubmoduleRepos(); String submoduleDirectoryName = "local-submodule"; submoduleRepos.addSubmodule(SUBMODULE, submoduleDirectoryName); GitMaterial material = new GitMaterial(submoduleRepos.projectRepositoryUrl(), true); updateTo(material, new RevisionContext(new StringRevision("origin/HEAD")), JobResult.Passed); submoduleRepos.changeSubmoduleUrl(submoduleDirectoryName); updateTo(material, new RevisionContext(new StringRevision("origin/HEAD")), JobResult.Passed); assertThat(console.output(), containsString("Synchronizing submodule url for 'local-submodule'")); } @Test public void shouldOutputSubmoduleRevisionsAfterUpdate() throws Exception { GitSubmoduleRepos submoduleRepos = new GitSubmoduleRepos(); submoduleRepos.addSubmodule(SUBMODULE, "sub1"); GitMaterial material = new GitMaterial(submoduleRepos.projectRepositoryUrl(), true); updateTo(material, new RevisionContext(new StringRevision("origin/HEAD")), JobResult.Passed); Matcher matcher = Pattern.compile(".*^\\s[a-f0-9A-F]{40} sub1 \\(heads/master\\)$.*", Pattern.MULTILINE | Pattern.DOTALL).matcher(console.output()); assertThat(matcher.matches(), Matchers.is(true)); } @Test public void shouldBombForFetchAndResetWhenSubmoduleUpdateFails() throws Exception { GitSubmoduleRepos submoduleRepos = new GitSubmoduleRepos(); File submoduleFolder = submoduleRepos.addSubmodule(SUBMODULE, "sub1"); GitMaterial material = new GitMaterial(submoduleRepos.projectRepositoryUrl(), true); FileUtils.deleteDirectory(submoduleFolder); assertThat(submoduleFolder.exists(), Matchers.is(false)); updateTo(material, new RevisionContext(new StringRevision("origin/HEAD")), JobResult.Failed); assertThat(console.output(), // different versions of git use different messages // git on windows prints full submodule paths new RegexMatcher(String.format("[Cc]lone of '%s' into submodule path '((.*)[\\/])?sub1' failed", Pattern.quote(submoduleFolder.getAbsolutePath()))) ); } private void updateTo(GitMaterial material, RevisionContext revisionContext, JobResult expectedResult) { BuildSession buildSession = newBuildSession(); JobResult result = buildSession.build(new GitMaterialUpdater(material).updateTo("working", revisionContext)); assertThat(buildInfo(), result, is(expectedResult)); } private GitCommand localRepoFor(GitMaterial material) { return new GitCommand(material.getFingerprint(), workingDir, GitMaterialConfig.DEFAULT_BRANCH, false, new HashMap<>(), null); } private List<File> allFilesIn(File directory, String prefixOfFiles) { return new ArrayList<>(FileUtils.listFiles(directory, andFileFilter(fileFileFilter(), prefixFileFilter(prefixOfFiles)), null)); } }