mirror of
https://github.com/facebook/sapling.git
synced 2024-10-06 14:58:03 +03:00
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:
parent
a80bcd779b
commit
4dc48ec7fa
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user