/**
* This file is part of git-as-svn. It is subject to the license terms
* in the LICENSE file found in the top-level directory of this distribution
* and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn,
* including this file, may be copied, modified, propagated, or distributed
* except according to the terms contained in the LICENSE file.
*/
package svnserver.repository.git;
import com.google.common.io.ByteStreams;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.io.diff.SVNDeltaProcessor;
import org.tmatesoft.svn.core.io.diff.SVNDiffWindow;
import svnserver.TemporaryOutputStream;
import svnserver.auth.User;
import svnserver.repository.VcsDeltaConsumer;
import svnserver.repository.git.filter.GitFilter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
/**
* Delta consumer for applying svn diff on git blob.
*
* @author a.navrotskiy
*/
public class GitDeltaConsumer implements VcsDeltaConsumer {
@NotNull
private static final Logger log = LoggerFactory.getLogger(GitDeltaConsumer.class);
@NotNull
private final Map<String, String> props;
@NotNull
private final GitWriter writer;
@NotNull
private final GitEntry entry;
@Nullable
private final User user;
@Nullable
private SVNDeltaProcessor window;
@Nullable
private final GitObject<ObjectId> originalId;
@Nullable
private final String originalMd5;
@Nullable
private GitObject<ObjectId> objectId;
@Nullable
private final GitFilter oldFilter;
@Nullable
private GitFilter newFilter;
@NotNull
private TemporaryOutputStream temporaryStream;
@Nullable
private String md5;
public GitDeltaConsumer(@NotNull GitWriter writer, @NotNull GitEntry entry, @Nullable GitFile file, @Nullable User user) throws IOException, SVNException {
this.writer = writer;
this.entry = entry;
this.user = user;
if (file != null) {
this.originalMd5 = file.getMd5();
this.originalId = file.getObjectId();
this.props = new HashMap<>(file.getProperties());
this.oldFilter = file.getFilter();
} else {
this.originalMd5 = null;
this.originalId = null;
this.props = new HashMap<>();
this.oldFilter = null;
}
this.newFilter = null;
this.objectId = originalId;
this.temporaryStream = new TemporaryOutputStream();
}
@NotNull
@Override
public Map<String, String> getProperties() {
return props;
}
@Nullable
public GitObject<ObjectId> getOriginalId() {
return originalId;
}
@Nullable
public GitObject<ObjectId> getObjectId() throws IOException, SVNException {
if ((originalId != null) && originalId.equals(objectId) && (newFilter == null)) {
this.newFilter = oldFilter;
this.objectId = originalId;
if (oldFilter == null) {
throw new IllegalStateException("Original object ID defined, but original Filter is not defined");
}
migrateFilter(writer.getRepository().getFilter(props.containsKey(SVNProperty.SPECIAL) ? FileMode.SYMLINK : FileMode.REGULAR_FILE, entry.getRawProperties()));
}
return objectId;
}
public boolean migrateFilter(@NotNull GitFilter filter) throws IOException, SVNException {
if (newFilter == null || objectId == null) {
throw new IllegalStateException("Original object ID defined, but original Filter is not defined");
}
final GitObject<ObjectId> beforeId = objectId;
if (!newFilter.equals(filter)) {
final Repository repo = writer.getRepository().getRepository();
try (
final TemporaryOutputStream content = new TemporaryOutputStream();
final TemporaryOutputStream.Holder holder = content.holder()
) {
try (InputStream inputStream = newFilter.inputStream(objectId);
OutputStream outputStream = filter.outputStream(content, user)) {
ByteStreams.copy(inputStream, outputStream);
}
try (InputStream inputStream = content.toInputStream()) {
objectId = new GitObject<>(repo, writer.getInserter().insert(Constants.OBJ_BLOB, content.size(), inputStream));
newFilter = filter;
}
}
}
return !beforeId.equals(objectId);
}
@Override
public void applyTextDelta(String path, @Nullable String baseChecksum) throws SVNException {
try {
if ((originalMd5 != null) && (baseChecksum != null)) {
if (!baseChecksum.equals(originalMd5)) {
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.CHECKSUM_MISMATCH));
}
}
if (window != null)
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.RA_SVN_CMD_ERR));
newFilter = writer.getRepository().getFilter(props.containsKey(SVNProperty.SPECIAL) ? FileMode.SYMLINK : FileMode.REGULAR_FILE, entry.getRawProperties());
window = new SVNDeltaProcessor();
window.applyTextDelta((oldFilter != null && objectId != null) ? oldFilter.inputStream(objectId) : new ByteArrayInputStream(GitRepository.emptyBytes), newFilter.outputStream(temporaryStream, user), true);
} catch (IOException e) {
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.IO_ERROR), e);
}
}
@Override
public OutputStream textDeltaChunk(String path, SVNDiffWindow diffWindow) throws SVNException {
if (window == null)
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.RA_SVN_CMD_ERR));
return window.textDeltaChunk(diffWindow);
}
@Override
public void textDeltaEnd(String path) throws SVNException {
try (TemporaryOutputStream.Holder holder = temporaryStream.holder()) {
if (window == null)
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.RA_SVN_CMD_ERR));
final Repository repo = writer.getRepository().getRepository();
md5 = window.textDeltaEnd();
try (InputStream stream = temporaryStream.toInputStream()) {
objectId = new GitObject<>(repo, writer.getInserter().insert(Constants.OBJ_BLOB, temporaryStream.size(), stream));
}
log.info("Created blob {} for file: {}", objectId.getObject().getName(), entry.getFullPath());
} catch (IOException e) {
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.IO_ERROR), e);
}
}
@Override
public void validateChecksum(@NotNull String md5) throws SVNException {
if (window != null) {
if (!md5.equals(this.md5)) {
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.CHECKSUM_MISMATCH));
}
} else if (originalMd5 != null) {
if (!originalMd5.equals(md5)) {
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.CHECKSUM_MISMATCH));
}
}
}
@NotNull
public String getFilterName() {
if (newFilter != null)
return newFilter.getName();
if (oldFilter != null)
return oldFilter.getName();
throw new IllegalStateException();
}
}