mirror of
https://github.com/facebook/sapling.git
synced 2024-10-05 14:28:17 +03:00
Implement Dirstate::remove().
Summary: This adds the logic to power `hg rm`. There are comprehensive tests that attempt to cover all of the edge cases. This evolved to become a complex change because I realized that I needed to change my internal representation of the dirstate to implement it properly. Specifically, we now maintain a map (`userDirectives`) of files that have been explicitly scheduled for change via `hg add` or `hg rm`. To compute the result of `hg status`, we find the changes between the manifest/root tree and the overlay and also consult `userDirectives`. `Dirstate::getStatus()` was updated considerably as part of this commit due to the introduction of `userDirectives`. As such, `Dirstate::remove()` must do several things: * Defend the integrity of the dirstate by throwing appropriate exceptions for invalid inputs. * Delete the specified file, if appropriate. * Update `userDirectives`, if appropriate. Although `Dirstate::add()` was not the focus of this commit, it also had to be updated to match the pattern introduced by `Dirstate::remove()`. Some important features that are still not supported are: * Handling ignored files correctly. * Storing copy/move information. Reviewed By: simpkins Differential Revision: D4104503 fbshipit-source-id: d5d45a279e16ded584c6cd4d528ba92d2c8e2993
This commit is contained in:
parent
9290e0e6c9
commit
12eac0f5db
@ -8,6 +8,8 @@
|
||||
*
|
||||
*/
|
||||
#include "Dirstate.h"
|
||||
#include <folly/io/Cursor.h>
|
||||
#include <folly/io/IOBuf.h>
|
||||
#include "eden/fs/inodes/TreeEntryFileInode.h"
|
||||
#include "eden/fs/inodes/TreeInode.h"
|
||||
#include "eden/fs/model/Blob.h"
|
||||
@ -17,6 +19,7 @@
|
||||
|
||||
namespace {
|
||||
struct DirectoryDelta {
|
||||
// The contents of each vector is sorted by compare().
|
||||
std::vector<facebook::eden::PathComponent> added;
|
||||
std::vector<facebook::eden::PathComponent> removed;
|
||||
std::vector<facebook::eden::PathComponent> modified;
|
||||
@ -26,6 +29,24 @@ struct DirectoryDelta {
|
||||
namespace facebook {
|
||||
namespace eden {
|
||||
|
||||
std::string HgStatus::toString() const {
|
||||
// Sort the entries in the map.
|
||||
std::vector<std::pair<RelativePath, HgStatusCode>> entries(
|
||||
statuses_.begin(), statuses_.end());
|
||||
std::sort(entries.begin(), entries.end());
|
||||
|
||||
auto buf = folly::IOBuf::create(50 * entries.size());
|
||||
folly::io::Appender appender(buf.get(), /* growSize */ 1024);
|
||||
for (auto pair : entries) {
|
||||
appender(HgStatusCode_toString(pair.second));
|
||||
appender(" ");
|
||||
appender(pair.first.stringPiece());
|
||||
appender("\n");
|
||||
}
|
||||
|
||||
return buf->moveToFbString().toStdString();
|
||||
}
|
||||
|
||||
std::unique_ptr<HgStatus> Dirstate::getStatus() {
|
||||
// Find the modified directories in the overlay and compare them with what is
|
||||
// in the root tree.
|
||||
@ -37,9 +58,9 @@ std::unique_ptr<HgStatus> Dirstate::getStatus() {
|
||||
std::unordered_map<RelativePath, HgStatusCode>());
|
||||
}
|
||||
|
||||
auto userChanges = userChanges_.rlock();
|
||||
std::unordered_map<RelativePathPiece, HgStatusCode> copyOfUserChanges(
|
||||
userChanges->begin(), userChanges->end());
|
||||
auto userDirectives = userDirectives_.rlock();
|
||||
std::unordered_map<RelativePathPiece, HgUserStatusDirective>
|
||||
copyOfUserDirectives(userDirectives->begin(), userDirectives->end());
|
||||
|
||||
std::unordered_map<RelativePath, HgStatusCode> manifest;
|
||||
auto rootTree = edenMount_->getRootTree();
|
||||
@ -55,43 +76,111 @@ std::unique_ptr<HgStatus> Dirstate::getStatus() {
|
||||
DCHECK_NOTNULL(tree.get());
|
||||
|
||||
DirectoryDelta delta;
|
||||
computeDelta(*rootTree, *treeInode, delta);
|
||||
computeDelta(rootTree.get(), *treeInode, delta);
|
||||
|
||||
// Look at the delta and convert the results into HgStatuses.
|
||||
// Files in delta.added fall into one of three categories:
|
||||
// 1. ADDED
|
||||
// 2. NOT_TRACKED
|
||||
// 3. IGNORED
|
||||
for (auto& addedPath : delta.added) {
|
||||
auto pathToEntry = directory + addedPath;
|
||||
auto result = userChanges->find(pathToEntry);
|
||||
if (result != userChanges->end()) {
|
||||
auto result = userDirectives->find(pathToEntry);
|
||||
if (result != userDirectives->end()) {
|
||||
auto statusCode = result->second;
|
||||
if (statusCode == HgStatusCode::ADDED) {
|
||||
manifest.emplace(pathToEntry, HgStatusCode::ADDED);
|
||||
} else {
|
||||
LOG(ERROR) << "File in delta.added was not ADDED. Support this!";
|
||||
switch (statusCode) {
|
||||
case HgUserStatusDirective::ADD:
|
||||
manifest.emplace(pathToEntry, HgStatusCode::ADDED);
|
||||
break;
|
||||
case HgUserStatusDirective::REMOVE:
|
||||
// TODO(mbolin): Is there any weird sequence of modifications with
|
||||
// adding/removed files matched by .hgignore that could lead to this
|
||||
// state?
|
||||
throw std::runtime_error(folly::sformat(
|
||||
"Invariant violation: The user has marked {} for removal, "
|
||||
"but it does not exist in the manifest.",
|
||||
pathToEntry.stringPiece()));
|
||||
}
|
||||
copyOfUserChanges.erase(pathToEntry);
|
||||
copyOfUserDirectives.erase(pathToEntry);
|
||||
} else {
|
||||
manifest.emplace(pathToEntry, HgStatusCode::NOT_TRACKED);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mbolin): It probably is not quite this simple.
|
||||
// Files in delta.modified fall into one of three categories:
|
||||
// 1. MODIFIED
|
||||
// 2. REMOVED
|
||||
// 3. IGNORED
|
||||
for (auto& modifiedPath : delta.modified) {
|
||||
auto pathToEntry = directory + modifiedPath;
|
||||
manifest.emplace(pathToEntry, HgStatusCode::MODIFIED);
|
||||
copyOfUserChanges.erase(pathToEntry);
|
||||
auto result = userDirectives->find(pathToEntry);
|
||||
if (result != userDirectives->end()) {
|
||||
auto statusCode = result->second;
|
||||
switch (statusCode) {
|
||||
case HgUserStatusDirective::ADD:
|
||||
// TODO(mbolin): Is there any weird sequence of modifications with
|
||||
// adding/removed files matched by .hgignore that could lead to this
|
||||
// state?
|
||||
throw std::runtime_error(folly::sformat(
|
||||
"Invariant violation: The user has marked {} for addition, "
|
||||
"but it already exists in the manifest.",
|
||||
pathToEntry.stringPiece()));
|
||||
case HgUserStatusDirective::REMOVE:
|
||||
manifest.emplace(pathToEntry, HgStatusCode::REMOVED);
|
||||
break;
|
||||
}
|
||||
copyOfUserDirectives.erase(pathToEntry);
|
||||
} else {
|
||||
manifest.emplace(pathToEntry, HgStatusCode::MODIFIED);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mbolin): Process the delta.removed collection.
|
||||
// Files in delta.removed fall into one of three categories:
|
||||
// 1. REMOVED
|
||||
// 2. MISSING
|
||||
// 3. IGNORED
|
||||
for (auto& removedPath : delta.removed) {
|
||||
auto pathToEntry = directory + removedPath;
|
||||
auto result = userDirectives->find(pathToEntry);
|
||||
if (result != userDirectives->end()) {
|
||||
auto statusCode = result->second;
|
||||
switch (statusCode) {
|
||||
case HgUserStatusDirective::ADD:
|
||||
// TODO(mbolin): Is there any weird sequence of modifications with
|
||||
// adding/removed files matched by .hgignore that could lead to this
|
||||
// state?
|
||||
throw std::runtime_error(folly::sformat(
|
||||
"Invariant violation: The user has marked {} for addition, "
|
||||
"but it already exists in the manifest "
|
||||
"(and is currently removed from disk).",
|
||||
pathToEntry.stringPiece()));
|
||||
case HgUserStatusDirective::REMOVE:
|
||||
manifest.emplace(pathToEntry, HgStatusCode::REMOVED);
|
||||
break;
|
||||
}
|
||||
copyOfUserDirectives.erase(pathToEntry);
|
||||
} else {
|
||||
// The file is not present on disk, but the user never ran `hg rm`.
|
||||
manifest.emplace(pathToEntry, HgStatusCode::MISSING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We should make sure that every entry in userChanges_ is accounted for in
|
||||
// We should make sure that every entry in userDirectives_ is accounted for in
|
||||
// the HgStatus that we return.
|
||||
for (auto& pair : copyOfUserChanges) {
|
||||
if (pair.second == HgStatusCode::ADDED) {
|
||||
manifest.emplace(RelativePath(pair.first), HgStatusCode::MISSING);
|
||||
} else {
|
||||
LOG(INFO) << "Leftover entry in copyOfUserChanges that is not handled: "
|
||||
<< pair.first;
|
||||
for (auto& pair : copyOfUserDirectives) {
|
||||
switch (pair.second) {
|
||||
case HgUserStatusDirective::ADD:
|
||||
// The file was marked for addition, but no longer exists in the working
|
||||
// copy. The user should either restore the file or run `hg forget`.
|
||||
manifest.emplace(RelativePath(pair.first), HgStatusCode::MISSING);
|
||||
break;
|
||||
case HgUserStatusDirective::REMOVE:
|
||||
// The file was marked for removal, but it still exists in the working
|
||||
// copy without any modifications. Although it may seem strange, it
|
||||
// should still show up as REMOVED in `hg status` even though it is
|
||||
// still on disk.
|
||||
manifest.emplace(RelativePath(pair.first), HgStatusCode::REMOVED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,40 +188,40 @@ std::unique_ptr<HgStatus> Dirstate::getStatus() {
|
||||
}
|
||||
|
||||
bool hasMatchingAttributes(
|
||||
TreeEntry& treeEntry,
|
||||
TreeInode::Entry& treeInode,
|
||||
const TreeEntry* treeEntry,
|
||||
const TreeInode::Entry* treeInode,
|
||||
ObjectStore& objectStore,
|
||||
TreeInode& parent, // Has rlock
|
||||
const TreeInode::Dir& dir) {
|
||||
if (treeEntry.getMode() != treeInode.mode) {
|
||||
if (treeEntry->getMode() != treeInode->mode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO(t12183419): Once the file size is available in the TreeEntry,
|
||||
// compare file sizes before fetching SHA-1s.
|
||||
|
||||
if (treeInode.materialized) {
|
||||
if (treeInode->materialized) {
|
||||
// If the the inode is materialized, then we cannot trust the Hash on the
|
||||
// TreeInode::Entry, so we must compare with the contents in the overlay.
|
||||
auto overlayInode =
|
||||
parent.lookupChildByNameLocked(&dir, treeEntry.getName());
|
||||
parent.lookupChildByNameLocked(&dir, treeEntry->getName());
|
||||
auto fileInode =
|
||||
std::dynamic_pointer_cast<TreeEntryFileInode>(overlayInode);
|
||||
auto overlaySHA1 = fileInode->getSHA1().get();
|
||||
auto blobSHA1 = objectStore.getSha1ForBlob(treeEntry.getHash());
|
||||
auto blobSHA1 = objectStore.getSha1ForBlob(treeEntry->getHash());
|
||||
return overlaySHA1 == *blobSHA1;
|
||||
} else {
|
||||
auto optionalHash = treeInode.hash;
|
||||
auto optionalHash = treeInode->hash;
|
||||
DCHECK(optionalHash.hasValue()) << "non-materialized file must have a hash";
|
||||
return *optionalHash.get_pointer() == treeEntry.getHash();
|
||||
return *optionalHash.get_pointer() == treeEntry->getHash();
|
||||
}
|
||||
}
|
||||
|
||||
void Dirstate::computeDelta(
|
||||
const Tree& original,
|
||||
const Tree* original,
|
||||
TreeInode& current,
|
||||
DirectoryDelta& delta) const {
|
||||
auto treeEntries = original.getTreeEntries();
|
||||
auto treeEntries = original->getTreeEntries();
|
||||
auto dir = current.getContents().rlock();
|
||||
auto& entries = dir->entries;
|
||||
|
||||
@ -169,8 +258,8 @@ void Dirstate::computeDelta(
|
||||
auto cmp = base.getName().stringPiece().compare(overlayName.stringPiece());
|
||||
if (cmp == 0) {
|
||||
if (!hasMatchingAttributes(
|
||||
base,
|
||||
*overlayIterator->second.get(),
|
||||
&base,
|
||||
overlayIterator->second.get(),
|
||||
*edenMount_->getObjectStore(),
|
||||
current,
|
||||
*dir)) {
|
||||
@ -191,41 +280,240 @@ void Dirstate::computeDelta(
|
||||
|
||||
void Dirstate::add(RelativePathPiece path) {
|
||||
// TODO(mbolin): Verify that path corresponds to a regular file or symlink.
|
||||
applyUserStatusChange_(path, HgStatusCode::ADDED);
|
||||
/*
|
||||
* Analogous to `hg add <path>`. Note that this can have one of several
|
||||
* possible outcomes:
|
||||
* 1. If the path does not exist in the working copy, return an error. (Note
|
||||
* that this happens even if path is in the userDirectives_ as REMOVE.
|
||||
* 2. If the path refers to a directory, return an error. (Currently, the
|
||||
* caller is responsible for enumerating the transitive set of files in
|
||||
* the directory and invoking this method once for each file.)
|
||||
* 3. If the path is already in the manifest, or if it is already present in
|
||||
* userDirectives_ as ADD, then return a warning as Hg does:
|
||||
* "<path> already tracked!".
|
||||
* 4. If the path was in the userDirectives_ as REMOVE, then this call to
|
||||
* add() cancels it out and should remove the entry from userDirectives_.
|
||||
* 5. Otherwise, `path` must not be in userDirectives_, so add it.
|
||||
*/
|
||||
// TODO(mbolin): Honor the detailed behavior described above. Currently, we
|
||||
// assume that none of the edge cases in 1-3 apply.
|
||||
{
|
||||
auto userDirectives = userDirectives_.wlock();
|
||||
auto result = userDirectives->find(path.copy());
|
||||
if (result != userDirectives->end()) {
|
||||
switch (result->second) {
|
||||
case HgUserStatusDirective::ADD:
|
||||
// No-op: already added.
|
||||
break;
|
||||
case HgUserStatusDirective::REMOVE:
|
||||
userDirectives->erase(path.copy());
|
||||
persistence_->save(*userDirectives);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
(*userDirectives)[path.copy()] = HgUserStatusDirective::ADD;
|
||||
persistence_->save(*userDirectives);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dirstate::applyUserStatusChange_(
|
||||
RelativePathPiece pathToFile,
|
||||
HgStatusCode code) {
|
||||
auto userChanges = userChanges_.wlock();
|
||||
|
||||
if (code == HgStatusCode::ADDED) {
|
||||
// TODO(mbolin): Honor the detailed behavior described below.
|
||||
/*
|
||||
* Analogous to `hg add <path>`. Note that this can have one of several
|
||||
* possible outcomes:
|
||||
* 1. If the path does not exist in the working copy, return an error. (Note
|
||||
* that this happens even if path is in the manifest as REMOVED. It will
|
||||
* also happen in the case where the path is MISSING.)
|
||||
* 2. If the path refers to a directory, return an error. (Currently, the
|
||||
* caller is responsible for enumerating the transitive set of files in
|
||||
* the directory and invoking this method once for each file.)
|
||||
* 3. If the path was in the manifest as ADDED or MODIFIED, or was
|
||||
* not in the manifest at all (implying it is a normal file), then return
|
||||
* a warning as Hg does: "<path> already tracked!".
|
||||
* 4. If the path was in the manifest as REMOVED, then it will be removed
|
||||
* from the manifest (transitioning it back to a normal file). However,
|
||||
* if its TreeEntry differs at all from its entry in the parent snapshot,
|
||||
* then it will be updated in the manifest as MODIFIED.
|
||||
* 5. Otherwise, the file must be in the manifest as NOT_TRACKED or IGNORED.
|
||||
* In either case, it will be updated in the manifest as ADDED.
|
||||
*/
|
||||
(*userChanges)[pathToFile.copy()] = code;
|
||||
/**
|
||||
* We need to delete the file from the working copy if either of the following
|
||||
* hold (note that it is a precondition that the file exists):
|
||||
* 1. The file is not materialized in the overlay, so it is unmodified.
|
||||
* 2. The file is in the overlay, but matches what is in the manifest.
|
||||
*/
|
||||
bool shouldFileBeDeletedByHgRemove(
|
||||
RelativePathPiece file,
|
||||
std::shared_ptr<fusell::DirInode> parent,
|
||||
const TreeEntry* treeEntry,
|
||||
ObjectStore& objectStore) {
|
||||
auto treeInode = std::dynamic_pointer_cast<TreeInode>(parent);
|
||||
if (treeInode == nullptr) {
|
||||
// The parent directory for the file is not in the overlay, so the file must
|
||||
// not have been modified. As such, `hg remove` should result in deleting
|
||||
// the file.
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO(mbolin): Make sure that all code paths that modify userChanges_
|
||||
// perform a save() like this.
|
||||
persistence_->save(*userChanges);
|
||||
auto name = file.basename();
|
||||
auto dir = treeInode->getContents().rlock();
|
||||
auto& entries = dir->entries;
|
||||
for (auto& entry : entries) {
|
||||
if (entry.first == name) {
|
||||
if (hasMatchingAttributes(
|
||||
treeEntry, entry.second.get(), objectStore, *treeInode, *dir)) {
|
||||
return true;
|
||||
} else {
|
||||
throw std::runtime_error(folly::sformat(
|
||||
"not removing {}: file is modified (use -f to force removal)",
|
||||
file.stringPiece()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have reached this point, then the file has already been removed. Note
|
||||
// that this line of code should be unreachable given the preconditions of
|
||||
// this function, but there could be a race condition where the file is
|
||||
// deleted after this function is entered and before we reach this line of
|
||||
// code, so we return false here just to be safe.
|
||||
return false;
|
||||
}
|
||||
|
||||
void Dirstate::remove(RelativePathPiece path, bool force) {
|
||||
/*
|
||||
* Analogous to `hg rm <path>`. Note that this can have one of several
|
||||
* possible outcomes:
|
||||
* 1. If the path does not exist in the working copy or the manifest, return
|
||||
* an error.
|
||||
* 2. If the path refers to a directory, return an error. (Currently, the
|
||||
* caller is responsible for enumerating the transitive set of files in
|
||||
* the directory and invoking this method once for each file.)
|
||||
* 3. If the path is in the manifest but not in userDirectives, then it should
|
||||
* be marked as REMOVED, but there are several cases to consider:
|
||||
* a. It has already been removed from the working copy. If the user ran
|
||||
* `hg status` right now, the file would be reported as MISSING at this
|
||||
* point. Regardless, it should now be set to REMOVED in userDirectives.
|
||||
* b. It exists in the working copy and matches what is in the manifest.
|
||||
* In this case, it should be set to REMOVED in userDirectives and
|
||||
* removed from the working copy.
|
||||
* c. It has local changes in the working copy. In this case, nothing
|
||||
* should be modified and an error should be returned:
|
||||
* "not removing: file is modified (use -f to force removal)".
|
||||
* 4. If the path is in userDirectives as REMOVED, then this should be a noop.
|
||||
* In particular, even if the user has performed the following sequence:
|
||||
* $ hg rm a-file.txt # deletes a-file.txt
|
||||
* $ echo random-stuff > a-file.txt
|
||||
* $ hg rm a-file.txt # leaves a-file.txt alone
|
||||
* The second call to `hg rm` should not delete a-file.txt even though
|
||||
* the first one did. It should not raise an error that the contents have
|
||||
* changed, either.
|
||||
* 5. If the path is in userChanges as ADD, then there are two possibilities:
|
||||
* a. If the file exists, then no action is taken and an error should be
|
||||
* returned:
|
||||
* "not removing: file has been marked for add "
|
||||
* "(use 'hg forget' to undo add)".
|
||||
* b. If the file does not exist, then technically, it is MISSING rather
|
||||
* than ADDED at this point. Regardless, now its entry should be removed
|
||||
* from userDirectives.
|
||||
*/
|
||||
// TODO(mbolin): Verify that path corresponds to a regular file or symlink in
|
||||
// either the manifest or the working copy.
|
||||
|
||||
// We look up the InodeBase and TreeEntry for `path` before acquiring the
|
||||
// write lock for userDirectives_ because these lookups could be slow, so we
|
||||
// prefer not to do them while holding the lock.
|
||||
std::shared_ptr<fusell::DirInode> parent;
|
||||
try {
|
||||
parent = edenMount_->getMountPoint()->getDirInodeForPath(path.dirname());
|
||||
} catch (const std::system_error& e) {
|
||||
auto value = e.code().value();
|
||||
if (value == ENOENT || value == ENOTDIR) {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<fusell::InodeBase> inode;
|
||||
if (parent != nullptr) {
|
||||
try {
|
||||
inode = parent->getChildByName(path.basename()).get();
|
||||
} catch (const std::system_error& e) {
|
||||
if (e.code().value() != ENOENT) {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto entry = getEntryForFile(
|
||||
path, edenMount_->getRootTree().get(), edenMount_->getObjectStore());
|
||||
|
||||
auto shouldDelete = false;
|
||||
{
|
||||
auto userDirectives = userDirectives_.wlock();
|
||||
auto result = userDirectives->find(path.copy());
|
||||
if (result == userDirectives->end()) {
|
||||
// When there is no entry for the file in userChanges, we find the
|
||||
// corresponding TreeEntry in the manifest and compare it to its Entry in
|
||||
// the Overlay, if it exists.
|
||||
if (entry == nullptr) {
|
||||
throw std::runtime_error(folly::sformat(
|
||||
"not removing {}: file is untracked", path.stringPiece()));
|
||||
}
|
||||
|
||||
if (inode != nullptr) {
|
||||
if (force) {
|
||||
shouldDelete = true;
|
||||
} else {
|
||||
// Note that shouldFileBeDeletedByHgRemove() may throw an exception if
|
||||
// the file has been modified, so we must perform this check before
|
||||
// updating userDirectives.
|
||||
shouldDelete = shouldFileBeDeletedByHgRemove(
|
||||
path, parent, entry.get(), *edenMount_->getObjectStore());
|
||||
}
|
||||
}
|
||||
(*userDirectives)[path.copy()] = HgUserStatusDirective::REMOVE;
|
||||
persistence_->save(*userDirectives);
|
||||
} else {
|
||||
switch (result->second) {
|
||||
case HgUserStatusDirective::REMOVE:
|
||||
// No-op: already removed.
|
||||
break;
|
||||
case HgUserStatusDirective::ADD:
|
||||
if (inode != nullptr) {
|
||||
throw std::runtime_error(folly::sformat(
|
||||
"not removing {}: file has been marked for add "
|
||||
"(use 'hg forget' to undo add)",
|
||||
path.stringPiece()));
|
||||
} else {
|
||||
userDirectives->erase(path.copy());
|
||||
persistence_->save(*userDirectives);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDelete) {
|
||||
auto dispatcher = edenMount_->getMountPoint()->getDispatcher();
|
||||
try {
|
||||
dispatcher->unlink(parent->getNodeId(), path.basename()).get();
|
||||
} catch (const std::system_error& e) {
|
||||
// If the file has already been deleted, then mission accomplished.
|
||||
if (e.code().value() != ENOENT) {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string kStatusCodeCharClean = "C";
|
||||
const std::string kStatusCodeCharModified = "M";
|
||||
const std::string kStatusCodeCharAdded = "A";
|
||||
const std::string kStatusCodeCharRemoved = "R";
|
||||
const std::string kStatusCodeCharMissing = "!";
|
||||
const std::string kStatusCodeCharNotTracked = "?";
|
||||
const std::string kStatusCodeCharIgnored = "I";
|
||||
|
||||
const std::string& HgStatusCode_toString(HgStatusCode code) {
|
||||
switch (code) {
|
||||
case HgStatusCode::CLEAN:
|
||||
return kStatusCodeCharClean;
|
||||
case HgStatusCode::MODIFIED:
|
||||
return kStatusCodeCharModified;
|
||||
case HgStatusCode::ADDED:
|
||||
return kStatusCodeCharAdded;
|
||||
case HgStatusCode::REMOVED:
|
||||
return kStatusCodeCharRemoved;
|
||||
case HgStatusCode::MISSING:
|
||||
return kStatusCodeCharMissing;
|
||||
case HgStatusCode::NOT_TRACKED:
|
||||
return kStatusCodeCharNotTracked;
|
||||
case HgStatusCode::IGNORED:
|
||||
return kStatusCodeCharIgnored;
|
||||
}
|
||||
throw std::runtime_error(folly::to<std::string>(
|
||||
"Unrecognized HgStatusCode: ",
|
||||
static_cast<typename std::underlying_type<HgStatusCode>::type>(code)));
|
||||
}
|
||||
|
||||
HgStatusCode HgStatus::statusForPath(RelativePath path) const {
|
||||
@ -237,5 +525,10 @@ HgStatusCode HgStatus::statusForPath(RelativePath path) const {
|
||||
return HgStatusCode::CLEAN;
|
||||
}
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const HgStatus& status) {
|
||||
os << status.toString();
|
||||
return os;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,26 +23,46 @@ namespace eden {
|
||||
|
||||
class TreeInode;
|
||||
|
||||
enum class HgStatusCode {
|
||||
// I think nothing in Dirstate.userChanges_ should ever have this state?
|
||||
// If so, it probably makes sense to remove it from the enum.
|
||||
CLEAN = 0,
|
||||
/**
|
||||
* Type of change to the manifest that the user has specified for a particular
|
||||
* file that will apply on the next commit.
|
||||
*/
|
||||
enum class HgUserStatusDirective {
|
||||
ADD,
|
||||
REMOVE,
|
||||
};
|
||||
|
||||
MODIFIED = 1,
|
||||
ADDED = 2,
|
||||
/**
|
||||
*
|
||||
* Mercurial status code for a file. This is a function of:
|
||||
* 1. Whether there is a HgUserStatusDirective for the file.
|
||||
* 2. Whether the file exists on disk.
|
||||
* 3. Whether the file is already in the repo.
|
||||
* 4. Whether the file is matched by a pattern in .hgignore.
|
||||
*/
|
||||
enum class HgStatusCode {
|
||||
// PLEASE DO NOT ALPHA-SORT! We prefer CLEAN to correspond to 0, so these are
|
||||
// not alphabetically sorted. They are roughly ordered by expected frequency
|
||||
// of use.
|
||||
CLEAN,
|
||||
|
||||
MODIFIED,
|
||||
ADDED,
|
||||
|
||||
/** Indicates file has been marked for removal by the user. */
|
||||
REMOVED = 3,
|
||||
REMOVED,
|
||||
|
||||
/**
|
||||
* Indicates file is tracked by the repo, is not on disk, but has not been
|
||||
* marked for removal by the user.
|
||||
*/
|
||||
MISSING = 4,
|
||||
NOT_TRACKED = 5,
|
||||
IGNORED = 6,
|
||||
MISSING,
|
||||
NOT_TRACKED,
|
||||
IGNORED,
|
||||
};
|
||||
|
||||
const std::string& HgStatusCode_toString(HgStatusCode code);
|
||||
|
||||
class HgStatus {
|
||||
public:
|
||||
explicit HgStatus(std::unordered_map<RelativePath, HgStatusCode>&& statuses)
|
||||
@ -62,15 +82,24 @@ class HgStatus {
|
||||
return statuses_ == other.statuses_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns something akin to what you would see when running `hg status`.
|
||||
* This is intended for debugging purposes: do not rely on the format of the
|
||||
* return value.
|
||||
*/
|
||||
std::string toString() const;
|
||||
|
||||
private:
|
||||
std::unordered_map<RelativePath, HgStatusCode> statuses_;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const HgStatus& status);
|
||||
|
||||
class DirstatePersistence {
|
||||
public:
|
||||
virtual ~DirstatePersistence() {}
|
||||
virtual void save(
|
||||
std::unordered_map<RelativePath, HgStatusCode>& userChanges) = 0;
|
||||
virtual void save(std::unordered_map<RelativePath, HgUserStatusDirective>&
|
||||
userDirectives) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -111,25 +140,24 @@ class Dirstate {
|
||||
*/
|
||||
void add(RelativePathPiece path);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Sets the entry in the userChanges_ map, ensuring that the appropriate
|
||||
* invariants are maintained. Assuming this modifies the userChanges_ map, it
|
||||
* must also save the changes via the DirstatePersistence abstraction.
|
||||
* Analogous to `hg rm <path>` where `<path>` is an ordinary file or symlink.
|
||||
*/
|
||||
void applyUserStatusChange_(RelativePathPiece file, HgStatusCode code);
|
||||
void remove(RelativePathPiece path, bool force);
|
||||
|
||||
private:
|
||||
void computeDelta(
|
||||
const Tree& original,
|
||||
const Tree* original,
|
||||
TreeInode& current,
|
||||
DirectoryDelta& delta) const;
|
||||
|
||||
/**
|
||||
* Manifest of files in the working copy whose status is not CLEAN. These are
|
||||
* also referred to as "nonnormal" files.
|
||||
* TODO(mbolin): Consider StringKeyedMap instead of unordered_map.
|
||||
*/
|
||||
folly::Synchronized<std::unordered_map<RelativePath, HgStatusCode>>
|
||||
userChanges_;
|
||||
folly::Synchronized<std::unordered_map<RelativePath, HgUserStatusDirective>>
|
||||
userDirectives_;
|
||||
std::shared_ptr<EdenMount> edenMount_;
|
||||
std::unique_ptr<DirstatePersistence> persistence_;
|
||||
};
|
||||
|
@ -14,10 +14,33 @@
|
||||
|
||||
using namespace facebook::eden;
|
||||
|
||||
TEST(HgStatus, toString) {
|
||||
std::unordered_map<RelativePath, HgStatusCode> statuses({{
|
||||
{RelativePath("clean.txt"), HgStatusCode::CLEAN},
|
||||
{RelativePath("modified.txt"), HgStatusCode::MODIFIED},
|
||||
{RelativePath("added.txt"), HgStatusCode::ADDED},
|
||||
{RelativePath("removed.txt"), HgStatusCode::REMOVED},
|
||||
{RelativePath("missing.txt"), HgStatusCode::MISSING},
|
||||
{RelativePath("not_tracked.txt"), HgStatusCode::NOT_TRACKED},
|
||||
{RelativePath("ignored.txt"), HgStatusCode::IGNORED},
|
||||
}});
|
||||
HgStatus hgStatus(std::move(statuses));
|
||||
EXPECT_EQ(
|
||||
"A added.txt\n"
|
||||
"C clean.txt\n"
|
||||
"I ignored.txt\n"
|
||||
"! missing.txt\n"
|
||||
"M modified.txt\n"
|
||||
"? not_tracked.txt\n"
|
||||
"R removed.txt\n",
|
||||
hgStatus.toString());
|
||||
}
|
||||
|
||||
class FakeDirstatePeristence : public DirstatePersistence {
|
||||
public:
|
||||
virtual ~FakeDirstatePeristence() {}
|
||||
void save(std::unordered_map<RelativePath, HgStatusCode>&) override {}
|
||||
void save(std::unordered_map<RelativePath, HgUserStatusDirective>&) override {
|
||||
}
|
||||
};
|
||||
|
||||
void verifyExpectedDirstate(
|
||||
@ -106,3 +129,152 @@ TEST(Dirstate, createDirstateWithTouchedFile) {
|
||||
// way.
|
||||
verifyEmptyDirstate(dirstate);
|
||||
}
|
||||
|
||||
TEST(Dirstate, createDirstateWithFileAndThenHgRemoveIt) {
|
||||
TestMountBuilder builder;
|
||||
builder.addFile({"hello.txt", "some contents"});
|
||||
auto testMount = builder.build();
|
||||
|
||||
auto persistence = std::make_unique<FakeDirstatePeristence>();
|
||||
Dirstate dirstate(testMount->getEdenMount(), std::move(persistence));
|
||||
dirstate.remove(RelativePathPiece("hello.txt"), /* force */ false);
|
||||
EXPECT_FALSE(testMount->hasFileAt("hello.txt"));
|
||||
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::REMOVED}});
|
||||
}
|
||||
|
||||
TEST(Dirstate, createDirstateWithFileRemoveItAndThenHgRemoveIt) {
|
||||
TestMountBuilder builder;
|
||||
builder.addFile({"hello.txt", "some contents"});
|
||||
auto testMount = builder.build();
|
||||
|
||||
auto persistence = std::make_unique<FakeDirstatePeristence>();
|
||||
Dirstate dirstate(testMount->getEdenMount(), std::move(persistence));
|
||||
testMount->deleteFile("hello.txt");
|
||||
dirstate.remove(RelativePathPiece("hello.txt"), /* force */ false);
|
||||
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::REMOVED}});
|
||||
}
|
||||
|
||||
TEST(Dirstate, createDirstateWithFileTouchItAndThenHgRemoveIt) {
|
||||
TestMountBuilder builder;
|
||||
builder.addFile({"hello.txt", "original contents"});
|
||||
auto testMount = builder.build();
|
||||
|
||||
auto persistence = std::make_unique<FakeDirstatePeristence>();
|
||||
Dirstate dirstate(testMount->getEdenMount(), std::move(persistence));
|
||||
testMount->overwriteFile("hello.txt", "some other contents");
|
||||
|
||||
try {
|
||||
dirstate.remove(RelativePathPiece("hello.txt"), /* force */ false);
|
||||
FAIL() << "Should error when trying to remove a modified file.";
|
||||
} catch (const std::runtime_error& e) {
|
||||
EXPECT_STREQ(
|
||||
"not removing hello.txt: file is modified (use -f to force removal)",
|
||||
e.what());
|
||||
}
|
||||
|
||||
testMount->overwriteFile("hello.txt", "original contents");
|
||||
dirstate.remove(RelativePathPiece("hello.txt"), /* force */ false);
|
||||
EXPECT_FALSE(testMount->hasFileAt("hello.txt"));
|
||||
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::REMOVED}});
|
||||
}
|
||||
|
||||
TEST(Dirstate, createDirstateWithFileModifyItAndThenHgForceRemoveIt) {
|
||||
TestMountBuilder builder;
|
||||
builder.addFile({"hello.txt", "original contents"});
|
||||
auto testMount = builder.build();
|
||||
|
||||
auto persistence = std::make_unique<FakeDirstatePeristence>();
|
||||
Dirstate dirstate(testMount->getEdenMount(), std::move(persistence));
|
||||
testMount->overwriteFile("hello.txt", "some other contents");
|
||||
|
||||
dirstate.remove(RelativePathPiece("hello.txt"), /* force */ true);
|
||||
EXPECT_FALSE(testMount->hasFileAt("hello.txt"));
|
||||
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::REMOVED}});
|
||||
}
|
||||
|
||||
TEST(Dirstate, ensureSubsequentCallsToHgRemoveHaveNoEffect) {
|
||||
TestMountBuilder builder;
|
||||
builder.addFile({"hello.txt", "original contents"});
|
||||
auto testMount = builder.build();
|
||||
|
||||
auto persistence = std::make_unique<FakeDirstatePeristence>();
|
||||
Dirstate dirstate(testMount->getEdenMount(), std::move(persistence));
|
||||
|
||||
dirstate.remove(RelativePathPiece("hello.txt"), /* force */ false);
|
||||
EXPECT_FALSE(testMount->hasFileAt("hello.txt"));
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::REMOVED}});
|
||||
|
||||
// Calling `hg remove` again should have no effect and not throw any errors.
|
||||
dirstate.remove(RelativePathPiece("hello.txt"), /* force */ false);
|
||||
EXPECT_FALSE(testMount->hasFileAt("hello.txt"));
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::REMOVED}});
|
||||
|
||||
// Even if we restore the file, it should still show up as removed in
|
||||
// `hg status`.
|
||||
testMount->addFile("hello.txt", "original contents");
|
||||
EXPECT_TRUE(testMount->hasFileAt("hello.txt"));
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::REMOVED}});
|
||||
|
||||
// Calling `hg remove` again should have no effect and not throw any errors.
|
||||
dirstate.remove(RelativePathPiece("hello.txt"), /* force */ false);
|
||||
EXPECT_TRUE(testMount->hasFileAt("hello.txt"));
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::REMOVED}});
|
||||
}
|
||||
|
||||
TEST(Dirstate, createDirstateHgAddFileRemoveItThenHgRemoveIt) {
|
||||
TestMountBuilder builder;
|
||||
auto testMount = builder.build();
|
||||
|
||||
auto persistence = std::make_unique<FakeDirstatePeristence>();
|
||||
Dirstate dirstate(testMount->getEdenMount(), std::move(persistence));
|
||||
|
||||
testMount->addFile("hello.txt", "I will be added.");
|
||||
dirstate.add(RelativePathPiece("hello.txt"));
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::ADDED}});
|
||||
|
||||
testMount->deleteFile("hello.txt");
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::MISSING}});
|
||||
|
||||
dirstate.remove(RelativePathPiece("hello.txt"), /* force */ false);
|
||||
verifyEmptyDirstate(dirstate);
|
||||
}
|
||||
|
||||
TEST(Dirstate, createDirstateHgAddFileThenHgRemoveIt) {
|
||||
TestMountBuilder builder;
|
||||
auto testMount = builder.build();
|
||||
|
||||
auto persistence = std::make_unique<FakeDirstatePeristence>();
|
||||
Dirstate dirstate(testMount->getEdenMount(), std::move(persistence));
|
||||
|
||||
testMount->addFile("hello.txt", "I will be added.");
|
||||
dirstate.add(RelativePathPiece("hello.txt"));
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::ADDED}});
|
||||
|
||||
try {
|
||||
dirstate.remove(RelativePathPiece("hello.txt"), /* force */ false);
|
||||
FAIL() << "Should error when trying to remove a file scheduled for add.";
|
||||
} catch (const std::runtime_error& e) {
|
||||
EXPECT_STREQ(
|
||||
"not removing hello.txt: file has been marked for add "
|
||||
"(use 'hg forget' to undo add)",
|
||||
e.what());
|
||||
}
|
||||
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::ADDED}});
|
||||
}
|
||||
|
||||
TEST(Dirstate, createDirstateWithFileAndThenDeleteItWithoutCallingHgRemove) {
|
||||
TestMountBuilder builder;
|
||||
builder.addFile({"hello.txt", "some contents"});
|
||||
auto testMount = builder.build();
|
||||
|
||||
auto persistence = std::make_unique<FakeDirstatePeristence>();
|
||||
Dirstate dirstate(testMount->getEdenMount(), std::move(persistence));
|
||||
testMount->deleteFile("hello.txt");
|
||||
|
||||
verifyExpectedDirstate(dirstate, {{"hello.txt", HgStatusCode::MISSING}});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user