Change how the UNTRACKED_ADDED conflict and merges are handled.

Summary:
Previously, we used the Mercurial code `g` when faced with an `UNTRACKED_ADDED`
file conflict, but that was allowing merges to silently succeed that should not
have. This revision changes our logic to use the code `m` for merge, which
unearthed that we were not honoring the user's `update.check` setting properly.

Because we use `update.check=noconflict` internally at Facebook, we changed the
Eden integration tests to default to verifying Hg running with this setting. To
support it properly, we had to port this code from `update.py` in Mercurial to
our own `_determine_actions_for_conflicts()` function:

```
if updatecheck == 'noconflict':
    for f, (m, args, msg) in actionbyfile.iteritems():
        if m not in ('g', 'k', 'e', 'r', 'pr'):
            msg = _("conflicting changes")
            hint = _("commit or update --clean to discard changes")
            raise error.Abort(msg, hint=hint)
```

However, this introduced an interesting issue where the `checkOutRevision()`
Thrift call from Hg would update the `SNAPSHOT` file on the server, but
`.hg/dirstate` would not get updated with the new parents until the update
completed on the client. With the new call to `raise error.Abort` on the client,
we could get in a state where the `SNAPSHOT` file had the hash of the commit
assuming the update succeeded, but `.hg/dirstate` reflected the reality where it
failed.

To that end, we changed `checkOutRevision()` to take a new parameter,
`checkoutMode`, which can take on one of three values: `NORMAL`, `DRY_RUN`, and
`FORCE`. Now if the user tries to do an ordinary `hg update` with
`update.check=noconflict`, we first do a `DRY_RUN` and examine the potential
conflicts. Only if the conflicts should not block the update do we proceed with
a call to `checkOutRevision()` in `NORMAL` mode.

To make this work, we had to make a number of changes to `CheckoutAction`,
`CheckoutContext`, `EdenMount`, and `TreeInode` to keep track of the
`checkoutMode` and ensure that no changes are made to the working copy when a
`DRY_RUN` is in effect.

One minor issue (for which there is a `TODO`) is that a `DRY_RUN` will not
report any `DIRECTORY_NOT_EMPTY` conflicts that may exist. As `TreeInode` is
implemented today, it is a bit messy to report this type of conflict without
modifying the working copy along the way.

Finally, any `UNTRACKED_ADDED` conflict should cause an update to
abort to match the behavior in stock Mercurial if the user has the following
config setting:

```
[commands]
update.check = noconflict
```

Though the original name for this setting was:

```
[experimental]
updatecheck = noconflict
```

Although I am on Mercurial 4.4.1, the `update.check` setting does not seem to
take effect when I run the integration tests, but the `updatecheck` setting
does, so for now, I set both in `hg_extension_test_base.py` with a `TODO` to
remove `updatecheck` once I can get `update.check` to do its job.

Reviewed By: simpkins

Differential Revision: D6366007

fbshipit-source-id: bb3ecb1270e77d59d7d9e7baa36ada61971bbc49
This commit is contained in:
Michael Bolin 2017-11-29 21:38:12 -08:00 committed by Facebook Github Bot
parent b6a2b19b7c
commit 5e2afa735f
14 changed files with 242 additions and 85 deletions

View File

@ -238,7 +238,8 @@ class Config:
def checkout(self, path, snapshot_id):
'''Switch the active snapshot id for a given client'''
with self.get_thrift_client() as client:
client.checkOutRevision(path, snapshot_id)
client.checkOutRevision(path, snapshot_id,
eden_ttypes.CheckoutMode.NORMAL)
def add_repository(self, name, repo_type, source, with_buck=False):
# Check if repository already exists

View File

