LFS: support merge/rebase/cherry-pick/diff/compare with LFS files

Respect merge=lfs and diff=lfs attributes where required to replace (in
memory) the content of LFS pointers with the actual blob content from
the LFS storage (and vice versa when staging/merging).

Does not implement general support for merge/diff attributes for any
other use case apart from LFS.

Change-Id: Ibad8875de1e0bee8fe3a1dffb1add93111534cae
Signed-off-by: Markus Duft <markus.duft@ssi-schaefer.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
This commit is contained in:
Markus Duft 2018-03-02 10:11:42 +01:00 committed by Matthias Sohn
parent 169de08a78
commit d3ed64bcd4
25 changed files with 848 additions and 93 deletions

View File

@ -51,8 +51,7 @@
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lfs.CleanFilter; import org.eclipse.jgit.lfs.BuiltinLFS;
import org.eclipse.jgit.lfs.SmudgeFilter;
import org.eclipse.jgit.lfs.lib.LongObjectId; import org.eclipse.jgit.lfs.lib.LongObjectId;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.lib.StoredConfig;
@ -72,8 +71,7 @@ public class CheckoutTest extends LfsServerTest {
public void setup() throws Exception { public void setup() throws Exception {
super.setup(); super.setup();
SmudgeFilter.register(); BuiltinLFS.register();
CleanFilter.register();
Path tmp = Files.createTempDirectory("jgit_test_"); Path tmp = Files.createTempDirectory("jgit_test_");
Repository db = FileRepositoryBuilder Repository db = FileRepositoryBuilder

View File

@ -52,8 +52,7 @@
import org.eclipse.jgit.api.RemoteAddCommand; import org.eclipse.jgit.api.RemoteAddCommand;
import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lfs.CleanFilter; import org.eclipse.jgit.lfs.BuiltinLFS;
import org.eclipse.jgit.lfs.SmudgeFilter;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectLoader;
@ -84,8 +83,7 @@ public class PushTest extends LfsServerTest {
public void setup() throws Exception { public void setup() throws Exception {
super.setup(); super.setup();
SmudgeFilter.register(); BuiltinLFS.register();
CleanFilter.register();
Path rtmp = Files.createTempDirectory("jgit_test_"); Path rtmp = Files.createTempDirectory("jgit_test_");
remoteDb = FileRepositoryBuilder.create(rtmp.toFile()); remoteDb = FileRepositoryBuilder.create(rtmp.toFile());

View File

@ -1,5 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<component id="org.eclipse.jgit.lfs" version="2"> <component id="org.eclipse.jgit.lfs" version="2">
<resource path="src/org/eclipse/jgit/lfs/CleanFilter.java" type="org.eclipse.jgit.lfs.CleanFilter">
<filter id="421572723">
<message_arguments>
<message_argument value="org.eclipse.jgit.lfs.CleanFilter"/>
<message_argument value="register()"/>
</message_arguments>
</filter>
</resource>
<resource path="src/org/eclipse/jgit/lfs/LfsPointer.java" type="org.eclipse.jgit.lfs.LfsPointer"> <resource path="src/org/eclipse/jgit/lfs/LfsPointer.java" type="org.eclipse.jgit.lfs.LfsPointer">
<filter id="336658481"> <filter id="336658481">
<message_arguments> <message_arguments>
@ -8,4 +16,12 @@
</message_arguments> </message_arguments>
</filter> </filter>
</resource> </resource>
<resource path="src/org/eclipse/jgit/lfs/SmudgeFilter.java" type="org.eclipse.jgit.lfs.SmudgeFilter">
<filter id="421572723">
<message_arguments>
<message_argument value="org.eclipse.jgit.lfs.SmudgeFilter"/>
<message_argument value="register()"/>
</message_arguments>
</filter>
</resource>
</component> </component>

View File

@ -18,6 +18,7 @@ Import-Package: com.google.gson;version="[2.8.2,3.0.0)",
org.eclipse.jgit.annotations;version="[4.11.0,4.12.0)";resolution:=optional, org.eclipse.jgit.annotations;version="[4.11.0,4.12.0)";resolution:=optional,
org.eclipse.jgit.api.errors;version="[4.11.0,4.12.0)", org.eclipse.jgit.api.errors;version="[4.11.0,4.12.0)",
org.eclipse.jgit.attributes;version="[4.11.0,4.12.0)", org.eclipse.jgit.attributes;version="[4.11.0,4.12.0)",
org.eclipse.jgit.diff;version="[4.11.0,4.12.0)",
org.eclipse.jgit.errors;version="[4.11.0,4.12.0)", org.eclipse.jgit.errors;version="[4.11.0,4.12.0)",
org.eclipse.jgit.hooks;version="[4.11.0,4.12.0)", org.eclipse.jgit.hooks;version="[4.11.0,4.12.0)",
org.eclipse.jgit.internal.storage.file;version="[4.11.0,4.12.0)", org.eclipse.jgit.internal.storage.file;version="[4.11.0,4.12.0)",
@ -25,6 +26,7 @@ Import-Package: com.google.gson;version="[2.8.2,3.0.0)",
org.eclipse.jgit.nls;version="[4.11.0,4.12.0)", org.eclipse.jgit.nls;version="[4.11.0,4.12.0)",
org.eclipse.jgit.revwalk;version="[4.11.0,4.12.0)", org.eclipse.jgit.revwalk;version="[4.11.0,4.12.0)",
org.eclipse.jgit.storage.file;version="[4.11.0,4.12.0)", org.eclipse.jgit.storage.file;version="[4.11.0,4.12.0)",
org.eclipse.jgit.storage.pack;version="[4.11.0,4.12.0)",
org.eclipse.jgit.transport;version="[4.11.0,4.12.0)", org.eclipse.jgit.transport;version="[4.11.0,4.12.0)",
org.eclipse.jgit.transport.http;version="[4.11.0,4.12.0)", org.eclipse.jgit.transport.http;version="[4.11.0,4.12.0)",
org.eclipse.jgit.treewalk;version="[4.11.0,4.12.0)", org.eclipse.jgit.treewalk;version="[4.11.0,4.12.0)",

View File

@ -0,0 +1,140 @@
/*
* Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.lfs;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.attributes.Attribute;
import org.eclipse.jgit.hooks.PrePushHook;
import org.eclipse.jgit.lfs.lib.Constants;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.LfsFactory;
/**
* Implementation of {@link LfsFactory}, using built-in (optional) LFS support.
*
* @since 4.11
*/
public class BuiltinLFS extends LfsFactory {
private BuiltinLFS() {
SmudgeFilter.register();
CleanFilter.register();
}
/**
* Activates the built-in LFS support.
*/
public static void register() {
setInstance(new BuiltinLFS());
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public ObjectLoader applySmudgeFilter(Repository db, ObjectLoader loader,
Attribute attribute) throws IOException {
if (isEnabled(db) && (attribute == null || isEnabled(db, attribute))) {
return LfsBlobFilter.smudgeLfsBlob(db, loader);
} else {
return loader;
}
}
@Override
public LfsInputStream applyCleanFilter(Repository db, InputStream input,
long length, Attribute attribute) throws IOException {
if (isEnabled(db, attribute)) {
return new LfsInputStream(LfsBlobFilter.cleanLfsBlob(db, input));
} else {
return new LfsInputStream(input, length);
}
}
@Override
public @Nullable PrePushHook getPrePushHook(Repository repo,
PrintStream outputStream) {
if (isEnabled(repo)) {
return new LfsPrePushHook(repo, outputStream);
}
return null;
}
/**
* @param db
* the repository
* @return whether LFS is requested for the given repo.
*/
private boolean isEnabled(Repository db) {
if (db == null) {
return false;
}
return db.getConfig().getBoolean(ConfigConstants.CONFIG_FILTER_SECTION,
Constants.LFS, ConfigConstants.CONFIG_KEY_USEJGITBUILTIN,
false);
}
/**
* @param db
* the repository
* @param attribute
* the attribute to check
* @return whether LFS filter is enabled for the given .gitattribute
* attribute.
*/
private boolean isEnabled(Repository db, Attribute attribute) {
if (attribute == null) {
return false;
}
return isEnabled(db) && Constants.LFS.equals(attribute.getValue());
}
}

View File

@ -91,7 +91,7 @@ public FilterCommand create(Repository db, InputStream in,
* Registers this filter by calling * Registers this filter by calling
* {@link FilterCommandRegistry#register(String, FilterCommandFactory)} * {@link FilterCommandRegistry#register(String, FilterCommandFactory)}
*/ */
public final static void register() { static void register() {
FilterCommandRegistry FilterCommandRegistry
.register(org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX .register(org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX
+ Constants.ATTR_FILTER_DRIVER_PREFIX + Constants.ATTR_FILTER_DRIVER_PREFIX

View File

@ -0,0 +1,130 @@
/*
* Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.lfs;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.TemporaryBuffer.LocalFile;
/**
* Provides transparently either a stream to the blob or a LFS media file if
* managed by LFS.
*
* @since 4.11
*/
public class LfsBlobFilter {
/**
* In case the given {@link ObjectLoader} points to a LFS pointer file
* replace the loader with one pointing to the LFS media file contents.
* Missing LFS files are downloaded on the fly - same logic as the smudge
* filter.
*
* @param db
* the repo
* @param loader
* the loader for the blob
* @return either the original loader, or a loader for the LFS media file if
* managed by LFS. Files are downloaded on demand if required.
* @throws IOException
* in case of an error
*/
public static ObjectLoader smudgeLfsBlob(Repository db, ObjectLoader loader)
throws IOException {
if (loader.getSize() > LfsPointer.SIZE_THRESHOLD) {
return loader;
}
try (InputStream is = loader.openStream()) {
LfsPointer ptr = LfsPointer.parseLfsPointer(is);
if (ptr != null) {
Lfs lfs = new Lfs(db);
AnyLongObjectId oid = ptr.getOid();
Path mediaFile = lfs.getMediaFile(oid);
if (!Files.exists(mediaFile)) {
SmudgeFilter.downloadLfsResource(lfs, db, ptr);
}
return new LfsBlobLoader(mediaFile);
}
}
return loader;
}
/**
* Run the LFS clean filter on the given stream and return a stream to the
* LFS pointer file buffer. Used when inserting objects.
*
* @param db
* the {@link Repository}
* @param originalContent
* the {@link InputStream} to the original content
* @return a {@link TemporaryBuffer} representing the LFS pointer. The
* caller is responsible to destroy the buffer.
* @throws IOException
* in case of any error.
*/
public static TemporaryBuffer cleanLfsBlob(Repository db,
InputStream originalContent) throws IOException {
LocalFile buffer = new TemporaryBuffer.LocalFile(null);
CleanFilter f = new CleanFilter(db, originalContent, buffer);
try {
while (f.run() != -1) {
// loop as long as f.run() tells there is work to do
}
} catch (IOException e) {
buffer.destroy();
throw e;
}
return buffer;
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.lfs;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectStream;
import org.eclipse.jgit.storage.pack.PackConfig;
import org.eclipse.jgit.util.IO;
/**
* An {@link ObjectLoader} implementation that reads a media file from the LFS
* storage.
*
* @since 4.11
*/
public class LfsBlobLoader extends ObjectLoader {
private Path mediaFile;
private BasicFileAttributes attributes;
private byte[] cached;
/**
* Create a loader for the LFS media file at the given path.
*
* @param mediaFile
* path to the file
* @throws IOException
* in case of an error reading attributes
*/
public LfsBlobLoader(Path mediaFile) throws IOException {
this.mediaFile = mediaFile;
this.attributes = Files.readAttributes(mediaFile,
BasicFileAttributes.class);
}
@Override
public int getType() {
return Constants.OBJ_BLOB;
}
@Override
public long getSize() {
return attributes.size();
}
@Override
public byte[] getCachedBytes() throws LargeObjectException {
if (getSize() > PackConfig.DEFAULT_BIG_FILE_THRESHOLD) {
throw new LargeObjectException();
}
if (cached == null) {
try {
cached = IO.readFully(mediaFile.toFile());
} catch (IOException ioe) {
throw new LargeObjectException(ioe);
}
}
return cached;
}
@Override
public ObjectStream openStream()
throws MissingObjectException, IOException {
return new ObjectStream.Filter(getType(), getSize(),
Files.newInputStream(mediaFile));
}
}

View File

@ -100,9 +100,9 @@ public FilterCommand create(Repository db, InputStream in,
}; };
/** /**
* Registers this filter in JGit by calling * Register this filter in JGit
*/ */
public final static void register() { static void register() {
FilterCommandRegistry FilterCommandRegistry
.register(org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX .register(org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX
+ Constants.ATTR_FILTER_DRIVER_PREFIX + Constants.ATTR_FILTER_DRIVER_PREFIX
@ -110,8 +110,6 @@ public final static void register() {
FACTORY); FACTORY);
} }
private Lfs lfs;
/** /**
* Constructor for SmudgeFilter. * Constructor for SmudgeFilter.
* *
@ -126,13 +124,13 @@ public final static void register() {
public SmudgeFilter(Repository db, InputStream in, OutputStream out) public SmudgeFilter(Repository db, InputStream in, OutputStream out)
throws IOException { throws IOException {
super(in, out); super(in, out);
lfs = new Lfs(db); Lfs lfs = new Lfs(db);
LfsPointer res = LfsPointer.parseLfsPointer(in); LfsPointer res = LfsPointer.parseLfsPointer(in);
if (res != null) { if (res != null) {
AnyLongObjectId oid = res.getOid(); AnyLongObjectId oid = res.getOid();
Path mediaFile = lfs.getMediaFile(oid); Path mediaFile = lfs.getMediaFile(oid);
if (!Files.exists(mediaFile)) { if (!Files.exists(mediaFile)) {
downloadLfsResource(db, res); downloadLfsResource(lfs, db, res);
} }
this.in = Files.newInputStream(mediaFile); this.in = Files.newInputStream(mediaFile);
} }
@ -141,6 +139,8 @@ public SmudgeFilter(Repository db, InputStream in, OutputStream out)
/** /**
* Download content which is hosted on a LFS server * Download content which is hosted on a LFS server
* *
* @param lfs
* local {@link Lfs} storage.
* @param db * @param db
* the repository to work with * the repository to work with
* @param res * @param res
@ -148,9 +148,8 @@ public SmudgeFilter(Repository db, InputStream in, OutputStream out)
* @return the paths of all mediafiles which have been downloaded * @return the paths of all mediafiles which have been downloaded
* @throws IOException * @throws IOException
*/ */
private Collection<Path> downloadLfsResource(Repository db, public static Collection<Path> downloadLfsResource(Lfs lfs, Repository db,
LfsPointer... res) LfsPointer... res) throws IOException {
throws IOException {
Collection<Path> downloadedPaths = new ArrayList<>(); Collection<Path> downloadedPaths = new ArrayList<>();
Map<String, LfsPointer> oidStr2ptr = new HashMap<>(); Map<String, LfsPointer> oidStr2ptr = new HashMap<>();
for (LfsPointer p : res) { for (LfsPointer p : res) {

View File

@ -56,7 +56,7 @@
@SuppressWarnings("nls") @SuppressWarnings("nls")
public final class Constants { public final class Constants {
/** /**
* lfs folder * lfs folder/section/filter name
* *
* @since 4.6 * @since 4.6
*/ */

View File

@ -65,8 +65,7 @@
import org.eclipse.jgit.awtui.AwtAuthenticator; import org.eclipse.jgit.awtui.AwtAuthenticator;
import org.eclipse.jgit.awtui.AwtCredentialsProvider; import org.eclipse.jgit.awtui.AwtCredentialsProvider;
import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.lfs.CleanFilter; import org.eclipse.jgit.lfs.BuiltinLFS;
import org.eclipse.jgit.lfs.SmudgeFilter;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryBuilder; import org.eclipse.jgit.lib.RepositoryBuilder;
import org.eclipse.jgit.pgm.internal.CLIText; import org.eclipse.jgit.pgm.internal.CLIText;
@ -111,8 +110,7 @@ public class Main {
*/ */
public Main() { public Main() {
HttpTransport.setConnectionFactory(new HttpClientConnectionFactory()); HttpTransport.setConnectionFactory(new HttpClientConnectionFactory());
CleanFilter.register(); BuiltinLFS.register();
SmudgeFilter.register();
gcExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { gcExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
private final ThreadFactory baseFactory = Executors private final ThreadFactory baseFactory = Executors
.defaultThreadFactory(); .defaultThreadFactory();

View File

@ -63,8 +63,7 @@
import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lfs.CleanFilter; import org.eclipse.jgit.lfs.BuiltinLFS;
import org.eclipse.jgit.lfs.SmudgeFilter;
import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.FileMode;
@ -92,8 +91,7 @@ public class AddCommandTest extends RepositoryTestCase {
@Override @Override
public void setUp() throws Exception { public void setUp() throws Exception {
CleanFilter.register(); BuiltinLFS.register();
SmudgeFilter.register();
super.setUp(); super.setUp();
} }

View File

@ -74,8 +74,7 @@
import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lfs.CleanFilter; import org.eclipse.jgit.lfs.BuiltinLFS;
import org.eclipse.jgit.lfs.SmudgeFilter;
import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref;
@ -102,8 +101,7 @@ public class CheckoutCommandTest extends RepositoryTestCase {
@Override @Override
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
CleanFilter.register(); BuiltinLFS.register();
SmudgeFilter.register();
super.setUp(); super.setUp();
git = new Git(db); git = new Git(db);
// commit something // commit something

View File

@ -1,5 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<component id="org.eclipse.jgit" version="2"> <component id="org.eclipse.jgit" version="2">
<resource path="src/org/eclipse/jgit/diff/DiffEntry.java" type="org.eclipse.jgit.diff.DiffEntry">
<filter id="336658481">
<message_arguments>
<message_argument value="org.eclipse.jgit.diff.DiffEntry"/>
<message_argument value="diffAttribute"/>
</message_arguments>
</filter>
</resource>
<resource path="src/org/eclipse/jgit/lib/ConfigConstants.java" type="org.eclipse.jgit.lib.ConfigConstants"> <resource path="src/org/eclipse/jgit/lib/ConfigConstants.java" type="org.eclipse.jgit.lib.ConfigConstants">
<filter id="336658481"> <filter id="336658481">
<message_arguments> <message_arguments>

View File

@ -462,6 +462,7 @@ noHMACsupport=No {0} support: {1}
noMergeBase=No merge base could be determined. Reason={0}. {1} noMergeBase=No merge base could be determined. Reason={0}. {1}
noMergeHeadSpecified=No merge head specified noMergeHeadSpecified=No merge head specified
nonBareLinkFilesNotSupported=Link files are not supported with nonbare repos nonBareLinkFilesNotSupported=Link files are not supported with nonbare repos
noPathAttributesFound=No Attributes found for {0}.
noSuchRef=no such ref noSuchRef=no such ref
noSuchSubmodule=no such submodule {0} noSuchSubmodule=no such submodule {0}
notABoolean=Not a boolean: {0} notABoolean=Not a boolean: {0}

View File

@ -303,7 +303,8 @@ public BlameGenerator push(String description, RawText contents)
throws IOException { throws IOException {
if (description == null) if (description == null)
description = JGitText.get().blameNotCommittedYet; description = JGitText.get().blameNotCommittedYet;
BlobCandidate c = new BlobCandidate(description, resultPath); BlobCandidate c = new BlobCandidate(getRepository(), description,
resultPath);
c.sourceText = contents; c.sourceText = contents;
c.regionList = new Region(0, 0, contents.size()); c.regionList = new Region(0, 0, contents.size());
remaining = contents.size(); remaining = contents.size();
@ -333,7 +334,8 @@ public BlameGenerator push(String description, AnyObjectId id)
if (ldr.getType() == OBJ_BLOB) { if (ldr.getType() == OBJ_BLOB) {
if (description == null) if (description == null)
description = JGitText.get().blameNotCommittedYet; description = JGitText.get().blameNotCommittedYet;
BlobCandidate c = new BlobCandidate(description, resultPath); BlobCandidate c = new BlobCandidate(getRepository(), description,
resultPath);
c.sourceBlob = id.toObjectId(); c.sourceBlob = id.toObjectId();
c.sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE)); c.sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE));
c.regionList = new Region(0, 0, c.sourceText.size()); c.regionList = new Region(0, 0, c.sourceText.size());
@ -346,7 +348,7 @@ public BlameGenerator push(String description, AnyObjectId id)
if (!find(commit, resultPath)) if (!find(commit, resultPath))
return this; return this;
Candidate c = new Candidate(commit, resultPath); Candidate c = new Candidate(getRepository(), commit, resultPath);
c.sourceBlob = idBuf.toObjectId(); c.sourceBlob = idBuf.toObjectId();
c.loadText(reader); c.loadText(reader);
c.regionList = new Region(0, 0, c.sourceText.size()); c.regionList = new Region(0, 0, c.sourceText.size());
@ -430,7 +432,8 @@ public BlameGenerator reverse(AnyObjectId start,
// just pump the queue // just pump the queue
} }
ReverseCandidate c = new ReverseCandidate(result, resultPath); ReverseCandidate c = new ReverseCandidate(getRepository(), result,
resultPath);
c.sourceBlob = idBuf.toObjectId(); c.sourceBlob = idBuf.toObjectId();
c.loadText(reader); c.loadText(reader);
c.regionList = new Region(0, 0, c.sourceText.size()); c.regionList = new Region(0, 0, c.sourceText.size());
@ -637,7 +640,8 @@ private boolean processOne(Candidate n) throws IOException {
return false; return false;
} }
Candidate next = n.create(parent, PathFilter.create(r.getOldPath())); Candidate next = n.create(getRepository(), parent,
PathFilter.create(r.getOldPath()));
next.sourceBlob = r.getOldId().toObjectId(); next.sourceBlob = r.getOldId().toObjectId();
next.renameScore = r.getScore(); next.renameScore = r.getScore();
next.loadText(reader); next.loadText(reader);
@ -653,7 +657,7 @@ private boolean blameEntireRegionOnParent(Candidate n, RevCommit parent) {
private boolean splitBlameWithParent(Candidate n, RevCommit parent) private boolean splitBlameWithParent(Candidate n, RevCommit parent)
throws IOException { throws IOException {
Candidate next = n.create(parent, n.sourcePath); Candidate next = n.create(getRepository(), parent, n.sourcePath);
next.sourceBlob = idBuf.toObjectId(); next.sourceBlob = idBuf.toObjectId();
next.loadText(reader); next.loadText(reader);
return split(next, n); return split(next, n);
@ -740,12 +744,12 @@ private boolean processMerge(Candidate n) throws IOException {
Candidate p; Candidate p;
if (renames != null && renames[pIdx] != null) { if (renames != null && renames[pIdx] != null) {
p = n.create(parent, p = n.create(getRepository(), parent,
PathFilter.create(renames[pIdx].getOldPath())); PathFilter.create(renames[pIdx].getOldPath()));
p.renameScore = renames[pIdx].getScore(); p.renameScore = renames[pIdx].getScore();
p.sourceBlob = renames[pIdx].getOldId().toObjectId(); p.sourceBlob = renames[pIdx].getOldId().toObjectId();
} else if (ids != null && ids[pIdx] != null) { } else if (ids != null && ids[pIdx] != null) {
p = n.create(parent, n.sourcePath); p = n.create(getRepository(), parent, n.sourcePath);
p.sourceBlob = ids[pIdx]; p.sourceBlob = ids[pIdx];
} else { } else {
continue; continue;

View File

@ -55,10 +55,12 @@
import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag; import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.util.LfsFactory;
/** /**
* A source that may have supplied some (or all) of the result file. * A source that may have supplied some (or all) of the result file.
@ -109,7 +111,11 @@ class Candidate {
*/ */
int renameScore; int renameScore;
Candidate(RevCommit commit, PathFilter path) { /** repository used for LFS blob handling */
private Repository sourceRepository;
Candidate(Repository repo, RevCommit commit, PathFilter path) {
sourceRepository = repo;
sourceCommit = commit; sourceCommit = commit;
sourcePath = path; sourcePath = path;
} }
@ -150,12 +156,12 @@ PersonIdent getAuthor() {
return sourceCommit.getAuthorIdent(); return sourceCommit.getAuthorIdent();
} }
Candidate create(RevCommit commit, PathFilter path) { Candidate create(Repository repo, RevCommit commit, PathFilter path) {
return new Candidate(commit, path); return new Candidate(repo, commit, path);
} }
Candidate copy(RevCommit commit) { Candidate copy(RevCommit commit) {
Candidate r = create(commit, sourcePath); Candidate r = create(sourceRepository, commit, sourcePath);
r.sourceBlob = sourceBlob; r.sourceBlob = sourceBlob;
r.sourceText = sourceText; r.sourceText = sourceText;
r.regionList = regionList; r.regionList = regionList;
@ -164,7 +170,11 @@ Candidate copy(RevCommit commit) {
} }
void loadText(ObjectReader reader) throws IOException { void loadText(ObjectReader reader) throws IOException {
ObjectLoader ldr = reader.open(sourceBlob, Constants.OBJ_BLOB); ObjectLoader ldr = LfsFactory.getInstance().applySmudgeFilter(sourceRepository,
reader.open(sourceBlob, Constants.OBJ_BLOB),
LfsFactory.getAttributesForPath(sourceRepository,
sourcePath.getPath(), sourceCommit)
.get(Constants.ATTR_DIFF));
sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE)); sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE));
} }
@ -349,8 +359,9 @@ public String toString() {
* children pointers, allowing reverse navigation of history. * children pointers, allowing reverse navigation of history.
*/ */
static final class ReverseCandidate extends Candidate { static final class ReverseCandidate extends Candidate {
ReverseCandidate(ReverseCommit commit, PathFilter path) { ReverseCandidate(Repository repo, ReverseCommit commit,
super(commit, path); PathFilter path) {
super(repo, commit, path);
} }
@Override @Override
@ -370,8 +381,8 @@ int getTime() {
} }
@Override @Override
Candidate create(RevCommit commit, PathFilter path) { Candidate create(Repository repo, RevCommit commit, PathFilter path) {
return new ReverseCandidate((ReverseCommit) commit, path); return new ReverseCandidate(repo, (ReverseCommit) commit, path);
} }
@Override @Override
@ -400,8 +411,8 @@ static final class BlobCandidate extends Candidate {
/** Author name to refer to this blob with. */ /** Author name to refer to this blob with. */
String description; String description;
BlobCandidate(String name, PathFilter path) { BlobCandidate(Repository repo, String name, PathFilter path) {
super(null, path); super(repo, null, path);
description = name; description = name;
} }

View File

@ -48,9 +48,11 @@
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.eclipse.jgit.attributes.Attribute;
import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.MutableObjectId;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
@ -196,6 +198,11 @@ public static List<DiffEntry> scan(TreeWalk walk, boolean includeTrees,
entry.newMode = walk.getFileMode(1); entry.newMode = walk.getFileMode(1);
entry.newPath = entry.oldPath = walk.getPathString(); entry.newPath = entry.oldPath = walk.getPathString();
if (walk.getAttributesNodeProvider() != null) {
entry.diffAttribute = walk.getAttributes()
.get(Constants.ATTR_DIFF);
}
if (treeFilterMarker != null) if (treeFilterMarker != null)
entry.treeFilterMarks = treeFilterMarker.getMarks(walk); entry.treeFilterMarks = treeFilterMarker.getMarks(walk);
@ -282,6 +289,7 @@ static List<DiffEntry> breakModify(DiffEntry entry) {
del.newMode = FileMode.MISSING; del.newMode = FileMode.MISSING;
del.newPath = DiffEntry.DEV_NULL; del.newPath = DiffEntry.DEV_NULL;
del.changeType = ChangeType.DELETE; del.changeType = ChangeType.DELETE;
del.diffAttribute = entry.diffAttribute;
DiffEntry add = new DiffEntry(); DiffEntry add = new DiffEntry();
add.oldId = A_ZERO; add.oldId = A_ZERO;
@ -292,6 +300,7 @@ static List<DiffEntry> breakModify(DiffEntry entry) {
add.newMode = entry.getNewMode(); add.newMode = entry.getNewMode();
add.newPath = entry.getNewPath(); add.newPath = entry.getNewPath();
add.changeType = ChangeType.ADD; add.changeType = ChangeType.ADD;
add.diffAttribute = entry.diffAttribute;
return Arrays.asList(del, add); return Arrays.asList(del, add);
} }
@ -306,6 +315,7 @@ static DiffEntry pair(ChangeType changeType, DiffEntry src, DiffEntry dst,
r.newId = dst.newId; r.newId = dst.newId;
r.newMode = dst.newMode; r.newMode = dst.newMode;
r.newPath = dst.newPath; r.newPath = dst.newPath;
r.diffAttribute = dst.diffAttribute;
r.changeType = changeType; r.changeType = changeType;
r.score = score; r.score = score;
@ -321,6 +331,13 @@ static DiffEntry pair(ChangeType changeType, DiffEntry src, DiffEntry dst,
/** File name of the new (post-image). */ /** File name of the new (post-image). */
protected String newPath; protected String newPath;
/**
* diff filter attribute
*
* @since 4.11
*/
protected Attribute diffAttribute;
/** Old mode of the file, if described by the patch, else null. */ /** Old mode of the file, if described by the patch, else null. */
protected FileMode oldMode; protected FileMode oldMode;
@ -394,6 +411,14 @@ public String getPath(Side side) {
return side == Side.OLD ? getOldPath() : getNewPath(); return side == Side.OLD ? getOldPath() : getNewPath();
} }
/**
* @return the {@link Attribute} determining filters to be applied.
* @since 4.11
*/
public Attribute getDiffAttribute() {
return diffAttribute;
}
/** /**
* Get the old file mode * Get the old file mode
* *

View File

@ -98,6 +98,7 @@
import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter; import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.LfsFactory;
import org.eclipse.jgit.util.QuotedString; import org.eclipse.jgit.util.QuotedString;
/** /**
@ -141,6 +142,8 @@ public class DiffFormatter implements AutoCloseable {
private ContentSource.Pair source; private ContentSource.Pair source;
private Repository repository;
/** /**
* Create a new formatter with a default level of context. * Create a new formatter with a default level of context.
* *
@ -172,6 +175,7 @@ protected OutputStream getOutputStream() {
* source repository holding referenced objects. * source repository holding referenced objects.
*/ */
public void setRepository(Repository repository) { public void setRepository(Repository repository) {
this.repository = repository;
setReader(repository.newObjectReader(), repository.getConfig(), true); setReader(repository.newObjectReader(), repository.getConfig(), true);
} }
@ -1057,7 +1061,8 @@ private RawText open(DiffEntry.Side side, DiffEntry entry)
throw new AmbiguousObjectException(id, ids); throw new AmbiguousObjectException(id, ids);
} }
ObjectLoader ldr = source.open(side, entry); ObjectLoader ldr = LfsFactory.getInstance().applySmudgeFilter(repository,
source.open(side, entry), entry.getDiffAttribute());
return RawText.load(ldr, binaryFileThreshold); return RawText.load(ldr, binaryFileThreshold);
} }

View File

@ -110,6 +110,14 @@ public RawText(File file) throws IOException {
this(IO.readFully(file)); this(IO.readFully(file));
} }
/**
* @return the raw, unprocessed content read.
* @since 4.11
*/
public byte[] getRawContent() {
return content;
}
/** @return total number of items in the sequence. */ /** @return total number of items in the sequence. */
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override

View File

@ -43,13 +43,11 @@
package org.eclipse.jgit.hooks; package org.eclipse.jgit.hooks;
import java.io.PrintStream; import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.text.MessageFormat; import java.text.MessageFormat;
import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.util.LfsFactory;
/** /**
* Factory class for instantiating supported hooks. * Factory class for instantiating supported hooks.
@ -112,28 +110,16 @@ public static CommitMsgHook commitMsg(Repository repo,
* @since 4.2 * @since 4.2
*/ */
public static PrePushHook prePush(Repository repo, PrintStream outputStream) { public static PrePushHook prePush(Repository repo, PrintStream outputStream) {
PrePushHook lfsHook = null; if (LfsFactory.getInstance().isAvailable()) {
try { PrePushHook hook = LfsFactory.getInstance().getPrePushHook(repo,
StoredConfig cfg = repo.getConfig(); outputStream);
if (cfg.getBoolean(ConfigConstants.CONFIG_FILTER_SECTION, "lfs", //$NON-NLS-1$ if (hook != null) {
ConfigConstants.CONFIG_KEY_USEJGITBUILTIN, false)) { if (hook.isNativeHookPresent()) {
@SuppressWarnings("unchecked") throw new IllegalStateException(MessageFormat
Class<? extends PrePushHook> cls = (Class<? extends PrePushHook>) Class .format(JGitText.get().lfsHookConflict, repo));
.forName("org.eclipse.jgit.lfs.LfsPrePushHook"); //$NON-NLS-1$ }
Constructor<? extends PrePushHook> constructor = cls return hook;
.getConstructor(Repository.class, PrintStream.class);
lfsHook = constructor.newInstance(repo, outputStream);
} }
} catch (Exception e) {
// no problem :) no LFS support present
}
if (lfsHook != null) {
if (lfsHook.isNativeHookPresent()) {
throw new IllegalStateException(MessageFormat
.format(JGitText.get().lfsHookConflict, repo));
}
return lfsHook;
} }
return new PrePushHook(repo, outputStream); return new PrePushHook(repo, outputStream);
} }

View File

@ -523,6 +523,7 @@ public static JGitText get() {
/***/ public String noMergeBase; /***/ public String noMergeBase;
/***/ public String noMergeHeadSpecified; /***/ public String noMergeHeadSpecified;
/***/ public String nonBareLinkFilesNotSupported; /***/ public String nonBareLinkFilesNotSupported;
/***/ public String noPathAttributesFound;
/***/ public String noSuchRef; /***/ public String noSuchRef;
/***/ public String noSuchSubmodule; /***/ public String noSuchSubmodule;
/***/ public String notABoolean; /***/ public String notABoolean;

View File

@ -437,6 +437,13 @@ public final class Constants {
*/ */
public static final String ATTR_MERGE = "merge"; //$NON-NLS-1$ public static final String ATTR_MERGE = "merge"; //$NON-NLS-1$
/**
* Diff attribute.
*
* @since 4.11
*/
public static final String ATTR_DIFF = "diff"; //$NON-NLS-1$
/** /**
* Binary value for custom merger. * Binary value for custom merger.
* *

View File

@ -87,12 +87,12 @@
import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.storage.pack.PackConfig; import org.eclipse.jgit.storage.pack.PackConfig;
@ -106,6 +106,8 @@
import org.eclipse.jgit.treewalk.WorkingTreeOptions; import org.eclipse.jgit.treewalk.WorkingTreeOptions;
import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.LfsFactory;
import org.eclipse.jgit.util.LfsFactory.LfsInputStream;
import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.io.EolStreamTypeUtil; import org.eclipse.jgit.util.io.EolStreamTypeUtil;
@ -769,11 +771,12 @@ protected boolean processEntry(CanonicalTreeParser base,
return false; return false;
} }
MergeResult<RawText> result = contentMerge(base, ours, theirs); MergeResult<RawText> result = contentMerge(base, ours, theirs,
attributes);
if (ignoreConflicts) { if (ignoreConflicts) {
result.setContainsConflicts(false); result.setContainsConflicts(false);
} }
updateIndex(base, ours, theirs, result); updateIndex(base, ours, theirs, result, attributes);
if (result.containsConflicts() && !ignoreConflicts) if (result.containsConflicts() && !ignoreConflicts)
unmergedPaths.add(tw.getPathString()); unmergedPaths.add(tw.getPathString());
modifiedFiles.add(tw.getPathString()); modifiedFiles.add(tw.getPathString());
@ -781,7 +784,8 @@ protected boolean processEntry(CanonicalTreeParser base,
// OURS or THEIRS has been deleted // OURS or THEIRS has been deleted
if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw
.idEqual(T_BASE, T_THEIRS)))) { .idEqual(T_BASE, T_THEIRS)))) {
MergeResult<RawText> result = contentMerge(base, ours, theirs); MergeResult<RawText> result = contentMerge(base, ours, theirs,
attributes);
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0);
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0); add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0);
@ -816,12 +820,14 @@ protected boolean processEntry(CanonicalTreeParser base,
* @param base * @param base
* @param ours * @param ours
* @param theirs * @param theirs
* @param attributes
* *
* @return the result of the content merge * @return the result of the content merge
* @throws IOException * @throws IOException
*/ */
private MergeResult<RawText> contentMerge(CanonicalTreeParser base, private MergeResult<RawText> contentMerge(CanonicalTreeParser base,
CanonicalTreeParser ours, CanonicalTreeParser theirs) CanonicalTreeParser ours, CanonicalTreeParser theirs,
Attributes attributes)
throws IOException { throws IOException {
RawText baseText; RawText baseText;
RawText ourText; RawText ourText;
@ -829,11 +835,11 @@ private MergeResult<RawText> contentMerge(CanonicalTreeParser base,
try { try {
baseText = base == null ? RawText.EMPTY_TEXT : getRawText( baseText = base == null ? RawText.EMPTY_TEXT : getRawText(
base.getEntryObjectId(), reader); base.getEntryObjectId(), attributes);
ourText = ours == null ? RawText.EMPTY_TEXT : getRawText( ourText = ours == null ? RawText.EMPTY_TEXT : getRawText(
ours.getEntryObjectId(), reader); ours.getEntryObjectId(), attributes);
theirsText = theirs == null ? RawText.EMPTY_TEXT : getRawText( theirsText = theirs == null ? RawText.EMPTY_TEXT : getRawText(
theirs.getEntryObjectId(), reader); theirs.getEntryObjectId(), attributes);
} catch (BinaryBlobException e) { } catch (BinaryBlobException e) {
MergeResult<RawText> r = new MergeResult<>(Collections.<RawText>emptyList()); MergeResult<RawText> r = new MergeResult<>(Collections.<RawText>emptyList());
r.setContainsConflicts(true); r.setContainsConflicts(true);
@ -897,17 +903,20 @@ private boolean isWorktreeDirty(WorkingTreeIterator work,
* @param ours * @param ours
* @param theirs * @param theirs
* @param result * @param result
* @param attributes
* @throws FileNotFoundException * @throws FileNotFoundException
* @throws IOException * @throws IOException
*/ */
private void updateIndex(CanonicalTreeParser base, private void updateIndex(CanonicalTreeParser base,
CanonicalTreeParser ours, CanonicalTreeParser theirs, CanonicalTreeParser ours, CanonicalTreeParser theirs,
MergeResult<RawText> result) throws FileNotFoundException, MergeResult<RawText> result, Attributes attributes)
throws FileNotFoundException,
IOException { IOException {
TemporaryBuffer rawMerged = null; TemporaryBuffer rawMerged = null;
try { try {
rawMerged = doMerge(result); rawMerged = doMerge(result);
File mergedFile = inCore ? null : writeMergedFile(rawMerged); File mergedFile = inCore ? null
: writeMergedFile(rawMerged, attributes);
if (result.containsConflicts()) { if (result.containsConflicts()) {
// A conflict occurred, the file will contain conflict markers // A conflict occurred, the file will contain conflict markers
// the index will be populated with the three stages and the // the index will be populated with the three stages and the
@ -934,7 +943,7 @@ private void updateIndex(CanonicalTreeParser base,
nonNullRepo().getFS().lastModified(mergedFile)); nonNullRepo().getFS().lastModified(mergedFile));
dce.setLength((int) mergedFile.length()); dce.setLength((int) mergedFile.length());
} }
dce.setObjectId(insertMergeResult(rawMerged)); dce.setObjectId(insertMergeResult(rawMerged, attributes));
builder.add(dce); builder.add(dce);
} finally { } finally {
if (rawMerged != null) { if (rawMerged != null) {
@ -948,11 +957,14 @@ private void updateIndex(CanonicalTreeParser base,
* *
* @param rawMerged * @param rawMerged
* the raw merged content * the raw merged content
* @param attributes
* the files .gitattributes entries
* @return the working tree file to which the merged content was written. * @return the working tree file to which the merged content was written.
* @throws FileNotFoundException * @throws FileNotFoundException
* @throws IOException * @throws IOException
*/ */
private File writeMergedFile(TemporaryBuffer rawMerged) private File writeMergedFile(TemporaryBuffer rawMerged,
Attributes attributes)
throws FileNotFoundException, IOException { throws FileNotFoundException, IOException {
File workTree = nonNullRepo().getWorkTree(); File workTree = nonNullRepo().getWorkTree();
FS fs = nonNullRepo().getFS(); FS fs = nonNullRepo().getFS();
@ -963,7 +975,7 @@ private File writeMergedFile(TemporaryBuffer rawMerged)
} }
EolStreamType streamType = EolStreamTypeUtil.detectStreamType( EolStreamType streamType = EolStreamTypeUtil.detectStreamType(
OperationType.CHECKOUT_OP, workingTreeOptions, OperationType.CHECKOUT_OP, workingTreeOptions,
tw.getAttributes()); attributes);
try (OutputStream os = EolStreamTypeUtil.wrapOutputStream( try (OutputStream os = EolStreamTypeUtil.wrapOutputStream(
new BufferedOutputStream(new FileOutputStream(of)), new BufferedOutputStream(new FileOutputStream(of)),
streamType)) { streamType)) {
@ -987,9 +999,13 @@ private TemporaryBuffer doMerge(MergeResult<RawText> result)
return buf; return buf;
} }
private ObjectId insertMergeResult(TemporaryBuffer buf) throws IOException { private ObjectId insertMergeResult(TemporaryBuffer buf,
try (InputStream in = buf.openInputStream()) { Attributes attributes) throws IOException {
return getObjectInserter().insert(OBJ_BLOB, buf.length(), in); InputStream in = buf.openInputStream();
try (LfsInputStream is = LfsFactory.getInstance().applyCleanFilter(
getRepository(), in,
buf.length(), attributes.get(Constants.ATTR_MERGE))) {
return getObjectInserter().insert(OBJ_BLOB, is.getLength(), is);
} }
} }
@ -1021,12 +1037,15 @@ private int mergeFileModes(int modeB, int modeO, int modeT) {
return FileMode.MISSING.getBits(); return FileMode.MISSING.getBits();
} }
private static RawText getRawText(ObjectId id, ObjectReader reader) private RawText getRawText(ObjectId id,
Attributes attributes)
throws IOException, BinaryBlobException { throws IOException, BinaryBlobException {
if (id.equals(ObjectId.zeroId())) if (id.equals(ObjectId.zeroId()))
return new RawText(new byte[] {}); return new RawText(new byte[] {});
ObjectLoader loader = reader.open(id, OBJ_BLOB); ObjectLoader loader = LfsFactory.getInstance().applySmudgeFilter(
getRepository(), reader.open(id, OBJ_BLOB),
attributes.get(Constants.ATTR_MERGE));
int threshold = PackConfig.DEFAULT_BIG_FILE_THRESHOLD; int threshold = PackConfig.DEFAULT_BIG_FILE_THRESHOLD;
return RawText.load(loader, threshold); return RawText.load(loader, threshold);
} }

View File

@ -0,0 +1,284 @@
/*
* Copyright (C) 2018, Markus Duft <markus.duft@ssi-schaefer.com>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.text.MessageFormat;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.attributes.Attribute;
import org.eclipse.jgit.attributes.Attributes;
import org.eclipse.jgit.hooks.PrePushHook;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
/**
* Represents an optionally present LFS support implementation
*
* @since 4.11
*/
public class LfsFactory {
private static LfsFactory instance = new LfsFactory();
/**
* Constructor
*/
protected LfsFactory() {
}
/**
* @return the current LFS implementation
*/
public static LfsFactory getInstance() {
return instance;
}
/**
* @param instance
* register a {@link LfsFactory} instance as the
* {@link LfsFactory} implementation to use.
*/
public static void setInstance(LfsFactory instance) {
LfsFactory.instance = instance;
}
/**
* @return whether LFS support is available
*/
public boolean isAvailable() {
return false;
}
/**
* Apply clean filtering to the given stream, writing the file content to
* the LFS storage if required and returning a stream to the LFS pointer
* instead.
*
* @param db
* the repository
* @param input
* the original input
* @param length
* the expected input stream length
* @param attribute
* the attribute used to check for LFS enablement (i.e. "merge",
* "diff", "filter" from .gitattributes).
* @return a stream to the content that should be written to the object
* store along with the expected length of the stream. the original
* stream is not applicable.
* @throws IOException
* in case of an error
*/
public LfsInputStream applyCleanFilter(Repository db,
InputStream input, long length, Attribute attribute)
throws IOException {
return new LfsInputStream(input, length);
}
/**
* Apply smudge filtering to a given loader, potentially redirecting it to a
* LFS blob which is downloaded on demand.
*
* @param db
* the repository
* @param loader
* the loader for the blob
* @param attribute
* the attribute used to check for LFS enablement (i.e. "merge",
* "diff", "filter" from .gitattributes).
* @return a loader for the actual data of a blob, or the original loader in
* case LFS is not applicable.
* @throws IOException
*/
public ObjectLoader applySmudgeFilter(Repository db,
ObjectLoader loader, Attribute attribute) throws IOException {
return loader;
}
/**
* Retrieve a pre-push hook to be applied.
*
* @param repo
* the {@link Repository} the hook is applied to.
* @param outputStream
* @return a {@link PrePushHook} implementation or <code>null</code>
*/
public @Nullable PrePushHook getPrePushHook(Repository repo,
PrintStream outputStream) {
return null;
}
/**
* @param db
* the repository
* @param path
* the path to find attributes for
* @return the {@link Attributes} for the given path.
* @throws IOException
* in case of an error
*/
public static Attributes getAttributesForPath(Repository db, String path)
throws IOException {
try (TreeWalk walk = new TreeWalk(db)) {
walk.addTree(new FileTreeIterator(db));
PathFilter f = PathFilter.create(path);
walk.setFilter(f);
walk.setRecursive(false);
Attributes attr = null;
while (walk.next()) {
if (f.isDone(walk)) {
attr = walk.getAttributes();
break;
} else if (walk.isSubtree()) {
walk.enterSubtree();
}
}
if (attr == null) {
throw new IOException(MessageFormat
.format(JGitText.get().noPathAttributesFound, path));
}
return attr;
}
}
/**
* Get attributes for given path and commit
*
* @param db
* the repository
* @param path
* the path to find attributes for
* @param commit
* the commit to inspect.
* @return the {@link Attributes} for the given path.
* @throws IOException
* in case of an error
*/
public static Attributes getAttributesForPath(Repository db, String path,
RevCommit commit) throws IOException {
if (commit == null) {
return getAttributesForPath(db, path);
}
try (TreeWalk walk = TreeWalk.forPath(db, path, commit.getTree())) {
Attributes attr = walk == null ? null : walk.getAttributes();
if (attr == null) {
throw new IOException(MessageFormat
.format(JGitText.get().noPathAttributesFound, path));
}
return attr;
}
}
/**
* Encapsulate a potentially exchanged {@link InputStream} along with the
* expected stream content length.
*/
public static final class LfsInputStream extends InputStream {
/**
* The actual stream.
*/
private InputStream stream;
/**
* The expected stream content length.
*/
private long length;
/**
* Create a new wrapper around a certain stream
*
* @param stream
* the stream to wrap. the stream will be closed on
* {@link #close()}.
* @param length
* the expected length of the stream
*/
public LfsInputStream(InputStream stream, long length) {
this.stream = stream;
this.length = length;
}
/**
* Create a new wrapper around a temporary buffer.
*
* @param buffer
* the buffer to initialize stream and length from. The
* buffer will be destroyed on {@link #close()}
* @throws IOException
* in case of an error opening the stream to the buffer.
*/
public LfsInputStream(TemporaryBuffer buffer) throws IOException {
this.stream = buffer.openInputStream();
this.length = buffer.length();
}
@Override
public void close() throws IOException {
stream.close();
}
@Override
public int read() throws IOException {
return stream.read();
}
/**
* @return the length of the stream
*/
public long getLength() {
return length;
}
}
}