blob: b1b146c85438201cf64dc6dec8de7a8cf9a6b2f9 [file] [log] [blame]
/*
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
* Copyright (C) 2010, Matthias Sohn <matthias.sohn@sap.com>
* Copyright (C) 2012-2013, Robin Rosenberg
* 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.treewalk;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetEncoder;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import org.eclipse.jgit.api.errors.FilterFailedException;
import org.eclipse.jgit.attributes.AttributesNode;
import org.eclipse.jgit.attributes.AttributesRule;
import org.eclipse.jgit.attributes.FilterCommand;
import org.eclipse.jgit.attributes.FilterCommandRegistry;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.ignore.FastIgnoreRule;
import org.eclipse.jgit.ignore.IgnoreNode;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.CoreConfig;
import org.eclipse.jgit.lib.CoreConfig.CheckStat;
import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.lib.CoreConfig.SymLinks;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.submodule.SubmoduleWalk;
import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.eclipse.jgit.util.Holder;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.Paths;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.TemporaryBuffer.LocalFile;
import org.eclipse.jgit.util.io.AutoLFInputStream;
import org.eclipse.jgit.util.io.EolStreamTypeUtil;
import org.eclipse.jgit.util.sha1.SHA1;
/**
* Walks a working directory tree as part of a {@link TreeWalk}.
* <p>
* Most applications will want to use the standard implementation of this
* iterator, {@link FileTreeIterator}, as that does all IO through the standard
* <code>java.io</code> package. Plugins for a Java based IDE may however wish
* to create their own implementations of this class to allow traversal of the
* IDE's project space, as well as benefit from any caching the IDE may have.
*
* @see FileTreeIterator
*/
public abstract class WorkingTreeIterator extends AbstractTreeIterator {
private static final int MAX_EXCEPTION_TEXT_SIZE = 10 * 1024;
/** An empty entry array, suitable for {@link #init(Entry[])}. */
protected static final Entry[] EOF = {};
/** Size we perform file IO in if we have to read and hash a file. */
static final int BUFFER_SIZE = 2048;
/**
* Maximum size of files which may be read fully into memory for performance
* reasons.
*/
private static final long MAXIMUM_FILE_SIZE_TO_READ_FULLY = 65536;
/** Inherited state of this iterator, describing working tree, etc. */
private final IteratorState state;
/** The {@link #idBuffer()} for the current entry. */
private byte[] contentId;
/** Index within {@link #entries} that {@link #contentId} came from. */
private int contentIdFromPtr;
/** List of entries obtained from the subclass. */
private Entry[] entries;
/** Total number of entries in {@link #entries} that are valid. */
private int entryCnt;
/** Current position within {@link #entries}. */
private int ptr;
/** If there is a .gitignore file present, the parsed rules from it. */
private IgnoreNode ignoreNode;
/**
* cached clean filter command. Use a Ref in order to distinguish between
* the ref not cached yet and the value null
*/
private Holder<String> cleanFilterCommandHolder;
/**
* cached eol stream type. Use a Ref in order to distinguish between the ref
* not cached yet and the value null
*/
private Holder<EolStreamType> eolStreamTypeHolder;
/** Repository that is the root level being iterated over */
protected Repository repository;
/** Cached canonical length, initialized from {@link #idBuffer()} */
private long canonLen = -1;
/** The offset of the content id in {@link #idBuffer()} */
private int contentIdOffset;
/**
* Create a new iterator with no parent.
*
* @param options
* working tree options to be used
*/
protected WorkingTreeIterator(WorkingTreeOptions options) {
super();
state = new IteratorState(options);
}
/**
* Create a new iterator with no parent and a prefix.
* <p>
* The prefix path supplied is inserted in front of all paths generated by
* this iterator. It is intended to be used when an iterator is being
* created for a subsection of an overall repository and needs to be
* combined with other iterators that are created to run over the entire
* repository namespace.
*
* @param prefix
* position of this iterator in the repository tree. The value
* may be null or the empty string to indicate the prefix is the
* root of the repository. A trailing slash ('/') is
* automatically appended if the prefix does not end in '/'.
* @param options
* working tree options to be used
*/
protected WorkingTreeIterator(final String prefix,
WorkingTreeOptions options) {
super(prefix);
state = new IteratorState(options);
}
/**
* Create an iterator for a subtree of an existing iterator.
*
* @param p
* parent tree iterator.
*/
protected WorkingTreeIterator(final WorkingTreeIterator p) {
super(p);
state = p.state;
repository = p.repository;
}
/**
* Initialize this iterator for the root level of a repository.
* <p>
* This method should only be invoked after calling {@link #init(Entry[])},
* and only for the root iterator.
*
* @param repo
* the repository.
*/
protected void initRootIterator(Repository repo) {
repository = repo;
Entry entry;
if (ignoreNode instanceof PerDirectoryIgnoreNode)
entry = ((PerDirectoryIgnoreNode) ignoreNode).entry;
else
entry = null;
ignoreNode = new RootIgnoreNode(entry, repo);
}
/**
* Define the matching {@link DirCacheIterator}, to optimize ObjectIds.
*
* Once the DirCacheIterator has been set this iterator must only be
* advanced by the TreeWalk that is supplied, as it assumes that itself and
* the corresponding DirCacheIterator are positioned on the same file path
* whenever {@link #idBuffer()} is invoked.
*
* @param walk
* the walk that will be advancing this iterator.
* @param treeId
* index of the matching {@link DirCacheIterator}.
*/
public void setDirCacheIterator(TreeWalk walk, int treeId) {
state.walk = walk;
state.dirCacheTree = treeId;
}
@Override
public boolean hasId() {
if (contentIdFromPtr == ptr)
return true;
return (mode & FileMode.TYPE_MASK) == FileMode.TYPE_FILE;
}
@Override
public byte[] idBuffer() {
if (contentIdFromPtr == ptr)
return contentId;
if (state.walk != null) {
// If there is a matching DirCacheIterator, we can reuse
// its idBuffer, but only if we appear to be clean against
// the cached index information for the path.
DirCacheIterator i = state.walk.getTree(state.dirCacheTree,
DirCacheIterator.class);
if (i != null) {
DirCacheEntry ent = i.getDirCacheEntry();
if (ent != null && compareMetadata(ent) == MetadataDiff.EQUAL
&& ((ent.getFileMode().getBits()
& FileMode.TYPE_MASK) != FileMode.TYPE_GITLINK)) {
contentIdOffset = i.idOffset();
contentIdFromPtr = ptr;
return contentId = i.idBuffer();
}
contentIdOffset = 0;
} else {
contentIdOffset = 0;
}
}
switch (mode & FileMode.TYPE_MASK) {
case FileMode.TYPE_SYMLINK:
case FileMode.TYPE_FILE:
contentIdFromPtr = ptr;
return contentId = idBufferBlob(entries[ptr]);
case FileMode.TYPE_GITLINK:
contentIdFromPtr = ptr;
return contentId = idSubmodule(entries[ptr]);
}
return zeroid;
}
@Override
public boolean isWorkTree() {
return true;
}
/**
* Get submodule id for given entry.
*
* @param e
* @return non-null submodule id
*/
protected byte[] idSubmodule(Entry e) {
if (repository == null)
return zeroid;
File directory;
try {
directory = repository.getWorkTree();
} catch (NoWorkTreeException nwte) {
return zeroid;
}
return idSubmodule(directory, e);
}
/**
* Get submodule id using the repository at the location of the entry
* relative to the directory.
*
* @param directory
* @param e
* @return non-null submodule id
*/
protected byte[] idSubmodule(File directory, Entry e) {
final Repository submoduleRepo;
try {
submoduleRepo = SubmoduleWalk.getSubmoduleRepository(directory,
e.getName());
} catch (IOException exception) {
return zeroid;
}
if (submoduleRepo == null)
return zeroid;
final ObjectId head;
try {
head = submoduleRepo.resolve(Constants.HEAD);
} catch (IOException exception) {
return zeroid;
} finally {
submoduleRepo.close();
}
if (head == null)
return zeroid;
final byte[] id = new byte[Constants.OBJECT_ID_LENGTH];
head.copyRawTo(id, 0);
return id;
}
private static final byte[] digits = { '0', '1', '2', '3', '4', '5', '6',
'7', '8', '9' };
private static final byte[] hblob = Constants
.encodedTypeString(Constants.OBJ_BLOB);
private byte[] idBufferBlob(final Entry e) {
try {
final InputStream is = e.openInputStream();
if (is == null)
return zeroid;
try {
state.initializeReadBuffer();
final long len = e.getLength();
InputStream filteredIs = possiblyFilteredInputStream(e, is, len,
OperationType.CHECKIN_OP);
return computeHash(filteredIs, canonLen);
} finally {
safeClose(is);
}
} catch (IOException err) {
// Can't read the file? Don't report the failure either.
return zeroid;
}
}
private InputStream possiblyFilteredInputStream(final Entry e,
final InputStream is, final long len) throws IOException {
return possiblyFilteredInputStream(e, is, len, null);
}
private InputStream possiblyFilteredInputStream(final Entry e,
final InputStream is, final long len, OperationType opType)
throws IOException {
if (getCleanFilterCommand() == null
&& getEolStreamType(opType) == EolStreamType.DIRECT) {
canonLen = len;
return is;
}
if (len <= MAXIMUM_FILE_SIZE_TO_READ_FULLY) {
ByteBuffer rawbuf = IO.readWholeStream(is, (int) len);
rawbuf = filterClean(rawbuf.array(), rawbuf.limit(), opType);
canonLen = rawbuf.limit();
return new ByteArrayInputStream(rawbuf.array(), 0, (int) canonLen);
}
if (getCleanFilterCommand() == null && isBinary(e)) {
canonLen = len;
return is;
}
final InputStream lenIs = filterClean(e.openInputStream(),
opType);
try {
canonLen = computeLength(lenIs);
} finally {
safeClose(lenIs);
}
return filterClean(is, opType);
}
private static void safeClose(final InputStream in) {
try {
in.close();
} catch (IOException err2) {
// Suppress any error related to closing an input
// stream. We don't care, we should not have any
// outstanding data to flush or anything like that.
}
}
private static boolean isBinary(Entry entry) throws IOException {
InputStream in = entry.openInputStream();
try {
return RawText.isBinary(in);
} finally {
safeClose(in);
}
}
private ByteBuffer filterClean(byte[] src, int n, OperationType opType)
throws IOException {
InputStream in = new ByteArrayInputStream(src);
try {
return IO.readWholeStream(filterClean(in, opType), n);
} finally {
safeClose(in);
}
}
private InputStream filterClean(InputStream in) throws IOException {
return filterClean(in, null);
}
private InputStream filterClean(InputStream in, OperationType opType)
throws IOException {
in = handleAutoCRLF(in, opType);
String filterCommand = getCleanFilterCommand();
if (filterCommand != null) {
if (FilterCommandRegistry.isRegistered(filterCommand)) {
LocalFile buffer = new TemporaryBuffer.LocalFile(null);
FilterCommand command = FilterCommandRegistry
.createFilterCommand(filterCommand, repository, in,
buffer);
while (command.run() != -1) {
// loop as long as command.run() tells there is work to do
}
return buffer.openInputStream();
}
FS fs = repository.getFS();
ProcessBuilder filterProcessBuilder = fs.runInShell(filterCommand,
new String[0]);
filterProcessBuilder.directory(repository.getWorkTree());
filterProcessBuilder.environment().put(Constants.GIT_DIR_KEY,
repository.getDirectory().getAbsolutePath());
ExecutionResult result;
try {
result = fs.execute(filterProcessBuilder, in);
} catch (IOException | InterruptedException e) {
throw new IOException(new FilterFailedException(e,
filterCommand, getEntryPathString()));
}
int rc = result.getRc();
if (rc != 0) {
throw new IOException(new FilterFailedException(rc,
filterCommand, getEntryPathString(),
result.getStdout().toByteArray(MAX_EXCEPTION_TEXT_SIZE),
RawParseUtils.decode(result.getStderr()
.toByteArray(MAX_EXCEPTION_TEXT_SIZE))));
}
return result.getStdout().openInputStream();
}
return in;
}
private InputStream handleAutoCRLF(InputStream in, OperationType opType)
throws IOException {
return EolStreamTypeUtil.wrapInputStream(in, getEolStreamType(opType));
}
/**
* Returns the working tree options used by this iterator.
*
* @return working tree options
*/
public WorkingTreeOptions getOptions() {
return state.options;
}
@Override
public int idOffset() {
return contentIdOffset;
}
@Override
public void reset() {
if (!first()) {
ptr = 0;
if (!eof())
parseEntry();
}
}
@Override
public boolean first() {
return ptr == 0;
}
@Override
public boolean eof() {
return ptr == entryCnt;
}
@Override
public void next(final int delta) throws CorruptObjectException {
ptr += delta;
if (!eof()) {
parseEntry();
}
}
@Override
public void back(final int delta) throws CorruptObjectException {
ptr -= delta;
parseEntry();
}
private void parseEntry() {
final Entry e = entries[ptr];
mode = e.getMode().getBits();
final int nameLen = e.encodedNameLen;
ensurePathCapacity(pathOffset + nameLen, pathOffset);
System.arraycopy(e.encodedName, 0, path, pathOffset, nameLen);
pathLen = pathOffset + nameLen;
canonLen = -1;
cleanFilterCommandHolder = null;
eolStreamTypeHolder = null;
}
/**
* Get the raw byte length of this entry.
*
* @return size of this file, in bytes.
*/
public long getEntryLength() {
return current().getLength();
}
/**
* Get the filtered input length of this entry
*
* @return size of the content, in bytes
* @throws IOException
*/
public long getEntryContentLength() throws IOException {
if (canonLen == -1) {
long rawLen = getEntryLength();
if (rawLen == 0)
canonLen = 0;
InputStream is = current().openInputStream();
try {
// canonLen gets updated here
possiblyFilteredInputStream(current(), is, current()
.getLength());
} finally {
safeClose(is);
}
}
return canonLen;
}
/**
* Get the last modified time of this entry.
*
* @return last modified time of this file, in milliseconds since the epoch
* (Jan 1, 1970 UTC).
*/
public long getEntryLastModified() {
return current().getLastModified();
}
/**
* Obtain an input stream to read the file content.
* <p>
* Efficient implementations are not required. The caller will usually
* obtain the stream only once per entry, if at all.
* <p>
* The input stream should not use buffering if the implementation can avoid
* it. The caller will buffer as necessary to perform efficient block IO
* operations.
* <p>
* The caller will close the stream once complete.
*
* @return a stream to read from the file.
* @throws IOException
* the file could not be opened for reading.
*/
public InputStream openEntryStream() throws IOException {
InputStream rawis = current().openInputStream();
if (getCleanFilterCommand() == null
&& getEolStreamType() == EolStreamType.DIRECT)
return rawis;
else
return filterClean(rawis);
}
/**
* Determine if the current entry path is ignored by an ignore rule.
*
* @return true if the entry was ignored by an ignore rule file.
* @throws IOException
* a relevant ignore rule file exists but cannot be read.
*/
public boolean isEntryIgnored() throws IOException {
return isEntryIgnored(pathLen);
}
/**
* Determine if the entry path is ignored by an ignore rule.
*
* @param pLen
* the length of the path in the path buffer.
* @return true if the entry is ignored by an ignore rule.
* @throws IOException
* a relevant ignore rule file exists but cannot be read.
*/
protected boolean isEntryIgnored(final int pLen) throws IOException {
return isEntryIgnored(pLen, mode, false);
}
/**
* Determine if the entry path is ignored by an ignore rule. Consider
* possible rule negation from child iterator.
*
* @param pLen
* the length of the path in the path buffer.
* @param fileMode
* the original iterator file mode
* @param negatePrevious
* true if the previous matching iterator rule was negation
* @return true if the entry is ignored by an ignore rule.
* @throws IOException
* a relevant ignore rule file exists but cannot be read.
*/
private boolean isEntryIgnored(final int pLen, int fileMode,
boolean negatePrevious)
throws IOException {
IgnoreNode rules = getIgnoreNode();
if (rules != null) {
// The ignore code wants path to start with a '/' if possible.
// If we have the '/' in our path buffer because we are inside
// a subdirectory include it in the range we convert to string.
//
int pOff = pathOffset;
if (0 < pOff)
pOff--;
String p = TreeWalk.pathOf(path, pOff, pLen);
switch (rules.isIgnored(p, FileMode.TREE.equals(fileMode),
negatePrevious)) {
case IGNORED:
return true;
case NOT_IGNORED:
return false;
case CHECK_PARENT:
negatePrevious = false;
break;
case CHECK_PARENT_NEGATE_FIRST_MATCH:
negatePrevious = true;
break;
}
}
if (parent instanceof WorkingTreeIterator)
return ((WorkingTreeIterator) parent).isEntryIgnored(pLen, fileMode,
negatePrevious);
return false;
}
private IgnoreNode getIgnoreNode() throws IOException {
if (ignoreNode instanceof PerDirectoryIgnoreNode)
ignoreNode = ((PerDirectoryIgnoreNode) ignoreNode).load();
return ignoreNode;
}
/**
* Retrieves the {@link AttributesNode} for the current entry.
*
* @return {@link AttributesNode} for the current entry.
* @throws IOException
* if an error is raised while parsing the .gitattributes file
* @since 3.7
*/
public AttributesNode getEntryAttributesNode() throws IOException {
if (attributesNode instanceof PerDirectoryAttributesNode)
attributesNode = ((PerDirectoryAttributesNode) attributesNode)
.load();
return attributesNode;
}
private static final Comparator<Entry> ENTRY_CMP = new Comparator<Entry>() {
@Override
public int compare(Entry a, Entry b) {
return Paths.compare(
a.encodedName, 0, a.encodedNameLen, a.getMode().getBits(),
b.encodedName, 0, b.encodedNameLen, b.getMode().getBits());
}
};
/**
* Constructor helper.
*
* @param list
* files in the subtree of the work tree this iterator operates
* on
*/
protected void init(final Entry[] list) {
// Filter out nulls, . and .. as these are not valid tree entries,
// also cache the encoded forms of the path names for efficient use
// later on during sorting and iteration.
//
entries = list;
int i, o;
final CharsetEncoder nameEncoder = state.nameEncoder;
for (i = 0, o = 0; i < entries.length; i++) {
final Entry e = entries[i];
if (e == null)
continue;
final String name = e.getName();
if (".".equals(name) || "..".equals(name)) //$NON-NLS-1$ //$NON-NLS-2$
continue;
if (Constants.DOT_GIT.equals(name))
continue;
if (Constants.DOT_GIT_IGNORE.equals(name))
ignoreNode = new PerDirectoryIgnoreNode(e);
if (Constants.DOT_GIT_ATTRIBUTES.equals(name))
attributesNode = new PerDirectoryAttributesNode(e);
if (i != o)
entries[o] = e;
e.encodeName(nameEncoder);
o++;
}
entryCnt = o;
Arrays.sort(entries, 0, entryCnt, ENTRY_CMP);
contentIdFromPtr = -1;
ptr = 0;
if (!eof())
parseEntry();
else if (pathLen == 0) // see bug 445363
pathLen = pathOffset;
}
/**
* Obtain the current entry from this iterator.
*
* @return the currently selected entry.
*/
protected Entry current() {
return entries[ptr];
}
/**
* The result of a metadata-comparison between the current entry and a
* {@link DirCacheEntry}
*/
public enum MetadataDiff {
/**
* The entries are equal by metaData (mode, length,
* modification-timestamp) or the <code>assumeValid</code> attribute of
* the index entry is set
*/
EQUAL,
/**
* The entries are not equal by metaData (mode, length) or the
* <code>isUpdateNeeded</code> attribute of the index entry is set
*/
DIFFER_BY_METADATA,
/** index entry is smudged - can't use that entry for comparison */
SMUDGED,
/**
* The entries are equal by metaData (mode, length) but differ by
* modification-timestamp.
*/
DIFFER_BY_TIMESTAMP
}
/**
* Is the file mode of the current entry different than the given raw mode?
*
* @param rawMode
* @return true if different, false otherwise
*/
public boolean isModeDifferent(final int rawMode) {
// Determine difference in mode-bits of file and index-entry. In the
// bitwise presentation of modeDiff we'll have a '1' when the two modes
// differ at this position.
int modeDiff = getEntryRawMode() ^ rawMode;
if (modeDiff == 0)
return false;
// Do not rely on filemode differences in case of symbolic links
if (getOptions().getSymLinks() == SymLinks.FALSE)
if (FileMode.SYMLINK.equals(rawMode))
return false;
// Ignore the executable file bits if WorkingTreeOptions tell me to
// do so. Ignoring is done by setting the bits representing a
// EXECUTABLE_FILE to '0' in modeDiff
if (!state.options.isFileMode())
modeDiff &= ~FileMode.EXECUTABLE_FILE.getBits();
return modeDiff != 0;
}
/**
* Compare the metadata (mode, length, modification-timestamp) of the
* current entry and a {@link DirCacheEntry}
*
* @param entry
* the {@link DirCacheEntry} to compare with
* @return a {@link MetadataDiff} which tells whether and how the entries
* metadata differ
*/
public MetadataDiff compareMetadata(DirCacheEntry entry) {
if (entry.isAssumeValid())
return MetadataDiff.EQUAL;
if (entry.isUpdateNeeded())
return MetadataDiff.DIFFER_BY_METADATA;
if (isModeDifferent(entry.getRawMode()))
return MetadataDiff.DIFFER_BY_METADATA;
// Don't check for length or lastmodified on folders
int type = mode & FileMode.TYPE_MASK;
if (type == FileMode.TYPE_TREE || type == FileMode.TYPE_GITLINK)
return MetadataDiff.EQUAL;
if (!entry.isSmudged() && entry.getLength() != (int) getEntryLength())
return MetadataDiff.DIFFER_BY_METADATA;
// Git under windows only stores seconds so we round the timestamp
// Java gives us if it looks like the timestamp in index is seconds
// only. Otherwise we compare the timestamp at millisecond precision,
// unless core.checkstat is set to "minimal", in which case we only
// compare the whole second part.
long cacheLastModified = entry.getLastModified();
long fileLastModified = getEntryLastModified();
long lastModifiedMillis = fileLastModified % 1000;
long cacheMillis = cacheLastModified % 1000;
if (getOptions().getCheckStat() == CheckStat.MINIMAL) {
fileLastModified = fileLastModified - lastModifiedMillis;
cacheLastModified = cacheLastModified - cacheMillis;
} else if (cacheMillis == 0)
fileLastModified = fileLastModified - lastModifiedMillis;
// Some Java version on Linux return whole seconds only even when
// the file systems supports more precision.
else if (lastModifiedMillis == 0)
cacheLastModified = cacheLastModified - cacheMillis;
if (fileLastModified != cacheLastModified)
return MetadataDiff.DIFFER_BY_TIMESTAMP;
else if (!entry.isSmudged())
// The file is clean when you look at timestamps.
return MetadataDiff.EQUAL;
else
return MetadataDiff.SMUDGED;
}
/**
* Checks whether this entry differs from a given entry from the
* {@link DirCache}.
*
* File status information is used and if status is same we consider the
* file identical to the state in the working directory. Native git uses
* more stat fields than we have accessible in Java.
*
* @param entry
* the entry from the dircache we want to compare against
* @param forceContentCheck
* True if the actual file content should be checked if
* modification time differs.
* @param reader
* access to repository objects if necessary. Should not be null.
* @return true if content is most likely different.
* @throws IOException
* @since 3.3
*/
public boolean isModified(DirCacheEntry entry, boolean forceContentCheck,
ObjectReader reader) throws IOException {
if (entry == null)
return !FileMode.MISSING.equals(getEntryFileMode());
MetadataDiff diff = compareMetadata(entry);
switch (diff) {
case DIFFER_BY_TIMESTAMP:
if (forceContentCheck)
// But we are told to look at content even though timestamps
// tell us about modification
return contentCheck(entry, reader);
else
// We are told to assume a modification if timestamps differs
return true;
case SMUDGED:
// The file is clean by timestamps but the entry was smudged.
// Lets do a content check
return contentCheck(entry, reader);
case EQUAL:
if (mode == FileMode.SYMLINK.getBits()) {
return contentCheck(entry, reader);
}
return false;
case DIFFER_BY_METADATA:
if (mode == FileMode.SYMLINK.getBits())
return contentCheck(entry, reader);
return true;
default:
throw new IllegalStateException(MessageFormat.format(
JGitText.get().unexpectedCompareResult, diff.name()));
}
}
/**
* Get the file mode to use for the current entry when it is to be updated
* in the index.
*
* @param indexIter
* {@link DirCacheIterator} positioned at the same entry as this
* iterator or null if no {@link DirCacheIterator} is available
* at this iterator's current entry
* @return index file mode
*/
public FileMode getIndexFileMode(final DirCacheIterator indexIter) {
final FileMode wtMode = getEntryFileMode();
if (indexIter == null) {
return wtMode;
}
final FileMode iMode = indexIter.getEntryFileMode();
if (getOptions().isFileMode() && iMode != FileMode.GITLINK && iMode != FileMode.TREE) {
return wtMode;
}
if (!getOptions().isFileMode()) {
if (FileMode.REGULAR_FILE == wtMode
&& FileMode.EXECUTABLE_FILE == iMode) {
return iMode;
}
if (FileMode.EXECUTABLE_FILE == wtMode
&& FileMode.REGULAR_FILE == iMode) {
return iMode;
}
}
if (FileMode.GITLINK == iMode
&& FileMode.TREE == wtMode) {
return iMode;
}
if (FileMode.TREE == iMode
&& FileMode.GITLINK == wtMode) {
return iMode;
}
return wtMode;
}
/**
* Compares the entries content with the content in the filesystem.
* Unsmudges the entry when it is detected that it is clean.
*
* @param entry
* the entry to be checked
* @param reader
* acccess to repository data if necessary
* @return <code>true</code> if the content doesn't match,
* <code>false</code> if it matches
* @throws IOException
*/
private boolean contentCheck(DirCacheEntry entry, ObjectReader reader)
throws IOException {
if (getEntryObjectId().equals(entry.getObjectId())) {
// Content has not changed
// We know the entry can't be racily clean because it's still clean.
// Therefore we unsmudge the entry!
// If by any chance we now unsmudge although we are still in the
// same time-slot as the last modification to the index file the
// next index write operation will smudge again.
// Caution: we are unsmudging just by setting the length of the
// in-memory entry object. It's the callers task to detect that we
// have modified the entry and to persist the modified index.
entry.setLength((int) getEntryLength());
return false;
} else {
if (mode == FileMode.SYMLINK.getBits()) {
return !new File(readSymlinkTarget(current())).equals(
new File(readContentAsNormalizedString(entry, reader)));
}
// Content differs: that's a real change, perhaps
if (reader == null) // deprecated use, do no further checks
return true;
switch (getEolStreamType()) {
case DIRECT:
return true;
default:
try {
ObjectLoader loader = reader.open(entry.getObjectId());
if (loader == null)
return true;
// We need to compute the length, but only if it is not
// a binary stream.
long dcInLen;
try (InputStream dcIn = new AutoLFInputStream(
loader.openStream(), true,
true /* abort if binary */)) {
dcInLen = computeLength(dcIn);
} catch (AutoLFInputStream.IsBinaryException e) {
return true;
}
try (InputStream dcIn = new AutoLFInputStream(
loader.openStream(), true)) {
byte[] autoCrLfHash = computeHash(dcIn, dcInLen);
boolean changed = getEntryObjectId()
.compareTo(autoCrLfHash, 0) != 0;
return changed;
}
} catch (IOException e) {
return true;
}
}
}
}
private static String readContentAsNormalizedString(DirCacheEntry entry,
ObjectReader reader) throws MissingObjectException, IOException {
ObjectLoader open = reader.open(entry.getObjectId());
byte[] cachedBytes = open.getCachedBytes();
return FS.detect().normalize(RawParseUtils.decode(cachedBytes));
}
/**
* Reads the target of a symlink as a string. This default implementation
* fully reads the entry's input stream and converts it to a normalized
* string. Subclasses may override to provide more specialized
* implementations.
*
* @param entry
* to read
* @return the entry's content as a normalized string
* @throws IOException
* if the entry cannot be read or does not denote a symlink
* @since 4.6
*/
protected String readSymlinkTarget(Entry entry) throws IOException {
if (!entry.getMode().equals(FileMode.SYMLINK)) {
throw new java.nio.file.NotLinkException(entry.getName());
}
long length = entry.getLength();
byte[] content = new byte[(int) length];
try (InputStream is = entry.openInputStream()) {
int bytesRead = IO.readFully(is, content, 0);
return FS.detect()
.normalize(RawParseUtils.decode(content, 0, bytesRead));
}
}
private static long computeLength(InputStream in) throws IOException {
// Since we only care about the length, use skip. The stream
// may be able to more efficiently wade through its data.
//
long length = 0;
for (;;) {
long n = in.skip(1 << 20);
if (n <= 0)
break;
length += n;
}
return length;
}
private byte[] computeHash(InputStream in, long length) throws IOException {
SHA1 contentDigest = SHA1.newInstance();
final byte[] contentReadBuffer = state.contentReadBuffer;
contentDigest.update(hblob);
contentDigest.update((byte) ' ');
long sz = length;
if (sz == 0) {
contentDigest.update((byte) '0');
} else {
final int bufn = contentReadBuffer.length;
int p = bufn;
do {
contentReadBuffer[--p] = digits[(int) (sz % 10)];
sz /= 10;
} while (sz > 0);
contentDigest.update(contentReadBuffer, p, bufn - p);
}
contentDigest.update((byte) 0);
for (;;) {
final int r = in.read(contentReadBuffer);
if (r <= 0)
break;
contentDigest.update(contentReadBuffer, 0, r);
sz += r;
}
if (sz != length)
return zeroid;
return contentDigest.digest();
}
/** A single entry within a working directory tree. */
protected static abstract class Entry {
byte[] encodedName;
int encodedNameLen;
void encodeName(final CharsetEncoder enc) {
final ByteBuffer b;
try {
b = enc.encode(CharBuffer.wrap(getName()));
} catch (CharacterCodingException e) {
// This should so never happen.
throw new RuntimeException(MessageFormat.format(
JGitText.get().unencodeableFile, getName()));
}
encodedNameLen = b.limit();
if (b.hasArray() && b.arrayOffset() == 0)
encodedName = b.array();
else
b.get(encodedName = new byte[encodedNameLen]);
}
@Override
public String toString() {
return getMode().toString() + " " + getName(); //$NON-NLS-1$
}
/**
* Get the type of this entry.
* <p>
* <b>Note: Efficient implementation required.</b>
* <p>
* The implementation of this method must be efficient. If a subclass
* needs to compute the value they should cache the reference within an
* instance member instead.
*
* @return a file mode constant from {@link FileMode}.
*/
public abstract FileMode getMode();
/**
* Get the byte length of this entry.
* <p>
* <b>Note: Efficient implementation required.</b>
* <p>
* The implementation of this method must be efficient. If a subclass
* needs to compute the value they should cache the reference within an
* instance member instead.
*
* @return size of this file, in bytes.
*/
public abstract long getLength();
/**
* Get the last modified time of this entry.
* <p>
* <b>Note: Efficient implementation required.</b>
* <p>
* The implementation of this method must be efficient. If a subclass
* needs to compute the value they should cache the reference within an
* instance member instead.
*
* @return time since the epoch (in ms) of the last change.
*/
public abstract long getLastModified();
/**
* Get the name of this entry within its directory.
* <p>
* Efficient implementations are not required. The caller will obtain
* the name only once and cache it once obtained.
*
* @return name of the entry.
*/
public abstract String getName();
/**
* Obtain an input stream to read the file content.
* <p>
* Efficient implementations are not required. The caller will usually
* obtain the stream only once per entry, if at all.
* <p>
* The input stream should not use buffering if the implementation can
* avoid it. The caller will buffer as necessary to perform efficient
* block IO operations.
* <p>
* The caller will close the stream once complete.
*
* @return a stream to read from the file.
* @throws IOException
* the file could not be opened for reading.
*/
public abstract InputStream openInputStream() throws IOException;
}
/** Magic type indicating we know rules exist, but they aren't loaded. */
private static class PerDirectoryIgnoreNode extends IgnoreNode {
final Entry entry;
PerDirectoryIgnoreNode(Entry entry) {
super(Collections.<FastIgnoreRule> emptyList());
this.entry = entry;
}
IgnoreNode load() throws IOException {
IgnoreNode r = new IgnoreNode();
InputStream in = entry.openInputStream();
try {
r.parse(in);
} finally {
in.close();
}
return r.getRules().isEmpty() ? null : r;
}
}
/** Magic type indicating there may be rules for the top level. */
private static class RootIgnoreNode extends PerDirectoryIgnoreNode {
final Repository repository;
RootIgnoreNode(Entry entry, Repository repository) {
super(entry);
this.repository = repository;
}
@Override
IgnoreNode load() throws IOException {
IgnoreNode r;
if (entry != null) {
r = super.load();
if (r == null)
r = new IgnoreNode();
} else {
r = new IgnoreNode();
}
FS fs = repository.getFS();
String path = repository.getConfig().get(CoreConfig.KEY)
.getExcludesFile();
if (path != null) {
File excludesfile;
if (path.startsWith("~/")) //$NON-NLS-1$
excludesfile = fs.resolve(fs.userHome(), path.substring(2));
else
excludesfile = fs.resolve(null, path);
loadRulesFromFile(r, excludesfile);
}
File exclude = fs.resolve(repository.getDirectory(),
Constants.INFO_EXCLUDE);
loadRulesFromFile(r, exclude);
return r.getRules().isEmpty() ? null : r;
}
private static void loadRulesFromFile(IgnoreNode r, File exclude)
throws FileNotFoundException, IOException {
if (FS.DETECTED.exists(exclude)) {
FileInputStream in = new FileInputStream(exclude);
try {
r.parse(in);
} finally {
in.close();
}
}
}
}
/** Magic type indicating we know rules exist, but they aren't loaded. */
private static class PerDirectoryAttributesNode extends AttributesNode {
final Entry entry;
PerDirectoryAttributesNode(Entry entry) {
super(Collections.<AttributesRule> emptyList());
this.entry = entry;
}
AttributesNode load() throws IOException {
AttributesNode r = new AttributesNode();
InputStream in = entry.openInputStream();
try {
r.parse(in);
} finally {
in.close();
}
return r.getRules().isEmpty() ? null : r;
}
}
private static final class IteratorState {
/** Options used to process the working tree. */
final WorkingTreeOptions options;
/** File name character encoder. */
final CharsetEncoder nameEncoder;
/** Buffer used to perform {@link #contentId} computations. */
byte[] contentReadBuffer;
/** TreeWalk with a (supposedly) matching DirCacheIterator. */
TreeWalk walk;
/** Position of the matching {@link DirCacheIterator}. */
int dirCacheTree;
IteratorState(WorkingTreeOptions options) {
this.options = options;
this.nameEncoder = Constants.CHARSET.newEncoder();
}
void initializeReadBuffer() {
if (contentReadBuffer == null) {
contentReadBuffer = new byte[BUFFER_SIZE];
}
}
}
/**
* @return the clean filter command for the current entry or
* <code>null</code> if no such command is defined
* @throws IOException
* @since 4.2
*/
public String getCleanFilterCommand() throws IOException {
if (cleanFilterCommandHolder == null) {
String cmd = null;
if (state.walk != null) {
cmd = state.walk
.getFilterCommand(Constants.ATTR_FILTER_TYPE_CLEAN);
}
cleanFilterCommandHolder = new Holder<>(cmd);
}
return cleanFilterCommandHolder.get();
}
/**
* @return the eol stream type for the current entry or <code>null</code> if
* it cannot be determined. When state or state.walk is null or the
* {@link TreeWalk} is not based on a {@link Repository} then null
* is returned.
* @throws IOException
* @since 4.3
*/
public EolStreamType getEolStreamType() throws IOException {
return getEolStreamType(null);
}
/**
* @param opType
* The operationtype (checkin/checkout) which should be used
* @return the eol stream type for the current entry or <code>null</code> if
* it cannot be determined. When state or state.walk is null or the
* {@link TreeWalk} is not based on a {@link Repository} then null
* is returned.
* @throws IOException
*/
private EolStreamType getEolStreamType(OperationType opType)
throws IOException {
if (eolStreamTypeHolder == null) {
EolStreamType type=null;
if (state.walk != null) {
if (opType != null) {
type = state.walk.getEolStreamType(opType);
} else {
type=state.walk.getEolStreamType();
}
} else {
switch (getOptions().getAutoCRLF()) {
case FALSE:
type = EolStreamType.DIRECT;
break;
case TRUE:
case INPUT:
type = EolStreamType.AUTO_LF;
break;
}
}
eolStreamTypeHolder = new Holder<>(type);
}
return eolStreamTypeHolder.get();
}
}