@ -283,8 +283,15 @@ Future<Unit> CheckoutAction::doAction() {
// All the data is ready and we're ready to go!
// Check for conflicts first.
if (hasConflict() && !ctx_->forceUpdate()) {
// hasConflict will have added the conflict information to ctx_
auto conflictWasAddedToCtx = hasConflict();
// Note that even if we know we are not going to apply the changes, we must
// still run hasConflict() first because we rely on its side-effects.
if (conflictWasAddedToCtx && !ctx_->forceUpdate()) {
// We only report conflicts for files, not directories. The only possible
// conflict that can occur here if this inode is a TreeInode is that the old
// source control state was for a file. There aren't really any other
// conflicts than this to report, even if we recurse. Anything inside this
// directory is basically just untracked (or possibly ignored) files.
return makeFuture();
}

View File

@ -22,8 +22,8 @@ namespace eden {
CheckoutContext::CheckoutContext(
folly::Synchronized<EdenMount::ParentInfo>::LockedPtr&& parentsLock,
bool force)
: force_{force}, parentsLock_(std::move(parentsLock)) {}
CheckoutMode checkoutMode)
: checkoutMode_{checkoutMode}, parentsLock_(std::move(parentsLock)) {}
CheckoutContext::~CheckoutContext() {}
@ -32,8 +32,11 @@ void CheckoutContext::start(RenameLock&& renameLock) {
}
vector<CheckoutConflict> CheckoutContext::finish(Hash newSnapshot) {
// Update the in-memory snapshot ID
parentsLock_->parents.setParents(newSnapshot);
// Only update the parents if it is not a dry run.
if (!isDryRun()) {
// Update the in-memory snapshot ID
parentsLock_->parents.setParents(newSnapshot);
}
// Release our locks.
// This would release automatically when the CheckoutContext is destroyed,

View File

@ -37,17 +37,17 @@ class CheckoutContext {
public:
CheckoutContext(
folly::Synchronized<EdenMount::ParentInfo>::LockedPtr&& parentsLock,
bool force);
CheckoutMode checkoutMode);
~CheckoutContext();
/**
* Returns true if the checkout operation should actually update the inodes,
* or false if it should do a dry run, looking for conflicts without actually
* updating the inode contents.
* Returns true if the checkout operation should do a dry run, looking for
* conflicts without actually updating the inode contents. If it returns
* false, it should actually update the inodes as part of the checkout.
*/
bool shouldApplyChanges() const {
bool isDryRun() const {
// TODO: make this configurable on checkout start
return true;
return checkoutMode_ == CheckoutMode::DRY_RUN;
}
/**
@ -59,10 +59,10 @@ class CheckoutContext {
* new contents, rather than just reporting and skipping files with
* conflicts.
*
* forceUpdate() can only return true when shouldApplyChanges() is also true.
* forceUpdate() can only return true when isDryRun() is false.
*/
bool forceUpdate() const {
return force_;
return checkoutMode_ == CheckoutMode::FORCE;
}
/**
@ -99,7 +99,7 @@ class CheckoutContext {
}
private:
bool const force_{false};
CheckoutMode checkoutMode_;
folly::Synchronized<EdenMount::ParentInfo>::LockedPtr parentsLock_;
RenameLock renameLock_;

View File

@ -392,13 +392,14 @@ FileInodePtr EdenMount::getFileInodeBlocking(RelativePathPiece path) const {
folly::Future<std::vector<CheckoutConflict>> EdenMount::checkout(
Hash snapshotHash,
bool force) {
CheckoutMode checkoutMode) {
// Hold the snapshot lock for the duration of the entire checkout operation.
//
// This prevents multiple checkout operations from running in parallel.
auto parentsLock = parentInfo_.wlock();
auto oldParents = parentsLock->parents;
auto ctx = std::make_shared<CheckoutContext>(std::move(parentsLock), force);
auto ctx =
std::make_shared<CheckoutContext>(std::move(parentsLock), checkoutMode);
XLOG(DBG1) << "starting checkout for " << this->getPath() << ": "
<< oldParents << " to " << snapshotHash;
@ -434,8 +435,15 @@ folly::Future<std::vector<CheckoutConflict>> EdenMount::checkout(
// Save the new snapshot hash
XLOG(DBG1) << "updating snapshot for " << this->getPath() << " from "
<< oldParents << " to " << snapshotHash;
this->config_->setParentCommits(snapshotHash);
auto conflicts = ctx->finish(snapshotHash);
if (ctx->isDryRun()) {
// This is a dry run, so all we need to do is tell the caller about
// the conflicts: we should not modify any files or add any entries to
// the journal.
return folly::makeFuture(conflicts);
}
this->config_->setParentCommits(snapshotHash);
// Write a journal entry
return journalDiffCallback

View File

@ -24,6 +24,7 @@
#include "eden/fs/inodes/InodePtrFwd.h"
#include "eden/fs/journal/JournalDelta.h"
#include "eden/fs/model/ParentCommits.h"
#include "eden/fs/service/gen-cpp2/eden_types.h"
#include "eden/fs/utils/PathFuncs.h"
namespace folly {
@ -257,7 +258,7 @@ class EdenMount {
*/
folly::Future<std::vector<CheckoutConflict>> checkout(
Hash snapshotHash,
bool force = false);
CheckoutMode checkoutMode = CheckoutMode::NORMAL);
/**
* Compute differences between the current commit and the working directory

View File

@ -2194,7 +2194,7 @@ unique_ptr<CheckoutAction> TreeInode::processCheckoutEntry(
// This is a new entry being added, that did not exist in the old tree
// and does not currently exist in the filesystem. Go ahead and add it
// now.
if (ctx->shouldApplyChanges()) {
if (!ctx->isDryRun()) {
auto newEntry =
make_unique<Entry>(newScmEntry->getMode(), newScmEntry->getHash());
contents.entries.emplace(newScmEntry->getName(), std::move(newEntry));
@ -2212,7 +2212,7 @@ unique_ptr<CheckoutAction> TreeInode::processCheckoutEntry(
ctx->addConflict(
ConflictType::REMOVED_MODIFIED, this, oldScmEntry->getName());
if (ctx->forceUpdate()) {
DCHECK(ctx->shouldApplyChanges());
DCHECK(!ctx->isDryRun());
auto newEntry =
make_unique<Entry>(newScmEntry->getMode(), newScmEntry->getHash());
contents.entries.emplace(newScmEntry->getName(), std::move(newEntry));
@ -2275,7 +2275,7 @@ unique_ptr<CheckoutAction> TreeInode::processCheckoutEntry(
}
// Bail out now if we aren't actually supposed to apply changes.
if (!ctx->shouldApplyChanges()) {
if (ctx->isDryRun()) {
return nullptr;
}
@ -2303,10 +2303,14 @@ Future<Unit> TreeInode::checkoutUpdateEntry(
std::shared_ptr<const Tree> oldTree,
std::shared_ptr<const Tree> newTree,
const folly::Optional<TreeEntry>& newScmEntry) {
CHECK(ctx->shouldApplyChanges());
auto treeInode = inode.asTreePtrOrNull();
if (!treeInode) {
// If the target of the update is not a directory, then we know we do not
// need to recurse into it, looking for more conflicts, so we can exit here.
if (ctx->isDryRun()) {
return makeFuture();
}
std::unique_ptr<InodeBase> deletedInode;
auto contents = contents_.wlock();
@ -2359,6 +2363,15 @@ Future<Unit> TreeInode::checkoutUpdateEntry(
return treeInode->checkout(ctx, std::move(oldTree), std::move(newTree));
}
if (ctx->isDryRun()) {
// TODO(mbolin): As it stands, if this is a dry run, we will not report a
// DIRECTORY_NOT_EMPTY conflict if it exists. We need to do further
// investigation to determine whether this is acceptible behavior.
// Currently, the Hg extension ignores DIRECTORY_NOT_EMPTY conflicts, but
// that may not be the right thing to do.
return makeFuture();
}
// We need to remove this directory (and possibly replace it with a file).
// First we have to recursively unlink everything inside the directory.
// Fortunately, calling checkout() with an empty destination tree does
@ -2411,6 +2424,12 @@ Future<Unit> TreeInode::checkoutUpdateEntry(
void TreeInode::saveOverlayPostCheckout(
CheckoutContext* ctx,
const Tree* tree) {
if (ctx->isDryRun()) {
// If this is a dry run, then we do not want to update the parents or make
// any sort of unnecessary writes to the overlay, so we bail out.
return;
}
bool isMaterialized;
bool stateChanged;
bool deleteSelf;

View File

@ -435,7 +435,7 @@ TEST(Checkout, modifyFile) {
void testModifyConflict(
folly::StringPiece path,
LoadBehavior loadType,
bool force,
CheckoutMode checkoutMode,
folly::StringPiece contents1,
int perms1,
folly::StringPiece currentContents,
@ -470,6 +470,7 @@ void testModifyConflict(
// Prepare the destination tree
auto builder2 = builder1.clone();
builder2.replaceFile(path, contents2, perms2);
builder2.replaceFile("a/b/dddd.c", "new dddd contents\n");
builder2.finalize(testMount.getBackingStore(), true);
auto commit2 = testMount.getBackingStore()->putCommit("b", builder2);
commit2->setReady();
@ -477,7 +478,7 @@ void testModifyConflict(
loadInodes(testMount, path, loadType, currentContents, currentPerms);
auto checkoutResult =
testMount.getEdenMount()->checkout(makeTestHash("b"), force);
testMount.getEdenMount()->checkout(makeTestHash("b"), checkoutMode);
ASSERT_TRUE(checkoutResult.isReady());
auto results = checkoutResult.get();
ASSERT_EQ(1, results.size());
@ -486,12 +487,16 @@ void testModifyConflict(
EXPECT_EQ(ConflictType::MODIFIED_MODIFIED, results[0].type);
auto postInode = testMount.getFileInode(path);
if (force) {
// Make sure the path is updated as expected
EXPECT_FILE_INODE(postInode, contents2, perms2);
} else {
// Make sure the path has not been changed
EXPECT_FILE_INODE(postInode, currentContents, currentPerms);
switch (checkoutMode) {
case CheckoutMode::FORCE:
// Make sure the path is updated as expected
EXPECT_FILE_INODE(postInode, contents2, perms2);
break;
case CheckoutMode::DRY_RUN:
case CheckoutMode::NORMAL:
// Make sure the path has not been changed
EXPECT_FILE_INODE(postInode, currentContents, currentPerms);
break;
}
// Unmount and remount the mount point, and verify the changes persisted
@ -499,22 +504,33 @@ void testModifyConflict(
postInode.reset();
testMount.remount();
postInode = testMount.getFileInode(path);
if (force) {
EXPECT_FILE_INODE(postInode, contents2, perms2);
} else {
EXPECT_FILE_INODE(postInode, currentContents, currentPerms);
auto ddddInode = testMount.getFileInode("a/b/dddd.c");
switch (checkoutMode) {
case CheckoutMode::FORCE:
EXPECT_FILE_INODE(postInode, contents2, perms2);
EXPECT_FILE_INODE(ddddInode, "new dddd contents\n", 0644);
break;
case CheckoutMode::DRY_RUN:
EXPECT_FILE_INODE(postInode, currentContents, currentPerms);
EXPECT_FILE_INODE(ddddInode, "this is dddd.c\n", 0644);
break;
case CheckoutMode::NORMAL:
EXPECT_FILE_INODE(postInode, currentContents, currentPerms);
EXPECT_FILE_INODE(ddddInode, "new dddd contents\n", 0644);
break;
}
}
void runModifyConflictTests(folly::StringPiece path) {
for (auto loadType : kAllLoadTypes) {
for (bool force : {true, false}) {
for (auto checkoutMode :
{CheckoutMode::NORMAL, CheckoutMode::DRY_RUN, CheckoutMode::FORCE}) {
SCOPED_TRACE(folly::to<string>(
"path ", path, " load type ", loadType, " force=", force));
"path ", path, " load type ", loadType, " force=", checkoutMode));
testModifyConflict(
path,
loadType,
force,
checkoutMode,
"orig file contents.txt",
0644,
"current file contents.txt",
@ -550,9 +566,8 @@ TEST(Checkout, modifyThenRevert) {
// Now perform a forced checkout to the current commit,
// which should discard our edits.
bool force = true;
auto checkoutResult =
testMount.getEdenMount()->checkout(originalCommit, force);
testMount.getEdenMount()->checkout(originalCommit, CheckoutMode::FORCE);
ASSERT_TRUE(checkoutResult.isReady());
// The checkout should report a/test.txt as a conflict
EXPECT_THAT(

View File

@ -146,16 +146,17 @@ void EdenServiceHandler::checkOutRevision(
std::vector<CheckoutConflict>& results,
std::unique_ptr<std::string> mountPoint,
std::unique_ptr<std::string> hash,
bool force) {
CheckoutMode checkoutMode) {
INSTRUMENT_THRIFT_CALL(
DBG1,
*mountPoint,
hashFromThrift(*hash).toString(),
folly::format("force={}", force ? "true" : "false"));
folly::get_default(
_CheckoutMode_VALUES_TO_NAMES, checkoutMode, "(unknown)"));
auto hashObj = hashFromThrift(*hash);
auto edenMount = server_->getMount(*mountPoint);
auto checkoutFuture = edenMount->checkout(hashObj, force);
auto checkoutFuture = edenMount->checkout(hashObj, checkoutMode);
results = checkoutFuture.get();
}

View File

@ -46,7 +46,7 @@ class EdenServiceHandler : virtual public StreamingEdenServiceSvIf,
std::vector<CheckoutConflict>& results,
std::unique_ptr<std::string> mountPoint,
std::unique_ptr<std::string> hash,
bool force) override;
CheckoutMode checkoutMode) override;
void resetParentCommits(
std::unique_ptr<std::string> mountPoint,

View File

@ -139,6 +139,29 @@ struct ScmStatus {
1: map<string, ScmFileStatus> entries
}
/** Option for use with checkOutRevision(). */
enum CheckoutMode {
/**
* Perform a "normal" checkout, analogous to `hg checkout` in Mercurial. Files
* in the working copy will be changed to reflect the destination snapshot,
* though files with conflicts will not be modified.
*/
NORMAL = 0,
/**
* Do not checkout: exercise the checkout logic to discover potential
* conflicts.
*/
DRY_RUN = 1,
/**
* Perform a "forced" checkout, analogous to `hg checkout --clean` in
* Mercurial. Conflicts between the working copy and destination snapshot will
* be forcibly ignored in favor of the state of the new snapshot.
*/
FORCE = 2,
}
enum ConflictType {
/**
* We failed to update this particular path due to an error
@ -285,31 +308,31 @@ service EdenService extends fb303.FacebookService {
void unmount(1: string mountPoint) throws (1: EdenError ex)
/**
* Check out the specified snapshot.
* Potentially check out the specified snapshot, reporting conflicts (and
* possibly errors), as appropriate.
*
* This updates the contents of the mount point so that they match the
* contents of the given snapshot.
*
* Returns a list of conflicts and errors that occurred when performing the
* checkout operation.
*
* If the force parameter is true, the working directory will be forcibly
* If the checkoutMode is FORCE, the working directory will be forcibly
* updated to the contents of the new snapshot, even if there were conflicts.
* Conflicts will still be reported in the return value, but the files will
* be updated to their new state. If the force parameter is false files with
* conflicts will be left unmodified. Files that are untracked in both the
* source and destination snapshots are always left unchanged, even if force
* is true.
* Conflicts will still be reported in the return value, but the files will be
* updated to their new state.
*
* On successful return from this function the mount point will point to the
* new commit, even if some paths had conflicts or errors. The caller is
* responsible for taking appropriate action to update these paths as desired
* after checkOutRevision() returns.
* If the checkoutMode is NORMAL, files with conflicts will be left
* unmodified. Files that are untracked in both the source and destination
* snapshots are always left unchanged, even if force is true.
*
* If the checkoutMode is DRY_RUN, then no files are modified in the working
* copy and the current snapshot does not change. However, potential conflicts
* are still reported in the return value.
*
* On successful return from this function (unless it is a DRY_RUN), the mount
* point will point to the new snapshot, even if some paths had conflicts or
* errors. The caller is responsible for taking appropriate action to update
* these paths as desired after checkOutRevision() returns.
*/
list<CheckoutConflict> checkOutRevision(
1: string mountPoint,
2: BinaryHash snapshotHash,
3: bool force)
3: CheckoutMode checkoutMode)
throws (1: EdenError ex)
/**

View File

@ -56,6 +56,14 @@ def get_default_hgrc() -> configparser.ConfigParser:
cases and test case variants.
'''
hgrc = configparser.ConfigParser()
# TODO(mbolin): This is supposed to replace experimental.updatecheck,
# but it does not appear to be taking effect today. The
# experimental.updatecheck setting on this hgrc should be removed once
# it has been deprecated and update.check does what it is supposed to
# do.
hgrc['commands'] = {
'update.check': 'noconflict',
}
hgrc['ui'] = {
'origbackuppath': '.hg/origbackups',
'username': 'Kevin Flynn <lightcyclist@example.com>',
@ -63,6 +71,7 @@ def get_default_hgrc() -> configparser.ConfigParser:
hgrc['experimental'] = {
'evolution': 'createmarkers',
'evolutioncommands': 'prev next split fold obsolete metaedit',
'updatecheck': 'noconflict',
}
hgrc['extensions'] = {
'absorb': '',

View File

@ -9,8 +9,7 @@
import os
from eden.integration.hg.lib.hg_extension_test_base import (
EdenHgTestCase,
hg_test
EdenHgTestCase, hg_test
)
from eden.integration.lib import hgrepo
from textwrap import dedent
@ -201,23 +200,68 @@ class UpdateTest(EdenHgTestCase):
)
self.assertEqual(expected_contents, self.read_file('foo/bar.txt'))
def test_update_with_added_file_that_is_tracked_in_destination(
def test_merge_update_added_file_with_same_contents_in_destination(
self
) -> None:
self._test_update_with_local_file_that_is_tracked_in_destination(True)
base_commit = self.repo.get_head_hash()
def test_update_with_untracked_file_that_is_tracked_in_destination(
file_contents = 'new file\n'
self.write_file('bar/some_new_file.txt', file_contents)
self.hg('add', 'bar/some_new_file.txt')
self.write_file('foo/bar.txt', 'Modify existing file.\n')
new_commit = self.repo.commit('add some_new_file.txt')
self.assert_status_empty()
self.repo.update(base_commit)
self.assert_status_empty()
self.write_file('bar/some_new_file.txt', file_contents)
self.hg('add', 'bar/some_new_file.txt')
self.assert_status({'bar/some_new_file.txt': 'A'})
# Note the update fails even though some_new_file.txt is the same in
# both the working copy and the destination.
with self.assertRaises(hgrepo.HgError) as context:
self.repo.update(new_commit)
self.assertIn(b'abort: conflicting changes', context.exception.stderr)
self.assertEqual(
base_commit,
self.repo.get_head_hash(),
msg='We should still be on the base commit because '
'the merge was aborted.'
)
self.assert_dirstate(
{
'bar/some_new_file.txt': ('a', 0, 'MERGE_BOTH'),
}
)
self.assert_status({'bar/some_new_file.txt': 'A'})
self.assertEqual(file_contents, self.read_file('bar/some_new_file.txt'))
# Now do the update with --merge specified.
self.repo.update(new_commit, merge=True)
self.assert_status_empty()
self.assertEqual(
new_commit,
self.repo.get_head_hash(),
msg='Should be expected commit hash because nothing has changed.'
)
def test_merge_update_added_file_with_conflict_in_destination(self) -> None:
self._test_merge_update_file_with_conflict_in_destination(True)
def test_merge_update_untracked_file_with_conflict_in_destination(
self
) -> None:
self._test_update_with_local_file_that_is_tracked_in_destination(False)
self._test_merge_update_file_with_conflict_in_destination(False)
def _test_update_with_local_file_that_is_tracked_in_destination(
def _test_merge_update_file_with_conflict_in_destination(
self, add_before_updating: bool
) -> None:
base_commit = self.repo.get_head_hash()
original_contents = 'Original contents.\n'
self.write_file('some_new_file.txt', original_contents)
self.hg('add', 'some_new_file.txt')
self.write_file('foo/bar.txt', 'Modify existing file.\n')
commit = self.repo.commit('Commit a new file.')
self.assert_status_empty()
@ -243,10 +287,34 @@ class UpdateTest(EdenHgTestCase):
path_to_backup = '.hg/origbackups/some_new_file.txt'
expected_backup_file = os.path.join(self.mount, path_to_backup)
self.assertFalse(os.path.isfile(expected_backup_file))
self.repo.update(commit)
self.assertEqual(commit, self.repo.get_head_hash())
self.assertEqual(original_contents, self.read_file('some_new_file.txt'))
self.assert_status_empty()
with self.assertRaises(hgrepo.HgError) as context:
self.repo.update(commit, merge=True)
self.assertIn(
b'warning: conflicts while merging some_new_file.txt! '
b'(edit, then use \'hg resolve --mark\')', context.exception.stderr
)
self.assertEqual(
commit,
self.repo.get_head_hash(),
msg='Even though we have a merge conflict, '
'we should still be at the new commit.'
)
self.assert_dirstate({
'some_new_file.txt': ('n', 0, 'MERGE_BOTH'),
})
self.assert_status({
'some_new_file.txt': 'M',
})
merge_contents = dedent(
'''\
<<<<<<< working copy
Re-create the file with different contents.
=======
Original contents.
>>>>>>> destination
'''
)
self.assertEqual(merge_contents, self.read_file('some_new_file.txt'))
# Verify the previous version of the file was backed up as expected.
self.assertTrue(os.path.isfile(expected_backup_file))
@ -275,16 +343,14 @@ class UpdateTest(EdenHgTestCase):
self.hg('add', 'some_new_file.txt')
self.repo.commit('Commit a new file.')
new_contents = 'Make some changes to that new file.\n'
self.write_file(
'some_new_file.txt', new_contents
)
self.write_file('some_new_file.txt', new_contents)
self.hg('update', '.^', '--merge', '--tool', ':local')
self.assertEqual(new_contents, self.read_file('some_new_file.txt'))
self.assert_status({'some_new_file.txt': 'A'})
def test_update_ignores_untracked_directory(self) -> None:
head = self.repo.log()[-1]
base_commit = self.repo.get_head_hash()
self.mkdir('foo/bar')
self.write_file('foo/bar/a.txt', 'File in directory two levels deep.\n')
self.write_file('foo/bar/b.txt', 'Another file.\n')
@ -297,7 +363,7 @@ class UpdateTest(EdenHgTestCase):
self.assert_status({
'foo/bar/b.txt': '?',
})
self.hg('update', head)
self.repo.update(base_commit)
self.assert_status({
'foo/bar/b.txt': '?',
})

View File

@ -196,12 +196,16 @@ class HgRepository(repobase.Repository):
'''Returns the output of `hg status` as a string.'''
return self.hg('status')
def update(self, rev: str, clean: bool = False) -> None:
def update(
self, rev: str, clean: bool = False, merge: bool = False
) -> None:
args = ['update']
if clean:
args = ['update', '--clean', rev]
else:
args = ['update', rev]
self.hg(*args, stdout=None, stderr=None)
args.append('--clean')
if merge:
args.append('--merge')
args.append(rev)
self.hg(*args)
def reset(self, rev: str, keep: bool = True) -> None:
if keep: