##// END OF EJS Templates
rhg: Use binary search in manifest lookup...
Simon Sapin -
r49324:e293ff80 default
parent child Browse files
Show More
@@ -1,119 +1,193 b''
1 use crate::errors::HgError;
1 use crate::errors::HgError;
2 use crate::repo::Repo;
2 use crate::repo::Repo;
3 use crate::revlog::revlog::{Revlog, RevlogError};
3 use crate::revlog::revlog::{Revlog, RevlogError};
4 use crate::revlog::Revision;
4 use crate::revlog::Revision;
5 use crate::revlog::{Node, NodePrefix};
5 use crate::revlog::{Node, NodePrefix};
6 use crate::utils::hg_path::HgPath;
6 use crate::utils::hg_path::HgPath;
7 use crate::utils::SliceExt;
7 use crate::utils::SliceExt;
8
8
9 /// A specialized `Revlog` to work with `manifest` data format.
9 /// A specialized `Revlog` to work with `manifest` data format.
10 pub struct Manifestlog {
10 pub struct Manifestlog {
11 /// The generic `revlog` format.
11 /// The generic `revlog` format.
12 revlog: Revlog,
12 revlog: Revlog,
13 }
13 }
14
14
15 impl Manifestlog {
15 impl Manifestlog {
16 /// Open the `manifest` of a repository given by its root.
16 /// Open the `manifest` of a repository given by its root.
17 pub fn open(repo: &Repo) -> Result<Self, HgError> {
17 pub fn open(repo: &Repo) -> Result<Self, HgError> {
18 let revlog = Revlog::open(repo, "00manifest.i", None)?;
18 let revlog = Revlog::open(repo, "00manifest.i", None)?;
19 Ok(Self { revlog })
19 Ok(Self { revlog })
20 }
20 }
21
21
22 /// Return the `Manifest` for the given node ID.
22 /// Return the `Manifest` for the given node ID.
23 ///
23 ///
24 /// Note: this is a node ID in the manifestlog, typically found through
24 /// Note: this is a node ID in the manifestlog, typically found through
25 /// `ChangelogEntry::manifest_node`. It is *not* the node ID of any
25 /// `ChangelogEntry::manifest_node`. It is *not* the node ID of any
26 /// changeset.
26 /// changeset.
27 ///
27 ///
28 /// See also `Repo::manifest_for_node`
28 /// See also `Repo::manifest_for_node`
29 pub fn data_for_node(
29 pub fn data_for_node(
30 &self,
30 &self,
31 node: NodePrefix,
31 node: NodePrefix,
32 ) -> Result<Manifest, RevlogError> {
32 ) -> Result<Manifest, RevlogError> {
33 let rev = self.revlog.rev_from_node(node)?;
33 let rev = self.revlog.rev_from_node(node)?;
34 self.data_for_rev(rev)
34 self.data_for_rev(rev)
35 }
35 }
36
36
37 /// Return the `Manifest` of a given revision number.
37 /// Return the `Manifest` of a given revision number.
38 ///
38 ///
39 /// Note: this is a revision number in the manifestlog, *not* of any
39 /// Note: this is a revision number in the manifestlog, *not* of any
40 /// changeset.
40 /// changeset.
41 ///
41 ///
42 /// See also `Repo::manifest_for_rev`
42 /// See also `Repo::manifest_for_rev`
43 pub fn data_for_rev(
43 pub fn data_for_rev(
44 &self,
44 &self,
45 rev: Revision,
45 rev: Revision,
46 ) -> Result<Manifest, RevlogError> {
46 ) -> Result<Manifest, RevlogError> {
47 let bytes = self.revlog.get_rev_data(rev)?;
47 let bytes = self.revlog.get_rev_data(rev)?;
48 Ok(Manifest { bytes })
48 Ok(Manifest { bytes })
49 }
49 }
50 }
50 }
51
51
52 /// `Manifestlog` entry which knows how to interpret the `manifest` data bytes.
52 /// `Manifestlog` entry which knows how to interpret the `manifest` data bytes.
53 #[derive(Debug)]
53 #[derive(Debug)]
54 pub struct Manifest {
54 pub struct Manifest {
55 /// Format for a manifest: flat sequence of variable-size entries,
56 /// sorted by path, each as:
57 ///
58 /// ```text
59 /// <path> \0 <hex_node_id> <flags> \n
60 /// ```
61 ///
62 /// The last entry is also terminated by a newline character.
63 /// Flags is one of `b""` (the empty string), `b"x"`, `b"l"`, or `b"t"`.
55 bytes: Vec<u8>,
64 bytes: Vec<u8>,
56 }
65 }
57
66
58 impl Manifest {
67 impl Manifest {
59 pub fn iter(
68 pub fn iter(
60 &self,
69 &self,
61 ) -> impl Iterator<Item = Result<ManifestEntry, HgError>> {
70 ) -> impl Iterator<Item = Result<ManifestEntry, HgError>> {
62 self.bytes
71 self.bytes
63 .split(|b| b == &b'\n')
72 .split(|b| b == &b'\n')
64 .filter(|line| !line.is_empty())
73 .filter(|line| !line.is_empty())
65 .map(|line| {
74 .map(ManifestEntry::from_raw)
66 let (path, rest) = line.split_2(b'\0').ok_or_else(|| {
67 HgError::corrupted("manifest line should contain \\0")
68 })?;
69 let path = HgPath::new(path);
70 let (hex_node_id, flags) = match rest.split_last() {
71 Some((&b'x', rest)) => (rest, Some(b'x')),
72 Some((&b'l', rest)) => (rest, Some(b'l')),
73 Some((&b't', rest)) => (rest, Some(b't')),
74 _ => (rest, None),
75 };
76 Ok(ManifestEntry {
77 path,
78 hex_node_id,
79 flags,
80 })
81 })
82 }
75 }
83
76
84 /// If the given path is in this manifest, return its filelog node ID
77 /// If the given path is in this manifest, return its filelog node ID
85 pub fn find_file(
78 pub fn find_by_path(
86 &self,
79 &self,
87 path: &HgPath,
80 path: &HgPath,
88 ) -> Result<Option<ManifestEntry>, HgError> {
81 ) -> Result<Option<ManifestEntry>, HgError> {
89 // TODO: use binary search instead of linear scan. This may involve
82 use std::cmp::Ordering::*;
90 // building (and caching) an index of the byte indicex of each manifest
83 let path = path.as_bytes();
91 // line.
84 // Both boundaries of this `&[u8]` slice are always at the boundary of
85 // an entry
86 let mut bytes = &*self.bytes;
92
87
93 // TODO: use try_find when available (if still using linear scan)
88 // Binary search algorithm derived from `[T]::binary_search_by`
94 // https://github.com/rust-lang/rust/issues/63178
89 // <https://github.com/rust-lang/rust/blob/1.57.0/library/core/src/slice/mod.rs#L2221>
95 for entry in self.iter() {
90 // except we don’t have a slice of entries. Instead we jump to the
96 let entry = entry?;
91 // middle of the byte slice and look around for entry delimiters
97 if entry.path == path {
92 // (newlines).
98 return Ok(Some(entry));
93 while let Some(entry_range) = Self::find_entry_near_middle_of(bytes)? {
94 let (entry_path, rest) =
95 ManifestEntry::split_path(&bytes[entry_range.clone()])?;
96 let cmp = entry_path.cmp(path);
97 if cmp == Less {
98 let after_newline = entry_range.end + 1;
99 bytes = &bytes[after_newline..];
100 } else if cmp == Greater {
101 bytes = &bytes[..entry_range.start];
102 } else {
103 return Ok(Some(ManifestEntry::from_path_and_rest(
104 entry_path, rest,
105 )));
99 }
106 }
100 }
107 }
101 Ok(None)
108 Ok(None)
102 }
109 }
110
111 /// If there is at least one, return the byte range of an entry *excluding*
112 /// the final newline.
113 fn find_entry_near_middle_of(
114 bytes: &[u8],
115 ) -> Result<Option<std::ops::Range<usize>>, HgError> {
116 let len = bytes.len();
117 if len > 0 {
118 let middle = bytes.len() / 2;
119 // Integer division rounds down, so `middle < len`.
120 let (before, after) = bytes.split_at(middle);
121 let is_newline = |&byte: &u8| byte == b'\n';
122 let entry_start = match before.iter().rposition(is_newline) {
123 Some(i) => i + 1,
124 None => 0, // We choose the first entry in `bytes`
125 };
126 let entry_end = match after.iter().position(is_newline) {
127 Some(i) => {
128 // No `+ 1` here to exclude this newline from the range
129 middle + i
130 }
131 None => {
132 // In a well-formed manifest:
133 //
134 // * Since `len > 0`, `bytes` contains at least one entry
135 // * Every entry ends with a newline
136 // * Since `middle < len`, `after` contains at least the
137 // newline at the end of the last entry of `bytes`.
138 //
139 // We didn’t find a newline, so this manifest is not
140 // well-formed.
141 return Err(HgError::corrupted(
142 "manifest entry without \\n delimiter",
143 ));
144 }
145 };
146 Ok(Some(entry_start..entry_end))
147 } else {
148 // len == 0
149 Ok(None)
150 }
151 }
103 }
152 }
104
153
105 /// `Manifestlog` entry which knows how to interpret the `manifest` data bytes.
154 /// `Manifestlog` entry which knows how to interpret the `manifest` data bytes.
106 #[derive(Debug)]
155 #[derive(Debug)]
107 pub struct ManifestEntry<'manifest> {
156 pub struct ManifestEntry<'manifest> {
108 pub path: &'manifest HgPath,
157 pub path: &'manifest HgPath,
109 pub hex_node_id: &'manifest [u8],
158 pub hex_node_id: &'manifest [u8],
110
159
111 /// `Some` values are b'x', b'l', or 't'
160 /// `Some` values are b'x', b'l', or 't'
112 pub flags: Option<u8>,
161 pub flags: Option<u8>,
113 }
162 }
114
163
115 impl ManifestEntry<'_> {
164 impl<'a> ManifestEntry<'a> {
165 fn split_path(bytes: &[u8]) -> Result<(&[u8], &[u8]), HgError> {
166 bytes.split_2(b'\0').ok_or_else(|| {
167 HgError::corrupted("manifest entry without \\0 delimiter")
168 })
169 }
170
171 fn from_path_and_rest(path: &'a [u8], rest: &'a [u8]) -> Self {
172 let (hex_node_id, flags) = match rest.split_last() {
173 Some((&b'x', rest)) => (rest, Some(b'x')),
174 Some((&b'l', rest)) => (rest, Some(b'l')),
175 Some((&b't', rest)) => (rest, Some(b't')),
176 _ => (rest, None),
177 };
178 Self {
179 path: HgPath::new(path),
180 hex_node_id,
181 flags,
182 }
183 }
184
185 fn from_raw(bytes: &'a [u8]) -> Result<Self, HgError> {
186 let (path, rest) = Self::split_path(bytes)?;
187 Ok(Self::from_path_and_rest(path, rest))
188 }
189
116 pub fn node_id(&self) -> Result<Node, HgError> {
190 pub fn node_id(&self) -> Result<Node, HgError> {
117 Node::from_hex_for_repo(self.hex_node_id)
191 Node::from_hex_for_repo(self.hex_node_id)
118 }
192 }
119 }
193 }
@@ -1,506 +1,506 b''
1 // status.rs
1 // status.rs
2 //
2 //
3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 use crate::error::CommandError;
8 use crate::error::CommandError;
9 use crate::ui::Ui;
9 use crate::ui::Ui;
10 use crate::utils::path_utils::RelativizePaths;
10 use crate::utils::path_utils::RelativizePaths;
11 use clap::{Arg, SubCommand};
11 use clap::{Arg, SubCommand};
12 use format_bytes::format_bytes;
12 use format_bytes::format_bytes;
13 use hg;
13 use hg;
14 use hg::config::Config;
14 use hg::config::Config;
15 use hg::dirstate::has_exec_bit;
15 use hg::dirstate::has_exec_bit;
16 use hg::dirstate::status::StatusPath;
16 use hg::dirstate::status::StatusPath;
17 use hg::dirstate::TruncatedTimestamp;
17 use hg::dirstate::TruncatedTimestamp;
18 use hg::dirstate::RANGE_MASK_31BIT;
18 use hg::dirstate::RANGE_MASK_31BIT;
19 use hg::errors::{HgError, IoResultExt};
19 use hg::errors::{HgError, IoResultExt};
20 use hg::lock::LockError;
20 use hg::lock::LockError;
21 use hg::manifest::Manifest;
21 use hg::manifest::Manifest;
22 use hg::matchers::AlwaysMatcher;
22 use hg::matchers::AlwaysMatcher;
23 use hg::repo::Repo;
23 use hg::repo::Repo;
24 use hg::utils::files::get_bytes_from_os_string;
24 use hg::utils::files::get_bytes_from_os_string;
25 use hg::utils::files::get_path_from_bytes;
25 use hg::utils::files::get_path_from_bytes;
26 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
26 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
27 use hg::StatusOptions;
27 use hg::StatusOptions;
28 use log::{info, warn};
28 use log::{info, warn};
29 use std::io;
29 use std::io;
30 use std::path::PathBuf;
30 use std::path::PathBuf;
31
31
32 pub const HELP_TEXT: &str = "
32 pub const HELP_TEXT: &str = "
33 Show changed files in the working directory
33 Show changed files in the working directory
34
34
35 This is a pure Rust version of `hg status`.
35 This is a pure Rust version of `hg status`.
36
36
37 Some options might be missing, check the list below.
37 Some options might be missing, check the list below.
38 ";
38 ";
39
39
40 pub fn args() -> clap::App<'static, 'static> {
40 pub fn args() -> clap::App<'static, 'static> {
41 SubCommand::with_name("status")
41 SubCommand::with_name("status")
42 .alias("st")
42 .alias("st")
43 .about(HELP_TEXT)
43 .about(HELP_TEXT)
44 .arg(
44 .arg(
45 Arg::with_name("all")
45 Arg::with_name("all")
46 .help("show status of all files")
46 .help("show status of all files")
47 .short("-A")
47 .short("-A")
48 .long("--all"),
48 .long("--all"),
49 )
49 )
50 .arg(
50 .arg(
51 Arg::with_name("modified")
51 Arg::with_name("modified")
52 .help("show only modified files")
52 .help("show only modified files")
53 .short("-m")
53 .short("-m")
54 .long("--modified"),
54 .long("--modified"),
55 )
55 )
56 .arg(
56 .arg(
57 Arg::with_name("added")
57 Arg::with_name("added")
58 .help("show only added files")
58 .help("show only added files")
59 .short("-a")
59 .short("-a")
60 .long("--added"),
60 .long("--added"),
61 )
61 )
62 .arg(
62 .arg(
63 Arg::with_name("removed")
63 Arg::with_name("removed")
64 .help("show only removed files")
64 .help("show only removed files")
65 .short("-r")
65 .short("-r")
66 .long("--removed"),
66 .long("--removed"),
67 )
67 )
68 .arg(
68 .arg(
69 Arg::with_name("clean")
69 Arg::with_name("clean")
70 .help("show only clean files")
70 .help("show only clean files")
71 .short("-c")
71 .short("-c")
72 .long("--clean"),
72 .long("--clean"),
73 )
73 )
74 .arg(
74 .arg(
75 Arg::with_name("deleted")
75 Arg::with_name("deleted")
76 .help("show only deleted files")
76 .help("show only deleted files")
77 .short("-d")
77 .short("-d")
78 .long("--deleted"),
78 .long("--deleted"),
79 )
79 )
80 .arg(
80 .arg(
81 Arg::with_name("unknown")
81 Arg::with_name("unknown")
82 .help("show only unknown (not tracked) files")
82 .help("show only unknown (not tracked) files")
83 .short("-u")
83 .short("-u")
84 .long("--unknown"),
84 .long("--unknown"),
85 )
85 )
86 .arg(
86 .arg(
87 Arg::with_name("ignored")
87 Arg::with_name("ignored")
88 .help("show only ignored files")
88 .help("show only ignored files")
89 .short("-i")
89 .short("-i")
90 .long("--ignored"),
90 .long("--ignored"),
91 )
91 )
92 .arg(
92 .arg(
93 Arg::with_name("copies")
93 Arg::with_name("copies")
94 .help("show source of copied files (DEFAULT: ui.statuscopies)")
94 .help("show source of copied files (DEFAULT: ui.statuscopies)")
95 .short("-C")
95 .short("-C")
96 .long("--copies"),
96 .long("--copies"),
97 )
97 )
98 .arg(
98 .arg(
99 Arg::with_name("no-status")
99 Arg::with_name("no-status")
100 .help("hide status prefix")
100 .help("hide status prefix")
101 .short("-n")
101 .short("-n")
102 .long("--no-status"),
102 .long("--no-status"),
103 )
103 )
104 }
104 }
105
105
106 /// Pure data type allowing the caller to specify file states to display
106 /// Pure data type allowing the caller to specify file states to display
107 #[derive(Copy, Clone, Debug)]
107 #[derive(Copy, Clone, Debug)]
108 pub struct DisplayStates {
108 pub struct DisplayStates {
109 pub modified: bool,
109 pub modified: bool,
110 pub added: bool,
110 pub added: bool,
111 pub removed: bool,
111 pub removed: bool,
112 pub clean: bool,
112 pub clean: bool,
113 pub deleted: bool,
113 pub deleted: bool,
114 pub unknown: bool,
114 pub unknown: bool,
115 pub ignored: bool,
115 pub ignored: bool,
116 }
116 }
117
117
118 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
118 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
119 modified: true,
119 modified: true,
120 added: true,
120 added: true,
121 removed: true,
121 removed: true,
122 clean: false,
122 clean: false,
123 deleted: true,
123 deleted: true,
124 unknown: true,
124 unknown: true,
125 ignored: false,
125 ignored: false,
126 };
126 };
127
127
128 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
128 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
129 modified: true,
129 modified: true,
130 added: true,
130 added: true,
131 removed: true,
131 removed: true,
132 clean: true,
132 clean: true,
133 deleted: true,
133 deleted: true,
134 unknown: true,
134 unknown: true,
135 ignored: true,
135 ignored: true,
136 };
136 };
137
137
138 impl DisplayStates {
138 impl DisplayStates {
139 pub fn is_empty(&self) -> bool {
139 pub fn is_empty(&self) -> bool {
140 !(self.modified
140 !(self.modified
141 || self.added
141 || self.added
142 || self.removed
142 || self.removed
143 || self.clean
143 || self.clean
144 || self.deleted
144 || self.deleted
145 || self.unknown
145 || self.unknown
146 || self.ignored)
146 || self.ignored)
147 }
147 }
148 }
148 }
149
149
150 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
150 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
151 let status_enabled_default = false;
151 let status_enabled_default = false;
152 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
152 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
153 if !status_enabled.unwrap_or(status_enabled_default) {
153 if !status_enabled.unwrap_or(status_enabled_default) {
154 return Err(CommandError::unsupported(
154 return Err(CommandError::unsupported(
155 "status is experimental in rhg (enable it with 'rhg.status = true' \
155 "status is experimental in rhg (enable it with 'rhg.status = true' \
156 or enable fallback with 'rhg.on-unsupported = fallback')"
156 or enable fallback with 'rhg.on-unsupported = fallback')"
157 ));
157 ));
158 }
158 }
159
159
160 // TODO: lift these limitations
160 // TODO: lift these limitations
161 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
161 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
162 return Err(CommandError::unsupported(
162 return Err(CommandError::unsupported(
163 "ui.tweakdefaults is not yet supported with rhg status",
163 "ui.tweakdefaults is not yet supported with rhg status",
164 ));
164 ));
165 }
165 }
166 if invocation.config.get_bool(b"ui", b"statuscopies")? {
166 if invocation.config.get_bool(b"ui", b"statuscopies")? {
167 return Err(CommandError::unsupported(
167 return Err(CommandError::unsupported(
168 "ui.statuscopies is not yet supported with rhg status",
168 "ui.statuscopies is not yet supported with rhg status",
169 ));
169 ));
170 }
170 }
171 if invocation
171 if invocation
172 .config
172 .config
173 .get(b"commands", b"status.terse")
173 .get(b"commands", b"status.terse")
174 .is_some()
174 .is_some()
175 {
175 {
176 return Err(CommandError::unsupported(
176 return Err(CommandError::unsupported(
177 "status.terse is not yet supported with rhg status",
177 "status.terse is not yet supported with rhg status",
178 ));
178 ));
179 }
179 }
180
180
181 let ui = invocation.ui;
181 let ui = invocation.ui;
182 let config = invocation.config;
182 let config = invocation.config;
183 let args = invocation.subcommand_args;
183 let args = invocation.subcommand_args;
184 let all = args.is_present("all");
184 let all = args.is_present("all");
185 let display_states = if all {
185 let display_states = if all {
186 // TODO when implementing `--quiet`: it excludes clean files
186 // TODO when implementing `--quiet`: it excludes clean files
187 // from `--all`
187 // from `--all`
188 ALL_DISPLAY_STATES
188 ALL_DISPLAY_STATES
189 } else {
189 } else {
190 let requested = DisplayStates {
190 let requested = DisplayStates {
191 modified: args.is_present("modified"),
191 modified: args.is_present("modified"),
192 added: args.is_present("added"),
192 added: args.is_present("added"),
193 removed: args.is_present("removed"),
193 removed: args.is_present("removed"),
194 clean: args.is_present("clean"),
194 clean: args.is_present("clean"),
195 deleted: args.is_present("deleted"),
195 deleted: args.is_present("deleted"),
196 unknown: args.is_present("unknown"),
196 unknown: args.is_present("unknown"),
197 ignored: args.is_present("ignored"),
197 ignored: args.is_present("ignored"),
198 };
198 };
199 if requested.is_empty() {
199 if requested.is_empty() {
200 DEFAULT_DISPLAY_STATES
200 DEFAULT_DISPLAY_STATES
201 } else {
201 } else {
202 requested
202 requested
203 }
203 }
204 };
204 };
205 let no_status = args.is_present("no-status");
205 let no_status = args.is_present("no-status");
206 let list_copies = all
206 let list_copies = all
207 || args.is_present("copies")
207 || args.is_present("copies")
208 || config.get_bool(b"ui", b"statuscopies")?;
208 || config.get_bool(b"ui", b"statuscopies")?;
209
209
210 let repo = invocation.repo?;
210 let repo = invocation.repo?;
211
211
212 if repo.has_sparse() || repo.has_narrow() {
212 if repo.has_sparse() || repo.has_narrow() {
213 return Err(CommandError::unsupported(
213 return Err(CommandError::unsupported(
214 "rhg status is not supported for sparse checkouts or narrow clones yet"
214 "rhg status is not supported for sparse checkouts or narrow clones yet"
215 ));
215 ));
216 }
216 }
217
217
218 let mut dmap = repo.dirstate_map_mut()?;
218 let mut dmap = repo.dirstate_map_mut()?;
219
219
220 let options = StatusOptions {
220 let options = StatusOptions {
221 // we're currently supporting file systems with exec flags only
221 // we're currently supporting file systems with exec flags only
222 // anyway
222 // anyway
223 check_exec: true,
223 check_exec: true,
224 list_clean: display_states.clean,
224 list_clean: display_states.clean,
225 list_unknown: display_states.unknown,
225 list_unknown: display_states.unknown,
226 list_ignored: display_states.ignored,
226 list_ignored: display_states.ignored,
227 list_copies,
227 list_copies,
228 collect_traversed_dirs: false,
228 collect_traversed_dirs: false,
229 };
229 };
230 let (mut ds_status, pattern_warnings) = dmap.status(
230 let (mut ds_status, pattern_warnings) = dmap.status(
231 &AlwaysMatcher,
231 &AlwaysMatcher,
232 repo.working_directory_path().to_owned(),
232 repo.working_directory_path().to_owned(),
233 ignore_files(repo, config),
233 ignore_files(repo, config),
234 options,
234 options,
235 )?;
235 )?;
236 if !pattern_warnings.is_empty() {
236 if !pattern_warnings.is_empty() {
237 warn!("Pattern warnings: {:?}", &pattern_warnings);
237 warn!("Pattern warnings: {:?}", &pattern_warnings);
238 }
238 }
239
239
240 for (path, error) in ds_status.bad {
240 for (path, error) in ds_status.bad {
241 let error = match error {
241 let error = match error {
242 hg::BadMatch::OsError(code) => {
242 hg::BadMatch::OsError(code) => {
243 std::io::Error::from_raw_os_error(code).to_string()
243 std::io::Error::from_raw_os_error(code).to_string()
244 }
244 }
245 hg::BadMatch::BadType(ty) => {
245 hg::BadMatch::BadType(ty) => {
246 format!("unsupported file type (type is {})", ty)
246 format!("unsupported file type (type is {})", ty)
247 }
247 }
248 };
248 };
249 ui.write_stderr(&format_bytes!(
249 ui.write_stderr(&format_bytes!(
250 b"{}: {}\n",
250 b"{}: {}\n",
251 path.as_bytes(),
251 path.as_bytes(),
252 error.as_bytes()
252 error.as_bytes()
253 ))?
253 ))?
254 }
254 }
255 if !ds_status.unsure.is_empty() {
255 if !ds_status.unsure.is_empty() {
256 info!(
256 info!(
257 "Files to be rechecked by retrieval from filelog: {:?}",
257 "Files to be rechecked by retrieval from filelog: {:?}",
258 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
258 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
259 );
259 );
260 }
260 }
261 let mut fixup = Vec::new();
261 let mut fixup = Vec::new();
262 if !ds_status.unsure.is_empty()
262 if !ds_status.unsure.is_empty()
263 && (display_states.modified || display_states.clean)
263 && (display_states.modified || display_states.clean)
264 {
264 {
265 let p1 = repo.dirstate_parents()?.p1;
265 let p1 = repo.dirstate_parents()?.p1;
266 let manifest = repo.manifest_for_node(p1).map_err(|e| {
266 let manifest = repo.manifest_for_node(p1).map_err(|e| {
267 CommandError::from((e, &*format!("{:x}", p1.short())))
267 CommandError::from((e, &*format!("{:x}", p1.short())))
268 })?;
268 })?;
269 for to_check in ds_status.unsure {
269 for to_check in ds_status.unsure {
270 if unsure_is_modified(repo, &manifest, &to_check.path)? {
270 if unsure_is_modified(repo, &manifest, &to_check.path)? {
271 if display_states.modified {
271 if display_states.modified {
272 ds_status.modified.push(to_check);
272 ds_status.modified.push(to_check);
273 }
273 }
274 } else {
274 } else {
275 if display_states.clean {
275 if display_states.clean {
276 ds_status.clean.push(to_check.clone());
276 ds_status.clean.push(to_check.clone());
277 }
277 }
278 fixup.push(to_check.path.into_owned())
278 fixup.push(to_check.path.into_owned())
279 }
279 }
280 }
280 }
281 }
281 }
282 let relative_paths = (!ui.plain())
282 let relative_paths = (!ui.plain())
283 && config
283 && config
284 .get_option(b"commands", b"status.relative")?
284 .get_option(b"commands", b"status.relative")?
285 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
285 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
286 let output = DisplayStatusPaths {
286 let output = DisplayStatusPaths {
287 ui,
287 ui,
288 no_status,
288 no_status,
289 relativize: if relative_paths {
289 relativize: if relative_paths {
290 Some(RelativizePaths::new(repo)?)
290 Some(RelativizePaths::new(repo)?)
291 } else {
291 } else {
292 None
292 None
293 },
293 },
294 };
294 };
295 if display_states.modified {
295 if display_states.modified {
296 output.display(b"M", ds_status.modified)?;
296 output.display(b"M", ds_status.modified)?;
297 }
297 }
298 if display_states.added {
298 if display_states.added {
299 output.display(b"A", ds_status.added)?;
299 output.display(b"A", ds_status.added)?;
300 }
300 }
301 if display_states.removed {
301 if display_states.removed {
302 output.display(b"R", ds_status.removed)?;
302 output.display(b"R", ds_status.removed)?;
303 }
303 }
304 if display_states.deleted {
304 if display_states.deleted {
305 output.display(b"!", ds_status.deleted)?;
305 output.display(b"!", ds_status.deleted)?;
306 }
306 }
307 if display_states.unknown {
307 if display_states.unknown {
308 output.display(b"?", ds_status.unknown)?;
308 output.display(b"?", ds_status.unknown)?;
309 }
309 }
310 if display_states.ignored {
310 if display_states.ignored {
311 output.display(b"I", ds_status.ignored)?;
311 output.display(b"I", ds_status.ignored)?;
312 }
312 }
313 if display_states.clean {
313 if display_states.clean {
314 output.display(b"C", ds_status.clean)?;
314 output.display(b"C", ds_status.clean)?;
315 }
315 }
316
316
317 let mut dirstate_write_needed = ds_status.dirty;
317 let mut dirstate_write_needed = ds_status.dirty;
318 let filesystem_time_at_status_start = ds_status
318 let filesystem_time_at_status_start = ds_status
319 .filesystem_time_at_status_start
319 .filesystem_time_at_status_start
320 .map(TruncatedTimestamp::from);
320 .map(TruncatedTimestamp::from);
321
321
322 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
322 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
323 && !dirstate_write_needed
323 && !dirstate_write_needed
324 {
324 {
325 // Nothing to update
325 // Nothing to update
326 return Ok(());
326 return Ok(());
327 }
327 }
328
328
329 // Update the dirstate on disk if we can
329 // Update the dirstate on disk if we can
330 let with_lock_result =
330 let with_lock_result =
331 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
331 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
332 if let Some(mtime_boundary) = filesystem_time_at_status_start {
332 if let Some(mtime_boundary) = filesystem_time_at_status_start {
333 for hg_path in fixup {
333 for hg_path in fixup {
334 use std::os::unix::fs::MetadataExt;
334 use std::os::unix::fs::MetadataExt;
335 let fs_path = hg_path_to_path_buf(&hg_path)
335 let fs_path = hg_path_to_path_buf(&hg_path)
336 .expect("HgPath conversion");
336 .expect("HgPath conversion");
337 // Specifically do not reuse `fs_metadata` from
337 // Specifically do not reuse `fs_metadata` from
338 // `unsure_is_clean` which was needed before reading
338 // `unsure_is_clean` which was needed before reading
339 // contents. Here we access metadata again after reading
339 // contents. Here we access metadata again after reading
340 // content, in case it changed in the meantime.
340 // content, in case it changed in the meantime.
341 let fs_metadata = repo
341 let fs_metadata = repo
342 .working_directory_vfs()
342 .working_directory_vfs()
343 .symlink_metadata(&fs_path)?;
343 .symlink_metadata(&fs_path)?;
344 if let Some(mtime) =
344 if let Some(mtime) =
345 TruncatedTimestamp::for_reliable_mtime_of(
345 TruncatedTimestamp::for_reliable_mtime_of(
346 &fs_metadata,
346 &fs_metadata,
347 &mtime_boundary,
347 &mtime_boundary,
348 )
348 )
349 .when_reading_file(&fs_path)?
349 .when_reading_file(&fs_path)?
350 {
350 {
351 let mode = fs_metadata.mode();
351 let mode = fs_metadata.mode();
352 let size = fs_metadata.len() as u32 & RANGE_MASK_31BIT;
352 let size = fs_metadata.len() as u32 & RANGE_MASK_31BIT;
353 let mut entry = dmap
353 let mut entry = dmap
354 .get(&hg_path)?
354 .get(&hg_path)?
355 .expect("ambiguous file not in dirstate");
355 .expect("ambiguous file not in dirstate");
356 entry.set_clean(mode, size, mtime);
356 entry.set_clean(mode, size, mtime);
357 dmap.add_file(&hg_path, entry)?;
357 dmap.add_file(&hg_path, entry)?;
358 dirstate_write_needed = true
358 dirstate_write_needed = true
359 }
359 }
360 }
360 }
361 }
361 }
362 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
362 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
363 if dirstate_write_needed {
363 if dirstate_write_needed {
364 repo.write_dirstate()?
364 repo.write_dirstate()?
365 }
365 }
366 Ok(())
366 Ok(())
367 });
367 });
368 match with_lock_result {
368 match with_lock_result {
369 Ok(closure_result) => closure_result?,
369 Ok(closure_result) => closure_result?,
370 Err(LockError::AlreadyHeld) => {
370 Err(LockError::AlreadyHeld) => {
371 // Not updating the dirstate is not ideal but not critical:
371 // Not updating the dirstate is not ideal but not critical:
372 // don’t keep our caller waiting until some other Mercurial
372 // don’t keep our caller waiting until some other Mercurial
373 // process releases the lock.
373 // process releases the lock.
374 }
374 }
375 Err(LockError::Other(HgError::IoError { error, .. }))
375 Err(LockError::Other(HgError::IoError { error, .. }))
376 if error.kind() == io::ErrorKind::PermissionDenied =>
376 if error.kind() == io::ErrorKind::PermissionDenied =>
377 {
377 {
378 // `hg status` on a read-only repository is fine
378 // `hg status` on a read-only repository is fine
379 }
379 }
380 Err(LockError::Other(error)) => {
380 Err(LockError::Other(error)) => {
381 // Report other I/O errors
381 // Report other I/O errors
382 Err(error)?
382 Err(error)?
383 }
383 }
384 }
384 }
385 Ok(())
385 Ok(())
386 }
386 }
387
387
388 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
388 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
389 let mut ignore_files = Vec::new();
389 let mut ignore_files = Vec::new();
390 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
390 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
391 if repo_ignore.exists() {
391 if repo_ignore.exists() {
392 ignore_files.push(repo_ignore)
392 ignore_files.push(repo_ignore)
393 }
393 }
394 for (key, value) in config.iter_section(b"ui") {
394 for (key, value) in config.iter_section(b"ui") {
395 if key == b"ignore" || key.starts_with(b"ignore.") {
395 if key == b"ignore" || key.starts_with(b"ignore.") {
396 let path = get_path_from_bytes(value);
396 let path = get_path_from_bytes(value);
397 // TODO: expand "~/" and environment variable here, like Python
397 // TODO: expand "~/" and environment variable here, like Python
398 // does with `os.path.expanduser` and `os.path.expandvars`
398 // does with `os.path.expanduser` and `os.path.expandvars`
399
399
400 let joined = repo.working_directory_path().join(path);
400 let joined = repo.working_directory_path().join(path);
401 ignore_files.push(joined);
401 ignore_files.push(joined);
402 }
402 }
403 }
403 }
404 ignore_files
404 ignore_files
405 }
405 }
406
406
407 struct DisplayStatusPaths<'a> {
407 struct DisplayStatusPaths<'a> {
408 ui: &'a Ui,
408 ui: &'a Ui,
409 no_status: bool,
409 no_status: bool,
410 relativize: Option<RelativizePaths>,
410 relativize: Option<RelativizePaths>,
411 }
411 }
412
412
413 impl DisplayStatusPaths<'_> {
413 impl DisplayStatusPaths<'_> {
414 // Probably more elegant to use a Deref or Borrow trait rather than
414 // Probably more elegant to use a Deref or Borrow trait rather than
415 // harcode HgPathBuf, but probably not really useful at this point
415 // harcode HgPathBuf, but probably not really useful at this point
416 fn display(
416 fn display(
417 &self,
417 &self,
418 status_prefix: &[u8],
418 status_prefix: &[u8],
419 mut paths: Vec<StatusPath<'_>>,
419 mut paths: Vec<StatusPath<'_>>,
420 ) -> Result<(), CommandError> {
420 ) -> Result<(), CommandError> {
421 paths.sort_unstable();
421 paths.sort_unstable();
422 for StatusPath { path, copy_source } in paths {
422 for StatusPath { path, copy_source } in paths {
423 let relative;
423 let relative;
424 let path = if let Some(relativize) = &self.relativize {
424 let path = if let Some(relativize) = &self.relativize {
425 relative = relativize.relativize(&path);
425 relative = relativize.relativize(&path);
426 &*relative
426 &*relative
427 } else {
427 } else {
428 path.as_bytes()
428 path.as_bytes()
429 };
429 };
430 // TODO optim, probably lots of unneeded copies here, especially
430 // TODO optim, probably lots of unneeded copies here, especially
431 // if out stream is buffered
431 // if out stream is buffered
432 if self.no_status {
432 if self.no_status {
433 self.ui.write_stdout(&format_bytes!(b"{}\n", path))?
433 self.ui.write_stdout(&format_bytes!(b"{}\n", path))?
434 } else {
434 } else {
435 self.ui.write_stdout(&format_bytes!(
435 self.ui.write_stdout(&format_bytes!(
436 b"{} {}\n",
436 b"{} {}\n",
437 status_prefix,
437 status_prefix,
438 path
438 path
439 ))?
439 ))?
440 }
440 }
441 if let Some(source) = copy_source {
441 if let Some(source) = copy_source {
442 self.ui.write_stdout(&format_bytes!(
442 self.ui.write_stdout(&format_bytes!(
443 b" {}\n",
443 b" {}\n",
444 source.as_bytes()
444 source.as_bytes()
445 ))?
445 ))?
446 }
446 }
447 }
447 }
448 Ok(())
448 Ok(())
449 }
449 }
450 }
450 }
451
451
452 /// Check if a file is modified by comparing actual repo store and file system.
452 /// Check if a file is modified by comparing actual repo store and file system.
453 ///
453 ///
454 /// This meant to be used for those that the dirstate cannot resolve, due
454 /// This meant to be used for those that the dirstate cannot resolve, due
455 /// to time resolution limits.
455 /// to time resolution limits.
456 fn unsure_is_modified(
456 fn unsure_is_modified(
457 repo: &Repo,
457 repo: &Repo,
458 manifest: &Manifest,
458 manifest: &Manifest,
459 hg_path: &HgPath,
459 hg_path: &HgPath,
460 ) -> Result<bool, HgError> {
460 ) -> Result<bool, HgError> {
461 let vfs = repo.working_directory_vfs();
461 let vfs = repo.working_directory_vfs();
462 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
462 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
463 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
463 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
464 let is_symlink = fs_metadata.file_type().is_symlink();
464 let is_symlink = fs_metadata.file_type().is_symlink();
465 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
465 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
466 // dirstate
466 // dirstate
467 let fs_flags = if is_symlink {
467 let fs_flags = if is_symlink {
468 Some(b'l')
468 Some(b'l')
469 } else if has_exec_bit(&fs_metadata) {
469 } else if has_exec_bit(&fs_metadata) {
470 Some(b'x')
470 Some(b'x')
471 } else {
471 } else {
472 None
472 None
473 };
473 };
474
474
475 let entry = manifest
475 let entry = manifest
476 .find_file(hg_path)?
476 .find_by_path(hg_path)?
477 .expect("ambgious file not in p1");
477 .expect("ambgious file not in p1");
478 if entry.flags != fs_flags {
478 if entry.flags != fs_flags {
479 return Ok(true);
479 return Ok(true);
480 }
480 }
481 let filelog = repo.filelog(hg_path)?;
481 let filelog = repo.filelog(hg_path)?;
482 let fs_len = fs_metadata.len();
482 let fs_len = fs_metadata.len();
483 // TODO: check `fs_len` here like below, but based on
483 // TODO: check `fs_len` here like below, but based on
484 // `RevlogEntry::uncompressed_len` without decompressing the full filelog
484 // `RevlogEntry::uncompressed_len` without decompressing the full filelog
485 // contents where possible. This is only valid if the revlog data does not
485 // contents where possible. This is only valid if the revlog data does not
486 // contain metadata. See how Python’s `revlog.rawsize` calls
486 // contain metadata. See how Python’s `revlog.rawsize` calls
487 // `storageutil.filerevisioncopied`.
487 // `storageutil.filerevisioncopied`.
488 // (Maybe also check for content-modifying flags? See `revlog.size`.)
488 // (Maybe also check for content-modifying flags? See `revlog.size`.)
489 let filelog_entry =
489 let filelog_entry =
490 filelog.data_for_node(entry.node_id()?).map_err(|_| {
490 filelog.data_for_node(entry.node_id()?).map_err(|_| {
491 HgError::corrupted("filelog missing node from manifest")
491 HgError::corrupted("filelog missing node from manifest")
492 })?;
492 })?;
493 let contents_in_p1 = filelog_entry.data()?;
493 let contents_in_p1 = filelog_entry.data()?;
494 if contents_in_p1.len() as u64 != fs_len {
494 if contents_in_p1.len() as u64 != fs_len {
495 // No need to read the file contents:
495 // No need to read the file contents:
496 // it cannot be equal if it has a different length.
496 // it cannot be equal if it has a different length.
497 return Ok(true);
497 return Ok(true);
498 }
498 }
499
499
500 let fs_contents = if is_symlink {
500 let fs_contents = if is_symlink {
501 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
501 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
502 } else {
502 } else {
503 vfs.read(fs_path)?
503 vfs.read(fs_path)?
504 };
504 };
505 Ok(contents_in_p1 != &*fs_contents)
505 Ok(contents_in_p1 != &*fs_contents)
506 }
506 }
General Comments 0
You need to be logged in to leave comments. Login now