Skip to content

Commit d545ed8

Browse files
Durham Goodefacebook-github-bot
Durham Goode
authored andcommitted
status: prevent accidentally overwriting dirstate during native status
Summary: There was a bug where hg status would take the lock, then write the treestate and dirstate without checking to see if another process had written the dirstate/treestate since the current process had read it. This would cause files to show up as dirty when they weren't, and potentially result in invalid store id errors. For example: ``` Process 1: Starts an hg checkout and takes the wlock. Process 2: Starts hg status and begins updating the in-memory treestate Process 1: Completes the checkout and writes a new dirstate/treestate showing all the newly clean files. It releases the wlock. Process 2: Takes the wlock and writes the watchman file updates it saw during status. This overwrites process 1's written state. Future hg status runs now see a bunch of empty files because it's using the pre-checkout treestate to compare against the post-checkout working copy. ``` The new code prevents the status process from writing the update (since it's mostly just an optimization anyway) if the dirstate changed out from under it. Reviewed By: quark-zju Differential Revision: D41052635 fbshipit-source-id: aa5459eab5b4fe5e86e037e7ffa364149cb686a8
1 parent 6e1789b commit d545ed8

File tree

4 files changed

+42
-2
lines changed

4 files changed

+42
-2
lines changed

eden/scm/lib/treestate/src/dirstate.rs

+19
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use types::HgId;
1919
use crate::serialization::Serializable;
2020
use crate::store::BlockId;
2121
use crate::treestate::TreeState;
22+
use crate::ErrorKind;
2223

2324
/// A dirstate object. This maintains .hg/dirstate file
2425
#[derive(Debug, PartialEq)]
@@ -49,6 +50,23 @@ pub fn flush(config: &dyn Config, root: &Path, treestate: &mut TreeState) -> Res
4950
let dirstate_input = util::file::read(&dirstate_path)?;
5051
let mut dirstate = Dirstate::deserialize(&mut dirstate_input.as_slice())?;
5152

53+
// If the dirstate has changed since we last loaded it, don't flush since we might
54+
// overwrite data. For instance, if we start running 'hg status', it loads the dirstate and
55+
// treestate and starts updating the treestate. Before status gets to this flush, if
56+
// another process, like 'hg checkout' writes to the dirstate/treestate, then if we let
57+
// this 'hg status' flush it's old data, we'd result in a dirty working copy where the
58+
// clean checkout data was thought to be dirty because we had old treestate data.
59+
//
60+
// In that situation, just return an error and the client can decide if that's ok or not.
61+
62+
if let Some(dirstate_fields) = &dirstate.tree_state {
63+
if treestate.file_name()? != dirstate_fields.tree_filename
64+
|| treestate.original_root_id() != dirstate_fields.tree_root_id
65+
{
66+
return Err(ErrorKind::TreestateOutOfDate.into());
67+
}
68+
}
69+
5270
dirstate.p1 = treestate
5371
.get_metadata_by_key("p1")?
5472
.map_or(Ok(NULL_ID), |p| HgId::from_hex(p.as_bytes()))?;
@@ -62,6 +80,7 @@ pub fn flush(config: &dyn Config, root: &Path, treestate: &mut TreeState) -> Res
6280
})?;
6381

6482
let root_id = treestate.flush()?;
83+
treestate_fields.tree_filename = treestate.file_name()?;
6584
treestate_fields.tree_root_id = root_id;
6685

6786
let mut dirstate_output: Vec<u8> = Vec::new();

eden/scm/lib/treestate/src/errors.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::path::PathBuf;
1111

1212
use thiserror::Error;
1313

14-
#[derive(Debug, Error)]
14+
#[derive(Debug, Error, PartialEq)]
1515
pub enum ErrorKind {
1616
#[error("the provided store file is not a valid store file: {0}")]
1717
NotAStoreFile(PathBuf),
@@ -27,4 +27,6 @@ pub enum ErrorKind {
2727
CorruptTree,
2828
#[error("callback error: {0}")]
2929
CallbackError(String),
30+
#[error("dirstate/treestate was out of date and therefore did not flush")]
31+
TreestateOutOfDate,
3032
}

eden/scm/lib/treestate/src/treestate.rs

+10
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub struct TreeState {
4646
store: FileStore,
4747
tree: Tree<FileStateV2>,
4848
root: TreeStateRoot,
49+
original_root_id: BlockId,
4950
// eden_dirstate_path is only used in the case the case that the treestate is
5051
// wrapping a legacy eden dirstate which is necessary for EdenFS compatility.
5152
// TODO: Remove once EdenFS has migrated to treestate.
@@ -72,6 +73,7 @@ impl TreeState {
7273
store,
7374
tree,
7475
root,
76+
original_root_id: root_id,
7577
eden_dirstate_path: None,
7678
case_sensitive,
7779
})
@@ -88,6 +90,7 @@ impl TreeState {
8890
store,
8991
tree,
9092
root,
93+
original_root_id: BlockId(0),
9194
eden_dirstate_path: None,
9295
case_sensitive,
9396
};
@@ -119,6 +122,7 @@ impl TreeState {
119122
store,
120123
tree,
121124
root,
125+
original_root_id: BlockId(0),
122126
eden_dirstate_path: Some(path),
123127
case_sensitive,
124128
};
@@ -144,6 +148,11 @@ impl TreeState {
144148
.to_string())
145149
}
146150

151+
/// The root_id from when the treestate was loaded or last saved. Gets updated upon flush.
152+
pub fn original_root_id(&self) -> BlockId {
153+
self.original_root_id
154+
}
155+
147156
pub fn dirty(&self) -> bool {
148157
self.tree.dirty() || self.root.dirty()
149158
}
@@ -183,6 +192,7 @@ impl TreeState {
183192
write_eden_dirstate(&eden_dirstate_path, metadata, entries)?;
184193
}
185194

195+
self.original_root_id = result;
186196
Ok(result)
187197
}
188198

eden/scm/lib/workingcopy/src/watchmanfs/treestate.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use treestate::filestate::StateFlags;
1818
use treestate::metadata::Metadata;
1919
use treestate::serialization::Serializable;
2020
use treestate::treestate::TreeState;
21+
use treestate::ErrorKind;
2122
use types::RepoPathBuf;
2223
use watchman_client::prelude::*;
2324

@@ -115,7 +116,15 @@ impl WatchmanTreeStateWrite for WatchmanTreeState<'_> {
115116
}
116117

117118
fn flush(self, config: &dyn Config) -> Result<()> {
118-
dirstate::flush(config, &self.root, &mut self.treestate.lock())
119+
match dirstate::flush(config, &self.root, &mut self.treestate.lock()) {
120+
Ok(()) => Ok(()),
121+
// If the dirstate was changed before we flushed, that's ok. Let the other write win
122+
// since writes during status are just optimizations.
123+
Err(e) => match e.downcast_ref::<ErrorKind>() {
124+
Some(e) if *e == ErrorKind::TreestateOutOfDate => Ok(()),
125+
_ => Err(e),
126+
},
127+
}
119128
}
120129
}
121130

0 commit comments

Comments
 (0)