sapling/eden/fs/inodes/Dirstate.cpp
Michael Bolin ac5b213e92 Include the dirstate tuples and copymap when backing up the dirstate.
Summary:
Previously, the `savebackup()` and `restorebackup()` methods in `eden_dirstate`
only retained the parent commit hashes. With this change, now the dirstate tuples
and entries in the copymap for the dirstate are also included as part of the saved
state.

Failing to restore all of the state caused issues when doing things like aborting
an `hg split`, as observed by one of our users. Although this fix works, we ultimately
plan to move the responsibility for persisting dirstate data out of Eden and into the
Hg extension. Then the data will live in `.hg/dirstate` like it would for the default
dirstate implementation.

Reviewed By: simpkins

Differential Revision: D6145420

fbshipit-source-id: baa077dee73847a47cc171cd980cdd272b3a3a99
2017-10-25 22:36:06 -07:00

473 lines
15 KiB
C++

/*
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#include "Dirstate.h"
#include <folly/Format.h>
#include <folly/MapUtil.h>
#include <folly/Range.h>
#include <folly/Unit.h>
#include <folly/experimental/StringKeyedUnorderedMap.h>
#include <folly/experimental/logging/xlog.h>
#include "eden/fs/config/ClientConfig.h"
#include "eden/fs/inodes/EdenMount.h"
#include "eden/fs/inodes/FileInode.h"
#include "eden/fs/inodes/InodeBase.h"
#include "eden/fs/inodes/InodeDiffCallback.h"
#include "eden/fs/inodes/Overlay.h"
#include "eden/fs/inodes/TreeInode.h"
#include "eden/fs/store/ObjectStore.h"
using facebook::eden::hgdirstate::DirstateMergeState;
using facebook::eden::hgdirstate::DirstateNonnormalFileStatus;
using facebook::eden::hgdirstate::DirstateTuple;
using folly::Future;
using folly::StringKeyedUnorderedMap;
using folly::StringPiece;
using folly::Unit;
using folly::makeFuture;
using std::string;
namespace facebook {
namespace eden {
namespace {
class ThriftStatusCallback : public InodeDiffCallback {
public:
explicit ThriftStatusCallback(
const folly::StringKeyedUnorderedMap<DirstateTuple>& hgDirstateTuples)
: data_{folly::in_place, hgDirstateTuples} {}
void ignoredFile(RelativePathPiece path) override {
processChangedFile(
path,
DirstateNonnormalFileStatus::MarkedForAddition,
StatusCode::ADDED,
StatusCode::IGNORED);
}
void untrackedFile(RelativePathPiece path) override {
auto data = data_.wlock();
auto dirstateTuple =
folly::get_ptr(data->hgDirstateTuples, path.stringPiece());
auto statusCode = StatusCode::NOT_TRACKED;
if (dirstateTuple != nullptr) {
auto nnFileStatus = dirstateTuple->get_status();
if (nnFileStatus == DirstateNonnormalFileStatus::MarkedForAddition) {
statusCode = StatusCode::ADDED;
} else if (nnFileStatus == DirstateNonnormalFileStatus::Normal) {
auto mergeState = dirstateTuple->get_mergeState();
// TODO(mbolin): Also need to set to ADDED if path is in the copymap.
if (mergeState == DirstateMergeState::OtherParent) {
statusCode = StatusCode::ADDED;
}
}
}
data->status.emplace(path.stringPiece().str(), statusCode);
}
void removedFile(
RelativePathPiece path,
const TreeEntry& /* sourceControlEntry */) override {
processChangedFile(
path,
DirstateNonnormalFileStatus::MarkedForRemoval,
StatusCode::REMOVED,
StatusCode::MISSING);
}
void modifiedFile(
RelativePathPiece path,
const TreeEntry& /* sourceControlEntry */) override {
processChangedFile(
path,
DirstateNonnormalFileStatus::MarkedForRemoval,
StatusCode::REMOVED,
StatusCode::MODIFIED);
}
void diffError(RelativePathPiece path, const folly::exception_wrapper& ew)
override {
// TODO: It would be nice to have a mechanism to return error info as part
// of the thrift result.
XLOG(WARNING) << "error computing status data for " << path << ": "
<< folly::exceptionStr(ew);
}
/**
* Extract the ThriftHgStatus object from this callback.
*
* This method should be called no more than once, as this destructively
* moves the results out of the callback. It should only be invoked after
* the diff operation has completed.
*/
ThriftHgStatus extractStatus() {
ThriftHgStatus status;
{
auto data = data_.wlock();
status.entries.swap(data->status);
// Process any remaining user directives that weren't seen during the diff
// walk.
//
// TODO: I believe this isn't really right, but it should be good enough
// for initial testing.
//
// We really need to also check if these entries exist currently on
// disk and in source control. For files that are removed but exist on
// disk we also need to check their ignored status.
//
// - UserStatusDirective::Add, exists on disk, and in source control:
// -> skip
// - UserStatusDirective::Add, exists on disk, not in SCM, but ignored:
// -> ADDED
// - UserStatusDirective::Add, not on disk or in source control:
// -> MISSING
// - UserStatusDirective::Remove, exists on disk, and in source control:
// -> REMOVED
// - UserStatusDirective::Remove, exists on disk, not in SCM, but ignored:
// -> skip
// - UserStatusDirective::Remove, not on disk, not in source control:
// -> skip
for (const auto& entry : data->hgDirstateTuples) {
switch (entry.second.get_status()) {
case DirstateNonnormalFileStatus::NotTracked:
case DirstateNonnormalFileStatus::Normal:
continue;
case DirstateNonnormalFileStatus::NeedsMerging:
if (entry.second.get_mergeState() ==
DirstateMergeState::NotApplicable) {
XLOG(ERR) << "Unexpected Nonnormal file " << entry.first
<< " has a merge state of NotApplicable while its "
<< "NonnormalFileStatus is NeedsMerging.";
} else {
status.entries.emplace(entry.first.str(), StatusCode::MODIFIED);
}
break;
case DirstateNonnormalFileStatus::MarkedForAddition:
status.entries.emplace(entry.first.str(), StatusCode::MISSING);
break;
case DirstateNonnormalFileStatus::MarkedForRemoval:
status.entries.emplace(entry.first.str(), StatusCode::REMOVED);
break;
default:
throw std::runtime_error(folly::to<string>(
"Unexpected file status: ", entry.second.get_status()));
}
}
}
return status;
}
private:
/**
* The implementation used for the ignoredFile(), untrackedFile(),
* removedFile(), and modifiedFile().
*
* The logic is:
* - If the file is present in hgDirstateTuples as userDirectiveStatus,
* then remove it from hgDirstateTuples and report the status as
* userDirectiveStatus.
* - Otherwise, report the status as defaultStatus
*/
void processChangedFile(
RelativePathPiece path,
DirstateNonnormalFileStatus userDirectiveType,
StatusCode userDirectiveStatus,
StatusCode defaultStatus) {
auto data = data_.wlock();
auto iter = data->hgDirstateTuples.find(path.stringPiece());
StatusCode newStatus = defaultStatus;
if (iter != data->hgDirstateTuples.end() &&
iter->second.get_status() == userDirectiveType) {
newStatus = userDirectiveStatus;
data->hgDirstateTuples.erase(iter);
}
data->status.emplace(path.stringPiece().str(), newStatus);
XLOG(INFO) << "ThriftStatusCallback::processChangedFile(" << path << ") -> "
<< _StatusCode_VALUES_TO_NAMES.at(newStatus);
}
struct Data {
explicit Data(const folly::StringKeyedUnorderedMap<DirstateTuple>& ud)
: hgDirstateTuples(ud) {}
std::map<std::string, StatusCode> status;
StringKeyedUnorderedMap<DirstateTuple> hgDirstateTuples;
};
folly::Synchronized<Data> data_;
};
} // unnamed namespace
Dirstate::Dirstate(EdenMount* mount)
: mount_(mount),
persistence_(mount->getConfig()->getDirstateStoragePath()) {
auto loadedData = persistence_.load();
}
Dirstate::~Dirstate() {}
ThriftHgStatus Dirstate::getStatus(bool listIgnored) const {
ThriftStatusCallback callback(data_.rlock()->hgDirstateTuples);
mount_->diff(&callback, listIgnored).get();
return callback.extractStatus();
}
namespace {
static bool isMagicPath(RelativePathPiece path) {
// If any component of the path name is .eden, then this path is a magic
// path that we won't allow to be checked in or show up in the dirstate.
for (auto c : path.components()) {
if (c.stringPiece() == kDotEdenName) {
return true;
}
}
return false;
}
} // namespace
Future<Unit> Dirstate::onSnapshotChanged(const Tree* rootTree) {
XLOG(INFO) << "Dirstate::onSnapshotChanged(" << rootTree->getHash() << ")";
{
auto data = data_.wlock();
bool madeChanges = false;
// For now, we assume that when the snapshot changes, we should
// remove all dirstate tuples with a merge state of NotApplicable.
auto iter = data->hgDirstateTuples.begin();
while (iter != data->hgDirstateTuples.end()) {
// If we need to erase this element, it will erase iterators pointing to
// it, but other iterators will be unaffected.
auto current = iter;
++iter;
if (current->second.get_mergeState() ==
DirstateMergeState::NotApplicable &&
current->second.get_status() == DirstateNonnormalFileStatus::Normal) {
XLOG(INFO)
<< "Removing " << current->first.str()
<< " from hgDirstateTuples via onSnapshotChanged("
<< rootTree->getHash()
<< ") because it has merge state NotApplicable and status Normal.";
data->hgDirstateTuples.erase(current);
madeChanges = true;
}
}
if (madeChanges) {
persistence_.save(*data);
}
}
return makeFuture();
}
void Dirstate::clear() {
bool madeChanges = false;
auto data = data_.wlock();
if (!data->hgDirstateTuples.empty()) {
data->hgDirstateTuples.clear();
madeChanges = true;
}
if (!data->hgDestToSourceCopyMap.empty()) {
data->hgDestToSourceCopyMap.clear();
madeChanges = true;
}
if (madeChanges) {
persistence_.save(*data);
}
}
void Dirstate::createBackup(PathComponent backupName) {
AbsolutePath backupPath = createBackupPath(backupName);
{
auto data = data_.rlock();
DirstatePersistence::save(*data, backupPath);
}
}
void Dirstate::restoreBackup(PathComponent backupName) {
AbsolutePath backupPath = createBackupPath(backupName);
auto dirstateData = DirstatePersistence::load(backupPath);
{
auto data = data_.wlock();
folly::checkUnixError(rename(
backupPath.c_str(),
mount_->getConfig()->getDirstateStoragePath().c_str()));
std::swap(*data, dirstateData);
}
}
AbsolutePath Dirstate::createBackupPath(PathComponent backupName) {
return (mount_->getConfig()->getDirstateStoragePath().dirname()) +
RelativePath{"dirstate-backup-" + backupName.stringPiece().str()};
}
DirstateTuple Dirstate::hgGetDirstateTuple(const RelativePathPiece filename) {
{
auto data = data_.rlock();
auto& hgDirstateTuples = data->hgDirstateTuples;
auto* ptr = folly::get_ptr(hgDirstateTuples, filename.stringPiece());
if (ptr != nullptr) {
return *ptr;
}
}
if (filename == RelativePathPiece{".hgsub"} ||
filename == RelativePathPiece{".hgsubstate"}) {
// Currently, these are the only files that Hg appears to ask about that are
// not expected to be in the dirstate when the request is made. This is
// admittedly pretty sloppy, but since we don't seem to be planning to
// support subrepos in Eden, this seems to have the desired effect as it is
// ultimately reflected as a KeyError in the Hg extension (though it could
// be swallowing a real logical error in that case, as well).
throw std::out_of_range(folly::to<std::string>(
"No hgDirstateTuple for ",
filename.stringPiece(),
" because Eden acts as if this file does not exist."));
}
// If the filename is in the manifest, return it.
auto mode = isInManifestAsFile(filename);
if (mode.hasValue()) {
DirstateTuple tuple;
tuple.set_status(DirstateNonnormalFileStatus::Normal);
// Lower bits? Should be 644 not 100644.
tuple.set_mode(mode.value());
tuple.set_mergeState(DirstateMergeState::NotApplicable);
return tuple;
} else {
throw std::out_of_range(folly::to<std::string>(
"No hgDirstateTuple for ",
filename.stringPiece(),
" because there is no entry for it in the root Tree as a file."));
}
}
folly::Optional<mode_t> Dirstate::isInManifestAsFile(
const RelativePathPiece filename) const {
auto tree = mount_->getRootTree();
auto parentDirectory = filename.dirname();
auto objectStore = mount_->getObjectStore();
for (auto piece : parentDirectory.components()) {
auto entry = tree->getEntryPtr(piece);
if (entry != nullptr && entry->getFileType() == FileType::DIRECTORY) {
tree = objectStore->getTree(entry->getHash()).get();
} else {
return folly::none;
}
}
if (tree != nullptr) {
auto entry = tree->getEntryPtr(filename.basename());
if (entry != nullptr && entry->getFileType() != FileType::DIRECTORY) {
return entry->getMode();
}
}
return folly::none;
}
void Dirstate::hgSetDirstateTuple(
const RelativePathPiece filename,
const DirstateTuple* tuple) {
auto data = data_.wlock();
data->hgDirstateTuples[filename.stringPiece()] = *tuple;
persistence_.save(*data);
}
bool Dirstate::hgDeleteDirstateTuple(const RelativePathPiece filename) {
return data_.wlock()->hgDirstateTuples.erase(filename.stringPiece());
}
std::unordered_map<RelativePath, DirstateTuple> Dirstate::hgGetNonnormalFiles()
const {
std::unordered_map<RelativePath, DirstateTuple> out;
auto& hgDirstateTuples = data_.rlock()->hgDirstateTuples;
for (const auto& pair : hgDirstateTuples) {
out.emplace(RelativePath{pair.first}, pair.second);
}
return out;
}
void Dirstate::hgCopyMapPut(
const RelativePathPiece dest,
const RelativePathPiece source) {
auto data = data_.wlock();
if (source.empty()) {
data->hgDestToSourceCopyMap.erase(dest.stringPiece());
} else {
data->hgDestToSourceCopyMap.emplace(dest.stringPiece(), source.copy());
}
persistence_.save(*data);
}
RelativePath Dirstate::hgCopyMapGet(const RelativePathPiece dest) const {
auto& hgDestToSourceCopyMap = data_.rlock()->hgDestToSourceCopyMap;
return folly::get_or_throw(hgDestToSourceCopyMap, dest.stringPiece());
}
folly::StringKeyedUnorderedMap<RelativePath> Dirstate::hgCopyMapGetAll() const {
return data_.rlock()->hgDestToSourceCopyMap;
}
std::ostream& operator<<(
std::ostream& os,
const DirstateAddRemoveError& error) {
return os << error.errorMessage;
}
const char kStatusCodeCharClean = 'C';
const char kStatusCodeCharModified = 'M';
const char kStatusCodeCharAdded = 'A';
const char kStatusCodeCharRemoved = 'R';
const char kStatusCodeCharMissing = '!';
const char kStatusCodeCharNotTracked = '?';
const char kStatusCodeCharIgnored = 'I';
char hgStatusCodeChar(StatusCode code) {
switch (code) {
case StatusCode::CLEAN:
return kStatusCodeCharClean;
case StatusCode::MODIFIED:
return kStatusCodeCharModified;
case StatusCode::ADDED:
return kStatusCodeCharAdded;
case StatusCode::REMOVED:
return kStatusCodeCharRemoved;
case StatusCode::MISSING:
return kStatusCodeCharMissing;
case StatusCode::NOT_TRACKED:
return kStatusCodeCharNotTracked;
case StatusCode::IGNORED:
return kStatusCodeCharIgnored;
}
throw std::runtime_error(folly::to<std::string>(
"Unrecognized StatusCode: ",
static_cast<typename std::underlying_type<StatusCode>::type>(code)));
}
std::ostream& operator<<(std::ostream& os, const ThriftHgStatus& status) {
os << "{";
for (const auto& pair : status.get_entries()) {
os << hgStatusCodeChar(pair.second) << " " << pair.first << "; ";
}
os << "}";
return os;
}
} // namespace eden
} // namespace facebook