use gitignore in tree differ

Summary: Pass a GitIgnoreStack* and isIgnored flag through the diff operation. It is used later in the stack when we go down this code path in the TreeInode diffing code. When context->loadFileContentsFromPath() is null, the gitignore loading code will be ignored, used when we do not want to honor a gitignore file. For example, during `getScmStatusBetweenRevisions()`.

Reviewed By: chadaustin

Differential Revision: D18647089

fbshipit-source-id: 20d2abd2ef61669465e134165da5a0ac5e987cca
This commit is contained in:
Genevieve Helsel 2020-01-29 11:15:29 -08:00 committed by Facebook Github Bot
parent a80bcd779b
commit 4dc48ec7fa
4 changed files with 1420 additions and 149 deletions

View File

@ -1182,6 +1182,147 @@ TEST(DiffTest, directoryToFileWithGitIgnore) {
std::make_pair(".gitignore", ScmFileStatus::ADDED)));
}
// Tests the case in which a file becomes a directory and the directory is
// ignored but the parent directory is not ignored.
TEST(DiffTest, addIgnoredDirectory) {
DiffTest test({
{"a/b.txt", "test\n"},
{"a/b/c.txt", "test\n"},
{"a/b/r", "test\n"},
});
// The following won't be tracked
test.getMount().deleteFile("a/b/r");
test.getMount().mkdir("a/b/r");
test.getMount().addFile("a/b/r/e.txt", "ignored");
test.getMount().mkdir("a/b/r/d");
// It is not possible to re-include a file if a parent directory of that file
// is excluded.
test.getMount().addFile("a/b/r/d/g.txt", "ignored too");
// The following should be tracked
test.getMount().mkdir("a/b/g");
test.getMount().addFile("a/b/g/e.txt", "added");
auto systemIgnore = "a/b/r/\n!a/b/r/d/g.txt\n";
auto result = test.diff(true, systemIgnore);
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("a/b/r", ScmFileStatus::REMOVED),
std::make_pair("a/b/r/e.txt", ScmFileStatus::IGNORED),
std::make_pair("a/b/r/d/g.txt", ScmFileStatus::IGNORED),
std::make_pair("a/b/g/e.txt", ScmFileStatus::ADDED)));
}
// Tests the case in which a directory is ignored but later down a file is
// unignored, makes sure that the file is correctly marked as added.
TEST(DiffTest, nestedGitIgnoreFiles) {
DiffTest test({
{"a/b.txt", "test\n"},
{"a/b/c.txt", "test\n"},
{"a/b/r", "file"},
});
auto gitIgnoreContents = "!e.txt\n";
test.getMount().deleteFile("a/b/r");
test.getMount().mkdir("a/b/r");
test.getMount().addFile("a/b/r/.gitignore", gitIgnoreContents);
test.getMount().addFile("a/b/r/e.txt", "shouldn't be ignored");
test.getMount().addFile("a/b/r/f.txt", "should be ignored");
auto systemIgnore = "a/b/r/*\n!a/b/r/.gitignore\n";
auto result = test.diff(true, systemIgnore);
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("a/b/r", ScmFileStatus::REMOVED),
std::make_pair("a/b/r/.gitignore", ScmFileStatus::ADDED),
std::make_pair("a/b/r/e.txt", ScmFileStatus::ADDED),
std::make_pair("a/b/r/f.txt", ScmFileStatus::IGNORED)));
}
// Tests the case in which a tracked file in source control is modified locally.
// In this case, the file should be recorded as MODIFIED, since it matches
// an ignore rule but was already tracked
TEST(DiffTest, diff_trees_with_tracked_ignored_file_modified) {
DiffTest test({
{"src/foo/a.txt", "a"},
{"src/foo/b.txt", "b"},
{"src/foo/a", "regular file"},
{"src/bar/c.txt", "c"},
{"src/bar/d.txt", "d"},
{"src/bar/c", "regular file"},
{"src/foo/.gitignore", "a.txt\n"},
});
test.getMount().addFile("src/bar/e.txt", "e");
test.getMount().deleteFile("src/bar/d.txt");
// Even though this is modified, it will be ignored because it matches an
// ignore rule.
test.getMount().overwriteFile("src/foo/a.txt", "aa");
auto result = test.diff();
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("src/bar/e.txt", ScmFileStatus::ADDED),
std::make_pair("src/bar/d.txt", ScmFileStatus::REMOVED),
std::make_pair("src/foo/a.txt", ScmFileStatus::MODIFIED)));
result = test.diff(true);
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("src/bar/e.txt", ScmFileStatus::ADDED),
std::make_pair("src/bar/d.txt", ScmFileStatus::REMOVED),
std::make_pair("src/foo/a.txt", ScmFileStatus::MODIFIED)));
}
// Tests the case in which a tracked file in source control is modified locally.
// In this case, the file should be recorded as MODIFIED, since it matches
// an ignore rule but was already tracked
TEST(DiffTest, tree_file_matches_new_ignore_rule_modified_locally) {
DiffTest test({
{"src/foo/a.txt", "a"},
{"src/foo/b.txt", "b"},
{"src/foo/a", "regular file"},
{"src/bar/c.txt", "c"},
{"src/bar/d.txt", "d"},
{"src/bar/c", "regular file"},
});
test.getMount().addFile("src/foo/.gitignore", "a.txt\n");
test.getMount().addFile("src/bar/e.txt", "e");
test.getMount().deleteFile("src/bar/d.txt");
// Even though this is modified, it will be ignored because it matches an
// ignore rule.
test.getMount().overwriteFile("src/foo/a.txt", "aa");
auto result = test.diff();
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("src/foo/.gitignore", ScmFileStatus::ADDED),
std::make_pair("src/bar/e.txt", ScmFileStatus::ADDED),
std::make_pair("src/bar/d.txt", ScmFileStatus::REMOVED),
std::make_pair("src/foo/a.txt", ScmFileStatus::MODIFIED)));
result = test.diff(true);
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("src/foo/.gitignore", ScmFileStatus::ADDED),
std::make_pair("src/bar/e.txt", ScmFileStatus::ADDED),
std::make_pair("src/bar/d.txt", ScmFileStatus::REMOVED),
std::make_pair("src/foo/a.txt", ScmFileStatus::MODIFIED)));
}
TEST(DiffTest, fileNotReady) {
TestMount mount;
auto backingStore = mount.getBackingStore();

View File

@ -16,6 +16,7 @@
#include "eden/fs/model/Tree.h"
#include "eden/fs/model/TreeEntry.h"
#include "eden/fs/model/git/GitIgnoreStack.h"
#include "eden/fs/store/DiffContext.h"
#include "eden/fs/store/ObjectStore.h"
#include "eden/fs/store/ScmStatusDiffCallback.h"
@ -31,6 +32,12 @@ using std::vector;
namespace facebook {
namespace eden {
/*
* In practice, while the functions in this file are comparing two source
* control Tree objects, they are used for comparing the current
* (non-materialized) working directory state (as wdTree) to its corresponding
* source control state (as scmTree).
*/
namespace {
struct ChildFutures {
@ -51,40 +58,42 @@ struct DiffState {
DiffContext context;
};
Future<Unit>
diffAddedTree(const DiffContext* context, RelativePathPiece path, Hash hash);
Future<Unit>
diffRemovedTree(const DiffContext* context, RelativePathPiece path, Hash hash);
static constexpr PathComponentPiece kIgnoreFilename{".gitignore"};
Future<Unit> diffAddedTree(
const DiffContext* context,
RelativePathPiece path,
const Tree& tree);
RelativePathPiece entryPath,
const Tree& wdTree,
const GitIgnoreStack* ignore,
bool isIgnored);
Future<Unit> diffRemovedTree(
const DiffContext* context,
RelativePathPiece path,
const Tree& tree);
RelativePathPiece entryPath,
const Tree& scmTree);
void processAddedSide(
const DiffContext* context,
ChildFutures& childFutures,
RelativePathPiece parentPath,
const TreeEntry& entry);
RelativePathPiece currentPath,
const TreeEntry& wdEntry,
const GitIgnoreStack* ignore,
bool isIgnored);
void processRemovedSide(
const DiffContext* context,
ChildFutures& childFutures,
RelativePathPiece parentPath,
const TreeEntry& entry);
RelativePathPiece currentPath,
const TreeEntry& scmEntry);
void processBothPresent(
const DiffContext* context,
ChildFutures& childFutures,
RelativePathPiece parentPath,
const TreeEntry& entry1,
const TreeEntry& entry2);
RelativePathPiece currentPath,
const TreeEntry& scmEntry,
const TreeEntry& wdEntry,
const GitIgnoreStack* ignore,
bool isIgnored);
Future<Unit> waitOnResults(
const DiffContext* context,
@ -98,132 +107,270 @@ Future<Unit> waitOnResults(
*
* The differences will be recorded using a callback provided by the caller.
*/
FOLLY_NODISCARD Future<Unit> diffTrees(
FOLLY_NODISCARD Future<Unit> computeTreeDiff(
const DiffContext* context,
RelativePathPiece path,
const Tree& tree1,
const Tree& tree2) {
RelativePathPiece currentPath,
const Tree& scmTree,
const Tree& wdTree,
std::unique_ptr<GitIgnoreStack> ignore,
bool isIgnored) {
// A list of Futures to wait on for our children's results.
ChildFutures childFutures;
// Walk through the entries in both trees.
// This relies on the fact that the entry list in each tree is always sorted.
const auto& entries1 = tree1.getTreeEntries();
const auto& entries2 = tree2.getTreeEntries();
size_t idx1 = 0;
size_t idx2 = 0;
const auto& scmEntries = scmTree.getTreeEntries();
const auto& wdEntries = wdTree.getTreeEntries();
size_t scmIdx = 0;
size_t wdIdx = 0;
while (true) {
if (idx1 >= entries1.size()) {
if (idx2 >= entries2.size()) {
if (scmIdx >= scmEntries.size()) {
if (wdIdx >= wdEntries.size()) {
// All Done
break;
}
// This entry is present in tree2 but not tree1
processAddedSide(context, childFutures, path, entries2[idx2]);
++idx2;
} else if (idx2 >= entries2.size()) {
// This entry is present in tree1 but not tree2
processRemovedSide(context, childFutures, path, entries1[idx1]);
++idx1;
} else if (entries1[idx1].getName() < entries2[idx2].getName()) {
processRemovedSide(context, childFutures, path, entries1[idx1]);
++idx1;
} else if (entries1[idx1].getName() > entries2[idx2].getName()) {
processAddedSide(context, childFutures, path, entries2[idx2]);
++idx2;
// This entry is present in wdTree but not scmTree
processAddedSide(
context,
childFutures,
currentPath,
wdEntries[wdIdx],
ignore.get(),
isIgnored);
++wdIdx;
} else if (wdIdx >= wdEntries.size()) {
// This entry is present in scmTree but not wdTree
processRemovedSide(
context, childFutures, currentPath, scmEntries[scmIdx]);
++scmIdx;
} else if (scmEntries[scmIdx].getName() < wdEntries[wdIdx].getName()) {
processRemovedSide(
context, childFutures, currentPath, scmEntries[scmIdx]);
++scmIdx;
} else if (scmEntries[scmIdx].getName() > wdEntries[wdIdx].getName()) {
processAddedSide(
context,
childFutures,
currentPath,
wdEntries[wdIdx],
ignore.get(),
isIgnored);
++wdIdx;
} else {
processBothPresent(
context, childFutures, path, entries1[idx1], entries2[idx2]);
++idx1;
++idx2;
context,
childFutures,
currentPath,
scmEntries[scmIdx],
wdEntries[wdIdx],
ignore.get(),
isIgnored);
++scmIdx;
++wdIdx;
}
}
return waitOnResults(context, std::move(childFutures));
// Add an ensure() block that makes sure the ignore stack exists until all of
// our children results have finished processing
return waitOnResults(context, std::move(childFutures))
.ensure([ignore = std::move(ignore)] {});
}
FOLLY_NODISCARD Future<Unit> loadGitIgnoreThenDiffTrees(
const TreeEntry& gitIgnoreEntry,
const DiffContext* context,
RelativePathPiece currentPath,
const Tree& scmTree,
const Tree& wdTree,
const GitIgnoreStack* parentIgnore,
bool isIgnored) {
// TODO: load file contents directly from context->store if gitIgnoreEntry is
// a regular file
auto loadFileContentsFromPath = context->getLoadFileContentsFromPath();
return loadFileContentsFromPath(currentPath + gitIgnoreEntry.getName())
.thenError([entryPath = currentPath + gitIgnoreEntry.getName()](
const folly::exception_wrapper& ex) {
// TODO: add an API to DiffCallback to report user errors like this
// (errors that do not indicate a problem with EdenFS itself) that can
// be returned to the caller in a thrift response
XLOG(WARN) << "error loading gitignore at " << entryPath << ": "
<< folly::exceptionStr(ex);
return std::string{};
})
.thenValue([context,
currentPath = currentPath.copy(),
scmTree,
wdTree,
parentIgnore,
isIgnored](std::string&& ignoreFileContents) mutable {
return computeTreeDiff(
context,
currentPath,
scmTree,
wdTree,
make_unique<GitIgnoreStack>(parentIgnore, ignoreFileContents),
isIgnored);
});
}
FOLLY_NODISCARD Future<Unit> diffTrees(
const DiffContext* context,
RelativePathPiece path,
Hash hash1,
Hash hash2) {
auto treeFuture1 = context->store->getTree(hash1);
auto treeFuture2 = context->store->getTree(hash2);
// Optimization for the case when both tree objects are immediately ready.
// We can avoid copying the input path in this case.
if (treeFuture1.isReady() && treeFuture2.isReady()) {
return diffTrees(
RelativePathPiece currentPath,
const Tree& scmTree,
const Tree& wdTree,
const GitIgnoreStack* parentIgnore,
bool isIgnored) {
// If this directory is already ignored, we don't need to bother loading its
// .gitignore file. Everything inside this directory must also be ignored,
// unless it is explicitly tracked in source control.
//
// Explicit include rules cannot be used to unignore files inside an ignored
// directory.
//
// We check context->getLoadFileContentsFromPath() here as a way to see if we
// are processing gitIgnore files or not, since this is only set from code
// that enters through eden/fs/inodes/Diff.cpp. Either way, it is
// impossible to load file contents without this set.
if (isIgnored || !context->getLoadFileContentsFromPath()) {
// We can pass in a null GitIgnoreStack pointer here.
// Since the entire directory is ignored, we don't need to check ignore
// status for any entries that aren't already tracked in source control.
return computeTreeDiff(
context, currentPath, scmTree, wdTree, nullptr, isIgnored);
}
// If this directory has a .gitignore file, load it first.
const auto* gitIgnoreEntry = wdTree.getEntryPtr(kIgnoreFilename);
if (gitIgnoreEntry && !gitIgnoreEntry->isTree()) {
return loadGitIgnoreThenDiffTrees(
*gitIgnoreEntry,
context,
path,
*(std::move(treeFuture1).get()),
*(std::move(treeFuture2).get()));
currentPath,
scmTree,
wdTree,
parentIgnore,
isIgnored);
}
return folly::collect(treeFuture1, treeFuture2)
.thenValue(
[context, path = path.copy()](std::tuple<
std::shared_ptr<const Tree>,
std::shared_ptr<const Tree>>&& tup) {
const auto& [tree1, tree2] = tup;
return diffTrees(context, path, *tree1, *tree2);
});
return computeTreeDiff(
context,
currentPath,
scmTree,
wdTree,
make_unique<GitIgnoreStack>(parentIgnore), // empty with no rules
isIgnored);
}
FOLLY_NODISCARD Future<Unit>
diffAddedTree(const DiffContext* context, RelativePathPiece path, Hash hash) {
auto future = context->store->getTree(hash);
// Optimization for the case when the tree object is immediately ready.
// We can avoid copying the input path in this case.
if (future.isReady()) {
return diffAddedTree(context, path, *std::move(future).get());
FOLLY_NODISCARD Future<Unit> processAddedChildren(
const DiffContext* context,
RelativePathPiece currentPath,
const Tree& wdTree,
std::unique_ptr<GitIgnoreStack> ignore,
bool isIgnored) {
ChildFutures childFutures;
for (const auto& childEntry : wdTree.getTreeEntries()) {
processAddedSide(
context,
childFutures,
currentPath,
childEntry,
ignore.get(),
isIgnored);
}
return std::move(future).thenValue(
[context, path = path.copy()](std::shared_ptr<const Tree>&& tree) {
return diffAddedTree(context, path, *tree);
});
// Add an ensure() block that makes sure the ignore stack exists until all of
// our children results have finished processing
return waitOnResults(context, std::move(childFutures))
.ensure([ignore = std::move(ignore)] {});
}
FOLLY_NODISCARD Future<Unit>
diffRemovedTree(const DiffContext* context, RelativePathPiece path, Hash hash) {
auto future = context->store->getTree(hash);
// Optimization for the case when the tree object is immediately ready.
// We can avoid copying the input path in this case.
if (future.isReady()) {
return diffRemovedTree(context, path, *(std::move(future).get()));
}
return std::move(future).thenValue(
[context, path = path.copy()](std::shared_ptr<const Tree>&& tree) {
return diffRemovedTree(context, path, *tree);
FOLLY_NODISCARD Future<Unit> loadGitIgnoreThenProcessAddedChildren(
const TreeEntry& gitIgnoreEntry,
const DiffContext* context,
RelativePathPiece currentPath,
const Tree& wdTree,
const GitIgnoreStack* parentIgnore,
bool isIgnored) {
auto loadFileContentsFromPath = context->getLoadFileContentsFromPath();
return loadFileContentsFromPath(currentPath + gitIgnoreEntry.getName())
.thenError([entryPath = currentPath + gitIgnoreEntry.getName()](
const folly::exception_wrapper& ex) {
XLOG(WARN) << "error loading gitignore at " << entryPath << ": "
<< folly::exceptionStr(ex);
return std::string{};
})
.thenValue([context,
currentPath = currentPath.copy(),
wdTree,
parentIgnore,
isIgnored](std::string&& ignoreFileContents) mutable {
return processAddedChildren(
context,
currentPath,
wdTree,
make_unique<GitIgnoreStack>(parentIgnore, ignoreFileContents),
isIgnored);
});
}
/**
* Process a Tree that is present only on one side of the diff.
*/
Future<Unit> diffAddedTree(
FOLLY_NODISCARD Future<Unit> diffAddedTree(
const DiffContext* context,
RelativePathPiece path,
const Tree& tree) {
RelativePathPiece currentPath,
const Tree& wdTree,
const GitIgnoreStack* parentIgnore,
bool isIgnored) {
ChildFutures childFutures;
for (const auto& childEntry : tree.getTreeEntries()) {
processAddedSide(context, childFutures, path, childEntry);
// If this directory is already ignored, we don't need to bother loading its
// .gitignore file. Everything inside this directory must also be ignored,
// unless it is explicitly tracked in source control.
//
// Also, if we are not honoring gitignored files, then do not bother loading
// its .gitignore file
//
// Explicit include rules cannot be used to unignore files inside an ignored
// directory.
//
// We check context->getLoadFileContentsFromPath() here as a way to see if we
// are processing gitIgnore files or not, since this is only set from code
// that enters through eden/fs/inodes/DiffTree.cpp. Either way, it is
// impossible to load file contents without this set.
if (isIgnored || !context->getLoadFileContentsFromPath()) {
// We can pass in a null GitIgnoreStack pointer here.
// Since the entire directory is ignored, we don't need to check ignore
// status for any entries that aren't already tracked in source control.
return processAddedChildren(
context, currentPath, wdTree, nullptr, isIgnored);
}
return waitOnResults(context, std::move(childFutures));
// If this directory has a .gitignore file, load it first.
const auto* gitIgnoreEntry = wdTree.getEntryPtr(kIgnoreFilename);
if (gitIgnoreEntry && !gitIgnoreEntry->isTree()) {
return loadGitIgnoreThenProcessAddedChildren(
*gitIgnoreEntry, context, currentPath, wdTree, parentIgnore, isIgnored);
}
return processAddedChildren(
context,
currentPath,
wdTree,
make_unique<GitIgnoreStack>(parentIgnore), // empty with no rules
isIgnored);
}
/**
* Process a Tree that is present only on one side of the diff.
*/
Future<Unit> diffRemovedTree(
FOLLY_NODISCARD Future<Unit> diffRemovedTree(
const DiffContext* context,
RelativePathPiece path,
const Tree& tree) {
RelativePathPiece currentPath,
const Tree& scmTree) {
ChildFutures childFutures;
for (const auto& childEntry : tree.getTreeEntries()) {
processRemovedSide(context, childFutures, path, childEntry);
for (const auto& childEntry : scmTree.getTreeEntries()) {
processRemovedSide(context, childFutures, currentPath, childEntry);
}
return waitOnResults(context, std::move(childFutures));
}
@ -238,15 +385,15 @@ Future<Unit> diffRemovedTree(
void processRemovedSide(
const DiffContext* context,
ChildFutures& childFutures,
RelativePathPiece parentPath,
const TreeEntry& entry) {
if (!entry.isTree()) {
context->callback->removedFile(parentPath + entry.getName());
RelativePathPiece currentPath,
const TreeEntry& scmEntry) {
if (!scmEntry.isTree()) {
context->callback->removedFile(currentPath + scmEntry.getName());
return;
}
auto childPath = parentPath + entry.getName();
auto childFuture = diffRemovedTree(context, childPath, entry.getHash());
childFutures.add(std::move(childPath), std::move(childFuture));
auto entryPath = currentPath + scmEntry.getName();
auto childFuture = diffRemovedTree(context, entryPath, scmEntry.getHash());
childFutures.add(std::move(entryPath), std::move(childFuture));
}
/**
@ -259,15 +406,40 @@ void processRemovedSide(
void processAddedSide(
const DiffContext* context,
ChildFutures& childFutures,
RelativePathPiece parentPath,
const TreeEntry& entry) {
if (!entry.isTree()) {
context->callback->addedFile(parentPath + entry.getName());
return;
RelativePathPiece currentPath,
const TreeEntry& wdEntry,
const GitIgnoreStack* ignore,
bool isIgnored) {
bool entryIgnored = isIgnored;
auto entryPath = currentPath + wdEntry.getName();
if (!isIgnored && ignore) {
auto fileType =
wdEntry.isTree() ? GitIgnore::TYPE_DIR : GitIgnore::TYPE_FILE;
auto ignoreStatus = ignore->match(entryPath, fileType);
if (ignoreStatus == GitIgnore::HIDDEN) {
// Completely skip over hidden entries.
// This is used for reserved directories like .hg and .eden
return;
}
entryIgnored = (ignoreStatus == GitIgnore::EXCLUDE);
}
if (wdEntry.isTree()) {
if (!entryIgnored || context->listIgnored) {
auto childFuture = diffAddedTree(
context, entryPath, wdEntry.getHash(), ignore, entryIgnored);
childFutures.add(std::move(entryPath), std::move(childFuture));
}
} else {
if (!entryIgnored) {
context->callback->addedFile(entryPath);
} else if (context->listIgnored) {
context->callback->ignoredFile(entryPath);
} else {
// Don't bother reporting this ignored file since
// listIgnored is false.
}
}
auto childPath = parentPath + entry.getName();
auto childFuture = diffAddedTree(context, childPath, entry.getHash());
childFutures.add(std::move(childPath), std::move(childFuture));
}
/**
@ -276,38 +448,77 @@ void processAddedSide(
void processBothPresent(
const DiffContext* context,
ChildFutures& childFutures,
RelativePathPiece parentPath,
const TreeEntry& entry1,
const TreeEntry& entry2) {
bool isTree1 = entry1.isTree();
bool isTree2 = entry2.isTree();
RelativePathPiece currentPath,
const TreeEntry& scmEntry,
const TreeEntry& wdEntry,
const GitIgnoreStack* ignore,
bool isIgnored) {
bool entryIgnored = isIgnored;
auto entryPath = currentPath + scmEntry.getName();
// If wdEntry and scmEntry are both files (or symlinks) then we don't need
// to bother computing the ignore status: the file is explicitly tracked in
// source control, so we should report it's status even if it would normally
// be ignored.
if (!isIgnored && (wdEntry.isTree() || scmEntry.isTree()) && ignore) {
auto fileType =
wdEntry.isTree() ? GitIgnore::TYPE_DIR : GitIgnore::TYPE_FILE;
auto ignoreStatus = ignore->match(entryPath, fileType);
if (ignoreStatus == GitIgnore::HIDDEN) {
// This is rather unexpected. We don't expect to find entries in
// source control using reserved hidden names.
// Treat this as ignored for now.
entryIgnored = true;
} else if (ignoreStatus == GitIgnore::EXCLUDE) {
entryIgnored = true;
} else {
entryIgnored = false;
}
}
if (isTree1) {
if (isTree2) {
bool isTreeSCM = scmEntry.isTree();
bool isTreeWD = wdEntry.isTree();
if (isTreeSCM) {
if (isTreeWD) {
// tree-to-tree diff
DCHECK_EQ(entry1.getType(), entry2.getType());
if (entry1.getHash() == entry2.getHash()) {
DCHECK_EQ(scmEntry.getType(), wdEntry.getType());
if (scmEntry.getHash() == wdEntry.getHash()) {
return;
}
auto childPath = parentPath + entry1.getName();
auto childFuture =
diffTrees(context, childPath, entry1.getHash(), entry2.getHash());
childFutures.add(std::move(childPath), std::move(childFuture));
auto childFuture = diffTrees(
context,
entryPath,
scmEntry.getHash(),
wdEntry.getHash(),
ignore,
entryIgnored);
childFutures.add(std::move(entryPath), std::move(childFuture));
} else {
// tree-to-file
// Record an ADDED entry for this path
context->callback->addedFile(parentPath + entry1.getName());
// Report everything in tree1 as REMOVED
processRemovedSide(context, childFutures, parentPath, entry1);
// Add a ADDED entry for this path
if (entryIgnored) {
if (context->listIgnored) {
context->callback->ignoredFile(entryPath);
}
} else {
context->callback->addedFile(entryPath);
}
// Report everything in scmTree as REMOVED
auto childFuture =
diffRemovedTree(context, entryPath, scmEntry.getHash());
childFutures.add(std::move(entryPath), std::move(childFuture));
}
} else {
if (isTree2) {
if (isTreeWD) {
// file-to-tree
// Add a REMOVED entry for this path
context->callback->removedFile(parentPath + entry1.getName());
context->callback->removedFile(entryPath);
// Report everything in tree2 as ADDED
processAddedSide(context, childFutures, parentPath, entry2);
// Report everything in wdEntry as ADDED
auto childFuture = diffAddedTree(
context, entryPath, wdEntry.getHash(), ignore, entryIgnored);
childFutures.add(std::move(entryPath), std::move(childFuture));
} else {
// file-to-file diff
// Even if blobs have different hashes, they could have the same contents.
@ -315,33 +526,36 @@ void processBothPresent(
// changed and then later reverted. In that case, the contents would be
// the same but the blobs would have different hashes
// If the types are different, then this entry is definitely modified
if (entry1.getType() != entry2.getType()) {
context->callback->modifiedFile(parentPath + entry1.getName());
if (scmEntry.getType() != wdEntry.getType()) {
context->callback->modifiedFile(entryPath);
} else {
// If Mercurial eventually switches to using blob IDs that are solely
// based on the file contents (as opposed to file contents + history)
// then we could drop this extra load of the blob SHA-1, and rely only
// on the blob ID comparison instead.
auto compareEntryContents = folly::makeFutureWith(
[context, path = parentPath + entry1.getName(), &entry1, &entry2] {
auto f1 = context->store->getBlobSha1(entry1.getHash());
auto f2 = context->store->getBlobSha1(entry2.getHash());
return folly::collect(f1, f2).thenValue(
[path, context](const std::tuple<Hash, Hash>& info) {
const auto& [info1, info2] = info;
if (info1 != info2) {
context->callback->modifiedFile(path);
auto compareEntryContents =
folly::makeFutureWith([context,
entryPath = currentPath + scmEntry.getName(),
&scmEntry,
&wdEntry] {
auto scmFuture = context->store->getBlobSha1(scmEntry.getHash());
auto wdFuture = context->store->getBlobSha1(wdEntry.getHash());
return folly::collect(scmFuture, wdFuture)
.thenValue([entryPath = entryPath.copy(),
context](const std::tuple<Hash, Hash>& info) {
const auto& [scmHash, wdHash] = info;
if (scmHash != wdHash) {
context->callback->modifiedFile(entryPath);
}
});
});
childFutures.add(
parentPath + entry1.getName(), std::move(compareEntryContents));
childFutures.add(std::move(entryPath), std::move(compareEntryContents));
}
}
}
}
Future<Unit> waitOnResults(
FOLLY_NODISCARD Future<Unit> waitOnResults(
const DiffContext* context,
ChildFutures&& childFutures) {
DCHECK_EQ(childFutures.paths.size(), childFutures.futures.size());
@ -388,7 +602,8 @@ diffCommits(const DiffContext* context, Hash hash1, Hash hash2) {
return makeFuture();
}
return diffTrees(context, RelativePathPiece{}, *tree1, *tree2);
return diffTrees(
context, RelativePathPiece{}, *tree1, *tree2, nullptr, false);
});
}
} // namespace
@ -406,5 +621,76 @@ diffCommitsForStatus(const ObjectStore* store, Hash hash1, Hash hash2) {
});
}
FOLLY_NODISCARD Future<Unit> diffTrees(
const DiffContext* context,
RelativePathPiece currentPath,
Hash scmHash,
Hash wdHash,
const GitIgnoreStack* ignore,
bool isIgnored) {
auto scmTreeFuture = context->store->getTree(scmHash);
auto wdTreeFuture = context->store->getTree(wdHash);
// Optimization for the case when both tree objects are immediately ready.
// We can avoid copying the input path in this case.
if (scmTreeFuture.isReady() && wdTreeFuture.isReady()) {
return diffTrees(
context,
currentPath,
*(std::move(scmTreeFuture).get()),
*(std::move(wdTreeFuture).get()),
ignore,
isIgnored);
}
return folly::collect(scmTreeFuture, wdTreeFuture)
.thenValue([context, currentPath = currentPath.copy(), ignore, isIgnored](
std::tuple<
std::shared_ptr<const Tree>,
std::shared_ptr<const Tree>>&& tup) {
const auto& [scmTree, wdTree] = tup;
return diffTrees(
context, currentPath, *scmTree, *wdTree, ignore, isIgnored);
});
}
FOLLY_NODISCARD Future<Unit> diffAddedTree(
const DiffContext* context,
RelativePathPiece currentPath,
Hash wdHash,
const GitIgnoreStack* ignore,
bool isIgnored) {
auto wdFuture = context->store->getTree(wdHash);
// Optimization for the case when the tree object is immediately ready.
// We can avoid copying the input path in this case.
if (wdFuture.isReady()) {
return diffAddedTree(
context, currentPath, *std::move(wdFuture).get(), ignore, isIgnored);
}
return std::move(wdFuture).thenValue(
[context, currentPath = currentPath.copy(), ignore, isIgnored](
std::shared_ptr<const Tree>&& wdTree) {
return diffAddedTree(context, currentPath, *wdTree, ignore, isIgnored);
});
}
FOLLY_NODISCARD Future<Unit> diffRemovedTree(
const DiffContext* context,
RelativePathPiece currentPath,
Hash scmHash) {
auto scmFuture = context->store->getTree(scmHash);
// Optimization for the case when the tree object is immediately ready.
// We can avoid copying the input path in this case.
if (scmFuture.isReady()) {
return diffRemovedTree(context, currentPath, *(std::move(scmFuture).get()));
}
return std::move(scmFuture).thenValue(
[context,
currentPath = currentPath.copy()](std::shared_ptr<const Tree>&& tree) {
return diffRemovedTree(context, currentPath, *tree);
});
}
} // namespace eden
} // namespace facebook

View File

@ -8,6 +8,7 @@
#pragma once
#include "eden/fs/service/gen-cpp2/eden_types.h"
#include "eden/fs/utils/PathFuncs.h"
namespace folly {
template <typename T>
@ -21,6 +22,7 @@ class Hash;
class ObjectStore;
class Tree;
class DiffContext;
class GitIgnoreStack;
/**
* Compute the diff between two commits.
@ -33,5 +35,66 @@ class DiffContext;
folly::Future<std::unique_ptr<ScmStatus>>
diffCommitsForStatus(const ObjectStore* store, Hash hash1, Hash hash2);
/**
* Compute the diff between a source control Tree and the current directory
* state. This function is called with the hashes of a source control tree
* entry and an unmaterialized inode entry.
*
* The path argument specifies the path to these trees, and will be prefixed
* to all differences recorded in the results.
*
* The caller is responsible for ensuring that the context remains valid
* until the returned Future completes.
*
* The differences will be recorded using the callback inside the passed
* DiffContext.
*/
folly::Future<folly::Unit> diffTrees(
const DiffContext* context,
RelativePathPiece currentPath,
Hash scmHash,
Hash wdHash,
const GitIgnoreStack* parentIgnore,
bool isIgnored);
/**
* Process an added tree (present locally but not present in the source control
* tree). This function is called with the hash of an unmaterialized inode
* entry. This whole subtree is marked as added using the DiffContext.
*
* The path argument specifies the path to these trees, and will be prefixed
* to all differences recorded in the results.
*
* The caller is responsible for ensuring that the context remains valid
* until the returned Future completes.
*
* The differences will be recorded using the callback inside the passed
* DiffContext.
*/
folly::Future<folly::Unit> diffAddedTree(
const DiffContext* context,
RelativePathPiece currentPath,
Hash wdHash,
const GitIgnoreStack* ignore,
bool isIgnored);
/**
* Process a removed tree (not present locally but present in the source control
* tree). This function is called with the hash of the source control tree
* entry. This whole subtree is marked as removed using the DiffContext.
*
* The path argument specifies the path to these trees, and will be prefixed
* to all differences recorded in the results.
*
* The caller is responsible for ensuring that the context remains valid
* until the returned Future completes.
*
* The differences will be recorded using the callback inside the passed
* DiffContext.
*/
folly::Future<folly::Unit> diffRemovedTree(
const DiffContext* context,
RelativePathPiece currentPath,
Hash scmHash);
} // namespace eden
} // namespace facebook

View File

@ -12,6 +12,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "eden/fs/model/git/TopLevelIgnores.h"
#include "eden/fs/store/DiffContext.h"
#include "eden/fs/store/MemoryLocalStore.h"
#include "eden/fs/store/ObjectStore.h"
@ -69,6 +70,41 @@ class DiffTest : public ::testing::Test {
store_.get(), makeTestHash(commit1), makeTestHash(commit2));
}
ScmStatus diffCommitsWithGitIgnore(
Hash hash1,
Hash hash2,
std::string gitIgnoreContents = {},
std::string userIgnoreContents = {},
std::string systemIgnoreContents = {},
bool listIgnored = true) {
auto callback = std::make_unique<ScmStatusDiffCallback>();
auto callbackPtr = callback.get();
auto mockedLoadFile = [gitIgnoreContents](RelativePathPiece /**/) {
return folly::makeFuture(gitIgnoreContents);
};
auto topLevelIgnores = std::make_unique<TopLevelIgnores>(
userIgnoreContents, systemIgnoreContents);
auto gitIgnoreStack = topLevelIgnores->getStack();
auto diffContext = DiffContext(
callbackPtr,
listIgnored,
store_.get(),
std::move(topLevelIgnores),
mockedLoadFile);
return diffTrees(
&diffContext,
RelativePathPiece{},
hash1,
hash2,
gitIgnoreStack,
false)
.thenValue([callback = std::move(callback)](auto&&) {
return callback->extractStatus();
})
.get(100ms);
}
std::shared_ptr<LocalStore> localStore_;
std::shared_ptr<FakeBackingStore> backingStore_;
std::shared_ptr<ObjectStore> store_;
@ -427,3 +463,748 @@ TEST_F(DiffTest, loadTreeError) {
result->entries,
UnorderedElementsAre(Pair("a/b/3.txt", ScmFileStatus::MODIFIED)));
}
// Generic test with no ignore files of a an added, modified, and removed file
TEST_F(DiffTest, nonignored_added_modified_and_removed_files) {
FakeTreeBuilder builder;
builder.setFile("src/foo/a.txt", "a");
builder.setFile("src/foo/a", "regular file");
builder.setFile("src/bar/c", "regular file");
builder.setFile("src/bar/d.txt", "d", /* executable */ true);
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
// add a file
auto builder2 = builder.clone();
builder2.setFile("src/bar/e.txt", "e");
builder2.removeFile("src/bar/d.txt");
builder2.replaceFile("src/foo/a.txt", "aa");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(), builder2.getRoot()->get().getHash());
EXPECT_THAT(result.errors, UnorderedElementsAre());
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
Pair("src/bar/e.txt", ScmFileStatus::ADDED),
Pair("src/bar/d.txt", ScmFileStatus::REMOVED),
Pair("src/foo/a.txt", ScmFileStatus::MODIFIED)));
}
// Directly test that diffAddedTree marks all files as ADDED in tree (no
// gitignore)
TEST_F(DiffTest, nonignored_added_files) {
FakeTreeBuilder builder;
builder.setFile("src/foo/a.txt", "a");
builder.setFile("src/foo/a", "regular file");
builder.setFile("src/bar/d.txt", "d", /* executable */ true);
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
// add a subdirectory
auto builder2 = builder.clone();
builder2.setFile("src/bar/foo/e.txt", "e");
builder2.setFile("src/bar/foo/f.txt", "f");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(), builder2.getRoot()->get().getHash());
EXPECT_THAT(result.errors, UnorderedElementsAre());
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
Pair("src/bar/foo/e.txt", ScmFileStatus::ADDED),
Pair("src/bar/foo/f.txt", ScmFileStatus::ADDED)));
// Test calling in directly with path to added entries
auto callback2 = std::make_unique<ScmStatusDiffCallback>();
auto callbackPtr2 = callback2.get();
auto diffContext2 = DiffContext(callbackPtr2, store_.get());
auto result2 = diffAddedTree(
&diffContext2,
RelativePathPiece{"src/bar/foo"},
builder2.getStoredTree(RelativePathPiece{"src/bar/foo"})
->get()
.getHash(),
nullptr,
false)
.thenValue([callback = std::move(callback2)](auto&&) {
return callback->extractStatus();
})
.get(100ms);
EXPECT_THAT(result2.errors, UnorderedElementsAre());
EXPECT_THAT(
result2.entries,
UnorderedElementsAre(
Pair("src/bar/foo/e.txt", ScmFileStatus::ADDED),
Pair("src/bar/foo/f.txt", ScmFileStatus::ADDED)));
}
// Directly test that diffRemovedTree marks all files as REMOVED in tree (no
// gitignore)
TEST_F(DiffTest, nonignored_removed_files) {
FakeTreeBuilder builder;
builder.setFile("src/foo/b.txt", "b", /* executable */ true);
builder.setFile("src/bar/c", "regular file");
builder.setFile("src/bar/foo/e.txt", "e");
builder.setFile("src/bar/foo/f.txt", "f");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
// remove a subdirectory
auto builder2 = builder.clone();
builder2.removeFile("src/bar/foo/e.txt");
builder2.removeFile("src/bar/foo/f.txt");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(), builder2.getRoot()->get().getHash());
EXPECT_THAT(result.errors, UnorderedElementsAre());
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
Pair("src/bar/foo/e.txt", ScmFileStatus::REMOVED),
Pair("src/bar/foo/f.txt", ScmFileStatus::REMOVED)));
// Test calling in directly with path to removed entries
auto callback2 = std::make_unique<ScmStatusDiffCallback>();
auto callbackPtr2 = callback2.get();
auto diffContext2 = DiffContext(callbackPtr2, store_.get());
auto result2 = diffRemovedTree(
&diffContext2,
RelativePathPiece{"src/bar/foo"},
builder.getStoredTree(RelativePathPiece{"src/bar/foo"})
->get()
.getHash())
.thenValue([callback = std::move(callback2)](auto&&) {
return callback->extractStatus();
})
.get(100ms);
EXPECT_THAT(result2.errors, UnorderedElementsAre());
EXPECT_THAT(
result2.entries,
UnorderedElementsAre(
Pair("src/bar/foo/e.txt", ScmFileStatus::REMOVED),
Pair("src/bar/foo/f.txt", ScmFileStatus::REMOVED)));
}
// Tests the case in which a tracked file in source control is modified locally.
// In this case, the file should be recorded as MODIFIED, since it matches
// an ignore rule but was already tracked
TEST_F(DiffTest, diff_trees_with_tracked_ignored_file_modified) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "a.txt\n";
builder.setFile("src/foo/a.txt", "a");
builder.setFile("src/foo/a", "regular file");
builder.setFile("src/bar/d.txt", "d", /* executable */ true);
builder.setFile("src/foo/.gitignore", "a.txt\n");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
// add a file
auto builder2 = builder.clone();
builder2.setFile("src/bar/e.txt", "e");
builder2.removeFile("src/bar/d.txt");
// Even though this is modified, it will be ignored because it matches an
// ignore rule.
builder2.replaceFile("src/foo/a.txt", "aa");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents);
EXPECT_THAT(result.errors, UnorderedElementsAre());
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
Pair("src/bar/e.txt", ScmFileStatus::ADDED),
Pair("src/bar/d.txt", ScmFileStatus::REMOVED),
Pair("src/foo/a.txt", ScmFileStatus::MODIFIED)));
}
// Tests the case in which a tracked file in source control is modified locally.
// In this case, the file should be recorded as MODIFIED, since it matches
// an ignore rule but was already tracked
TEST_F(DiffTest, ignored_added_modified_and_removed_files) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "a.txt\n";
builder.setFile("src/foo/a.txt", "a");
builder.setFile("src/bar/d.txt", "d", /* executable */ true);
builder.setFile("src/bar/c", "regular file");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
// add a file
auto builder2 = builder.clone();
builder2.setFile("src/foo/.gitignore", gitIgnoreContents);
builder2.setFile("src/bar/e.txt", "e");
builder2.removeFile("src/bar/d.txt");
builder2.replaceFile("src/foo/a.txt", "aa");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents);
EXPECT_THAT(result.errors, UnorderedElementsAre());
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
Pair("src/foo/.gitignore", ScmFileStatus::ADDED),
Pair("src/bar/e.txt", ScmFileStatus::ADDED),
Pair("src/bar/d.txt", ScmFileStatus::REMOVED),
Pair("src/foo/a.txt", ScmFileStatus::MODIFIED)));
}
// Tests that a file that is added that matches a ignore rule is marked as
// IGNORED
TEST_F(DiffTest, ignored_added_files) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "foo/e.txt";
builder.setFile("src/foo/e.txt", "e");
builder.setFile("src/bar/c.txt", "c");
builder.setFile("src/bar/.gitignore", gitIgnoreContents);
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
// add a subdirectory
auto builder2 = builder.clone();
builder2.setFile("src/bar/foo/e.txt", "e");
builder2.setFile("src/bar/foo/f.txt", "f");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents);
EXPECT_THAT(result.errors, UnorderedElementsAre());
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
Pair("src/bar/foo/e.txt", ScmFileStatus::IGNORED),
Pair("src/bar/foo/f.txt", ScmFileStatus::ADDED)));
auto result2 = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents,
"",
"",
false);
EXPECT_THAT(result2.errors, UnorderedElementsAre());
EXPECT_THAT(
result2.entries,
UnorderedElementsAre(Pair("src/bar/foo/f.txt", ScmFileStatus::ADDED)));
}
// Test that a file that is tracked by source control but matches an ignore rule
// and is removed is marked as REMOVED since it was previously tracked by source
// control
TEST_F(DiffTest, ignored_removed_files) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "foo";
builder.setFile("src/foo/a.txt", "a");
builder.setFile("src/bar/c", "regular file");
builder.setFile("src/bar/foo/e.txt", "e");
builder.setFile("src/bar/foo/f.txt", "f");
builder.setFile("src/bar/.gitignore", gitIgnoreContents);
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
// remove a subdirectory
auto builder2 = builder.clone();
// Even though this file is ignored, it should still be marked as removed
// since it was previously tracked by source control.
builder2.removeFile("src/bar/foo/e.txt");
builder2.removeFile("src/bar/foo/f.txt");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents);
EXPECT_THAT(result.errors, UnorderedElementsAre());
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
Pair("src/bar/foo/e.txt", ScmFileStatus::REMOVED),
Pair("src/bar/foo/f.txt", ScmFileStatus::REMOVED)));
}
TEST_F(DiffTest, ignoreToplevelOnly) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "/1.txt\nignore.txt\njunk/\n!important.txt\n";
builder.setFile(".gitignore", gitIgnoreContents);
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
auto builder2 = builder.clone();
// Add some untracked files, some of which match the ignore patterns
builder2.setFile("1.txt", "new\n");
builder2.setFile("ignore.txt", "new\n");
builder2.setFile("src/1.txt", "new\n");
builder2.setFile("src/foo/ignore.txt", "new\n");
builder2.mkdir("src/foo/abc");
builder2.mkdir("src/foo/abc/xyz");
builder2.setFile("src/foo/abc/xyz/ignore.txt", "new\n");
builder2.mkdir("junk");
builder2.setFile("junk/stuff.txt", "new\n");
// Even though important.txt matches an include rule, the fact that it
// is inside an excluded directory takes precedence.
builder2.setFile("junk/important.txt", "new\n");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents);
EXPECT_THAT(result.errors, UnorderedElementsAre());
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("src/1.txt", ScmFileStatus::ADDED),
std::make_pair("1.txt", ScmFileStatus::IGNORED),
std::make_pair("ignore.txt", ScmFileStatus::IGNORED),
std::make_pair("junk/stuff.txt", ScmFileStatus::IGNORED),
std::make_pair("junk/important.txt", ScmFileStatus::IGNORED),
std::make_pair("src/foo/ignore.txt", ScmFileStatus::IGNORED),
std::make_pair(
"src/foo/abc/xyz/ignore.txt", ScmFileStatus::IGNORED)));
}
// Test with a file that matches a .gitignore pattern but also is already in the
// Tree (so we should report the modification)
TEST_F(DiffTest, ignored_file_local_and_in_tree) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "/1.txt\nignore.txt\njunk/\n!important.txt\nxyz\n";
builder.setFile(".gitignore", gitIgnoreContents);
builder.setFile("src/foo/abc/xyz/ignore.txt", "test\n");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
auto builder2 = builder.clone();
// Add some untracked files, some of which match the ignore patterns
builder2.setFile("1.txt", "new\n");
builder2.setFile("ignore.txt", "new\n");
builder2.setFile("src/1.txt", "new\n");
builder2.setFile("src/foo/ignore.txt", "new\n");
builder2.mkdir("junk");
builder2.setFile("junk/stuff.txt", "new\n");
// overwrite a file that already exists and matches the ignore pattern
builder2.replaceFile("src/foo/abc/xyz/ignore.txt", "modified\n");
// Even though important.txt matches an include rule, the fact that it
// is inside an excluded directory takes precedence.
builder2.setFile("junk/important.txt", "new\n");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents);
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("src/1.txt", ScmFileStatus::ADDED),
std::make_pair("src/foo/abc/xyz/ignore.txt", ScmFileStatus::MODIFIED),
std::make_pair("1.txt", ScmFileStatus::IGNORED),
std::make_pair("ignore.txt", ScmFileStatus::IGNORED),
std::make_pair("junk/stuff.txt", ScmFileStatus::IGNORED),
std::make_pair("junk/important.txt", ScmFileStatus::IGNORED),
std::make_pair("src/foo/ignore.txt", ScmFileStatus::IGNORED)));
}
// Test with a file that matches a .gitignore pattern but also is already in the
// Tree but removed from mount (so we should report the file removal)
TEST_F(DiffTest, ignored_file_not_local_but_is_in_tree) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "/1.txt\nignore.txt\njunk/\n!important.txt\nxyz\n";
builder.setFile(".gitignore", gitIgnoreContents);
builder.setFile("src/foo/abc/xyz/ignore.txt", "test\n");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
auto builder2 = builder.clone();
// Add some untracked files, some of which match the ignore patterns
builder2.setFile("1.txt", "new\n");
builder2.setFile("ignore.txt", "new\n");
builder2.setFile("src/1.txt", "new\n");
builder2.setFile("src/foo/ignore.txt", "new\n");
builder2.mkdir("junk");
builder2.setFile("junk/stuff.txt", "new\n");
// remove a file that already exists and matches the ignore pattern
builder2.removeFile("src/foo/abc/xyz/ignore.txt");
// Even though important.txt matches an include rule, the fact that it
// is inside an excluded directory takes precedence.
builder2.setFile("junk/important.txt", "new\n");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents);
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("src/1.txt", ScmFileStatus::ADDED),
std::make_pair("src/foo/abc/xyz/ignore.txt", ScmFileStatus::REMOVED),
std::make_pair("1.txt", ScmFileStatus::IGNORED),
std::make_pair("ignore.txt", ScmFileStatus::IGNORED),
std::make_pair("junk/stuff.txt", ScmFileStatus::IGNORED),
std::make_pair("junk/important.txt", ScmFileStatus::IGNORED),
std::make_pair("src/foo/ignore.txt", ScmFileStatus::IGNORED)));
}
// Test with a .gitignore file in the top-level directory
// and the presence of both of system level and user specific ignore files
TEST_F(DiffTest, ignoreSystemLevelAndUser) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "/1.txt\nignore.txt\njunk/\n!important.txt\n";
builder.setFile(".gitignore", gitIgnoreContents);
builder.setFile("src/foo/bar.txt", "test\n");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
auto builder2 = builder.clone();
// Add some untracked files, matching either global or user patterns
builder2.setFile("skip_global.txt", "new\n");
builder2.setFile("skip_user.txt", "new\n");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents,
"skip_global.txt\n",
"skip_user.txt\n");
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("skip_global.txt", ScmFileStatus::IGNORED),
std::make_pair("skip_user.txt", ScmFileStatus::IGNORED)));
}
// Test with a .gitignore file in the top-level directory
// and the presence of user specific ignore file
TEST_F(DiffTest, ignoreUserLevel) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "/1.txt\nignore.txt\njunk/\n!important.txt\n";
builder.setFile(".gitignore", gitIgnoreContents);
builder.setFile("src/foo/bar.txt", "test\n");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
auto builder2 = builder.clone();
// Add some untracked files, matching either global or user patterns
builder2.setFile("skip_global.txt", "new\n");
builder2.setFile("skip_user.txt", "new\n");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents,
"",
"skip_user.txt\n");
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("skip_global.txt", ScmFileStatus::ADDED),
std::make_pair("skip_user.txt", ScmFileStatus::IGNORED)));
}
// Test with a .gitignore file in the top-level directory
// and the presence of system level ignore file
TEST_F(DiffTest, ignoreSystemLevel) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "/1.txt\nignore.txt\njunk/\n!important.txt\n";
builder.setFile(".gitignore", gitIgnoreContents);
builder.setFile("src/foo/bar.txt", "test\n");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
auto builder2 = builder.clone();
// Add some untracked files, matching either global or user patterns
builder2.setFile("skip_global.txt", "new\n");
builder2.setFile("skip_user.txt", "new\n");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents,
"skip_global.txt\n",
"");
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("skip_global.txt", ScmFileStatus::IGNORED),
std::make_pair("skip_user.txt", ScmFileStatus::ADDED)));
}
// Tests the case in which a tracked directory in source control is replaced by
// a file locally, and the directory matches an ignore rule. In this case,
// the file should be recorded as ADDED, since the ignore rule is specifically
// for directories
TEST_F(DiffTest, directory_to_file_with_directory_ignored) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "a/b/";
builder.setFile("a/b.txt", "test\n");
builder.setFile("a/b/c.txt", "test\n");
builder.setFile("a/b/d.txt", "test\n");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
auto builder2 = builder.clone();
builder2.removeFile("a/b/c.txt");
builder2.removeFile("a/b/d.txt");
builder2.setFile("a/b", "regular file");
builder2.setFile(".gitignore", gitIgnoreContents);
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents);
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("a/b/c.txt", ScmFileStatus::REMOVED),
std::make_pair("a/b/d.txt", ScmFileStatus::REMOVED),
std::make_pair("a/b", ScmFileStatus::ADDED),
std::make_pair(".gitignore", ScmFileStatus::ADDED)));
}
// Tests the case in which a tracked directory in source control is replaced by
// a file locally, and the file matches an ignore rule. In this case, the file
// should be recorded as IGNORED, since the ignore rule is specifically for
// files
TEST_F(DiffTest, directory_to_file_with_file_ignored) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "a/b";
builder.setFile("a/b.txt", "test\n");
builder.setFile("a/b/c.txt", "test\n");
builder.setFile("a/b/d.txt", "test\n");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
auto builder2 = builder.clone();
builder2.removeFile("a/b/c.txt");
builder2.removeFile("a/b/d.txt");
builder2.setFile("a/b", "regular file");
builder2.setFile(".gitignore", gitIgnoreContents);
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents);
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("a/b/c.txt", ScmFileStatus::REMOVED),
std::make_pair("a/b/d.txt", ScmFileStatus::REMOVED),
std::make_pair("a/b", ScmFileStatus::IGNORED),
std::make_pair(".gitignore", ScmFileStatus::ADDED)));
}
// Tests the case in which a tracked file in source control is replaced by
// a directory locally, and the file matches an ignore rule. In this case,
// the directory should be recorded as ADDED, since the ignore rule is
// specifically for files
TEST_F(DiffTest, file_to_directory_with_gitignore) {
FakeTreeBuilder builder;
auto gitIgnoreContents = "a/b/d\n!a/b/d/";
builder.setFile("a/b.txt", "test\n");
builder.setFile("a/b/c.txt", "test\n");
builder.setFile("a/b/d", "test\n");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
auto builder2 = builder.clone();
builder2.removeFile("a/b/d");
builder2.mkdir("a/b/d");
builder2.setFile("a/b/d/e.txt", "test");
builder2.setFile(".gitignore", gitIgnoreContents);
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents);
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("a/b/d", ScmFileStatus::REMOVED),
std::make_pair("a/b/d/e.txt", ScmFileStatus::ADDED),
std::make_pair(".gitignore", ScmFileStatus::ADDED)));
}
// Tests the case in which a file is replaced by a directory, and a directory
// is ignored, but a file inside the directory is not ignored.
TEST_F(DiffTest, addIgnoredDirectory) {
FakeTreeBuilder builder;
builder.setFile("a/b.txt", "test\n");
builder.setFile("a/b/c.txt", "test\n");
builder.setFile("a/b/r", "test\n");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
auto builder2 = builder.clone();
// The following won't be tracked
builder2.removeFile("a/b/r");
builder2.mkdir("a/b/r");
builder2.setFile("a/b/r/e.txt", "ignored");
builder2.mkdir("a/b/r/d");
builder2.setFile("a/b/r/d/g.txt", "ignored too");
// The following should be tracked
builder2.mkdir("a/b/g");
builder2.setFile("a/b/g/e.txt", "added");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
// It is not possible to re-include a file if a parent directory of that file
// is excluded.
auto systemIgnore = "a/b/r/\n!a/b/r/d/g.txt\n";
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
"",
systemIgnore);
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("a/b/r", ScmFileStatus::REMOVED),
std::make_pair("a/b/r/e.txt", ScmFileStatus::IGNORED),
std::make_pair("a/b/r/d/g.txt", ScmFileStatus::IGNORED),
std::make_pair("a/b/g/e.txt", ScmFileStatus::ADDED)));
}
// Tests the case in which a file becomes a directory and the directory is
// ignored but the parent directory is not ignored.
TEST_F(DiffTest, nestedGitIgnoreFiles) {
FakeTreeBuilder builder;
// a/b/r/e.txt is not ignored.
builder.setFile("a/b.txt", "test\n");
builder.setFile("a/b/c.txt", "test\n");
builder.setFile("a/b/r", "test\n");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
auto builder2 = builder.clone();
auto gitIgnoreContents = "!e.txt\n";
builder2.removeFile("a/b/r");
builder2.mkdir("a/b/r");
builder2.setFile("a/b/r/e.txt", "not ignored");
builder2.setFile("a/b/r/f.txt", "is ignored");
builder2.setFile("a/b/r/.gitignore", gitIgnoreContents);
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto systemIgnore = "a/b/r/*\n!a/b/r/.gitignore\n";
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
gitIgnoreContents,
systemIgnore);
EXPECT_THAT(
result.entries,
UnorderedElementsAre(
std::make_pair("a/b/r", ScmFileStatus::REMOVED),
std::make_pair("a/b/r/e.txt", ScmFileStatus::ADDED),
std::make_pair("a/b/r/f.txt", ScmFileStatus::IGNORED),
std::make_pair("a/b/r/.gitignore", ScmFileStatus::ADDED)));
}
// Tests the case in which hidden folders (like .hg/.eden) are not reported
TEST_F(DiffTest, hiddenFolder) {
FakeTreeBuilder builder;
builder.setFile("a/b.txt", "test\n");
builder.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("1", builder)->setReady();
auto builder2 = builder.clone();
builder2.setFile("a/c.txt", "not ignored");
// There should be no mention of this in the results.
builder2.mkdir(".hg");
builder2.finalize(backingStore_, /* setReady */ true);
backingStore_->putCommit("2", builder2)->setReady();
auto result = diffCommitsWithGitIgnore(
builder.getRoot()->get().getHash(),
builder2.getRoot()->get().getHash(),
"");
EXPECT_THAT(
result.entries,
UnorderedElementsAre(std::make_pair("a/c.txt", ScmFileStatus::ADDED)));
}