/* * Copyright (C) 2010, Stefan Lay <stefan.lay@sap.com> * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available * under the terms of the Eclipse Distribution License v1.0 which * accompanies this distribution, is reproduced below, and is * available at http://www.eclipse.org/org/documents/edl-v10.php * * All rights reserved. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * - Neither the name of the Eclipse Foundation, Inc. nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.eclipse.jgit.api; import static org.eclipse.jgit.util.FileUtils.RECURSIVE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.Set; import org.eclipse.jgit.api.errors.FilterFailedException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.NoFilepatternException; import org.eclipse.jgit.attributes.FilterCommandRegistry; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lfs.CleanFilter; import org.eclipse.jgit.lfs.SmudgeFilter; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.WorkingTreeOptions; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.junit.Test; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; @RunWith(Theories.class) public class AddCommandTest extends RepositoryTestCase { @DataPoints public static boolean[] sleepBeforeAddOptions = { true, false }; @Override public void setUp() throws Exception { CleanFilter.register(); SmudgeFilter.register(); super.setUp(); } @Test public void testAddNothing() throws GitAPIException { try (Git git = new Git(db)) { git.add().call(); fail("Expected IllegalArgumentException"); } catch (NoFilepatternException e) { // expected } } @Test public void testAddNonExistingSingleFile() throws GitAPIException { try (Git git = new Git(db)) { DirCache dc = git.add().addFilepattern("a.txt").call(); assertEquals(0, dc.getEntryCount()); } } @Test public void testAddExistingSingleFile() throws IOException, GitAPIException { File file = new File(db.getWorkTree(), "a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); try (Git git = new Git(db)) { git.add().addFilepattern("a.txt").call(); assertEquals( "[a.txt, mode:100644, content:content]", indexState(CONTENT)); } } @Test public void testCleanFilter() throws IOException, GitAPIException { writeTrashFile(".gitattributes", "*.txt filter=tstFilter"); writeTrashFile("src/a.tmp", "foo"); // Caution: we need a trailing '\n' since sed on mac always appends // linefeeds if missing writeTrashFile("src/a.txt", "foo\n"); File script = writeTempFile("sed s/o/e/g"); try (Git git = new Git(db)) { StoredConfig config = git.getRepository().getConfig(); config.setString("filter", "tstFilter", "clean", "sh " + slashify(script.getPath())); config.save(); git.add().addFilepattern("src/a.txt").addFilepattern("src/a.tmp") .call(); assertEquals( "[src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:fee\n]", indexState(CONTENT)); } } @Theory public void testBuiltinFilters(boolean sleepBeforeAdd) throws IOException, GitAPIException, InterruptedException { writeTrashFile(".gitattributes", "*.txt filter=lfs"); writeTrashFile("src/a.tmp", "foo"); // Caution: we need a trailing '\n' since sed on mac always appends // linefeeds if missing File script = writeTempFile("sed s/o/e/g"); File f = writeTrashFile("src/a.txt", "foo\n"); try (Git git = new Git(db)) { if (!sleepBeforeAdd) { fsTick(f); } git.add().addFilepattern(".gitattributes").call(); StoredConfig config = git.getRepository().getConfig(); config.setString("filter", "lfs", "clean", "sh " + slashify(script.getPath())); config.setString("filter", "lfs", "smudge", "sh " + slashify(script.getPath())); config.setBoolean("filter", "lfs", "useJGitBuiltin", true); config.save(); if (!sleepBeforeAdd) { fsTick(f); } git.add().addFilepattern("src/a.txt").addFilepattern("src/a.tmp") .addFilepattern(".gitattributes").call(); assertEquals( "[.gitattributes, mode:100644, content:*.txt filter=lfs][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c\nsize 4\n]", indexState(CONTENT)); RevCommit c1 = git.commit().setMessage("c1").call(); assertTrue(git.status().call().isClean()); f = writeTrashFile("src/a.txt", "foobar\n"); if (!sleepBeforeAdd) { fsTick(f); } git.add().addFilepattern("src/a.txt").call(); git.commit().setMessage("c2").call(); assertTrue(git.status().call().isClean()); assertEquals( "[.gitattributes, mode:100644, content:*.txt filter=lfs][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f\nsize 7\n]", indexState(CONTENT)); assertEquals("foobar\n", read("src/a.txt")); git.checkout().setName(c1.getName()).call(); assertEquals( "[.gitattributes, mode:100644, content:*.txt filter=lfs][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c\nsize 4\n]", indexState(CONTENT)); assertEquals( "foo\n", read("src/a.txt")); } } @Theory public void testBuiltinCleanFilter(boolean sleepBeforeAdd) throws IOException, GitAPIException, InterruptedException { writeTrashFile(".gitattributes", "*.txt filter=lfs"); writeTrashFile("src/a.tmp", "foo"); // Caution: we need a trailing '\n' since sed on mac always appends // linefeeds if missing File script = writeTempFile("sed s/o/e/g"); File f = writeTrashFile("src/a.txt", "foo\n"); // unregister the smudge filter. Only clean filter should be builtin FilterCommandRegistry.unregister( org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX + "lfs/smudge"); try (Git git = new Git(db)) { if (!sleepBeforeAdd) { fsTick(f); } git.add().addFilepattern(".gitattributes").call(); StoredConfig config = git.getRepository().getConfig(); config.setString("filter", "lfs", "clean", "sh " + slashify(script.getPath())); config.setString("filter", "lfs", "smudge", "sh " + slashify(script.getPath())); config.setBoolean("filter", "lfs", "useJGitBuiltin", true); config.save(); if (!sleepBeforeAdd) { fsTick(f); } git.add().addFilepattern("src/a.txt").addFilepattern("src/a.tmp") .addFilepattern(".gitattributes").call(); assertEquals( "[.gitattributes, mode:100644, content:*.txt filter=lfs][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c\nsize 4\n]", indexState(CONTENT)); RevCommit c1 = git.commit().setMessage("c1").call(); assertTrue(git.status().call().isClean()); f = writeTrashFile("src/a.txt", "foobar\n"); if (!sleepBeforeAdd) { fsTick(f); } git.add().addFilepattern("src/a.txt").call(); git.commit().setMessage("c2").call(); assertTrue(git.status().call().isClean()); assertEquals( "[.gitattributes, mode:100644, content:*.txt filter=lfs][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f\nsize 7\n]", indexState(CONTENT)); assertEquals("foobar\n", read("src/a.txt")); git.checkout().setName(c1.getName()).call(); assertEquals( "[.gitattributes, mode:100644, content:*.txt filter=lfs][src/a.tmp, mode:100644, content:foo][src/a.txt, mode:100644, content:version https://git-lfs.github.com/spec/v1\noid sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c\nsize 4\n]", indexState(CONTENT)); // due to lfs clean filter but dummy smudge filter we expect strange // content. The smudge filter converts from real content to pointer // file content (starting with "version ") but the smudge filter // replaces 'o' by 'e' which results in a text starting with // "versien " assertEquals( "versien https://git-lfs.github.cem/spec/v1\neid sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c\nsize 4\n", read("src/a.txt")); } } @Test public void testAttributesWithTreeWalkFilter() throws IOException, GitAPIException { writeTrashFile(".gitattributes", "*.txt filter=lfs"); writeTrashFile("src/a.tmp", "foo"); writeTrashFile("src/a.txt", "foo\n"); File script = writeTempFile("sed s/o/e/g"); try (Git git = new Git(db)) { StoredConfig config = git.getRepository().getConfig(); config.setString("filter", "lfs", "clean", "sh " + slashify(script.getPath())); config.save(); git.add().addFilepattern(".gitattributes").call(); git.commit().setMessage("attr").call(); git.add().addFilepattern("src/a.txt").addFilepattern("src/a.tmp") .addFilepattern(".gitattributes").call(); git.commit().setMessage("c1").call(); assertTrue(git.status().call().isClean()); } } @Test public void testCleanFilterEnvironment() throws IOException, GitAPIException { writeTrashFile(".gitattributes", "*.txt filter=tstFilter"); writeTrashFile("src/a.txt", "foo"); File script = writeTempFile("echo $GIT_DIR; echo 1 >xyz"); try (Git git = new Git(db)) { StoredConfig config = git.getRepository().getConfig(); config.setString("filter", "tstFilter", "clean", "sh " + slashify(script.getPath())); config.save(); git.add().addFilepattern("src/a.txt").call(); String gitDir = db.getDirectory().getAbsolutePath(); assertEquals("[src/a.txt, mode:100644, content:" + gitDir + "\n]", indexState(CONTENT)); assertTrue(new File(db.getWorkTree(), "xyz").exists()); } } @Test public void testMultipleCleanFilter() throws IOException, GitAPIException { writeTrashFile(".gitattributes", "*.txt filter=tstFilter\n*.tmp filter=tstFilter2"); // Caution: we need a trailing '\n' since sed on mac always appends // linefeeds if missing writeTrashFile("src/a.tmp", "foo\n"); writeTrashFile("src/a.txt", "foo\n"); File script = writeTempFile("sed s/o/e/g"); File script2 = writeTempFile("sed s/f/x/g"); try (Git git = new Git(db)) { StoredConfig config = git.getRepository().getConfig(); config.setString("filter", "tstFilter", "clean", "sh " + slashify(script.getPath())); config.setString("filter", "tstFilter2", "clean", "sh " + slashify(script2.getPath())); config.save(); git.add().addFilepattern("src/a.txt").addFilepattern("src/a.tmp") .call(); assertEquals( "[src/a.tmp, mode:100644, content:xoo\n][src/a.txt, mode:100644, content:fee\n]", indexState(CONTENT)); // TODO: multiple clean filters for one file??? } } /** * The path of an added file name contains ';' and afterwards malicious * commands. Make sure when calling filter commands to properly escape the * filenames * * @throws IOException * @throws GitAPIException */ @Test public void testCommandInjection() throws IOException, GitAPIException { // Caution: we need a trailing '\n' since sed on mac always appends // linefeeds if missing writeTrashFile("; echo virus", "foo\n"); File script = writeTempFile("sed s/o/e/g"); try (Git git = new Git(db)) { StoredConfig config = git.getRepository().getConfig(); config.setString("filter", "tstFilter", "clean", "sh " + slashify(script.getPath()) + " %f"); writeTrashFile(".gitattributes", "* filter=tstFilter"); git.add().addFilepattern("; echo virus").call(); // Without proper escaping the content would be "feovirus". The sed // command and the "echo virus" would contribute to the content assertEquals("[; echo virus, mode:100644, content:fee\n]", indexState(CONTENT)); } } @Test public void testBadCleanFilter() throws IOException, GitAPIException { writeTrashFile("a.txt", "foo"); File script = writeTempFile("sedfoo s/o/e/g"); try (Git git = new Git(db)) { StoredConfig config = git.getRepository().getConfig(); config.setString("filter", "tstFilter", "clean", "sh " + script.getPath()); config.save(); writeTrashFile(".gitattributes", "*.txt filter=tstFilter"); try { git.add().addFilepattern("a.txt").call(); fail("Didn't received the expected exception"); } catch (FilterFailedException e) { assertEquals(127, e.getReturnCode()); } } } @Test public void testBadCleanFilter2() throws IOException, GitAPIException { writeTrashFile("a.txt", "foo"); File script = writeTempFile("sed s/o/e/g"); try (Git git = new Git(db)) { StoredConfig config = git.getRepository().getConfig(); config.setString("filter", "tstFilter", "clean", "shfoo " + script.getPath()); config.save(); writeTrashFile(".gitattributes", "*.txt filter=tstFilter"); try { git.add().addFilepattern("a.txt").call(); fail("Didn't received the expected exception"); } catch (FilterFailedException e) { assertEquals(127, e.getReturnCode()); } } } @Test public void testCleanFilterReturning12() throws IOException, GitAPIException { writeTrashFile("a.txt", "foo"); File script = writeTempFile("exit 12"); try (Git git = new Git(db)) { StoredConfig config = git.getRepository().getConfig(); config.setString("filter", "tstFilter", "clean", "sh " + slashify(script.getPath())); config.save(); writeTrashFile(".gitattributes", "*.txt filter=tstFilter"); try { git.add().addFilepattern("a.txt").call(); fail("Didn't received the expected exception"); } catch (FilterFailedException e) { assertEquals(12, e.getReturnCode()); } } } @Test public void testNotApplicableFilter() throws IOException, GitAPIException { writeTrashFile("a.txt", "foo"); File script = writeTempFile("sed s/o/e/g"); try (Git git = new Git(db)) { StoredConfig config = git.getRepository().getConfig(); config.setString("filter", "tstFilter", "something", "sh " + script.getPath()); config.save(); writeTrashFile(".gitattributes", "*.txt filter=tstFilter"); git.add().addFilepattern("a.txt").call(); assertEquals("[a.txt, mode:100644, content:foo]", indexState(CONTENT)); } } private File writeTempFile(String body) throws IOException { File f = File.createTempFile("AddCommandTest_", ""); JGitTestUtil.write(f, body); return f; } @Test public void testAddExistingSingleSmallFileWithNewLine() throws IOException, GitAPIException { File file = new File(db.getWorkTree(), "a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("row1\r\nrow2"); writer.close(); try (Git git = new Git(db)) { db.getConfig().setString("core", null, "autocrlf", "false"); git.add().addFilepattern("a.txt").call(); assertEquals("[a.txt, mode:100644, content:row1\r\nrow2]", indexState(CONTENT)); db.getConfig().setString("core", null, "autocrlf", "true"); git.add().addFilepattern("a.txt").call(); assertEquals("[a.txt, mode:100644, content:row1\nrow2]", indexState(CONTENT)); db.getConfig().setString("core", null, "autocrlf", "input"); git.add().addFilepattern("a.txt").call(); assertEquals("[a.txt, mode:100644, content:row1\nrow2]", indexState(CONTENT)); } } @Test public void testAddExistingSingleMediumSizeFileWithNewLine() throws IOException, GitAPIException { File file = new File(db.getWorkTree(), "a.txt"); FileUtils.createNewFile(file); StringBuilder data = new StringBuilder(); for (int i = 0; i < 1000; ++i) { data.append("row1\r\nrow2"); } String crData = data.toString(); PrintWriter writer = new PrintWriter(file); writer.print(crData); writer.close(); String lfData = data.toString().replaceAll("\r", ""); try (Git git = new Git(db)) { db.getConfig().setString("core", null, "autocrlf", "false"); git.add().addFilepattern("a.txt").call(); assertEquals("[a.txt, mode:100644, content:" + data + "]", indexState(CONTENT)); db.getConfig().setString("core", null, "autocrlf", "true"); git.add().addFilepattern("a.txt").call(); assertEquals("[a.txt, mode:100644, content:" + lfData + "]", indexState(CONTENT)); db.getConfig().setString("core", null, "autocrlf", "input"); git.add().addFilepattern("a.txt").call(); assertEquals("[a.txt, mode:100644, content:" + lfData + "]", indexState(CONTENT)); } } @Test public void testAddExistingSingleBinaryFile() throws IOException, GitAPIException { File file = new File(db.getWorkTree(), "a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("row1\r\nrow2\u0000"); writer.close(); try (Git git = new Git(db)) { db.getConfig().setString("core", null, "autocrlf", "false"); git.add().addFilepattern("a.txt").call(); assertEquals("[a.txt, mode:100644, content:row1\r\nrow2\u0000]", indexState(CONTENT)); db.getConfig().setString("core", null, "autocrlf", "true"); git.add().addFilepattern("a.txt").call(); assertEquals("[a.txt, mode:100644, content:row1\r\nrow2\u0000]", indexState(CONTENT)); db.getConfig().setString("core", null, "autocrlf", "input"); git.add().addFilepattern("a.txt").call(); assertEquals("[a.txt, mode:100644, content:row1\r\nrow2\u0000]", indexState(CONTENT)); } } @Test public void testAddExistingSingleFileInSubDir() throws IOException, GitAPIException { FileUtils.mkdir(new File(db.getWorkTree(), "sub")); File file = new File(db.getWorkTree(), "sub/a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); try (Git git = new Git(db)) { git.add().addFilepattern("sub/a.txt").call(); assertEquals( "[sub/a.txt, mode:100644, content:content]", indexState(CONTENT)); } } @Test public void testAddExistingSingleFileTwice() throws IOException, GitAPIException { File file = new File(db.getWorkTree(), "a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); try (Git git = new Git(db)) { DirCache dc = git.add().addFilepattern("a.txt").call(); dc.getEntry(0).getObjectId(); writer = new PrintWriter(file); writer.print("other content"); writer.close(); dc = git.add().addFilepattern("a.txt").call(); assertEquals( "[a.txt, mode:100644, content:other content]", indexState(CONTENT)); } } @Test public void testAddExistingSingleFileTwiceWithCommit() throws Exception { File file = new File(db.getWorkTree(), "a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); try (Git git = new Git(db)) { DirCache dc = git.add().addFilepattern("a.txt").call(); dc.getEntry(0).getObjectId(); git.commit().setMessage("commit a.txt").call(); writer = new PrintWriter(file); writer.print("other content"); writer.close(); dc = git.add().addFilepattern("a.txt").call(); assertEquals( "[a.txt, mode:100644, content:other content]", indexState(CONTENT)); } } @Test public void testAddRemovedFile() throws Exception { File file = new File(db.getWorkTree(), "a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); try (Git git = new Git(db)) { DirCache dc = git.add().addFilepattern("a.txt").call(); dc.getEntry(0).getObjectId(); FileUtils.delete(file); // is supposed to do nothing dc = git.add().addFilepattern("a.txt").call(); assertEquals( "[a.txt, mode:100644, content:content]", indexState(CONTENT)); } } @Test public void testAddRemovedCommittedFile() throws Exception { File file = new File(db.getWorkTree(), "a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); try (Git git = new Git(db)) { DirCache dc = git.add().addFilepattern("a.txt").call(); git.commit().setMessage("commit a.txt").call(); dc.getEntry(0).getObjectId(); FileUtils.delete(file); // is supposed to do nothing dc = git.add().addFilepattern("a.txt").call(); assertEquals( "[a.txt, mode:100644, content:content]", indexState(CONTENT)); } } @Test public void testAddWithConflicts() throws Exception { // prepare conflict File file = new File(db.getWorkTree(), "a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); File file2 = new File(db.getWorkTree(), "b.txt"); FileUtils.createNewFile(file2); writer = new PrintWriter(file2); writer.print("content b"); writer.close(); ObjectInserter newObjectInserter = db.newObjectInserter(); DirCache dc = db.lockDirCache(); DirCacheBuilder builder = dc.builder(); addEntryToBuilder("b.txt", file2, newObjectInserter, builder, 0); addEntryToBuilder("a.txt", file, newObjectInserter, builder, 1); writer = new PrintWriter(file); writer.print("other content"); writer.close(); addEntryToBuilder("a.txt", file, newObjectInserter, builder, 3); writer = new PrintWriter(file); writer.print("our content"); writer.close(); addEntryToBuilder("a.txt", file, newObjectInserter, builder, 2) .getObjectId(); builder.commit(); assertEquals( "[a.txt, mode:100644, stage:1, content:content]" + "[a.txt, mode:100644, stage:2, content:our content]" + "[a.txt, mode:100644, stage:3, content:other content]" + "[b.txt, mode:100644, content:content b]", indexState(CONTENT)); // now the test begins try (Git git = new Git(db)) { dc = git.add().addFilepattern("a.txt").call(); assertEquals( "[a.txt, mode:100644, content:our content]" + "[b.txt, mode:100644, content:content b]", indexState(CONTENT)); } } @Test public void testAddTwoFiles() throws Exception { File file = new File(db.getWorkTree(), "a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); File file2 = new File(db.getWorkTree(), "b.txt"); FileUtils.createNewFile(file2); writer = new PrintWriter(file2); writer.print("content b"); writer.close(); try (Git git = new Git(db)) { git.add().addFilepattern("a.txt").addFilepattern("b.txt").call(); assertEquals( "[a.txt, mode:100644, content:content]" + "[b.txt, mode:100644, content:content b]", indexState(CONTENT)); } } @Test public void testAddFolder() throws Exception { FileUtils.mkdir(new File(db.getWorkTree(), "sub")); File file = new File(db.getWorkTree(), "sub/a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); File file2 = new File(db.getWorkTree(), "sub/b.txt"); FileUtils.createNewFile(file2); writer = new PrintWriter(file2); writer.print("content b"); writer.close(); try (Git git = new Git(db)) { git.add().addFilepattern("sub").call(); assertEquals( "[sub/a.txt, mode:100644, content:content]" + "[sub/b.txt, mode:100644, content:content b]", indexState(CONTENT)); } } @Test public void testAddIgnoredFile() throws Exception { FileUtils.mkdir(new File(db.getWorkTree(), "sub")); File file = new File(db.getWorkTree(), "sub/a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); File ignoreFile = new File(db.getWorkTree(), ".gitignore"); FileUtils.createNewFile(ignoreFile); writer = new PrintWriter(ignoreFile); writer.print("sub/b.txt"); writer.close(); File file2 = new File(db.getWorkTree(), "sub/b.txt"); FileUtils.createNewFile(file2); writer = new PrintWriter(file2); writer.print("content b"); writer.close(); try (Git git = new Git(db)) { git.add().addFilepattern("sub").call(); assertEquals( "[sub/a.txt, mode:100644, content:content]", indexState(CONTENT)); } } @Test public void testAddWholeRepo() throws Exception { FileUtils.mkdir(new File(db.getWorkTree(), "sub")); File file = new File(db.getWorkTree(), "sub/a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); File file2 = new File(db.getWorkTree(), "sub/b.txt"); FileUtils.createNewFile(file2); writer = new PrintWriter(file2); writer.print("content b"); writer.close(); try (Git git = new Git(db)) { git.add().addFilepattern(".").call(); assertEquals( "[sub/a.txt, mode:100644, content:content]" + "[sub/b.txt, mode:100644, content:content b]", indexState(CONTENT)); } } // the same three cases as in testAddWithParameterUpdate // file a exists in workdir and in index -> added // file b exists not in workdir but in index -> unchanged // file c exists in workdir but not in index -> added @Test public void testAddWithoutParameterUpdate() throws Exception { FileUtils.mkdir(new File(db.getWorkTree(), "sub")); File file = new File(db.getWorkTree(), "sub/a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); File file2 = new File(db.getWorkTree(), "sub/b.txt"); FileUtils.createNewFile(file2); writer = new PrintWriter(file2); writer.print("content b"); writer.close(); try (Git git = new Git(db)) { git.add().addFilepattern("sub").call(); assertEquals( "[sub/a.txt, mode:100644, content:content]" + "[sub/b.txt, mode:100644, content:content b]", indexState(CONTENT)); git.commit().setMessage("commit").call(); // new unstaged file sub/c.txt File file3 = new File(db.getWorkTree(), "sub/c.txt"); FileUtils.createNewFile(file3); writer = new PrintWriter(file3); writer.print("content c"); writer.close(); // file sub/a.txt is modified writer = new PrintWriter(file); writer.print("modified content"); writer.close(); // file sub/b.txt is deleted FileUtils.delete(file2); git.add().addFilepattern("sub").call(); // change in sub/a.txt is staged // deletion of sub/b.txt is not staged // sub/c.txt is staged assertEquals( "[sub/a.txt, mode:100644, content:modified content]" + "[sub/b.txt, mode:100644, content:content b]" + "[sub/c.txt, mode:100644, content:content c]", indexState(CONTENT)); } } // file a exists in workdir and in index -> added // file b exists not in workdir but in index -> deleted // file c exists in workdir but not in index -> unchanged @Test public void testAddWithParameterUpdate() throws Exception { FileUtils.mkdir(new File(db.getWorkTree(), "sub")); File file = new File(db.getWorkTree(), "sub/a.txt"); FileUtils.createNewFile(file); PrintWriter writer = new PrintWriter(file); writer.print("content"); writer.close(); File file2 = new File(db.getWorkTree(), "sub/b.txt"); FileUtils.createNewFile(file2); writer = new PrintWriter(file2); writer.print("content b"); writer.close(); try (Git git = new Git(db)) { git.add().addFilepattern("sub").call(); assertEquals( "[sub/a.txt, mode:100644, content:content]" + "[sub/b.txt, mode:100644, content:content b]", indexState(CONTENT)); git.commit().setMessage("commit").call(); // new unstaged file sub/c.txt File file3 = new File(db.getWorkTree(), "sub/c.txt"); FileUtils.createNewFile(file3); writer = new PrintWriter(file3); writer.print("content c"); writer.close(); // file sub/a.txt is modified writer = new PrintWriter(file); writer.print("modified content"); writer.close(); FileUtils.delete(file2); // change in sub/a.txt is staged // deletion of sub/b.txt is staged // sub/c.txt is not staged git.add().addFilepattern("sub").setUpdate(true).call(); // change in sub/a.txt is staged assertEquals( "[sub/a.txt, mode:100644, content:modified content]", indexState(CONTENT)); } } @Test public void testAssumeUnchanged() throws Exception { try (Git git = new Git(db)) { String path = "a.txt"; writeTrashFile(path, "content"); git.add().addFilepattern(path).call(); String path2 = "b.txt"; writeTrashFile(path2, "content"); git.add().addFilepattern(path2).call(); git.commit().setMessage("commit").call(); assertEquals("[a.txt, mode:100644, content:" + "content, assume-unchanged:false]" + "[b.txt, mode:100644, content:content, " + "assume-unchanged:false]", indexState(CONTENT | ASSUME_UNCHANGED)); assumeUnchanged(path2); assertEquals("[a.txt, mode:100644, content:content, " + "assume-unchanged:false][b.txt, mode:100644, " + "content:content, assume-unchanged:true]", indexState(CONTENT | ASSUME_UNCHANGED)); writeTrashFile(path, "more content"); writeTrashFile(path2, "more content"); git.add().addFilepattern(".").call(); assertEquals("[a.txt, mode:100644, content:more content," + " assume-unchanged:false][b.txt, mode:100644," + " content:content, assume-unchanged:true]", indexState(CONTENT | ASSUME_UNCHANGED)); } } @Test public void testReplaceFileWithDirectory() throws IOException, NoFilepatternException, GitAPIException { try (Git git = new Git(db)) { writeTrashFile("df", "before replacement"); git.add().addFilepattern("df").call(); assertEquals("[df, mode:100644, content:before replacement]", indexState(CONTENT)); FileUtils.delete(new File(db.getWorkTree(), "df")); writeTrashFile("df/f", "after replacement"); git.add().addFilepattern("df").call(); assertEquals("[df/f, mode:100644, content:after replacement]", indexState(CONTENT)); } } @Test public void testReplaceDirectoryWithFile() throws IOException, NoFilepatternException, GitAPIException { try (Git git = new Git(db)) { writeTrashFile("df/f", "before replacement"); git.add().addFilepattern("df").call(); assertEquals("[df/f, mode:100644, content:before replacement]", indexState(CONTENT)); FileUtils.delete(new File(db.getWorkTree(), "df"), RECURSIVE); writeTrashFile("df", "after replacement"); git.add().addFilepattern("df").call(); assertEquals("[df, mode:100644, content:after replacement]", indexState(CONTENT)); } } @Test public void testReplaceFileByPartOfDirectory() throws IOException, NoFilepatternException, GitAPIException { try (Git git = new Git(db)) { writeTrashFile("src/main", "df", "before replacement"); writeTrashFile("src/main", "z", "z"); writeTrashFile("z", "z2"); git.add().addFilepattern("src/main/df") .addFilepattern("src/main/z") .addFilepattern("z") .call(); assertEquals( "[src/main/df, mode:100644, content:before replacement]" + "[src/main/z, mode:100644, content:z]" + "[z, mode:100644, content:z2]", indexState(CONTENT)); FileUtils.delete(new File(db.getWorkTree(), "src/main/df")); writeTrashFile("src/main/df", "a", "after replacement"); writeTrashFile("src/main/df", "b", "unrelated file"); git.add().addFilepattern("src/main/df/a").call(); assertEquals( "[src/main/df/a, mode:100644, content:after replacement]" + "[src/main/z, mode:100644, content:z]" + "[z, mode:100644, content:z2]", indexState(CONTENT)); } } @Test public void testReplaceDirectoryConflictsWithFile() throws IOException, NoFilepatternException, GitAPIException { DirCache dc = db.lockDirCache(); try (ObjectInserter oi = db.newObjectInserter()) { DirCacheBuilder builder = dc.builder(); File f = writeTrashFile("a", "df", "content"); addEntryToBuilder("a", f, oi, builder, 1); f = writeTrashFile("a", "df", "other content"); addEntryToBuilder("a/df", f, oi, builder, 3); f = writeTrashFile("a", "df", "our content"); addEntryToBuilder("a/df", f, oi, builder, 2); f = writeTrashFile("z", "z"); addEntryToBuilder("z", f, oi, builder, 0); builder.commit(); } assertEquals( "[a, mode:100644, stage:1, content:content]" + "[a/df, mode:100644, stage:2, content:our content]" + "[a/df, mode:100644, stage:3, content:other content]" + "[z, mode:100644, content:z]", indexState(CONTENT)); try (Git git = new Git(db)) { FileUtils.delete(new File(db.getWorkTree(), "a"), RECURSIVE); writeTrashFile("a", "merged"); git.add().addFilepattern("a").call(); assertEquals("[a, mode:100644, content:merged]" + "[z, mode:100644, content:z]", indexState(CONTENT)); } } @Test public void testExecutableRetention() throws Exception { StoredConfig config = db.getConfig(); config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_FILEMODE, true); config.save(); FS executableFs = new FS() { public boolean supportsExecute() { return true; } public boolean setExecute(File f, boolean canExec) { return true; } public ProcessBuilder runInShell(String cmd, String[] args) { return null; } public boolean retryFailedLockFileCommit() { return false; } public FS newInstance() { return this; } protected File discoverGitExe() { return null; } public boolean canExecute(File f) { try { return read(f).startsWith("binary:"); } catch (IOException e) { return false; } } @Override public boolean isCaseSensitive() { return false; } }; Git git = Git.open(db.getDirectory(), executableFs); String path = "a.txt"; String path2 = "a.sh"; writeTrashFile(path, "content"); writeTrashFile(path2, "binary: content"); git.add().addFilepattern(path).addFilepattern(path2).call(); RevCommit commit1 = git.commit().setMessage("commit").call(); try (TreeWalk walk = new TreeWalk(db)) { walk.addTree(commit1.getTree()); walk.next(); assertEquals(path2, walk.getPathString()); assertEquals(FileMode.EXECUTABLE_FILE, walk.getFileMode(0)); walk.next(); assertEquals(path, walk.getPathString()); assertEquals(FileMode.REGULAR_FILE, walk.getFileMode(0)); } config = db.getConfig(); config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_FILEMODE, false); config.save(); Git git2 = Git.open(db.getDirectory(), executableFs); writeTrashFile(path2, "content2"); writeTrashFile(path, "binary: content2"); git2.add().addFilepattern(path).addFilepattern(path2).call(); RevCommit commit2 = git2.commit().setMessage("commit2").call(); try (TreeWalk walk = new TreeWalk(db)) { walk.addTree(commit2.getTree()); walk.next(); assertEquals(path2, walk.getPathString()); assertEquals(FileMode.EXECUTABLE_FILE, walk.getFileMode(0)); walk.next(); assertEquals(path, walk.getPathString()); assertEquals(FileMode.REGULAR_FILE, walk.getFileMode(0)); } } @Test public void testAddGitlink() throws Exception { createNestedRepo("git-link-dir"); try (Git git = new Git(db)) { git.add().addFilepattern("git-link-dir").call(); assertEquals( "[git-link-dir, mode:160000]", indexState(0)); Set<String> untrackedFiles = git.status().call().getUntracked(); assert (untrackedFiles.isEmpty()); } } @Test public void testAddSubrepoWithDirNoGitlinks() throws Exception { createNestedRepo("nested-repo"); // Set DIR_NO_GITLINKS StoredConfig config = db.getConfig(); config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, true); config.save(); assert (db.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks()); try (Git git = new Git(db)) { git.add().addFilepattern("nested-repo").call(); assertEquals( "[nested-repo/README1.md, mode:100644]" + "[nested-repo/README2.md, mode:100644]", indexState(0)); } // Turn off DIR_NO_GITLINKS, ensure nested-repo is still treated as // a normal directory // Set DIR_NO_GITLINKS config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, false); config.save(); writeTrashFile("nested-repo", "README3.md", "content"); try (Git git = new Git(db)) { git.add().addFilepattern("nested-repo").call(); assertEquals( "[nested-repo/README1.md, mode:100644]" + "[nested-repo/README2.md, mode:100644]" + "[nested-repo/README3.md, mode:100644]", indexState(0)); } } @Test public void testAddGitlinkDoesNotChange() throws Exception { createNestedRepo("nested-repo"); try (Git git = new Git(db)) { git.add().addFilepattern("nested-repo").call(); assertEquals( "[nested-repo, mode:160000]", indexState(0)); } // Set DIR_NO_GITLINKS StoredConfig config = db.getConfig(); config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, true); config.save(); assert (db.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks()); try (Git git = new Git(db)) { git.add().addFilepattern("nested-repo").call(); assertEquals( "[nested-repo, mode:160000]", indexState(0)); } } private static DirCacheEntry addEntryToBuilder(String path, File file, ObjectInserter newObjectInserter, DirCacheBuilder builder, int stage) throws IOException { FileInputStream inputStream = new FileInputStream(file); ObjectId id = newObjectInserter.insert( Constants.OBJ_BLOB, file.length(), inputStream); inputStream.close(); DirCacheEntry entry = new DirCacheEntry(path, stage); entry.setObjectId(id); entry.setFileMode(FileMode.REGULAR_FILE); entry.setLastModified(file.lastModified()); entry.setLength((int) file.length()); builder.add(entry); return entry; } private void assumeUnchanged(String path) throws IOException { final DirCache dirc = db.lockDirCache(); final DirCacheEntry ent = dirc.getEntry(path); if (ent != null) ent.setAssumeValid(true); dirc.write(); if (!dirc.commit()) throw new IOException("could not commit"); } private void createNestedRepo(String path) throws IOException { File gitLinkDir = new File(db.getWorkTree(), path); FileUtils.mkdir(gitLinkDir); FileRepositoryBuilder nestedBuilder = new FileRepositoryBuilder(); nestedBuilder.setWorkTree(gitLinkDir); Repository nestedRepo = nestedBuilder.build(); nestedRepo.create(); writeTrashFile(path, "README1.md", "content"); writeTrashFile(path, "README2.md", "content"); // Commit these changes in the subrepo try (Git git = new Git(nestedRepo)) { git.add().addFilepattern(".").call(); git.commit().setMessage("subrepo commit").call(); } catch (GitAPIException e) { throw new RuntimeException(e); } } }