diff --git a/DEPENDENCIES b/DEPENDENCIES index ebbe4805a..93fa850d8 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,69 +1,68 @@ maven/mavencentral/args4j/args4j/2.33, MIT, approved, CQ11068 -maven/mavencentral/com.google.code.gson/gson/2.8.7, Apache-2.0, approved, CQ23496 -maven/mavencentral/com.googlecode.javaewah/JavaEWAH/1.1.12, Apache-2.0, approved, CQ11658 +maven/mavencentral/com.google.code.gson/gson/2.8.9, Apache-2.0, approved, CQ23496 +maven/mavencentral/com.googlecode.javaewah/JavaEWAH/1.1.13, Apache-2.0, approved, CQ11658 maven/mavencentral/com.jcraft/jsch/0.1.55, BSD-3-Clause, approved, CQ19435 maven/mavencentral/com.jcraft/jzlib/1.1.1, BSD-2-Clause, approved, CQ6218 maven/mavencentral/commons-codec/commons-codec/1.11, Apache-2.0 AND BSD-3-Clause, approved, CQ15971 maven/mavencentral/commons-logging/commons-logging/1.2, Apache-2.0, approved, CQ10162 -maven/mavencentral/javax.servlet/javax.servlet-api/3.1.0, Apache-2.0 AND (CDDL-1.1 OR GPL-2.0 WITH Classpath-exception-2.0), approved, CQ7248 -maven/mavencentral/junit/junit/4.13, , approved, CQ22796 -maven/mavencentral/log4j/log4j/1.2.15, Apache-2.0, approved, CQ7837 +maven/mavencentral/javax.servlet/javax.servlet-api/4.0.0, , approved, CQ16125 +maven/mavencentral/junit/junit/4.13.2, EPL-2.0, approved, CQ23636 maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.9.0, Apache-2.0, approved, clearlydefined maven/mavencentral/net.bytebuddy/byte-buddy/1.9.0, Apache-2.0, approved, clearlydefined maven/mavencentral/net.i2p.crypto/eddsa/0.3.0, CC0-1.0, approved, CQ22537 +maven/mavencentral/net.java.dev.jna/jna-platform/5.8.0, Apache-2.0 OR LGPL-2.1-or-later, approved, CQ23218 +maven/mavencentral/net.java.dev.jna/jna/5.8.0, Apache-2.0 OR LGPL-2.1-or-later, approved, CQ23217 maven/mavencentral/net.sf.jopt-simple/jopt-simple/4.6, MIT, approved, clearlydefined -maven/mavencentral/org.apache.ant/ant-launcher/1.10.10, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560 -maven/mavencentral/org.apache.ant/ant/1.10.10, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560 -maven/mavencentral/org.apache.commons/commons-compress/1.20, Apache-2.0 AND BSD-3-Clause AND LicenseRef-Public-Domain, approved, CQ21771 +maven/mavencentral/org.apache.ant/ant-launcher/1.10.12, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560 +maven/mavencentral/org.apache.ant/ant/1.10.12, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560 +maven/mavencentral/org.apache.commons/commons-compress/1.21, Apache-2.0 AND BSD-3-Clause AND bzip2-1.0.6 AND LicenseRef-Public-Domain, approved, CQ23710 maven/mavencentral/org.apache.commons/commons-math3/3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.apache.httpcomponents/httpclient/4.5.13, Apache-2.0 AND LicenseRef-Public-Domain, approved, CQ23527 maven/mavencentral/org.apache.httpcomponents/httpcore/4.4.14, Apache-2.0, approved, CQ23528 -maven/mavencentral/org.apache.sshd/sshd-common/2.7.0, Apache-2.0 and ISC, approved, CQ23469 -maven/mavencentral/org.apache.sshd/sshd-core/2.7.0, Apache-2.0, approved, CQ23469 -maven/mavencentral/org.apache.sshd/sshd-osgi/2.7.0, Apache-2.0 and ISC, approved, CQ23469 -maven/mavencentral/org.apache.sshd/sshd-sftp/2.7.0, Apache-2.0, approved, CQ23470 +maven/mavencentral/org.apache.sshd/sshd-osgi/2.8.0, Apache-2.0, approved, CQ23892 +maven/mavencentral/org.apache.sshd/sshd-sftp/2.8.0, Apache-2.0, approved, CQ23893 maven/mavencentral/org.assertj/assertj-core/3.20.2, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.bouncycastle/bcpg-jdk15on/1.69, MIT and Apache-2.0, approved, CQ23472 -maven/mavencentral/org.bouncycastle/bcpkix-jdk15on/1.69, MIT, approved, CQ23473 -maven/mavencentral/org.bouncycastle/bcprov-jdk15on/1.69, MIT, approved, CQ23471 -maven/mavencentral/org.bouncycastle/bcutil-jdk15on/1.69, MIT, approved, CQ23474 -maven/mavencentral/org.eclipse.jetty/jetty-http/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jetty/jetty-io/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jetty/jetty-security/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jetty/jetty-server/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jetty/jetty-servlet/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jetty/jetty-util-ajax/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jetty/jetty-util/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.archive/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.gpg.bc/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.apache/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.server/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.http/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.ssh/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.jsch/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ui/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.hamcrest/hamcrest-core/1.3, BSD-2-Clause, approved, CQ7063 -maven/mavencentral/org.hamcrest/hamcrest/2.2, BSD-2-Clause, approved, clearlydefined -maven/mavencentral/org.mockito/mockito-core/2.23.0, MIT, approved, CQ17976 +maven/mavencentral/org.bouncycastle/bcpg-jdk15on/1.70, Apache-2.0, approved, #1713 +maven/mavencentral/org.bouncycastle/bcpkix-jdk15on/1.70, MIT, approved, clearlydefined +maven/mavencentral/org.bouncycastle/bcprov-jdk15on/1.70, MIT, approved, #1712 +maven/mavencentral/org.bouncycastle/bcutil-jdk15on/1.70, MIT, approved, clearlydefined +maven/mavencentral/org.eclipse.jetty.toolchain/jetty-servlet-api/4.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-http/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-io/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-security/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-server/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-servlet/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-util/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.archive/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.gpg.bc/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.apache/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.server/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.http/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.ssh/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.agent/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.jsch/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ui/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.hamcrest/hamcrest-core/1.3, BSD-2-Clause, approved, CQ11429 +maven/mavencentral/org.mockito/mockito-core/2.23.0, Apache-2.0 AND MIT, approved, #958 maven/mavencentral/org.objenesis/objenesis/2.6, Apache-2.0, approved, CQ15478 -maven/mavencentral/org.openjdk.jmh/jmh-core/1.32, GPL-2.0, approved, CQ23499 -maven/mavencentral/org.openjdk.jmh/jmh-generator-annprocess/1.32, GPL-2.0, approved, CQ23500 -maven/mavencentral/org.osgi/org.osgi.core/4.3.1, Apache-2.0, approved, CQ10111 -maven/mavencentral/org.slf4j/jcl-over-slf4j/1.7.30, Apache-2.0, approved, CQ12843 +maven/mavencentral/org.openjdk.jmh/jmh-core/1.32, GPL-2.0-only with Classpath-exception-2.0, approved, #959 +maven/mavencentral/org.openjdk.jmh/jmh-generator-annprocess/1.32, GPL-2.0-only with Classpath-exception-2.0, approved, #962 +maven/mavencentral/org.osgi/org.osgi.core/6.0.0, Apache-2.0, approved, #1794 +maven/mavencentral/org.slf4j/jcl-over-slf4j/1.7.32, Apache-2.0, approved, CQ12843 maven/mavencentral/org.slf4j/slf4j-api/1.7.30, MIT, approved, CQ13368 -maven/mavencentral/org.slf4j/slf4j-log4j12/1.7.30, MIT, approved, CQ7665 +maven/mavencentral/org.slf4j/slf4j-simple/1.7.30, MIT, approved, CQ7952 maven/mavencentral/org.tukaani/xz/1.9, LicenseRef-Public-Domain, approved, CQ23498 diff --git a/org.eclipse.jgit.junit.ssh/pom.xml b/org.eclipse.jgit.junit.ssh/pom.xml index 504c7dcdf..395b14b0a 100644 --- a/org.eclipse.jgit.junit.ssh/pom.xml +++ b/org.eclipse.jgit.junit.ssh/pom.xml @@ -55,6 +55,16 @@ org.apache.sshd sshd-sftp ${apache-sshd-version} + + + org.apache.sshd + sshd-core + + + org.apache.sshd + sshd-common + + diff --git a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java new file mode 100644 index 000000000..98a0712e4 --- /dev/null +++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2022, Matthias Fromme + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lfs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ResetCommand.ResetType; +import org.eclipse.jgit.attributes.FilterCommand; +import org.eclipse.jgit.attributes.FilterCommandRegistry; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lfs.internal.LfsConnectionFactory; +import org.eclipse.jgit.lfs.lib.Constants; +import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.transport.http.HttpConnection; +import org.eclipse.jgit.util.HttpSupport; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Test if the lfs config is used in the correct way during checkout. + * + * Two lfs-files are created, one that comes before .gitattributes and + * .lfsconfig in git order (".aaa.txt") and one that comes after ("zzz.txt"). + * + * During checkout/reset it is tested if the correct version of the lfs config + * is used. + * + * TODO: The current behavior seems a little bit strange/unintuitive. Some files + * are checked out before and some after the config files. This leads to the + * behavior, that during a single command the config changes. Since this seems + * to be the same way in native git, the behavior is accepted for now. + * + */ +public class LfsConfigGitTest extends RepositoryTestCase { + + private static final String SMUDGE_NAME = org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX + + Constants.ATTR_FILTER_DRIVER_PREFIX + + org.eclipse.jgit.lib.Constants.ATTR_FILTER_TYPE_SMUDGE; + + private static final String LFS_SERVER_URI1 = "https://lfs.server1/test/uri"; + + private static final String EXPECTED_SERVER_URL1 = LFS_SERVER_URI1 + + Protocol.OBJECTS_LFS_ENDPOINT; + + private static final String LFS_SERVER_URI2 = "https://lfs.server2/test/uri"; + + private static final String EXPECTED_SERVER_URL2 = LFS_SERVER_URI2 + + Protocol.OBJECTS_LFS_ENDPOINT; + + private static final String LFS_SERVER_URI3 = "https://lfs.server3/test/uri"; + + private static final String EXPECTED_SERVER_URL3 = LFS_SERVER_URI3 + + Protocol.OBJECTS_LFS_ENDPOINT; + + private static final String FAKE_LFS_POINTER1 = "version https://git-lfs.github.com/spec/v1\n" + + "oid sha256:6ce9fab52ee9a6c4c097def4e049c6acdeba44c99d26e83ba80adec1473c9b2d\n" + + "size 253952\n"; + + private static final String FAKE_LFS_POINTER2 = "version https://git-lfs.github.com/spec/v1\n" + + "oid sha256:a4b711cd989863ae2038758a62672138347abbbae4076a7ad3a545fda7d08f82\n" + + "size 67072\n"; + + private static List checkoutURLs = new ArrayList<>(); + + static class SmudgeFilterMock extends FilterCommand { + public SmudgeFilterMock(Repository db, InputStream in, + OutputStream out) throws IOException { + super(in, out); + HttpConnection lfsServerConn = LfsConnectionFactory.getLfsConnection(db, + HttpSupport.METHOD_POST, Protocol.OPERATION_DOWNLOAD); + checkoutURLs.add(lfsServerConn.getURL().toString()); + } + + @Override + public int run() throws IOException { + // Stupid no impl + in.transferTo(out); + return -1; + } + } + + @BeforeClass + public static void installLfs() { + FilterCommandRegistry.register(SMUDGE_NAME, SmudgeFilterMock::new); + } + + @AfterClass + public static void removeLfs() { + FilterCommandRegistry.unregister(SMUDGE_NAME); + } + + private Git git; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + git = new Git(db); + // commit something + writeTrashFile("Test.txt", "Hello world"); + git.add().addFilepattern("Test.txt").call(); + git.commit().setMessage("Initial commit").call(); + // prepare the config for LFS + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "lfs", "smudge", SMUDGE_NAME); + config.setString(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_AUTOCRLF, "false"); + config.save(); + + fileBefore = null; + fileAfter = null; + configFile = null; + gitAttributesFile = null; + } + + File fileBefore; + + File fileAfter; + + File configFile; + + File gitAttributesFile; + + private void createLfsFiles(String lfsPointer) throws Exception { + /* + * FileNames ".aaa.txt" and "zzz.txt" seem to be sufficient to get the + * desired checkout order before and after ".lfsconfig", at least in a + * number of manual tries. Since the files to checkout are contained in + * a set (see DirCacheCheckout::doCheckout) the order cannot be + * guaranteed. + */ + + //File to be checked out before lfs config + String fileNameBefore = ".aaa.txt"; + fileBefore = writeTrashFile(fileNameBefore, lfsPointer); + git.add().addFilepattern(fileNameBefore).call(); + + // File to be checked out after lfs config + String fileNameAfter = "zzz.txt"; + fileAfter = writeTrashFile(fileNameAfter, lfsPointer); + git.add().addFilepattern(fileNameAfter).call(); + + git.commit().setMessage("Commit LFS Pointer files").call(); + } + + + private String addLfsConfigFiles(String lfsServerUrl) throws Exception { + // Add config files to the repo + String lfsConfig1 = createLfsConfig(lfsServerUrl); + git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call(); + // Modify gitattributes on second call, to force checkout too. + if (gitAttributesFile == null) { + gitAttributesFile = writeTrashFile(".gitattributes", + "*.txt filter=lfs"); + } else { + gitAttributesFile = writeTrashFile(".gitattributes", + "*.txt filter=lfs\n"); + } + + git.add().addFilepattern(".gitattributes").call(); + git.commit().setMessage("Commit config files").call(); + return lfsConfig1; + } + + private String createLfsConfig(String lfsServerUrl) throws IOException { + String lfsConfig1 = "[lfs]\n url = " + lfsServerUrl; + configFile = writeTrashFile(Constants.DOT_LFS_CONFIG, lfsConfig1); + return lfsConfig1; + } + + @Test + public void checkoutLfsObjects_reset() throws Exception { + createLfsFiles(FAKE_LFS_POINTER1); + String lfsConfig1 = addLfsConfigFiles(LFS_SERVER_URI1); + + // Delete files to force action on reset + assertTrue(configFile.delete()); + assertTrue(fileBefore.delete()); + assertTrue(fileAfter.delete()); + + assertTrue(gitAttributesFile.delete()); + + // create config file with different url + createLfsConfig(LFS_SERVER_URI3); + + checkoutURLs.clear(); + git.reset().setMode(ResetType.HARD).call(); + + checkFile(configFile, lfsConfig1); + checkFile(fileBefore, FAKE_LFS_POINTER1); + checkFile(fileAfter, FAKE_LFS_POINTER1); + + assertEquals(2, checkoutURLs.size()); + // TODO: Should may be EXPECTED_SERVR_URL1 + assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(0)); + assertEquals(EXPECTED_SERVER_URL1, checkoutURLs.get(1)); + } + + @Test + public void checkoutLfsObjects_BranchSwitch() throws Exception { + // Create a new branch "URL1" and add config files + git.checkout().setCreateBranch(true).setName("URL1").call(); + + createLfsFiles(FAKE_LFS_POINTER1); + String lfsConfig1 = addLfsConfigFiles(LFS_SERVER_URI1); + + // Create a second new branch "URL2" and add config files + git.checkout().setCreateBranch(true).setName("URL2").call(); + + createLfsFiles(FAKE_LFS_POINTER2); + String lfsConfig2 = addLfsConfigFiles(LFS_SERVER_URI2); + + checkFile(configFile, lfsConfig2); + checkFile(fileBefore, FAKE_LFS_POINTER2); + checkFile(fileAfter, FAKE_LFS_POINTER2); + + checkoutURLs.clear(); + git.checkout().setName("URL1").call(); + + checkFile(configFile, lfsConfig1); + checkFile(fileBefore, FAKE_LFS_POINTER1); + checkFile(fileAfter, FAKE_LFS_POINTER1); + + assertEquals(2, checkoutURLs.size()); + // TODO: Should may be EXPECTED_SERVR_URL1 + assertEquals(EXPECTED_SERVER_URL2, checkoutURLs.get(0)); + assertEquals(EXPECTED_SERVER_URL1, checkoutURLs.get(1)); + + checkoutURLs.clear(); + git.checkout().setName("URL2").call(); + + checkFile(configFile, lfsConfig2); + checkFile(fileBefore, FAKE_LFS_POINTER2); + checkFile(fileAfter, FAKE_LFS_POINTER2); + + assertEquals(2, checkoutURLs.size()); + // TODO: Should may be EXPECTED_SERVR_URL2 + assertEquals(EXPECTED_SERVER_URL1, checkoutURLs.get(0)); + assertEquals(EXPECTED_SERVER_URL2, checkoutURLs.get(1)); + } + + @Test + public void checkoutLfsObjects_BranchSwitch_ModifiedLocal() + throws Exception { + + // Create a new branch "URL1" and add config files + git.checkout().setCreateBranch(true).setName("URL1").call(); + + createLfsFiles(FAKE_LFS_POINTER1); + addLfsConfigFiles(LFS_SERVER_URI1); + + // Create a second new branch "URL2" and add config files + git.checkout().setCreateBranch(true).setName("URL2").call(); + + createLfsFiles(FAKE_LFS_POINTER2); + addLfsConfigFiles(LFS_SERVER_URI1); + + // create config file with different url + assertTrue(configFile.delete()); + String lfsConfig3 = createLfsConfig(LFS_SERVER_URI3); + + checkFile(configFile, lfsConfig3); + checkFile(fileBefore, FAKE_LFS_POINTER2); + checkFile(fileAfter, FAKE_LFS_POINTER2); + + checkoutURLs.clear(); + git.checkout().setName("URL1").call(); + + checkFile(fileBefore, FAKE_LFS_POINTER1); + checkFile(fileAfter, FAKE_LFS_POINTER1); + checkFile(configFile, lfsConfig3); + + assertEquals(2, checkoutURLs.size()); + + assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(0)); + assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(1)); + + checkoutURLs.clear(); + git.checkout().setName("URL2").call(); + + checkFile(fileBefore, FAKE_LFS_POINTER2); + checkFile(fileAfter, FAKE_LFS_POINTER2); + checkFile(configFile, lfsConfig3); + + assertEquals(2, checkoutURLs.size()); + assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(0)); + assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(1)); + } +} diff --git a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java index 8964310e4..3e83c8ef4 100644 --- a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java +++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021, Thomas Wolf and others + * Copyright (C) 2021, 2022 Thomas Wolf and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -67,6 +67,27 @@ public void setUp() throws Exception { config.save(); } + @Test + public void testBranchSwitch() throws Exception { + git.branchCreate().setName("abranch").call(); + git.checkout().setName("abranch").call(); + File aFile = writeTrashFile("a.bin", "aaa"); + writeTrashFile(".gitattributes", "a.bin filter=lfs"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("acommit").call(); + git.checkout().setName("master").call(); + git.branchCreate().setName("bbranch").call(); + git.checkout().setName("bbranch").call(); + File bFile = writeTrashFile("b.bin", "bbb"); + writeTrashFile(".gitattributes", "b.bin filter=lfs"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("bcommit").call(); + git.checkout().setName("abranch").call(); + checkFile(aFile, "aaa"); + git.checkout().setName("bbranch").call(); + checkFile(bFile, "bbb"); + } + @Test public void checkoutNonLfsPointer() throws Exception { String content = "size_t\nsome_function(void* ptr);\n"; diff --git a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java index c7bd48e12..badcb7d7e 100644 --- a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java +++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java @@ -9,10 +9,15 @@ */ package org.eclipse.jgit.lfs.internal; +import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; -import java.util.TreeMap; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.RemoteAddCommand; @@ -27,6 +32,8 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.http.HttpConnection; +import org.eclipse.jgit.util.HttpSupport; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -42,6 +49,12 @@ public class LfsConnectionFactoryTest extends RepositoryTestCase { + Constants.ATTR_FILTER_DRIVER_PREFIX + org.eclipse.jgit.lib.Constants.ATTR_FILTER_TYPE_CLEAN; + private final static String LFS_SERVER_URL1 = "https://lfs.server1/test/uri"; + + private final static String LFS_SERVER_URL2 = "https://lfs.server2/test/uri"; + + private final static String ORIGIN_URL = "https://git.server/test/uri"; + private Git git; @BeforeClass @@ -61,26 +74,23 @@ public static void removeLfs() { public void setUp() throws Exception { super.setUp(); git = new Git(db); + + // Just to have a non empty repo + writeTrashFile("Test.txt", "Hello world from the LFS Factory Test"); + git.add().addFilepattern("Test.txt").call(); + git.commit().setMessage("Initial commit").call(); } @Test public void lfsUrlFromRemoteUrlWithDotGit() throws Exception { addRemoteUrl("https://localhost/repo.git"); - - String lfsUrl = LfsConnectionFactory.getLfsUrl(db, - Protocol.OPERATION_DOWNLOAD, - new TreeMap<>()); - assertEquals("https://localhost/repo.git/info/lfs", lfsUrl); + checkLfsUrl("https://localhost/repo.git/info/lfs"); } @Test public void lfsUrlFromRemoteUrlWithoutDotGit() throws Exception { addRemoteUrl("https://localhost/repo"); - - String lfsUrl = LfsConnectionFactory.getLfsUrl(db, - Protocol.OPERATION_DOWNLOAD, - new TreeMap<>()); - assertEquals("https://localhost/repo.git/info/lfs", lfsUrl); + checkLfsUrl("https://localhost/repo.git/info/lfs"); } @Test @@ -94,10 +104,7 @@ public void lfsUrlFromLocalConfig() throws Exception { "https://localhost/repo/lfs"); cfg.save(); - String lfsUrl = LfsConnectionFactory.getLfsUrl(db, - Protocol.OPERATION_DOWNLOAD, - new TreeMap<>()); - assertEquals("https://localhost/repo/lfs", lfsUrl); + checkLfsUrl("https://localhost/repo/lfs"); } @Test @@ -111,16 +118,136 @@ public void lfsUrlFromOriginConfig() throws Exception { "https://localhost/repo/lfs"); cfg.save(); - String lfsUrl = LfsConnectionFactory.getLfsUrl(db, - Protocol.OPERATION_DOWNLOAD, - new TreeMap<>()); - assertEquals("https://localhost/repo/lfs", lfsUrl); + checkLfsUrl("https://localhost/repo/lfs"); } @Test public void lfsUrlNotConfigured() throws Exception { - assertThrows(LfsConfigInvalidException.class, () -> LfsConnectionFactory - .getLfsUrl(db, Protocol.OPERATION_DOWNLOAD, new TreeMap<>())); + assertThrows(LfsConfigInvalidException.class, + () -> LfsConnectionFactory.getLfsConnection(db, + HttpSupport.METHOD_POST, Protocol.OPERATION_DOWNLOAD)); + } + + @Test + public void checkGetLfsConnection_lfsurl_lfsconfigFromWorkingDir() + throws Exception { + writeLfsConfig(); + checkLfsUrl(LFS_SERVER_URL1); + } + + @Test + public void checkGetLfsConnection_lfsurl_lfsconfigFromIndex() + throws Exception { + writeLfsConfig(); + git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call(); + deleteTrashFile(Constants.DOT_LFS_CONFIG); + checkLfsUrl(LFS_SERVER_URL1); + } + + @Test + public void checkGetLfsConnection_lfsurl_lfsconfigFromHEAD() + throws Exception { + writeLfsConfig(); + git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call(); + git.commit().setMessage("Commit LFS Config").call(); + + /* + * reading .lfsconfig from HEAD seems only testable using a bare repo, + * since otherwise working tree or index are used + */ + File directory = createTempDirectory("testBareRepo"); + try (Repository bareRepoDb = Git.cloneRepository() + .setDirectory(directory) + .setURI(db.getDirectory().toURI().toString()).setBare(true) + .call().getRepository()) { + + checkLfsUrl(LFS_SERVER_URL1); + } + } + + @Test + public void checkGetLfsConnection_remote_lfsconfigFromWorkingDir() + throws Exception { + addRemoteUrl(ORIGIN_URL); + writeLfsConfig(LFS_SERVER_URL1, "lfs", DEFAULT_REMOTE_NAME, "url"); + checkLfsUrl(LFS_SERVER_URL1); + } + + /** + * Test the config file precedence. + * + * Checking only with the local repository config is sufficient since from + * that point the "normal" precedence is used. + * + * @throws Exception + */ + @Test + public void checkGetLfsConnection_ConfigFilePrecedence_lfsconfigFromWorkingDir() + throws Exception { + writeLfsConfig(); + checkLfsUrl(LFS_SERVER_URL1); + + StoredConfig config = git.getRepository().getConfig(); + config.setString(ConfigConstants.CONFIG_SECTION_LFS, null, + ConfigConstants.CONFIG_KEY_URL, LFS_SERVER_URL2); + config.save(); + + checkLfsUrl(LFS_SERVER_URL2); + } + + @Test + public void checkGetLfsConnection_InvalidLfsConfig_WorkingDir() + throws Exception { + writeInvalidLfsConfig(); + LfsConfigInvalidException actualException = assertThrows( + LfsConfigInvalidException.class, () -> { + LfsConnectionFactory.getLfsConnection(db, HttpSupport.METHOD_POST, + Protocol.OPERATION_DOWNLOAD); + }); + assertTrue(getStackTrace(actualException) + .contains("Invalid line in config file")); + } + + @Test + public void checkGetLfsConnection_InvalidLfsConfig_Index() + throws Exception { + writeInvalidLfsConfig(); + git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call(); + deleteTrashFile(Constants.DOT_LFS_CONFIG); + LfsConfigInvalidException actualException = assertThrows( + LfsConfigInvalidException.class, () -> { + LfsConnectionFactory.getLfsConnection(db, HttpSupport.METHOD_POST, + Protocol.OPERATION_DOWNLOAD); + }); + assertTrue(getStackTrace(actualException) + .contains("Invalid line in config file")); + } + + @Test + public void checkGetLfsConnection_InvalidLfsConfig_HEAD() throws Exception { + writeInvalidLfsConfig(); + git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call(); + git.commit().setMessage("Commit LFS Config").call(); + + /* + * reading .lfsconfig from HEAD seems only testable using a bare repo, + * since otherwise working tree or index are used + */ + File directory = createTempDirectory("testBareRepo"); + try (Repository bareRepoDb = Git.cloneRepository() + .setDirectory(directory) + .setURI(db.getDirectory().toURI().toString()).setBare(true) + .call().getRepository()) { + LfsConfigInvalidException actualException = assertThrows( + LfsConfigInvalidException.class, + () -> { + LfsConnectionFactory.getLfsConnection(db, + HttpSupport.METHOD_POST, + Protocol.OPERATION_DOWNLOAD); + }); + assertTrue(getStackTrace(actualException) + .contains("Invalid line in config file")); + } } private void addRemoteUrl(String remotUrl) throws Exception { @@ -129,4 +256,62 @@ private void addRemoteUrl(String remotUrl) throws Exception { add.setName(org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME); add.call(); } + + /** + * Returns the stack trace of the provided exception as string + * + * @param actualException + * @return The exception stack trace as string + */ + private String getStackTrace(Exception actualException) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + actualException.printStackTrace(pw); + return sw.toString(); + } + + private void writeLfsConfig() throws IOException { + writeLfsConfig(LFS_SERVER_URL1, "lfs", "url"); + } + + private void writeLfsConfig(String lfsUrl, String section, String name) + throws IOException { + writeLfsConfig(lfsUrl, section, null, name); + } + + /* + * Write simple lfs config with single entry. Do not use FileBasedConfig to + * avoid introducing new dependency (for now). + */ + private void writeLfsConfig(String lfsUrl, String section, + String subsection, String name) throws IOException { + StringBuilder config = new StringBuilder(); + config.append("["); + config.append(section); + if (subsection != null) { + config.append(" \""); + config.append(subsection); + config.append("\""); + } + config.append("]\n"); + config.append(" "); + config.append(name); + config.append(" = "); + config.append(lfsUrl); + writeTrashFile(Constants.DOT_LFS_CONFIG, config.toString()); + } + + private void writeInvalidLfsConfig() throws IOException { + writeTrashFile(Constants.DOT_LFS_CONFIG, + "{lfs]\n url = " + LFS_SERVER_URL1); + } + + private void checkLfsUrl(String lfsUrl) throws IOException { + HttpConnection lfsServerConn; + lfsServerConn = LfsConnectionFactory.getLfsConnection(db, + HttpSupport.METHOD_POST, Protocol.OPERATION_DOWNLOAD); + + assertEquals(lfsUrl + Protocol.OBJECTS_LFS_ENDPOINT, + lfsServerConn.getURL().toString()); + } } diff --git a/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties b/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties index 0e00f146a..642b83db4 100644 --- a/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties +++ b/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties @@ -1,19 +1,19 @@ corruptLongObject=The content hash ''{0}'' of the long object ''{1}'' doesn''t match its id, the corrupt object will be deleted. -incorrectLONG_OBJECT_ID_LENGTH=Incorrect LONG_OBJECT_ID_LENGTH. -inconsistentMediafileLength=Mediafile {0} has unexpected length; expected {1} but found {2}. +dotLfsConfigReadFailed=Reading .lfsconfig failed inconsistentContentLength=Unexpected content length reported by LFS server ({0}), expected {1} but reported was {2} +inconsistentMediafileLength=Mediafile {0} has unexpected length; expected {1} but found {2}. +incorrectLONG_OBJECT_ID_LENGTH=Incorrect LONG_OBJECT_ID_LENGTH. invalidLongId=Invalid id: {0} invalidLongIdLength=Invalid id length {0}; should be {1} -lfsUnavailable=LFS is not available for repository {0} -protocolError=LFS Protocol Error {0}: {1} -requiredHashFunctionNotAvailable=Required hash function {0} not available. -repositoryNotFound=Repository {0} not found -repositoryReadOnly=Repository {0} is read-only -lfsUnavailable=LFS is not available for repository {0} -lfsUnathorized=Not authorized to perform operation {0} on repository {1} lfsFailedToGetRepository=failed to get repository {0} lfsNoDownloadUrl="Need to download object from LFS server but couldn't determine LFS server URL" +lfsUnauthorized=Not authorized to perform operation {0} on repository {1} +lfsUnavailable=LFS is not available for repository {0} +missingLocalObject="Local Object {0} is missing" +protocolError=LFS Protocol Error {0}: {1} +repositoryNotFound=Repository {0} not found +repositoryReadOnly=Repository {0} is read-only +requiredHashFunctionNotAvailable=Required hash function {0} not available. serverFailure=When trying to open a connection to {0} the server responded with an error code. rc={1} -wrongAmoutOfDataReceived=While downloading data from the content server {0} {1} bytes have been received while {2} have been expected userConfigInvalid="User config file {0} invalid {1}" -missingLocalObject="Local Object {0} is missing" \ No newline at end of file +wrongAmountOfDataReceived=While downloading data from the content server {0} {1} bytes have been received while {2} have been expected \ No newline at end of file diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java index 341188756..c26a1bfbb 100644 --- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java @@ -205,7 +205,7 @@ public static Collection downloadLfsResource(Lfs lfs, Repository db, long bytesCopied = Files.copy(contentIn, path); if (bytesCopied != o.size) { throw new IOException(MessageFormat.format( - LfsText.get().wrongAmoutOfDataReceived, + LfsText.get().wrongAmountOfDataReceived, contentServerConn.getURL(), Long.valueOf(bytesCopied), Long.valueOf(o.size))); diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java index 36889db8a..0dc6aeab2 100644 --- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java @@ -31,7 +31,7 @@ public class LfsUnauthorized extends LfsException { * the repository name. */ public LfsUnauthorized(String operation, String name) { - super(MessageFormat.format(LfsText.get().lfsUnathorized, operation, + super(MessageFormat.format(LfsText.get().lfsUnauthorized, operation, name)); } } diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java new file mode 100644 index 000000000..71d395ca8 --- /dev/null +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2022, Matthias Fromme + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lfs.internal; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lfs.errors.LfsConfigInvalidException; +import org.eclipse.jgit.lfs.lib.Constants; +import org.eclipse.jgit.lib.BlobBasedConfig; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.treewalk.TreeWalk; + +import static org.eclipse.jgit.lib.Constants.HEAD; + +/** + * Encapsulate access to the .lfsconfig. + * + * According to the document + * https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-config.5.ronn + * the order to find the .lfsconfig file is: + * + *
+ *   1. in the root of the working tree
+ *   2. in the index
+ *   3. in the HEAD, for bare repositories this is the only place
+ *      that is searched
+ * 
+ * + * Values from the .lfsconfig are used only if not specified in another git + * config file to allow local override without modifiction of a committed file. + */ +public class LfsConfig { + private Repository db; + private Config delegate; + + /** + * Create a new instance of the LfsConfig. + * + * @param db + * the associated repo + * @throws IOException + */ + public LfsConfig(Repository db) throws IOException { + this.db = db; + delegate = this.load(); + } + + /** + * Read the .lfsconfig file from the repository + * + * @return The loaded lfs config or null if it does not exist + * + * @throws IOException + */ + private Config load() throws IOException { + Config result = null; + + if (!db.isBare()) { + result = loadFromWorkingTree(); + if (result == null) { + result = loadFromIndex(); + } + } + + if (result == null) { + result = loadFromHead(); + } + + if (result == null) { + result = emptyConfig(); + } + + return result; + } + + /** + * Try to read the lfs config from a file called .lfsconfig at the top level + * of the working tree. + * + * @return the config, or null + * @throws IOException + */ + @Nullable + private Config loadFromWorkingTree() + throws IOException { + File lfsConfig = db.getFS().resolve(db.getWorkTree(), + Constants.DOT_LFS_CONFIG); + if (lfsConfig.exists() && lfsConfig.isFile()) { + FileBasedConfig config = new FileBasedConfig(lfsConfig, db.getFS()); + try { + config.load(); + return config; + } catch (ConfigInvalidException e) { + throw new LfsConfigInvalidException( + LfsText.get().dotLfsConfigReadFailed, e); + } + } + return null; + } + + /** + * Try to read the lfs config from an entry called .lfsconfig contained in + * the index. + * + * @return the config, or null if the entry does not exist + * @throws IOException + */ + @Nullable + private Config loadFromIndex() + throws IOException { + try { + DirCacheEntry entry = db.readDirCache() + .getEntry(Constants.DOT_LFS_CONFIG); + if (entry != null) { + return new BlobBasedConfig(null, db, entry.getObjectId()); + } + } catch (ConfigInvalidException e) { + throw new LfsConfigInvalidException( + LfsText.get().dotLfsConfigReadFailed, e); + } + return null; + } + + /** + * Try to read the lfs config from an entry called .lfsconfig contained in + * the head revision. + * + * @return the config, or null if the file does not exist + * @throws IOException + */ + @Nullable + private Config loadFromHead() throws IOException { + try (RevWalk revWalk = new RevWalk(db)) { + ObjectId headCommitId = db.resolve(HEAD); + if (headCommitId == null) { + return null; + } + RevCommit commit = revWalk.parseCommit(headCommitId); + RevTree tree = commit.getTree(); + TreeWalk treewalk = TreeWalk.forPath(db, Constants.DOT_LFS_CONFIG, + tree); + if (treewalk != null) { + return new BlobBasedConfig(null, db, treewalk.getObjectId(0)); + } + } catch (ConfigInvalidException e) { + throw new LfsConfigInvalidException( + LfsText.get().dotLfsConfigReadFailed, e); + } + return null; + } + + /** + * Create an empty config as fallback to avoid null pointer checks. + * + * @return an empty config + */ + private Config emptyConfig() { + return new Config(); + } + + /** + * Get string value or null if not found. + * + * First tries to find the value in the git config files. If not found tries + * to find data in .lfsconfig. + * + * @param section + * the section + * @param subsection + * the subsection for the value + * @param name + * the key name + * @return a String value from the config, null if not found + */ + public String getString(final String section, final String subsection, + final String name) { + String result = db.getConfig().getString(section, subsection, name); + if (result == null) { + result = delegate.getString(section, subsection, name); + } + return result; + } +} diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java index 5a17d411c..12b688d15 100644 --- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java @@ -45,7 +45,6 @@ * Provides means to get a valid LFS connection for a given repository. */ public class LfsConnectionFactory { - private static final int SSH_AUTH_TIMEOUT_SECONDS = 30; private static final String SCHEME_HTTPS = "https"; //$NON-NLS-1$ private static final String SCHEME_SSH = "ssh"; //$NON-NLS-1$ @@ -104,19 +103,19 @@ public static HttpConnection getLfsConnection(Repository db, String method, * additional headers that can be used to connect to LFS server * @return the URL for the LFS server. e.g. * "https://github.com/github/git-lfs.git/info/lfs" - * @throws LfsConfigInvalidException - * if the LFS config is invalid + * @throws IOException + * if the LFS config is invalid or cannot be accessed * @see * Server Discovery documentation */ - static String getLfsUrl(Repository db, String purpose, + private static String getLfsUrl(Repository db, String purpose, Map additionalHeaders) - throws LfsConfigInvalidException { - StoredConfig config = db.getConfig(); + throws IOException { + LfsConfig config = new LfsConfig(db); String lfsUrl = config.getString(ConfigConstants.CONFIG_SECTION_LFS, - null, - ConfigConstants.CONFIG_KEY_URL); + null, ConfigConstants.CONFIG_KEY_URL); + Exception ex = null; if (lfsUrl == null) { String remoteUrl = null; @@ -124,6 +123,7 @@ static String getLfsUrl(Repository db, String purpose, lfsUrl = config.getString(ConfigConstants.CONFIG_SECTION_LFS, remote, ConfigConstants.CONFIG_KEY_URL); + // This could be done better (more precise logic), but according // to https://github.com/git-lfs/git-lfs/issues/1759 git-lfs // generally only supports 'origin' in an integrated workflow. diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java index 1ca37a9f6..06234c1d9 100644 --- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java @@ -28,21 +28,22 @@ public static LfsText get() { // @formatter:off /***/ public String corruptLongObject; - /***/ public String inconsistentMediafileLength; + /***/ public String dotLfsConfigReadFailed; /***/ public String inconsistentContentLength; + /***/ public String inconsistentMediafileLength; /***/ public String incorrectLONG_OBJECT_ID_LENGTH; /***/ public String invalidLongId; /***/ public String invalidLongIdLength; - /***/ public String lfsUnavailable; - /***/ public String protocolError; - /***/ public String requiredHashFunctionNotAvailable; - /***/ public String repositoryNotFound; - /***/ public String repositoryReadOnly; - /***/ public String lfsUnathorized; /***/ public String lfsFailedToGetRepository; /***/ public String lfsNoDownloadUrl; - /***/ public String serverFailure; - /***/ public String wrongAmoutOfDataReceived; - /***/ public String userConfigInvalid; + /***/ public String lfsUnauthorized; + /***/ public String lfsUnavailable; /***/ public String missingLocalObject; + /***/ public String protocolError; + /***/ public String repositoryNotFound; + /***/ public String repositoryReadOnly; + /***/ public String requiredHashFunctionNotAvailable; + /***/ public String serverFailure; + /***/ public String userConfigInvalid; + /***/ public String wrongAmountOfDataReceived; } diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java index 3212a6350..9b41ec31f 100644 --- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java @@ -81,6 +81,13 @@ public final class Constants { */ public static final String ATTR_FILTER_DRIVER_PREFIX = "lfs/"; + /** + * Config file name for lfs specific configuration + * + * @since 6.1 + */ + public static final String DOT_LFS_CONFIG = ".lfsconfig"; + /** * Create a new digest function for objects. * diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java index 1a1f5b487..6f7aa63ed 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java @@ -10,6 +10,7 @@ package org.eclipse.jgit.api; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -392,28 +393,64 @@ public void testPushDefaultMatching() throws Exception { git.add().addFilepattern("f").call(); RevCommit commit = git.commit().setMessage("adding f").call(); - git.checkout().setName("also-pushed").setCreateBranch(true).call(); + git.checkout().setName("not-pushed").setCreateBranch(true).call(); git.checkout().setName("branchtopush").setCreateBranch(true).call(); assertEquals(null, git2.getRepository().resolve("refs/heads/branchtopush")); assertEquals(null, - git2.getRepository().resolve("refs/heads/also-pushed")); + git2.getRepository().resolve("refs/heads/not-pushed")); assertEquals(null, git2.getRepository().resolve("refs/heads/master")); - git.push().setRemote("test").setPushDefault(PushDefault.MATCHING) + // push master and branchtopush + git.push().setRemote("test").setRefSpecs( + new RefSpec("refs/heads/master:refs/heads/master"), + new RefSpec( + "refs/heads/branchtopush:refs/heads/branchtopush")) .call(); - assertEquals(commit.getId(), - git2.getRepository().resolve("refs/heads/branchtopush")); - assertEquals(commit.getId(), - git2.getRepository().resolve("refs/heads/also-pushed")); assertEquals(commit.getId(), git2.getRepository().resolve("refs/heads/master")); - assertEquals(commit.getId(), git.getRepository() - .resolve("refs/remotes/origin/branchtopush")); - assertEquals(commit.getId(), git.getRepository() - .resolve("refs/remotes/origin/also-pushed")); assertEquals(commit.getId(), + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + // Create two different commits on these two branches + writeTrashFile("b", "on branchtopush"); + git.add().addFilepattern("b").call(); + RevCommit bCommit = git.commit().setMessage("on branchtopush") + .call(); + git.checkout().setName("master").call(); + writeTrashFile("m", "on master"); + git.add().addFilepattern("m").call(); + RevCommit mCommit = git.commit().setMessage("on master").call(); + // Now push with mode "matching": should push both branches. + Iterable result = git.push().setRemote("test") + .setPushDefault(PushDefault.MATCHING) + .call(); + int n = 0; + for (PushResult r : result) { + n++; + assertEquals(1, n); + assertEquals(2, r.getRemoteUpdates().size()); + for (RemoteRefUpdate update : r.getRemoteUpdates()) { + assertFalse(update.isMatching()); + assertTrue(update.getSrcRef() + .equals("refs/heads/branchtopush") + || update.getSrcRef().equals("refs/heads/master")); + assertEquals(RemoteRefUpdate.Status.OK, update.getStatus()); + } + } + assertEquals(bCommit.getId(), + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(mCommit.getId(), + git2.getRepository().resolve("refs/heads/master")); + assertEquals(bCommit.getId(), git.getRepository() + .resolve("refs/remotes/origin/branchtopush")); + assertEquals(null, git.getRepository() + .resolve("refs/remotes/origin/not-pushed")); + assertEquals(mCommit.getId(), git.getRepository().resolve("refs/remotes/origin/master")); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java index 5311edb0e..19281f6c9 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java @@ -21,8 +21,10 @@ import org.eclipse.jgit.api.errors.NoFilepatternException; import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Sets; import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.util.FS; import org.junit.Test; @@ -181,4 +183,31 @@ public void testFolderPrefix() throws Exception { } } + @Test + public void testNestedCommittedGitRepoAndPathFilter() throws Exception { + commitFile("file.txt", "file", "master"); + try (Repository inner = new FileRepositoryBuilder() + .setWorkTree(new File(db.getWorkTree(), "subgit")).build()) { + inner.create(); + writeTrashFile("subgit/sub.txt", "sub"); + try (Git outerGit = new Git(db); Git innerGit = new Git(inner)) { + innerGit.add().addFilepattern("sub.txt").call(); + innerGit.commit().setMessage("Inner commit").call(); + outerGit.add().addFilepattern("subgit").call(); + outerGit.commit().setMessage("Outer commit").call(); + assertTrue(innerGit.status().call().isClean()); + assertTrue(outerGit.status().call().isClean()); + writeTrashFile("subgit/sub.txt", "sub2"); + assertFalse(innerGit.status().call().isClean()); + assertFalse(outerGit.status().call().isClean()); + assertTrue( + outerGit.status().addPath("file.txt").call().isClean()); + assertTrue(outerGit.status().addPath("doesntexist").call() + .isClean()); + assertFalse( + outerGit.status().addPath("subgit").call().isClean()); + } + } + } + } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java index 692885962..2e8b30f15 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java @@ -14,14 +14,19 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.hooks.PrePushHook; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ProgressMonitor; @@ -31,6 +36,7 @@ import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase; import org.eclipse.jgit.transport.RemoteRefUpdate.Status; +import org.eclipse.jgit.util.io.NullOutputStream; import org.junit.Before; import org.junit.Test; @@ -220,7 +226,17 @@ public void testUpdateUnexpectedRemoteVsForce() throws IOException { .fromString("0000000000000000000000000000000000000001")); final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("ac7e7e44c1885efb472ad54a78327d66bfc4ecef")); - testOneUpdateStatus(rru, ref, Status.REJECTED_REMOTE_CHANGED, null); + try (ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bytes, true, + StandardCharsets.UTF_8); + PrintStream err = new PrintStream(NullOutputStream.INSTANCE)) { + MockPrePushHook hook = new MockPrePushHook(db, out, err); + testOneUpdateStatus(rru, ref, Status.REJECTED_REMOTE_CHANGED, null, + hook); + out.flush(); + String result = new String(bytes.toString(StandardCharsets.UTF_8)); + assertEquals("", result); + } } /** @@ -256,10 +272,22 @@ public void testUpdateMixedCases() throws IOException { refUpdates.add(rruOk); refUpdates.add(rruReject); advertisedRefs.add(refToChange); - executePush(); - assertEquals(Status.OK, rruOk.getStatus()); - assertTrue(rruOk.isFastForward()); - assertEquals(Status.NON_EXISTING, rruReject.getStatus()); + try (ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bytes, true, + StandardCharsets.UTF_8); + PrintStream err = new PrintStream(NullOutputStream.INSTANCE)) { + MockPrePushHook hook = new MockPrePushHook(db, out, err); + executePush(hook); + assertEquals(Status.OK, rruOk.getStatus()); + assertTrue(rruOk.isFastForward()); + assertEquals(Status.NON_EXISTING, rruReject.getStatus()); + out.flush(); + String result = new String(bytes.toString(StandardCharsets.UTF_8)); + assertEquals( + "null 0000000000000000000000000000000000000000 " + + "refs/heads/master 2c349335b7f797072cf729c4f3bb0914ecb6dec9\n", + result); + } } /** @@ -346,10 +374,18 @@ private PushResult testOneUpdateStatus(final RemoteRefUpdate rru, final Ref advertisedRef, final Status expectedStatus, Boolean fastForward) throws NotSupportedException, TransportException { + return testOneUpdateStatus(rru, advertisedRef, expectedStatus, + fastForward, null); + } + + private PushResult testOneUpdateStatus(final RemoteRefUpdate rru, + final Ref advertisedRef, final Status expectedStatus, + Boolean fastForward, PrePushHook hook) + throws NotSupportedException, TransportException { refUpdates.add(rru); if (advertisedRef != null) advertisedRefs.add(advertisedRef); - final PushResult result = executePush(); + final PushResult result = executePush(hook); assertEquals(expectedStatus, rru.getStatus()); if (fastForward != null) assertEquals(fastForward, Boolean.valueOf(rru.isFastForward())); @@ -358,7 +394,12 @@ private PushResult testOneUpdateStatus(final RemoteRefUpdate rru, private PushResult executePush() throws NotSupportedException, TransportException { - process = new PushProcess(transport, refUpdates); + return executePush(null); + } + + private PushResult executePush(PrePushHook hook) + throws NotSupportedException, TransportException { + process = new PushProcess(transport, refUpdates, hook); return process.execute(new TextProgressMonitor()); } @@ -416,4 +457,20 @@ public void push(ProgressMonitor monitor, } } } + + private static class MockPrePushHook extends PrePushHook { + + private final PrintStream output; + + public MockPrePushHook(Repository repo, PrintStream out, + PrintStream err) { + super(repo, out, err); + output = out; + } + + @Override + protected void doRun() throws AbortedByHookException, IOException { + output.print(getStdinArgs()); + } + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java index 5569bca23..b56308cb7 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java @@ -466,4 +466,18 @@ public void onlyWildCard() { assertTrue(a.matchSource("refs/heads/master")); assertNull(a.getDestination()); } + + @Test + public void matching() { + RefSpec a = new RefSpec(":"); + assertTrue(a.isMatching()); + assertFalse(a.isForceUpdate()); + } + + @Test + public void matchingForced() { + RefSpec a = new RefSpec("+:"); + assertTrue(a.isMatching()); + assertTrue(a.isForceUpdate()); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java index 36f94fbd2..89d31c3e8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016, Christian Halstrick and others + * Copyright (C) 2016, 2022 Christian Halstrick and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -10,12 +10,17 @@ package org.eclipse.jgit.util; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.HashSet; +import java.util.Set; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.attributes.FilterCommand; import org.eclipse.jgit.attributes.FilterCommandFactory; @@ -86,6 +91,14 @@ public void setUp() throws Exception { secondCommit = git.commit().setMessage("Second commit").call(); } + @Override + public void tearDown() throws Exception { + Set existingFilters = new HashSet<>( + FilterCommandRegistry.getRegisteredFilterCommands()); + existingFilters.forEach(FilterCommandRegistry::unregister); + super.tearDown(); + } + @Test public void testBuiltinCleanFilter() throws IOException, GitAPIException { @@ -217,4 +230,133 @@ public void testBuiltinCleanAndSmudgeFilter() throws IOException, GitAPIExceptio config.save(); } + @Test + public void testBranchSwitch() throws Exception { + String builtinCommandPrefix = "jgit://builtin/test/"; + FilterCommandRegistry.register(builtinCommandPrefix + "smudge", + new TestCommandFactory('s')); + FilterCommandRegistry.register(builtinCommandPrefix + "clean", + new TestCommandFactory('c')); + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "test", "smudge", + builtinCommandPrefix + "smudge"); + config.setString("filter", "test", "clean", + builtinCommandPrefix + "clean"); + config.save(); + // We're on the test branch + File aFile = writeTrashFile("a.txt", "a"); + writeTrashFile(".gitattributes", "a.txt filter=test"); + File cFile = writeTrashFile("cc/c.txt", "C"); + writeTrashFile("cc/.gitattributes", "c.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On test").call(); + git.checkout().setName("master").call(); + git.branchCreate().setName("other").call(); + git.checkout().setName("other").call(); + writeTrashFile("b.txt", "b"); + writeTrashFile(".gitattributes", "b.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On other").call(); + git.checkout().setName("test").call(); + checkFile(aFile, "scsa"); + checkFile(cFile, "scsC"); + } + + @Test + public void testCheckoutSingleFile() throws Exception { + String builtinCommandPrefix = "jgit://builtin/test/"; + FilterCommandRegistry.register(builtinCommandPrefix + "smudge", + new TestCommandFactory('s')); + FilterCommandRegistry.register(builtinCommandPrefix + "clean", + new TestCommandFactory('c')); + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "test", "smudge", + builtinCommandPrefix + "smudge"); + config.setString("filter", "test", "clean", + builtinCommandPrefix + "clean"); + config.save(); + // We're on the test branch + File aFile = writeTrashFile("a.txt", "a"); + File attributes = writeTrashFile(".gitattributes", "a.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On test").call(); + git.checkout().setName("master").call(); + git.branchCreate().setName("other").call(); + git.checkout().setName("other").call(); + writeTrashFile("b.txt", "b"); + writeTrashFile(".gitattributes", "b.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On other").call(); + git.checkout().setName("master").call(); + assertFalse(aFile.exists()); + assertFalse(attributes.exists()); + git.checkout().setStartPoint("test").addPath("a.txt").call(); + checkFile(aFile, "scsa"); + } + + @Test + public void testCheckoutSingleFile2() throws Exception { + String builtinCommandPrefix = "jgit://builtin/test/"; + FilterCommandRegistry.register(builtinCommandPrefix + "smudge", + new TestCommandFactory('s')); + FilterCommandRegistry.register(builtinCommandPrefix + "clean", + new TestCommandFactory('c')); + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "test", "smudge", + builtinCommandPrefix + "smudge"); + config.setString("filter", "test", "clean", + builtinCommandPrefix + "clean"); + config.save(); + // We're on the test branch + File aFile = writeTrashFile("a.txt", "a"); + File attributes = writeTrashFile(".gitattributes", "a.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On test").call(); + git.checkout().setName("master").call(); + git.branchCreate().setName("other").call(); + git.checkout().setName("other").call(); + writeTrashFile("b.txt", "b"); + writeTrashFile(".gitattributes", "b.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On other").call(); + git.checkout().setName("master").call(); + assertFalse(aFile.exists()); + assertFalse(attributes.exists()); + writeTrashFile(".gitattributes", ""); + git.checkout().setStartPoint("test").addPath("a.txt").call(); + checkFile(aFile, "scsa"); + } + + @Test + public void testMerge() throws Exception { + String builtinCommandPrefix = "jgit://builtin/test/"; + FilterCommandRegistry.register(builtinCommandPrefix + "smudge", + new TestCommandFactory('s')); + FilterCommandRegistry.register(builtinCommandPrefix + "clean", + new TestCommandFactory('c')); + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "test", "smudge", + builtinCommandPrefix + "smudge"); + config.setString("filter", "test", "clean", + builtinCommandPrefix + "clean"); + config.save(); + // We're on the test branch. Set up two branches that are expected to + // merge cleanly. + File aFile = writeTrashFile("a.txt", "a"); + writeTrashFile(".gitattributes", "a.txt filter=test"); + git.add().addFilepattern(".").call(); + RevCommit aCommit = git.commit().setMessage("On test").call(); + git.checkout().setName("master").call(); + assertFalse(aFile.exists()); + git.branchCreate().setName("other").call(); + git.checkout().setName("other").call(); + writeTrashFile("b/b.txt", "b"); + writeTrashFile("b/.gitattributes", "b.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On other").call(); + MergeResult result = git.merge().include(aCommit).call(); + assertEquals(MergeResult.MergeStatus.MERGED, result.getMergeStatus()); + checkFile(aFile, "scsa"); + } + } diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index e026e31dc..00b89a4b3 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -39,6 +39,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java index 4f57f3512..08353dfdf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java @@ -230,7 +230,7 @@ private void determineDefaultRefSpecs(Config config) refSpecs.add(new RefSpec(getCurrentBranch())); break; case MATCHING: - setPushAll(); + refSpecs.add(new RefSpec(":")); //$NON-NLS-1$ break; case NOTHING: throw new InvalidRefNameException( diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java index 638dd827e..7ec78597f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java @@ -1,43 +1,11 @@ /* - * Copyright (C) 2015, Ivan Motsch + * Copyright (C) 2015, 2022 Ivan Motsch and others * - * 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 + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://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. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.attributes; @@ -46,6 +14,7 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.function.Supplier; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.attributes.Attribute.State; @@ -84,6 +53,8 @@ public class AttributesHandler { private final TreeWalk treeWalk; + private final Supplier attributesTree; + private final AttributesNode globalNode; private final AttributesNode infoNode; @@ -98,22 +69,41 @@ public class AttributesHandler { * @param treeWalk * a {@link org.eclipse.jgit.treewalk.TreeWalk} * @throws java.io.IOException + * @deprecated since 6.1, use {@link #AttributesHandler(TreeWalk, Supplier)} + * instead */ + @Deprecated public AttributesHandler(TreeWalk treeWalk) throws IOException { + this(treeWalk, () -> treeWalk.getTree(CanonicalTreeParser.class)); + } + + /** + * Create an {@link org.eclipse.jgit.attributes.AttributesHandler} with + * default rules as well as merged rules from global, info and worktree root + * attributes + * + * @param treeWalk + * a {@link org.eclipse.jgit.treewalk.TreeWalk} + * @param attributesTree + * the tree to read .gitattributes from + * @throws java.io.IOException + * @since 6.1 + */ + public AttributesHandler(TreeWalk treeWalk, + Supplier attributesTree) throws IOException { this.treeWalk = treeWalk; - AttributesNodeProvider attributesNodeProvider =treeWalk.getAttributesNodeProvider(); + this.attributesTree = attributesTree; + AttributesNodeProvider attributesNodeProvider = treeWalk + .getAttributesNodeProvider(); this.globalNode = attributesNodeProvider != null ? attributesNodeProvider.getGlobalAttributesNode() : null; this.infoNode = attributesNodeProvider != null ? attributesNodeProvider.getInfoAttributesNode() : null; AttributesNode rootNode = attributesNode(treeWalk, - rootOf( - treeWalk.getTree(WorkingTreeIterator.class)), - rootOf( - treeWalk.getTree(DirCacheIterator.class)), - rootOf(treeWalk - .getTree(CanonicalTreeParser.class))); + rootOf(treeWalk.getTree(WorkingTreeIterator.class)), + rootOf(treeWalk.getTree(DirCacheIterator.class)), + rootOf(attributesTree.get())); expansions.put(BINARY_RULE_KEY, BINARY_RULE_ATTRIBUTES); for (AttributesNode node : new AttributesNode[] { globalNode, rootNode, @@ -152,7 +142,7 @@ public Attributes getAttributes() throws IOException { isDirectory, treeWalk.getTree(WorkingTreeIterator.class), treeWalk.getTree(DirCacheIterator.class), - treeWalk.getTree(CanonicalTreeParser.class), + attributesTree.get(), attributes); // Gets the attributes located in the global attribute file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java index c904a782d..3d50a8215 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -4,7 +4,8 @@ * Copyright (C) 2008, Roger C. Soares * Copyright (C) 2006, Shawn O. Pearce * Copyright (C) 2010, Chrisian Halstrick - * Copyright (C) 2019-2020, Andre Bossert + * Copyright (C) 2019, 2020, Andre Bossert + * Copyright (C) 2017, 2022, Thomas Wolf and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -299,7 +300,7 @@ public void preScanTwoTrees() throws CorruptObjectException, IOException { walk = new NameConflictTreeWalk(repo); builder = dc.builder(); - addTree(walk, headCommitTree); + walk.setHead(addTree(walk, headCommitTree)); addTree(walk, mergeCommitTree); int dciPos = walk.addTree(new DirCacheBuildIterator(builder)); walk.addTree(workingTree); @@ -315,13 +316,6 @@ public void preScanTwoTrees() throws CorruptObjectException, IOException { } } - private void addTree(TreeWalk tw, ObjectId id) throws MissingObjectException, IncorrectObjectTypeException, IOException { - if (id == null) - tw.addTree(new EmptyTreeIterator()); - else - tw.addTree(id); - } - /** * Scan index and merge tree (no HEAD). Used e.g. for initial checkout when * there is no head yet. @@ -341,7 +335,7 @@ public void prescanOneTree() builder = dc.builder(); walk = new NameConflictTreeWalk(repo); - addTree(walk, mergeCommitTree); + walk.setHead(addTree(walk, mergeCommitTree)); int dciPos = walk.addTree(new DirCacheBuildIterator(builder)); walk.addTree(workingTree); workingTree.setDirCacheIterator(walk, dciPos); @@ -356,6 +350,14 @@ public void prescanOneTree() conflicts.removeAll(removed); } + private int addTree(TreeWalk tw, ObjectId id) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + if (id == null) { + return tw.addTree(new EmptyTreeIterator()); + } + return tw.addTree(id); + } + /** * Processing an entry in the context of {@link #prescanOneTree()} when only * one tree is given @@ -382,17 +384,14 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i, // failOnConflict is false. Putting something to conflicts // would mean we delete it. Instead we want the mergeCommit // content to be checked out. - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); } } else - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); } else if (f == null || !m.idEqual(i)) { // The working tree file is missing or the merge content differs // from index content - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); } else if (i.getDirCacheEntry() != null) { // The index contains a file (and not a folder) if (f.isModified(i.getDirCacheEntry(), true, @@ -400,8 +399,7 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i, || i.getDirCacheEntry().getStage() != 0) // The working tree file is dirty or the index contains a // conflict - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); else { // update the timestamp of the index with the one from the // file if not set, as we are sure to be in sync here. @@ -802,7 +800,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m, if (f != null && isModifiedSubtree_IndexWorkingtree(name)) { conflict(name, dce, h, m); // 1 } else { - update(name, mId, mMode); // 2 + update(1, name, mId, mMode); // 2 } break; @@ -828,7 +826,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m, // are found later break; case 0xD0F: // 19 - update(name, mId, mMode); + update(1, name, mId, mMode); break; case 0xDF0: // conflict without a rule case 0x0FD: // 15 @@ -839,7 +837,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m, if (isModifiedSubtree_IndexWorkingtree(name)) conflict(name, dce, h, m); // 8 else - update(name, mId, mMode); // 7 + update(1, name, mId, mMode); // 7 } else conflict(name, dce, h, m); // 9 break; @@ -859,7 +857,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m, break; case 0x0DF: // 16 17 if (!isModifiedSubtree_IndexWorkingtree(name)) - update(name, mId, mMode); + update(1, name, mId, mMode); else conflict(name, dce, h, m); break; @@ -929,7 +927,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m, // At least one of Head, Index, Merge is not empty // -> only Merge contains something for this path. Use it! // Potentially update the file - update(name, mId, mMode); // 1 + update(1, name, mId, mMode); // 1 else if (m == null) // Nothing in Merge // Something in Head @@ -947,7 +945,7 @@ else if (m == null) // find in Merge. Potentially updates the file. if (equalIdAndMode(hId, hMode, mId, mMode)) { if (initialCheckout || force) { - update(name, mId, mMode); + update(1, name, mId, mMode); } else { keep(name, dce, f); } @@ -1131,7 +1129,7 @@ && isModified_IndexTree(name, iId, iMode, mId, mMode, // TODO check that we don't overwrite some unsaved // file content - update(name, mId, mMode); + update(1, name, mId, mMode); } else if (dce != null && (f != null && f.isModified(dce, true, this.walk.getObjectReader()))) { @@ -1150,7 +1148,7 @@ && isModified_IndexTree(name, iId, iMode, mId, mMode, // -> Standard case when switching between branches: // Nothing new in index but something different in // Merge. Update index and file - update(name, mId, mMode); + update(1, name, mId, mMode); } } else { // Head differs from index or merge is same as index @@ -1237,12 +1235,17 @@ private void remove(String path) { removed.add(path); } - private void update(String path, ObjectId mId, FileMode mode) - throws IOException { + private void update(CanonicalTreeParser tree) throws IOException { + update(0, tree.getEntryPathString(), tree.getEntryObjectId(), + tree.getEntryFileMode()); + } + + private void update(int index, String path, ObjectId mId, + FileMode mode) throws IOException { if (!FileMode.TREE.equals(mode)) { updated.put(path, new CheckoutMetadata( - walk.getEolStreamType(CHECKOUT_OP), - walk.getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE))); + walk.getCheckoutEolStreamType(index), + walk.getSmudgeCommand(index))); DirCacheEntry entry = new DirCacheEntry(path, DirCacheEntry.STAGE_0); entry.setObjectId(mId); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java index 28ea927b1..df9fd47ef 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java @@ -568,6 +568,9 @@ public boolean diff(ProgressMonitor monitor, int estWorkTreeSize, if (ignoreSubmoduleMode != IgnoreSubmoduleMode.ALL) { try (SubmoduleWalk smw = new SubmoduleWalk(repository)) { smw.setTree(new DirCacheIterator(dirCache)); + if (filter != null) { + smw.setFilter(filter); + } smw.setBuilderFactory(factory); while (smw.next()) { IgnoreSubmoduleMode localIgnoreSubmoduleMode = ignoreSubmoduleMode; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java index 776766286..b9ab1d1b7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -3,7 +3,7 @@ * Copyright (C) 2010-2012, Matthias Sohn * Copyright (C) 2012, Research In Motion Limited * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr) - * Copyright (C) 2018, Thomas Wolf and others + * Copyright (C) 2018, 2022 Thomas Wolf and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -276,11 +276,15 @@ public enum MergeFailureReason { private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT; /** - * Keeps {@link CheckoutMetadata} for {@link #checkout()} and - * {@link #cleanUp()}. + * Keeps {@link CheckoutMetadata} for {@link #checkout()}. */ private Map checkoutMetadata; + /** + * Keeps {@link CheckoutMetadata} for {@link #cleanUp()}. + */ + private Map cleanupMetadata; + private static MergeAlgorithm getMergeAlgorithm(Config config) { SupportedAlgorithm diffAlg = config.getEnum( CONFIG_DIFF_SECTION, null, CONFIG_KEY_ALGORITHM, @@ -383,12 +387,14 @@ protected boolean mergeImpl() throws IOException { } if (!inCore) { checkoutMetadata = new HashMap<>(); + cleanupMetadata = new HashMap<>(); } try { return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1], false); } finally { checkoutMetadata = null; + cleanupMetadata = null; if (implicitDirCache) { dircache.unlock(); } @@ -447,7 +453,7 @@ protected void cleanUp() throws NoWorkTreeException, DirCacheEntry entry = dc.getEntry(mpath); if (entry != null) { DirCacheCheckout.checkoutEntry(db, entry, reader, false, - checkoutMetadata.get(mpath)); + cleanupMetadata.get(mpath)); } mpathsIt.remove(); } @@ -501,22 +507,26 @@ private DirCacheEntry keep(DirCacheEntry e) { * Remembers the {@link CheckoutMetadata} for the given path; it may be * needed in {@link #checkout()} or in {@link #cleanUp()}. * + * @param map + * to add the metadata to * @param path * of the current node * @param attributes - * for the current node + * to use for determining the metadata * @throws IOException * if the smudge filter cannot be determined - * @since 5.1 + * @since 6.1 */ - protected void addCheckoutMetadata(String path, Attributes attributes) + protected void addCheckoutMetadata(Map map, + String path, Attributes attributes) throws IOException { - if (checkoutMetadata != null) { + if (map != null) { EolStreamType eol = EolStreamTypeUtil.detectStreamType( - OperationType.CHECKOUT_OP, workingTreeOptions, attributes); + OperationType.CHECKOUT_OP, workingTreeOptions, + attributes); CheckoutMetadata data = new CheckoutMetadata(eol, - tw.getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE)); - checkoutMetadata.put(path, data); + tw.getSmudgeCommand(attributes)); + map.put(path, data); } } @@ -529,15 +539,17 @@ protected void addCheckoutMetadata(String path, Attributes attributes) * @param entry * to add * @param attributes - * for the current entry + * the {@link Attributes} of the trees * @throws IOException * if the {@link CheckoutMetadata} cannot be determined - * @since 5.1 + * @since 6.1 */ protected void addToCheckout(String path, DirCacheEntry entry, - Attributes attributes) throws IOException { + Attributes[] attributes) + throws IOException { toBeCheckedOut.put(path, entry); - addCheckoutMetadata(path, attributes); + addCheckoutMetadata(cleanupMetadata, path, attributes[T_OURS]); + addCheckoutMetadata(checkoutMetadata, path, attributes[T_THEIRS]); } /** @@ -549,7 +561,7 @@ protected void addToCheckout(String path, DirCacheEntry entry, * @param isFile * whether it is a file * @param attributes - * for the entry + * to use for determining the {@link CheckoutMetadata} * @throws IOException * if the {@link CheckoutMetadata} cannot be determined * @since 5.1 @@ -558,7 +570,7 @@ protected void addDeletion(String path, boolean isFile, Attributes attributes) throws IOException { toBeDeleted.add(path); if (isFile) { - addCheckoutMetadata(path, attributes); + addCheckoutMetadata(cleanupMetadata, path, attributes); } } @@ -599,7 +611,7 @@ protected void addDeletion(String path, boolean isFile, * see * {@link org.eclipse.jgit.merge.ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)} * @param attributes - * the attributes defined for this entry + * the {@link Attributes} for the three trees * @return false if the merge will fail because the index entry * didn't match ours or the working-dir file was dirty and a * conflict occurred @@ -607,12 +619,12 @@ protected void addDeletion(String path, boolean isFile, * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException * @throws org.eclipse.jgit.errors.CorruptObjectException * @throws java.io.IOException - * @since 4.9 + * @since 6.1 */ protected boolean processEntry(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, DirCacheBuildIterator index, WorkingTreeIterator work, - boolean ignoreConflicts, Attributes attributes) + boolean ignoreConflicts, Attributes[] attributes) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { enterSubtree = true; @@ -729,7 +741,7 @@ protected boolean processEntry(CanonicalTreeParser base, // Base, ours, and theirs all contain a folder: don't delete return true; } - addDeletion(tw.getPathString(), nonTree(modeO), attributes); + addDeletion(tw.getPathString(), nonTree(modeO), attributes[T_OURS]); return true; } @@ -772,7 +784,7 @@ protected boolean processEntry(CanonicalTreeParser base, if (nonTree(modeO) && nonTree(modeT)) { // Check worktree before modifying files boolean worktreeDirty = isWorktreeDirty(work, ourDce); - if (!attributes.canBeContentMerged() && worktreeDirty) { + if (!attributes[T_OURS].canBeContentMerged() && worktreeDirty) { return false; } @@ -791,7 +803,7 @@ protected boolean processEntry(CanonicalTreeParser base, mergeResults.put(tw.getPathString(), result); unmergedPaths.add(tw.getPathString()); return true; - } else if (!attributes.canBeContentMerged()) { + } else if (!attributes[T_OURS].canBeContentMerged()) { // File marked as binary switch (getContentMergeStrategy()) { case OURS: @@ -842,13 +854,16 @@ protected boolean processEntry(CanonicalTreeParser base, if (ignoreConflicts) { result.setContainsConflicts(false); } - updateIndex(base, ours, theirs, result, attributes); + updateIndex(base, ours, theirs, result, attributes[T_OURS]); String currentPath = tw.getPathString(); if (result.containsConflicts() && !ignoreConflicts) { unmergedPaths.add(currentPath); } modifiedFiles.add(currentPath); - addCheckoutMetadata(currentPath, attributes); + addCheckoutMetadata(cleanupMetadata, currentPath, + attributes[T_OURS]); + addCheckoutMetadata(checkoutMetadata, currentPath, + attributes[T_THEIRS]); } else if (modeO != modeT) { // OURS or THEIRS has been deleted if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw @@ -881,7 +896,8 @@ protected boolean processEntry(CanonicalTreeParser base, // markers). But also stage 0 of the index is filled // with that content. result.setContainsConflicts(false); - updateIndex(base, ours, theirs, result, attributes); + updateIndex(base, ours, theirs, result, + attributes[T_OURS]); } else { add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); @@ -896,11 +912,9 @@ protected boolean processEntry(CanonicalTreeParser base, if (isWorktreeDirty(work, ourDce)) { return false; } - if (nonTree(modeT)) { - if (e != null) { - addToCheckout(tw.getPathString(), e, - attributes); - } + if (nonTree(modeT) && e != null) { + addToCheckout(tw.getPathString(), e, + attributes); } } @@ -945,14 +959,16 @@ private static MergeResult createGitLinksMergeResult( */ private MergeResult contentMerge(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, - Attributes attributes, ContentMergeStrategy strategy) + Attributes[] attributes, ContentMergeStrategy strategy) throws BinaryBlobException, IOException { + // TW: The attributes here are used to determine the LFS smudge filter. + // Is doing a content merge on LFS items really a good idea?? RawText baseText = base == null ? RawText.EMPTY_TEXT - : getRawText(base.getEntryObjectId(), attributes); + : getRawText(base.getEntryObjectId(), attributes[T_BASE]); RawText ourText = ours == null ? RawText.EMPTY_TEXT - : getRawText(ours.getEntryObjectId(), attributes); + : getRawText(ours.getEntryObjectId(), attributes[T_OURS]); RawText theirsText = theirs == null ? RawText.EMPTY_TEXT - : getRawText(theirs.getEntryObjectId(), attributes); + : getRawText(theirs.getEntryObjectId(), attributes[T_THEIRS]); mergeAlgorithm.setContentMergeStrategy(strategy); return mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText, ourText, theirsText); @@ -1342,7 +1358,7 @@ protected boolean mergeTrees(AbstractTreeIterator baseTree, tw = new NameConflictTreeWalk(db, reader); tw.addTree(baseTree); - tw.addTree(headTree); + tw.setHead(tw.addTree(headTree)); tw.addTree(mergeTree); int dciPos = tw.addTree(buildIt); if (workingTreeIterator != null) { @@ -1403,6 +1419,13 @@ protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts) boolean hasAttributeNodeProvider = treeWalk .getAttributesNodeProvider() != null; while (treeWalk.next()) { + Attributes[] attributes = { NO_ATTRIBUTES, NO_ATTRIBUTES, + NO_ATTRIBUTES }; + if (hasAttributeNodeProvider) { + attributes[T_BASE] = treeWalk.getAttributes(T_BASE); + attributes[T_OURS] = treeWalk.getAttributes(T_OURS); + attributes[T_THEIRS] = treeWalk.getAttributes(T_THEIRS); + } if (!processEntry( treeWalk.getTree(T_BASE, CanonicalTreeParser.class), treeWalk.getTree(T_OURS, CanonicalTreeParser.class), @@ -1410,9 +1433,7 @@ protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts) treeWalk.getTree(T_INDEX, DirCacheBuildIterator.class), hasWorkingTreeIterator ? treeWalk.getTree(T_FILE, WorkingTreeIterator.class) : null, - ignoreConflicts, hasAttributeNodeProvider - ? treeWalk.getAttributes() - : NO_ATTRIBUTES)) { + ignoreConflicts, attributes)) { cleanUp(); return false; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java index a244c55a3..942dad46e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java @@ -18,11 +18,15 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.hooks.PrePushHook; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; @@ -58,6 +62,8 @@ class PushProcess { /** A list of option strings associated with this push */ private List pushOptions; + private final PrePushHook prePush; + /** * Create process for specified transport and refs updates specification. * @@ -66,12 +72,14 @@ class PushProcess { * connection. * @param toPush * specification of refs updates (and local tracking branches). - * + * @param prePush + * {@link PrePushHook} to run after the remote advertisement has + * been gotten * @throws TransportException */ - PushProcess(final Transport transport, - final Collection toPush) throws TransportException { - this(transport, toPush, null); + PushProcess(Transport transport, Collection toPush, + PrePushHook prePush) throws TransportException { + this(transport, toPush, prePush, null); } /** @@ -82,16 +90,19 @@ class PushProcess { * connection. * @param toPush * specification of refs updates (and local tracking branches). + * @param prePush + * {@link PrePushHook} to run after the remote advertisement has + * been gotten * @param out * OutputStream to write messages to * @throws TransportException */ - PushProcess(final Transport transport, - final Collection toPush, OutputStream out) - throws TransportException { + PushProcess(Transport transport, Collection toPush, + PrePushHook prePush, OutputStream out) throws TransportException { this.walker = new RevWalk(transport.local); this.transport = transport; this.toPush = new LinkedHashMap<>(); + this.prePush = prePush; this.out = out; this.pushOptions = transport.getPushOptions(); for (RemoteRefUpdate rru : toPush) { @@ -129,10 +140,38 @@ PushResult execute(ProgressMonitor monitor) res.setAdvertisedRefs(transport.getURI(), connection .getRefsMap()); res.peerUserAgent = connection.getPeerUserAgent(); - res.setRemoteUpdates(toPush); monitor.endTask(); + Map expanded = expandMatching(); + toPush.clear(); + toPush.putAll(expanded); + + res.setRemoteUpdates(toPush); final Map preprocessed = prepareRemoteUpdates(); + List willBeAttempted = preprocessed.values() + .stream().filter(u -> { + switch (u.getStatus()) { + case NON_EXISTING: + case REJECTED_NODELETE: + case REJECTED_NONFASTFORWARD: + case REJECTED_OTHER_REASON: + case REJECTED_REMOTE_CHANGED: + case UP_TO_DATE: + return false; + default: + return true; + } + }).collect(Collectors.toList()); + if (!willBeAttempted.isEmpty()) { + if (prePush != null) { + try { + prePush.setRefs(willBeAttempted); + prePush.call(); + } catch (AbortedByHookException | IOException e) { + throw new TransportException(e.getMessage(), e); + } + } + } if (transport.isDryRun()) modifyUpdatesForDryRun(); else if (!preprocessed.isEmpty()) @@ -201,25 +240,8 @@ private Map prepareRemoteUpdates() continue; } - // check for fast-forward: - // - both old and new ref must point to commits, AND - // - both of them must be known for us, exist in repository, AND - // - old commit must be ancestor of new commit - boolean fastForward = true; - try { - RevObject oldRev = walker.parseAny(advertisedOld); - final RevObject newRev = walker.parseAny(rru.getNewObjectId()); - if (!(oldRev instanceof RevCommit) - || !(newRev instanceof RevCommit) - || !walker.isMergedInto((RevCommit) oldRev, - (RevCommit) newRev)) - fastForward = false; - } catch (MissingObjectException x) { - fastForward = false; - } catch (Exception x) { - throw new TransportException(transport.getURI(), MessageFormat.format( - JGitText.get().readingObjectsFromLocalRepositoryFailed, x.getMessage()), x); - } + boolean fastForward = isFastForward(advertisedOld, + rru.getNewObjectId()); rru.setFastForward(fastForward); if (!fastForward && !rru.isForceUpdate()) { rru.setStatus(Status.REJECTED_NONFASTFORWARD); @@ -233,6 +255,134 @@ private Map prepareRemoteUpdates() return result; } + /** + * Determines whether an update from {@code oldOid} to {@code newOid} is a + * fast-forward update: + *
    + *
  • both old and new must be commits, AND
  • + *
  • both of them must be known to us and exist in the repository, + * AND
  • + *
  • the old commit must be an ancestor of the new commit.
  • + *
+ * + * @param oldOid + * {@link ObjectId} of the old commit + * @param newOid + * {@link ObjectId} of the new commit + * @return {@code true} if the update fast-forwards, {@code false} otherwise + * @throws TransportException + */ + private boolean isFastForward(ObjectId oldOid, ObjectId newOid) + throws TransportException { + try { + RevObject oldRev = walker.parseAny(oldOid); + RevObject newRev = walker.parseAny(newOid); + if (!(oldRev instanceof RevCommit) || !(newRev instanceof RevCommit) + || !walker.isMergedInto((RevCommit) oldRev, + (RevCommit) newRev)) { + return false; + } + } catch (MissingObjectException x) { + return false; + } catch (Exception x) { + throw new TransportException(transport.getURI(), + MessageFormat.format(JGitText + .get().readingObjectsFromLocalRepositoryFailed, + x.getMessage()), + x); + } + return true; + } + + /** + * Expands all placeholder {@link RemoteRefUpdate}s for "matching" + * {@link RefSpec}s ":" in {@link #toPush} and returns the resulting map in + * which the placeholders have been replaced by their expansion. + * + * @return a new map of {@link RemoteRefUpdate}s keyed by remote name + * @throws TransportException + * if the expansion results in duplicate updates + */ + private Map expandMatching() + throws TransportException { + Map result = new LinkedHashMap<>(); + boolean hadMatch = false; + for (RemoteRefUpdate update : toPush.values()) { + if (update.isMatching()) { + if (hadMatch) { + throw new TransportException(MessageFormat.format( + JGitText.get().duplicateRemoteRefUpdateIsIllegal, + ":")); //$NON-NLS-1$ + } + expandMatching(result, update); + hadMatch = true; + } else if (result.put(update.getRemoteName(), update) != null) { + throw new TransportException(MessageFormat.format( + JGitText.get().duplicateRemoteRefUpdateIsIllegal, + update.getRemoteName())); + } + } + return result; + } + + /** + * Expands the placeholder {@link RemoteRefUpdate} {@code match} for a + * "matching" {@link RefSpec} ":" or "+:" and puts the expansion into the + * given map {@code updates}. + * + * @param updates + * map to put the expansion in + * @param match + * the placeholder {@link RemoteRefUpdate} to expand + * + * @throws TransportException + * if the expansion results in duplicate updates, or the local + * branches cannot be determined + */ + private void expandMatching(Map updates, + RemoteRefUpdate match) throws TransportException { + try { + Map advertisement = connection.getRefsMap(); + Collection fetchSpecs = match.getFetchSpecs(); + boolean forceUpdate = match.isForceUpdate(); + for (Ref local : transport.local.getRefDatabase() + .getRefsByPrefix(Constants.R_HEADS)) { + if (local.isSymbolic()) { + continue; + } + String name = local.getName(); + Ref advertised = advertisement.get(name); + if (advertised == null || advertised.isSymbolic()) { + continue; + } + ObjectId oldOid = advertised.getObjectId(); + if (oldOid == null || ObjectId.zeroId().equals(oldOid)) { + continue; + } + ObjectId newOid = local.getObjectId(); + if (newOid == null || ObjectId.zeroId().equals(newOid)) { + continue; + } + + RemoteRefUpdate rru = new RemoteRefUpdate(transport.local, name, + newOid, name, forceUpdate, + Transport.findTrackingRefName(name, fetchSpecs), + oldOid); + if (updates.put(rru.getRemoteName(), rru) != null) { + throw new TransportException(MessageFormat.format( + JGitText.get().duplicateRemoteRefUpdateIsIllegal, + rru.getRemoteName())); + } + } + } catch (IOException x) { + throw new TransportException(transport.getURI(), + MessageFormat.format(JGitText + .get().readingObjectsFromLocalRepositoryFailed, + x.getMessage()), + x); + } + } + private Map rejectAll() { for (RemoteRefUpdate rru : toPush.values()) { if (rru.getStatus() == Status.NOT_ATTEMPTED) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java index ac357afda..56d0036a2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java @@ -12,11 +12,11 @@ import java.io.Serializable; import java.text.MessageFormat; +import java.util.Objects; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.util.References; /** * Describes how refs in one repository copy into another repository. @@ -50,6 +50,9 @@ public static boolean isWildcard(String s) { /** Is this specification actually a wildcard match? */ private boolean wildcard; + /** Is this the special ":" RefSpec? */ + private boolean matching; + /** * How strict to be about wildcards. * @@ -71,6 +74,7 @@ public enum WildcardMode { */ ALLOW_MISMATCH } + /** Whether a wildcard is allowed on one side but not the other. */ private WildcardMode allowMismatchedWildcards; @@ -87,6 +91,7 @@ public enum WildcardMode { * applications, as at least one field must be set to match a source name. */ public RefSpec() { + matching = false; force = false; wildcard = false; srcName = Constants.HEAD; @@ -133,17 +138,25 @@ public RefSpec(String spec, WildcardMode mode) { s = s.substring(1); } + boolean matchPushSpec = false; final int c = s.lastIndexOf(':'); if (c == 0) { s = s.substring(1); - if (isWildcard(s)) { + if (s.isEmpty()) { + matchPushSpec = true; wildcard = true; - if (mode == WildcardMode.REQUIRE_MATCH) { - throw new IllegalArgumentException(MessageFormat - .format(JGitText.get().invalidWildcards, spec)); + srcName = Constants.R_HEADS + '*'; + dstName = srcName; + } else { + if (isWildcard(s)) { + wildcard = true; + if (mode == WildcardMode.REQUIRE_MATCH) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().invalidWildcards, spec)); + } } + dstName = checkValid(s); } - dstName = checkValid(s); } else if (c > 0) { String src = s.substring(0, c); String dst = s.substring(c + 1); @@ -168,6 +181,7 @@ public RefSpec(String spec, WildcardMode mode) { } srcName = checkValid(s); } + matching = matchPushSpec; } /** @@ -195,6 +209,7 @@ public RefSpec(String spec) { } private RefSpec(RefSpec p) { + matching = false; force = p.isForceUpdate(); wildcard = p.isWildcard(); srcName = p.getSource(); @@ -202,6 +217,17 @@ private RefSpec(RefSpec p) { allowMismatchedWildcards = p.allowMismatchedWildcards; } + /** + * Tells whether this {@link RefSpec} is the special "matching" RefSpec ":" + * for pushing. + * + * @return whether this is a "matching" RefSpec + * @since 6.1 + */ + public boolean isMatching() { + return matching; + } + /** * Check if this specification wants to forcefully update the destination. * @@ -220,6 +246,7 @@ public boolean isForceUpdate() { */ public RefSpec setForceUpdate(boolean forceUpdate) { final RefSpec r = new RefSpec(this); + r.matching = matching; r.force = forceUpdate; return r; } @@ -322,8 +349,7 @@ public RefSpec setDestination(String destination) { * The wildcard status of the new source disagrees with the * wildcard status of the new destination. */ - public RefSpec setSourceDestination(final String source, - final String destination) { + public RefSpec setSourceDestination(String source, String destination) { if (isWildcard(source) != isWildcard(destination)) throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch); final RefSpec r = new RefSpec(this); @@ -541,37 +567,36 @@ public boolean equals(Object obj) { if (!(obj instanceof RefSpec)) return false; final RefSpec b = (RefSpec) obj; - if (isForceUpdate() != b.isForceUpdate()) + if (isForceUpdate() != b.isForceUpdate()) { return false; - if (isWildcard() != b.isWildcard()) - return false; - if (!eq(getSource(), b.getSource())) - return false; - if (!eq(getDestination(), b.getDestination())) - return false; - return true; - } - - private static boolean eq(String a, String b) { - if (References.isSameObject(a, b)) { - return true; } - if (a == null || b == null) + if (isMatching()) { + return b.isMatching(); + } else if (b.isMatching()) { return false; - return a.equals(b); + } + return isWildcard() == b.isWildcard() + && Objects.equals(getSource(), b.getSource()) + && Objects.equals(getDestination(), b.getDestination()); } /** {@inheritDoc} */ @Override public String toString() { final StringBuilder r = new StringBuilder(); - if (isForceUpdate()) + if (isForceUpdate()) { r.append('+'); - if (getSource() != null) - r.append(getSource()); - if (getDestination() != null) { + } + if (isMatching()) { r.append(':'); - r.append(getDestination()); + } else { + if (getSource() != null) { + r.append(getSource()); + } + if (getDestination() != null) { + r.append(':'); + r.append(getDestination()); + } } return r.toString(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java index 43eaac792..218e62c10 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java @@ -12,7 +12,9 @@ import java.io.IOException; import java.text.MessageFormat; +import java.util.Collection; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -115,6 +117,12 @@ public enum Status { private RefUpdate localUpdate; + /** + * If set, the RemoteRefUpdate is a placeholder for the "matching" RefSpec + * to be expanded after the advertisements have been received in a push. + */ + private Collection fetchSpecs; + /** * Construct remote ref update request by providing an update specification. * Object is created with default @@ -157,9 +165,8 @@ public enum Status { * @throws java.lang.IllegalArgumentException * if some required parameter was null */ - public RemoteRefUpdate(final Repository localDb, final String srcRef, - final String remoteName, final boolean forceUpdate, - final String localName, final ObjectId expectedOldObjectId) + public RemoteRefUpdate(Repository localDb, String srcRef, String remoteName, + boolean forceUpdate, String localName, ObjectId expectedOldObjectId) throws IOException { this(localDb, srcRef, srcRef != null ? localDb.resolve(srcRef) : ObjectId.zeroId(), remoteName, forceUpdate, localName, @@ -203,9 +210,8 @@ public RemoteRefUpdate(final Repository localDb, final String srcRef, * @throws java.lang.IllegalArgumentException * if some required parameter was null */ - public RemoteRefUpdate(final Repository localDb, final Ref srcRef, - final String remoteName, final boolean forceUpdate, - final String localName, final ObjectId expectedOldObjectId) + public RemoteRefUpdate(Repository localDb, Ref srcRef, String remoteName, + boolean forceUpdate, String localName, ObjectId expectedOldObjectId) throws IOException { this(localDb, srcRef != null ? srcRef.getName() : null, srcRef != null ? srcRef.getObjectId() : null, remoteName, @@ -255,28 +261,41 @@ public RemoteRefUpdate(final Repository localDb, final Ref srcRef, * @throws java.lang.IllegalArgumentException * if some required parameter was null */ - public RemoteRefUpdate(final Repository localDb, final String srcRef, - final ObjectId srcId, final String remoteName, - final boolean forceUpdate, final String localName, - final ObjectId expectedOldObjectId) throws IOException { - if (remoteName == null) - throw new IllegalArgumentException(JGitText.get().remoteNameCannotBeNull); - if (srcId == null && srcRef != null) - throw new IOException(MessageFormat.format( - JGitText.get().sourceRefDoesntResolveToAnyObject, srcRef)); + public RemoteRefUpdate(Repository localDb, String srcRef, ObjectId srcId, + String remoteName, boolean forceUpdate, String localName, + ObjectId expectedOldObjectId) throws IOException { + this(localDb, srcRef, srcId, remoteName, forceUpdate, localName, null, + expectedOldObjectId); + } - if (srcRef != null) + private RemoteRefUpdate(Repository localDb, String srcRef, ObjectId srcId, + String remoteName, boolean forceUpdate, String localName, + Collection fetchSpecs, ObjectId expectedOldObjectId) + throws IOException { + if (fetchSpecs == null) { + if (remoteName == null) { + throw new IllegalArgumentException( + JGitText.get().remoteNameCannotBeNull); + } + if (srcId == null && srcRef != null) { + throw new IOException(MessageFormat.format( + JGitText.get().sourceRefDoesntResolveToAnyObject, + srcRef)); + } + } + if (srcRef != null) { this.srcRef = srcRef; - else if (srcId != null && !srcId.equals(ObjectId.zeroId())) + } else if (srcId != null && !srcId.equals(ObjectId.zeroId())) { this.srcRef = srcId.name(); - else + } else { this.srcRef = null; - - if (srcId != null) + } + if (srcId != null) { this.newObjectId = srcId; - else + } else { this.newObjectId = ObjectId.zeroId(); - + } + this.fetchSpecs = fetchSpecs; this.remoteName = remoteName; this.forceUpdate = forceUpdate; if (localName != null && localDb != null) { @@ -292,8 +311,9 @@ else if (srcId != null && !srcId.equals(ObjectId.zeroId())) ? localUpdate.getOldObjectId() : ObjectId.zeroId(), newObjectId); - } else + } else { trackingRefUpdate = null; + } this.localDb = localDb; this.expectedOldObjectId = expectedOldObjectId; this.status = Status.NOT_ATTEMPTED; @@ -316,11 +336,57 @@ else if (srcId != null && !srcId.equals(ObjectId.zeroId())) * local tracking branch or srcRef of base object no longer can * be resolved to any object. */ - public RemoteRefUpdate(final RemoteRefUpdate base, - final ObjectId newExpectedOldObjectId) throws IOException { - this(base.localDb, base.srcRef, base.remoteName, base.forceUpdate, + public RemoteRefUpdate(RemoteRefUpdate base, + ObjectId newExpectedOldObjectId) throws IOException { + this(base.localDb, base.srcRef, base.newObjectId, base.remoteName, + base.forceUpdate, (base.trackingRefUpdate == null ? null : base.trackingRefUpdate - .getLocalName()), newExpectedOldObjectId); + .getLocalName()), + base.fetchSpecs, newExpectedOldObjectId); + } + + /** + * Creates a "placeholder" update for the "matching" RefSpec ":". + * + * @param localDb + * local repository to push from + * @param forceUpdate + * whether non-fast-forward updates shall be allowed + * @param fetchSpecs + * The fetch {@link RefSpec}s to use when this placeholder is + * expanded to determine remote tracking branch updates + */ + RemoteRefUpdate(Repository localDb, boolean forceUpdate, + @NonNull Collection fetchSpecs) { + this.localDb = localDb; + this.forceUpdate = forceUpdate; + this.fetchSpecs = fetchSpecs; + this.trackingRefUpdate = null; + this.srcRef = null; + this.remoteName = null; + this.newObjectId = null; + this.status = Status.NOT_ATTEMPTED; + } + + /** + * Tells whether this {@link RemoteRefUpdate} is a placeholder for a + * "matching" {@link RefSpec}. + * + * @return {@code true} if this is a placeholder, {@code false} otherwise + * @since 6.1 + */ + public boolean isMatching() { + return fetchSpecs != null; + } + + /** + * Retrieves the fetch {@link RefSpec}s of this {@link RemoteRefUpdate}. + * + * @return the fetch {@link RefSpec}s, or {@code null} if + * {@code this.}{@link #isMatching()} {@code == false} + */ + Collection getFetchSpecs() { + return fetchSpecs; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java index bfe26d980..0eab4434e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -40,7 +40,6 @@ import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; -import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.hooks.Hooks; @@ -590,6 +589,11 @@ public static Collection findRemoteRefUpdatesFor( final Collection procRefs = expandPushWildcardsFor(db, specs); for (RefSpec spec : procRefs) { + if (spec.isMatching()) { + result.add(new RemoteRefUpdate(db, spec.isForceUpdate(), + fetchSpecs)); + continue; + } String srcSpec = spec.getSource(); final Ref srcRef = db.findRef(srcSpec); if (srcRef != null) @@ -660,7 +664,7 @@ private static Collection expandPushWildcardsFor( List localRefs = null; for (RefSpec spec : specs) { - if (spec.isWildcard()) { + if (!spec.isMatching() && spec.isWildcard()) { if (localRefs == null) { localRefs = db.getRefDatabase().getRefs(); } @@ -676,7 +680,7 @@ private static Collection expandPushWildcardsFor( return procRefs; } - private static String findTrackingRefName(final String remoteName, + static String findTrackingRefName(final String remoteName, final Collection fetchSpecs) { // try to find matching tracking refs for (RefSpec fetchSpec : fetchSpecs) { @@ -1375,16 +1379,9 @@ public PushResult push(final ProgressMonitor monitor, if (toPush.isEmpty()) throw new TransportException(JGitText.get().nothingToPush); } - if (prePush != null) { - try { - prePush.setRefs(toPush); - prePush.call(); - } catch (AbortedByHookException | IOException e) { - throw new TransportException(e.getMessage(), e); - } - } - final PushProcess pushProcess = new PushProcess(this, toPush, out); + final PushProcess pushProcess = new PushProcess(this, toPush, prePush, + out); return pushProcess.execute(monitor); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java index 1f614e31f..8269666d2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java @@ -1,6 +1,6 @@ /* - * Copyright (C) 2008-2009, Google Inc. - * Copyright (C) 2008, Shawn O. Pearce and others + * Copyright (C) 2008, 2009 Google Inc. + * Copyright (C) 2008, 2022 Shawn O. Pearce and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -14,6 +14,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -73,6 +74,7 @@ * threads. */ public class TreeWalk implements AutoCloseable, AttributesProvider { + private static final AbstractTreeIterator[] NO_TREES = {}; /** @@ -92,7 +94,7 @@ public enum OperationType { } /** - * Type of operation you want to retrieve the git attributes for. + * Type of operation you want to retrieve the git attributes for. */ private OperationType operationType = OperationType.CHECKOUT_OP; @@ -284,11 +286,20 @@ public static TreeWalk forPath(final Repository db, final String path, AbstractTreeIterator currentHead; - /** Cached attribute for the current entry */ - private Attributes attrs = null; + /** + * Cached attributes for the current entry; per tree. Index i+1 is for tree + * i; index 0 is for the deprecated legacy behavior. + */ + private Attributes[] attrs; - /** Cached attributes handler */ - private AttributesHandler attributesHandler; + /** + * Cached attributes handler; per tree. Index i+1 is for tree i; index 0 is + * for the deprecated legacy behavior. + */ + private AttributesHandler[] attributesHandlers; + + /** Can be set to identify the tree to use for {@link #getAttributes()}. */ + private int headIndex = -1; private Config config; @@ -514,6 +525,24 @@ public AttributesNodeProvider getAttributesNodeProvider() { return attributesNodeProvider; } + /** + * Identifies the tree at the given index as the head tree. This is the tree + * use by default to determine attributes and EOL modes. + * + * @param index + * of the tree to use as head + * @throws IllegalArgumentException + * if the index is out of range + * @since 6.1 + */ + public void setHead(int index) { + if (index < 0 || index >= trees.length) { + throw new IllegalArgumentException("Head index " + index //$NON-NLS-1$ + + " out of range [0," + trees.length + ')'); //$NON-NLS-1$ + } + headIndex = index; + } + /** * {@inheritDoc} *

@@ -556,25 +585,51 @@ public AttributesNodeProvider getAttributesNodeProvider() { */ @Override public Attributes getAttributes() { - if (attrs != null) - return attrs; + return getAttributes(headIndex); + } + /** + * Retrieves the git attributes based on the given tree. + * + * @param index + * of the tree to use as base for the attributes + * @return the attributes + * @since 6.1 + */ + public Attributes getAttributes(int index) { + int attrIndex = index + 1; + Attributes result = attrs[attrIndex]; + if (result != null) { + return result; + } if (attributesNodeProvider == null) { - // The work tree should have a AttributesNodeProvider to be able to - // retrieve the info and global attributes node throw new IllegalStateException( "The tree walk should have one AttributesNodeProvider set in order to compute the git attributes."); //$NON-NLS-1$ } try { - // Lazy create the attributesHandler on the first access of - // attributes. This requires the info, global and root - // attributes nodes - if (attributesHandler == null) { - attributesHandler = new AttributesHandler(this); + AttributesHandler handler = attributesHandlers[attrIndex]; + if (handler == null) { + if (index < 0) { + // Legacy behavior (headIndex not set, getAttributes() above + // called) + handler = new AttributesHandler(this, () -> { + return getTree(CanonicalTreeParser.class); + }); + } else { + handler = new AttributesHandler(this, () -> { + AbstractTreeIterator tree = trees[index]; + if (tree instanceof CanonicalTreeParser) { + return (CanonicalTreeParser) tree; + } + return null; + }); + } + attributesHandlers[attrIndex] = handler; } - attrs = attributesHandler.getAttributes(); - return attrs; + result = handler.getAttributes(); + attrs[attrIndex] = result; + return result; } catch (IOException e) { throw new JGitInternalException("Error while parsing attributes", //$NON-NLS-1$ e); @@ -595,11 +650,34 @@ public Attributes getAttributes() { */ @Nullable public EolStreamType getEolStreamType(OperationType opType) { - if (attributesNodeProvider == null || config == null) + if (attributesNodeProvider == null || config == null) { return null; - return EolStreamTypeUtil.detectStreamType( - opType != null ? opType : operationType, - config.get(WorkingTreeOptions.KEY), getAttributes()); + } + OperationType op = opType != null ? opType : operationType; + return EolStreamTypeUtil.detectStreamType(op, + config.get(WorkingTreeOptions.KEY), getAttributes()); + } + + /** + * Get the EOL stream type of the current entry for checking out using the + * config and {@link #getAttributes()}. + * + * @param tree + * index of the tree the check-out is to be from + * @return the EOL stream type of the current entry using the config and + * {@link #getAttributes()}. Note that this method may return null + * if the {@link org.eclipse.jgit.treewalk.TreeWalk} is not based on + * a working tree + * @since 6.1 + */ + @Nullable + public EolStreamType getCheckoutEolStreamType(int tree) { + if (attributesNodeProvider == null || config == null) { + return null; + } + Attributes attr = getAttributes(tree); + return EolStreamTypeUtil.detectStreamType(OperationType.CHECKOUT_OP, + config.get(WorkingTreeOptions.KEY), attr); } /** @@ -607,7 +685,8 @@ public EolStreamType getEolStreamType(OperationType opType) { */ public void reset() { attrs = null; - attributesHandler = null; + attributesHandlers = null; + headIndex = -1; trees = NO_TREES; advance = false; depth = 0; @@ -651,7 +730,9 @@ public void reset(AnyObjectId id) throws MissingObjectException, advance = false; depth = 0; - attrs = null; + attrs = new Attributes[2]; + attributesHandlers = new AttributesHandler[2]; + headIndex = -1; } /** @@ -701,7 +782,14 @@ public void reset(AnyObjectId... ids) throws MissingObjectException, trees = r; advance = false; depth = 0; - attrs = null; + if (oldLen == newLen) { + Arrays.fill(attrs, null); + Arrays.fill(attributesHandlers, null); + } else { + attrs = new Attributes[newLen + 1]; + attributesHandlers = new AttributesHandler[newLen + 1]; + } + headIndex = -1; } /** @@ -758,6 +846,16 @@ public int addTree(AbstractTreeIterator p) { p.matchShift = 0; trees = newTrees; + if (attrs == null) { + attrs = new Attributes[n + 2]; + } else { + attrs = Arrays.copyOf(attrs, n + 2); + } + if (attributesHandlers == null) { + attributesHandlers = new AttributesHandler[n + 2]; + } else { + attributesHandlers = Arrays.copyOf(attributesHandlers, n + 2); + } return n; } @@ -800,7 +898,7 @@ public boolean next() throws MissingObjectException, } for (;;) { - attrs = null; + Arrays.fill(attrs, null); final AbstractTreeIterator t = min(); if (t.eof()) { if (depth > 0) { @@ -1255,7 +1353,7 @@ public boolean isPostChildren() { */ public void enterSubtree() throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { - attrs = null; + Arrays.fill(attrs, null); final AbstractTreeIterator ch = currentHead; final AbstractTreeIterator[] tmp = new AbstractTreeIterator[trees.length]; for (int i = 0; i < trees.length; i++) { @@ -1374,11 +1472,12 @@ public T getTree(Class type) { /** * Inspect config and attributes to return a filtercommand applicable for - * the current path, but without expanding %f occurences + * the current path. * * @param filterCommandType * which type of filterCommand should be executed. E.g. "clean", - * "smudge" + * "smudge". For "smudge" consider using + * {{@link #getSmudgeCommand(int)} instead. * @return a filter command * @throws java.io.IOException * @since 4.2 @@ -1406,6 +1505,54 @@ public String getFilterCommand(String filterCommandType) QuotedString.BOURNE.quote((getPathString())))); } + /** + * Inspect config and attributes to return a filtercommand applicable for + * the current path. + * + * @param index + * of the tree the item to be smudged is in + * @return a filter command + * @throws java.io.IOException + * @since 6.1 + */ + public String getSmudgeCommand(int index) + throws IOException { + return getSmudgeCommand(getAttributes(index)); + } + + /** + * Inspect config and attributes to return a filtercommand applicable for + * the current path. + * + * @param attributes + * to use + * @return a filter command + * @throws java.io.IOException + * @since 6.1 + */ + public String getSmudgeCommand(Attributes attributes) throws IOException { + if (attributes == null) { + return null; + } + Attribute f = attributes.get(Constants.ATTR_FILTER); + if (f == null) { + return null; + } + String filterValue = f.getValue(); + if (filterValue == null) { + return null; + } + + String filterCommand = getFilterCommandDefinition(filterValue, + Constants.ATTR_FILTER_TYPE_SMUDGE); + if (filterCommand == null) { + return null; + } + return filterCommand.replaceAll("%f", //$NON-NLS-1$ + Matcher.quoteReplacement( + QuotedString.BOURNE.quote((getPathString())))); + } + /** * Get the filter command how it is defined in gitconfig. The returned * string may contain "%f" which needs to be replaced by the current path diff --git a/pom.xml b/pom.xml index f24b676e1..c6fa636dc 100644 --- a/pom.xml +++ b/pom.xml @@ -203,6 +203,14 @@ repo.eclipse.org.cbi-snapshots https://repo.eclipse.org/content/repositories/cbi-snapshots/ + + repo.eclipse.org.dash-releases + https://repo.eclipse.org/content/repositories/dash-licenses-releases/ + + + repo.eclipse.org.dash-snapshots + https://repo.eclipse.org/content/repositories/dash-licenses-snapshots/ + @@ -382,6 +390,11 @@ spring-boot-maven-plugin 2.5.4 + + org.eclipse.dash + license-tool-plugin + 0.0.1-SNAPSHOT + @@ -540,6 +553,10 @@ org.apache.maven.plugins maven-surefire-report-plugin + + org.eclipse.dash + license-tool-plugin +