##// END OF EJS Templates
rhg: refactor relativize_path into a struct + method...
Simon Sapin -
r49284:9b0e1f64 default
parent child Browse files
Show More
@@ -1,100 +1,101 b''
1 use crate::error::CommandError;
1 use crate::error::CommandError;
2 use crate::ui::Ui;
2 use crate::ui::Ui;
3 use crate::ui::UiError;
3 use crate::utils::path_utils::RelativizePaths;
4 use crate::utils::path_utils::relativize_paths;
5 use clap::Arg;
4 use clap::Arg;
6 use hg::errors::HgError;
5 use hg::errors::HgError;
7 use hg::operations::list_rev_tracked_files;
6 use hg::operations::list_rev_tracked_files;
8 use hg::operations::Dirstate;
7 use hg::operations::Dirstate;
9 use hg::repo::Repo;
8 use hg::repo::Repo;
10 use hg::utils::hg_path::HgPath;
9 use hg::utils::hg_path::HgPath;
11 use std::borrow::Cow;
12
10
13 pub const HELP_TEXT: &str = "
11 pub const HELP_TEXT: &str = "
14 List tracked files.
12 List tracked files.
15
13
16 Returns 0 on success.
14 Returns 0 on success.
17 ";
15 ";
18
16
19 pub fn args() -> clap::App<'static, 'static> {
17 pub fn args() -> clap::App<'static, 'static> {
20 clap::SubCommand::with_name("files")
18 clap::SubCommand::with_name("files")
21 .arg(
19 .arg(
22 Arg::with_name("rev")
20 Arg::with_name("rev")
23 .help("search the repository as it is in REV")
21 .help("search the repository as it is in REV")
24 .short("-r")
22 .short("-r")
25 .long("--revision")
23 .long("--revision")
26 .value_name("REV")
24 .value_name("REV")
27 .takes_value(true),
25 .takes_value(true),
28 )
26 )
29 .about(HELP_TEXT)
27 .about(HELP_TEXT)
30 }
28 }
31
29
32 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
30 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
33 let relative = invocation.config.get(b"ui", b"relative-paths");
31 let relative = invocation.config.get(b"ui", b"relative-paths");
34 if relative.is_some() {
32 if relative.is_some() {
35 return Err(CommandError::unsupported(
33 return Err(CommandError::unsupported(
36 "non-default ui.relative-paths",
34 "non-default ui.relative-paths",
37 ));
35 ));
38 }
36 }
39
37
40 let rev = invocation.subcommand_args.value_of("rev");
38 let rev = invocation.subcommand_args.value_of("rev");
41
39
42 let repo = invocation.repo?;
40 let repo = invocation.repo?;
43
41
44 // It seems better if this check is removed: this would correspond to
42 // It seems better if this check is removed: this would correspond to
45 // automatically enabling the extension if the repo requires it.
43 // automatically enabling the extension if the repo requires it.
46 // However we need this check to be in sync with vanilla hg so hg tests
44 // However we need this check to be in sync with vanilla hg so hg tests
47 // pass.
45 // pass.
48 if repo.has_sparse()
46 if repo.has_sparse()
49 && invocation.config.get(b"extensions", b"sparse").is_none()
47 && invocation.config.get(b"extensions", b"sparse").is_none()
50 {
48 {
51 return Err(CommandError::unsupported(
49 return Err(CommandError::unsupported(
52 "repo is using sparse, but sparse extension is not enabled",
50 "repo is using sparse, but sparse extension is not enabled",
53 ));
51 ));
54 }
52 }
55
53
56 if let Some(rev) = rev {
54 if let Some(rev) = rev {
57 if repo.has_narrow() {
55 if repo.has_narrow() {
58 return Err(CommandError::unsupported(
56 return Err(CommandError::unsupported(
59 "rhg files -r <rev> is not supported in narrow clones",
57 "rhg files -r <rev> is not supported in narrow clones",
60 ));
58 ));
61 }
59 }
62 let files = list_rev_tracked_files(repo, rev).map_err(|e| (e, rev))?;
60 let files = list_rev_tracked_files(repo, rev).map_err(|e| (e, rev))?;
63 display_files(invocation.ui, repo, files.iter())
61 display_files(invocation.ui, repo, files.iter())
64 } else {
62 } else {
65 // The dirstate always reflects the sparse narrowspec, so if
63 // The dirstate always reflects the sparse narrowspec, so if
66 // we only have sparse without narrow all is fine.
64 // we only have sparse without narrow all is fine.
67 // If we have narrow, then [hg files] needs to check if
65 // If we have narrow, then [hg files] needs to check if
68 // the store narrowspec is in sync with the one of the dirstate,
66 // the store narrowspec is in sync with the one of the dirstate,
69 // so we can't support that without explicit code.
67 // so we can't support that without explicit code.
70 if repo.has_narrow() {
68 if repo.has_narrow() {
71 return Err(CommandError::unsupported(
69 return Err(CommandError::unsupported(
72 "rhg files is not supported in narrow clones",
70 "rhg files is not supported in narrow clones",
73 ));
71 ));
74 }
72 }
75 let distate = Dirstate::new(repo)?;
73 let distate = Dirstate::new(repo)?;
76 let files = distate.tracked_files()?;
74 let files = distate.tracked_files()?;
77 display_files(invocation.ui, repo, files.into_iter().map(Ok))
75 display_files(invocation.ui, repo, files.into_iter().map(Ok))
78 }
76 }
79 }
77 }
80
78
81 fn display_files<'a>(
79 fn display_files<'a>(
82 ui: &Ui,
80 ui: &Ui,
83 repo: &Repo,
81 repo: &Repo,
84 files: impl IntoIterator<Item = Result<&'a HgPath, HgError>>,
82 files: impl IntoIterator<Item = Result<&'a HgPath, HgError>>,
85 ) -> Result<(), CommandError> {
83 ) -> Result<(), CommandError> {
86 let mut stdout = ui.stdout_buffer();
84 let mut stdout = ui.stdout_buffer();
87 let mut any = false;
85 let mut any = false;
88
86
89 relativize_paths(repo, files, |path: Cow<[u8]>| -> Result<(), UiError> {
87 let relativize = RelativizePaths::new(repo)?;
88 for result in files {
89 let path = result?;
90 stdout.write_all(&relativize.relativize(path))?;
91 stdout.write_all(b"\n")?;
90 any = true;
92 any = true;
91 stdout.write_all(path.as_ref())?;
93 }
92 stdout.write_all(b"\n")
94
93 })?;
94 stdout.flush()?;
95 stdout.flush()?;
95 if any {
96 if any {
96 Ok(())
97 Ok(())
97 } else {
98 } else {
98 Err(CommandError::Unsuccessful)
99 Err(CommandError::Unsuccessful)
99 }
100 }
100 }
101 }
@@ -1,465 +1,464 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::relativize_paths;
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::TruncatedTimestamp;
16 use hg::dirstate::TruncatedTimestamp;
17 use hg::dirstate::RANGE_MASK_31BIT;
17 use hg::dirstate::RANGE_MASK_31BIT;
18 use hg::errors::{HgError, IoResultExt};
18 use hg::errors::{HgError, IoResultExt};
19 use hg::lock::LockError;
19 use hg::lock::LockError;
20 use hg::manifest::Manifest;
20 use hg::manifest::Manifest;
21 use hg::matchers::AlwaysMatcher;
21 use hg::matchers::AlwaysMatcher;
22 use hg::repo::Repo;
22 use hg::repo::Repo;
23 use hg::utils::files::get_bytes_from_os_string;
23 use hg::utils::files::get_bytes_from_os_string;
24 use hg::utils::files::get_path_from_bytes;
24 use hg::utils::files::get_path_from_bytes;
25 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
25 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
26 use hg::{HgPathCow, StatusOptions};
26 use hg::{HgPathCow, StatusOptions};
27 use log::{info, warn};
27 use log::{info, warn};
28 use std::io;
28 use std::io;
29 use std::path::PathBuf;
29 use std::path::PathBuf;
30
30
31 pub const HELP_TEXT: &str = "
31 pub const HELP_TEXT: &str = "
32 Show changed files in the working directory
32 Show changed files in the working directory
33
33
34 This is a pure Rust version of `hg status`.
34 This is a pure Rust version of `hg status`.
35
35
36 Some options might be missing, check the list below.
36 Some options might be missing, check the list below.
37 ";
37 ";
38
38
39 pub fn args() -> clap::App<'static, 'static> {
39 pub fn args() -> clap::App<'static, 'static> {
40 SubCommand::with_name("status")
40 SubCommand::with_name("status")
41 .alias("st")
41 .alias("st")
42 .about(HELP_TEXT)
42 .about(HELP_TEXT)
43 .arg(
43 .arg(
44 Arg::with_name("all")
44 Arg::with_name("all")
45 .help("show status of all files")
45 .help("show status of all files")
46 .short("-A")
46 .short("-A")
47 .long("--all"),
47 .long("--all"),
48 )
48 )
49 .arg(
49 .arg(
50 Arg::with_name("modified")
50 Arg::with_name("modified")
51 .help("show only modified files")
51 .help("show only modified files")
52 .short("-m")
52 .short("-m")
53 .long("--modified"),
53 .long("--modified"),
54 )
54 )
55 .arg(
55 .arg(
56 Arg::with_name("added")
56 Arg::with_name("added")
57 .help("show only added files")
57 .help("show only added files")
58 .short("-a")
58 .short("-a")
59 .long("--added"),
59 .long("--added"),
60 )
60 )
61 .arg(
61 .arg(
62 Arg::with_name("removed")
62 Arg::with_name("removed")
63 .help("show only removed files")
63 .help("show only removed files")
64 .short("-r")
64 .short("-r")
65 .long("--removed"),
65 .long("--removed"),
66 )
66 )
67 .arg(
67 .arg(
68 Arg::with_name("clean")
68 Arg::with_name("clean")
69 .help("show only clean files")
69 .help("show only clean files")
70 .short("-c")
70 .short("-c")
71 .long("--clean"),
71 .long("--clean"),
72 )
72 )
73 .arg(
73 .arg(
74 Arg::with_name("deleted")
74 Arg::with_name("deleted")
75 .help("show only deleted files")
75 .help("show only deleted files")
76 .short("-d")
76 .short("-d")
77 .long("--deleted"),
77 .long("--deleted"),
78 )
78 )
79 .arg(
79 .arg(
80 Arg::with_name("unknown")
80 Arg::with_name("unknown")
81 .help("show only unknown (not tracked) files")
81 .help("show only unknown (not tracked) files")
82 .short("-u")
82 .short("-u")
83 .long("--unknown"),
83 .long("--unknown"),
84 )
84 )
85 .arg(
85 .arg(
86 Arg::with_name("ignored")
86 Arg::with_name("ignored")
87 .help("show only ignored files")
87 .help("show only ignored files")
88 .short("-i")
88 .short("-i")
89 .long("--ignored"),
89 .long("--ignored"),
90 )
90 )
91 .arg(
91 .arg(
92 Arg::with_name("no-status")
92 Arg::with_name("no-status")
93 .help("hide status prefix")
93 .help("hide status prefix")
94 .short("-n")
94 .short("-n")
95 .long("--no-status"),
95 .long("--no-status"),
96 )
96 )
97 }
97 }
98
98
99 /// Pure data type allowing the caller to specify file states to display
99 /// Pure data type allowing the caller to specify file states to display
100 #[derive(Copy, Clone, Debug)]
100 #[derive(Copy, Clone, Debug)]
101 pub struct DisplayStates {
101 pub struct DisplayStates {
102 pub modified: bool,
102 pub modified: bool,
103 pub added: bool,
103 pub added: bool,
104 pub removed: bool,
104 pub removed: bool,
105 pub clean: bool,
105 pub clean: bool,
106 pub deleted: bool,
106 pub deleted: bool,
107 pub unknown: bool,
107 pub unknown: bool,
108 pub ignored: bool,
108 pub ignored: bool,
109 }
109 }
110
110
111 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
111 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
112 modified: true,
112 modified: true,
113 added: true,
113 added: true,
114 removed: true,
114 removed: true,
115 clean: false,
115 clean: false,
116 deleted: true,
116 deleted: true,
117 unknown: true,
117 unknown: true,
118 ignored: false,
118 ignored: false,
119 };
119 };
120
120
121 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
121 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
122 modified: true,
122 modified: true,
123 added: true,
123 added: true,
124 removed: true,
124 removed: true,
125 clean: true,
125 clean: true,
126 deleted: true,
126 deleted: true,
127 unknown: true,
127 unknown: true,
128 ignored: true,
128 ignored: true,
129 };
129 };
130
130
131 impl DisplayStates {
131 impl DisplayStates {
132 pub fn is_empty(&self) -> bool {
132 pub fn is_empty(&self) -> bool {
133 !(self.modified
133 !(self.modified
134 || self.added
134 || self.added
135 || self.removed
135 || self.removed
136 || self.clean
136 || self.clean
137 || self.deleted
137 || self.deleted
138 || self.unknown
138 || self.unknown
139 || self.ignored)
139 || self.ignored)
140 }
140 }
141 }
141 }
142
142
143 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
143 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
144 let status_enabled_default = false;
144 let status_enabled_default = false;
145 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
145 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
146 if !status_enabled.unwrap_or(status_enabled_default) {
146 if !status_enabled.unwrap_or(status_enabled_default) {
147 return Err(CommandError::unsupported(
147 return Err(CommandError::unsupported(
148 "status is experimental in rhg (enable it with 'rhg.status = true' \
148 "status is experimental in rhg (enable it with 'rhg.status = true' \
149 or enable fallback with 'rhg.on-unsupported = fallback')"
149 or enable fallback with 'rhg.on-unsupported = fallback')"
150 ));
150 ));
151 }
151 }
152
152
153 // TODO: lift these limitations
153 // TODO: lift these limitations
154 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
154 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
155 return Err(CommandError::unsupported(
155 return Err(CommandError::unsupported(
156 "ui.tweakdefaults is not yet supported with rhg status",
156 "ui.tweakdefaults is not yet supported with rhg status",
157 ));
157 ));
158 }
158 }
159 if invocation.config.get_bool(b"ui", b"statuscopies")? {
159 if invocation.config.get_bool(b"ui", b"statuscopies")? {
160 return Err(CommandError::unsupported(
160 return Err(CommandError::unsupported(
161 "ui.statuscopies is not yet supported with rhg status",
161 "ui.statuscopies is not yet supported with rhg status",
162 ));
162 ));
163 }
163 }
164 if invocation
164 if invocation
165 .config
165 .config
166 .get(b"commands", b"status.terse")
166 .get(b"commands", b"status.terse")
167 .is_some()
167 .is_some()
168 {
168 {
169 return Err(CommandError::unsupported(
169 return Err(CommandError::unsupported(
170 "status.terse is not yet supported with rhg status",
170 "status.terse is not yet supported with rhg status",
171 ));
171 ));
172 }
172 }
173
173
174 let ui = invocation.ui;
174 let ui = invocation.ui;
175 let config = invocation.config;
175 let config = invocation.config;
176 let args = invocation.subcommand_args;
176 let args = invocation.subcommand_args;
177 let display_states = if args.is_present("all") {
177 let display_states = if args.is_present("all") {
178 // TODO when implementing `--quiet`: it excludes clean files
178 // TODO when implementing `--quiet`: it excludes clean files
179 // from `--all`
179 // from `--all`
180 ALL_DISPLAY_STATES
180 ALL_DISPLAY_STATES
181 } else {
181 } else {
182 let requested = DisplayStates {
182 let requested = DisplayStates {
183 modified: args.is_present("modified"),
183 modified: args.is_present("modified"),
184 added: args.is_present("added"),
184 added: args.is_present("added"),
185 removed: args.is_present("removed"),
185 removed: args.is_present("removed"),
186 clean: args.is_present("clean"),
186 clean: args.is_present("clean"),
187 deleted: args.is_present("deleted"),
187 deleted: args.is_present("deleted"),
188 unknown: args.is_present("unknown"),
188 unknown: args.is_present("unknown"),
189 ignored: args.is_present("ignored"),
189 ignored: args.is_present("ignored"),
190 };
190 };
191 if requested.is_empty() {
191 if requested.is_empty() {
192 DEFAULT_DISPLAY_STATES
192 DEFAULT_DISPLAY_STATES
193 } else {
193 } else {
194 requested
194 requested
195 }
195 }
196 };
196 };
197 let no_status = args.is_present("no-status");
197 let no_status = args.is_present("no-status");
198
198
199 let repo = invocation.repo?;
199 let repo = invocation.repo?;
200
200
201 if repo.has_sparse() || repo.has_narrow() {
201 if repo.has_sparse() || repo.has_narrow() {
202 return Err(CommandError::unsupported(
202 return Err(CommandError::unsupported(
203 "rhg status is not supported for sparse checkouts or narrow clones yet"
203 "rhg status is not supported for sparse checkouts or narrow clones yet"
204 ));
204 ));
205 }
205 }
206
206
207 let mut dmap = repo.dirstate_map_mut()?;
207 let mut dmap = repo.dirstate_map_mut()?;
208
208
209 let options = StatusOptions {
209 let options = StatusOptions {
210 // we're currently supporting file systems with exec flags only
210 // we're currently supporting file systems with exec flags only
211 // anyway
211 // anyway
212 check_exec: true,
212 check_exec: true,
213 list_clean: display_states.clean,
213 list_clean: display_states.clean,
214 list_unknown: display_states.unknown,
214 list_unknown: display_states.unknown,
215 list_ignored: display_states.ignored,
215 list_ignored: display_states.ignored,
216 collect_traversed_dirs: false,
216 collect_traversed_dirs: false,
217 };
217 };
218 let (mut ds_status, pattern_warnings) = dmap.status(
218 let (mut ds_status, pattern_warnings) = dmap.status(
219 &AlwaysMatcher,
219 &AlwaysMatcher,
220 repo.working_directory_path().to_owned(),
220 repo.working_directory_path().to_owned(),
221 ignore_files(repo, config),
221 ignore_files(repo, config),
222 options,
222 options,
223 )?;
223 )?;
224 if !pattern_warnings.is_empty() {
224 if !pattern_warnings.is_empty() {
225 warn!("Pattern warnings: {:?}", &pattern_warnings);
225 warn!("Pattern warnings: {:?}", &pattern_warnings);
226 }
226 }
227
227
228 if !ds_status.bad.is_empty() {
228 if !ds_status.bad.is_empty() {
229 warn!("Bad matches {:?}", &(ds_status.bad))
229 warn!("Bad matches {:?}", &(ds_status.bad))
230 }
230 }
231 if !ds_status.unsure.is_empty() {
231 if !ds_status.unsure.is_empty() {
232 info!(
232 info!(
233 "Files to be rechecked by retrieval from filelog: {:?}",
233 "Files to be rechecked by retrieval from filelog: {:?}",
234 &ds_status.unsure
234 &ds_status.unsure
235 );
235 );
236 }
236 }
237 let mut fixup = Vec::new();
237 let mut fixup = Vec::new();
238 if !ds_status.unsure.is_empty()
238 if !ds_status.unsure.is_empty()
239 && (display_states.modified || display_states.clean)
239 && (display_states.modified || display_states.clean)
240 {
240 {
241 let p1 = repo.dirstate_parents()?.p1;
241 let p1 = repo.dirstate_parents()?.p1;
242 let manifest = repo.manifest_for_node(p1).map_err(|e| {
242 let manifest = repo.manifest_for_node(p1).map_err(|e| {
243 CommandError::from((e, &*format!("{:x}", p1.short())))
243 CommandError::from((e, &*format!("{:x}", p1.short())))
244 })?;
244 })?;
245 for to_check in ds_status.unsure {
245 for to_check in ds_status.unsure {
246 if unsure_is_modified(repo, &manifest, &to_check)? {
246 if unsure_is_modified(repo, &manifest, &to_check)? {
247 if display_states.modified {
247 if display_states.modified {
248 ds_status.modified.push(to_check);
248 ds_status.modified.push(to_check);
249 }
249 }
250 } else {
250 } else {
251 if display_states.clean {
251 if display_states.clean {
252 ds_status.clean.push(to_check.clone());
252 ds_status.clean.push(to_check.clone());
253 }
253 }
254 fixup.push(to_check.into_owned())
254 fixup.push(to_check.into_owned())
255 }
255 }
256 }
256 }
257 }
257 }
258 let relative_paths = (!ui.plain())
258 let relative_paths = (!ui.plain())
259 && config
259 && config
260 .get_option(b"commands", b"status.relative")?
260 .get_option(b"commands", b"status.relative")?
261 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
261 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
262 let output = DisplayStatusPaths {
262 let output = DisplayStatusPaths {
263 ui,
263 ui,
264 repo,
265 no_status,
264 no_status,
266 relative_paths,
265 relativize: if relative_paths {
266 Some(RelativizePaths::new(repo)?)
267 } else {
268 None
269 },
267 };
270 };
268 if display_states.modified {
271 if display_states.modified {
269 output.display(b"M", ds_status.modified)?;
272 output.display(b"M", ds_status.modified)?;
270 }
273 }
271 if display_states.added {
274 if display_states.added {
272 output.display(b"A", ds_status.added)?;
275 output.display(b"A", ds_status.added)?;
273 }
276 }
274 if display_states.removed {
277 if display_states.removed {
275 output.display(b"R", ds_status.removed)?;
278 output.display(b"R", ds_status.removed)?;
276 }
279 }
277 if display_states.deleted {
280 if display_states.deleted {
278 output.display(b"!", ds_status.deleted)?;
281 output.display(b"!", ds_status.deleted)?;
279 }
282 }
280 if display_states.unknown {
283 if display_states.unknown {
281 output.display(b"?", ds_status.unknown)?;
284 output.display(b"?", ds_status.unknown)?;
282 }
285 }
283 if display_states.ignored {
286 if display_states.ignored {
284 output.display(b"I", ds_status.ignored)?;
287 output.display(b"I", ds_status.ignored)?;
285 }
288 }
286 if display_states.clean {
289 if display_states.clean {
287 output.display(b"C", ds_status.clean)?;
290 output.display(b"C", ds_status.clean)?;
288 }
291 }
289
292
290 let mut dirstate_write_needed = ds_status.dirty;
293 let mut dirstate_write_needed = ds_status.dirty;
291 let filesystem_time_at_status_start = ds_status
294 let filesystem_time_at_status_start = ds_status
292 .filesystem_time_at_status_start
295 .filesystem_time_at_status_start
293 .map(TruncatedTimestamp::from);
296 .map(TruncatedTimestamp::from);
294
297
295 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
298 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
296 && !dirstate_write_needed
299 && !dirstate_write_needed
297 {
300 {
298 // Nothing to update
301 // Nothing to update
299 return Ok(());
302 return Ok(());
300 }
303 }
301
304
302 // Update the dirstate on disk if we can
305 // Update the dirstate on disk if we can
303 let with_lock_result =
306 let with_lock_result =
304 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
307 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
305 if let Some(mtime_boundary) = filesystem_time_at_status_start {
308 if let Some(mtime_boundary) = filesystem_time_at_status_start {
306 for hg_path in fixup {
309 for hg_path in fixup {
307 use std::os::unix::fs::MetadataExt;
310 use std::os::unix::fs::MetadataExt;
308 let fs_path = hg_path_to_path_buf(&hg_path)
311 let fs_path = hg_path_to_path_buf(&hg_path)
309 .expect("HgPath conversion");
312 .expect("HgPath conversion");
310 // Specifically do not reuse `fs_metadata` from
313 // Specifically do not reuse `fs_metadata` from
311 // `unsure_is_clean` which was needed before reading
314 // `unsure_is_clean` which was needed before reading
312 // contents. Here we access metadata again after reading
315 // contents. Here we access metadata again after reading
313 // content, in case it changed in the meantime.
316 // content, in case it changed in the meantime.
314 let fs_metadata = repo
317 let fs_metadata = repo
315 .working_directory_vfs()
318 .working_directory_vfs()
316 .symlink_metadata(&fs_path)?;
319 .symlink_metadata(&fs_path)?;
317 if let Some(mtime) =
320 if let Some(mtime) =
318 TruncatedTimestamp::for_reliable_mtime_of(
321 TruncatedTimestamp::for_reliable_mtime_of(
319 &fs_metadata,
322 &fs_metadata,
320 &mtime_boundary,
323 &mtime_boundary,
321 )
324 )
322 .when_reading_file(&fs_path)?
325 .when_reading_file(&fs_path)?
323 {
326 {
324 let mode = fs_metadata.mode();
327 let mode = fs_metadata.mode();
325 let size = fs_metadata.len() as u32 & RANGE_MASK_31BIT;
328 let size = fs_metadata.len() as u32 & RANGE_MASK_31BIT;
326 let mut entry = dmap
329 let mut entry = dmap
327 .get(&hg_path)?
330 .get(&hg_path)?
328 .expect("ambiguous file not in dirstate");
331 .expect("ambiguous file not in dirstate");
329 entry.set_clean(mode, size, mtime);
332 entry.set_clean(mode, size, mtime);
330 dmap.add_file(&hg_path, entry)?;
333 dmap.add_file(&hg_path, entry)?;
331 dirstate_write_needed = true
334 dirstate_write_needed = true
332 }
335 }
333 }
336 }
334 }
337 }
335 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
338 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
336 if dirstate_write_needed {
339 if dirstate_write_needed {
337 repo.write_dirstate()?
340 repo.write_dirstate()?
338 }
341 }
339 Ok(())
342 Ok(())
340 });
343 });
341 match with_lock_result {
344 match with_lock_result {
342 Ok(closure_result) => closure_result?,
345 Ok(closure_result) => closure_result?,
343 Err(LockError::AlreadyHeld) => {
346 Err(LockError::AlreadyHeld) => {
344 // Not updating the dirstate is not ideal but not critical:
347 // Not updating the dirstate is not ideal but not critical:
345 // don’t keep our caller waiting until some other Mercurial
348 // don’t keep our caller waiting until some other Mercurial
346 // process releases the lock.
349 // process releases the lock.
347 }
350 }
348 Err(LockError::Other(HgError::IoError { error, .. }))
351 Err(LockError::Other(HgError::IoError { error, .. }))
349 if error.kind() == io::ErrorKind::PermissionDenied =>
352 if error.kind() == io::ErrorKind::PermissionDenied =>
350 {
353 {
351 // `hg status` on a read-only repository is fine
354 // `hg status` on a read-only repository is fine
352 }
355 }
353 Err(LockError::Other(error)) => {
356 Err(LockError::Other(error)) => {
354 // Report other I/O errors
357 // Report other I/O errors
355 Err(error)?
358 Err(error)?
356 }
359 }
357 }
360 }
358 Ok(())
361 Ok(())
359 }
362 }
360
363
361 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
364 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
362 let mut ignore_files = Vec::new();
365 let mut ignore_files = Vec::new();
363 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
366 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
364 if repo_ignore.exists() {
367 if repo_ignore.exists() {
365 ignore_files.push(repo_ignore)
368 ignore_files.push(repo_ignore)
366 }
369 }
367 for (key, value) in config.iter_section(b"ui") {
370 for (key, value) in config.iter_section(b"ui") {
368 if key == b"ignore" || key.starts_with(b"ignore.") {
371 if key == b"ignore" || key.starts_with(b"ignore.") {
369 let path = get_path_from_bytes(value);
372 let path = get_path_from_bytes(value);
370 // TODO: expand "~/" and environment variable here, like Python
373 // TODO: expand "~/" and environment variable here, like Python
371 // does with `os.path.expanduser` and `os.path.expandvars`
374 // does with `os.path.expanduser` and `os.path.expandvars`
372
375
373 let joined = repo.working_directory_path().join(path);
376 let joined = repo.working_directory_path().join(path);
374 ignore_files.push(joined);
377 ignore_files.push(joined);
375 }
378 }
376 }
379 }
377 ignore_files
380 ignore_files
378 }
381 }
379
382
380 struct DisplayStatusPaths<'a> {
383 struct DisplayStatusPaths<'a> {
381 ui: &'a Ui,
384 ui: &'a Ui,
382 repo: &'a Repo,
383 no_status: bool,
385 no_status: bool,
384 relative_paths: bool,
386 relativize: Option<RelativizePaths>,
385 }
387 }
386
388
387 impl DisplayStatusPaths<'_> {
389 impl DisplayStatusPaths<'_> {
388 // Probably more elegant to use a Deref or Borrow trait rather than
390 // Probably more elegant to use a Deref or Borrow trait rather than
389 // harcode HgPathBuf, but probably not really useful at this point
391 // harcode HgPathBuf, but probably not really useful at this point
390 fn display(
392 fn display(
391 &self,
393 &self,
392 status_prefix: &[u8],
394 status_prefix: &[u8],
393 mut paths: Vec<HgPathCow>,
395 mut paths: Vec<HgPathCow>,
394 ) -> Result<(), CommandError> {
396 ) -> Result<(), CommandError> {
395 paths.sort_unstable();
397 paths.sort_unstable();
396 let print_path = |path: &[u8]| {
398 for path in paths {
399 let relative;
400 let path = if let Some(relativize) = &self.relativize {
401 relative = relativize.relativize(&path);
402 &*relative
403 } else {
404 path.as_bytes()
405 };
397 // TODO optim, probably lots of unneeded copies here, especially
406 // TODO optim, probably lots of unneeded copies here, especially
398 // if out stream is buffered
407 // if out stream is buffered
399 if self.no_status {
408 if self.no_status {
400 self.ui.write_stdout(&format_bytes!(b"{}\n", path))
409 self.ui.write_stdout(&format_bytes!(b"{}\n", path))?
401 } else {
410 } else {
402 self.ui.write_stdout(&format_bytes!(
411 self.ui.write_stdout(&format_bytes!(
403 b"{} {}\n",
412 b"{} {}\n",
404 status_prefix,
413 status_prefix,
405 path
414 path
406 ))
415 ))?
407 }
408 };
409
410 if self.relative_paths {
411 relativize_paths(self.repo, paths.iter().map(Ok), |path| {
412 print_path(&path)
413 })?;
414 } else {
415 for path in paths {
416 print_path(path.as_bytes())?
417 }
416 }
418 }
417 }
419 Ok(())
418 Ok(())
420 }
419 }
421 }
420 }
422
421
423 /// Check if a file is modified by comparing actual repo store and file system.
422 /// Check if a file is modified by comparing actual repo store and file system.
424 ///
423 ///
425 /// This meant to be used for those that the dirstate cannot resolve, due
424 /// This meant to be used for those that the dirstate cannot resolve, due
426 /// to time resolution limits.
425 /// to time resolution limits.
427 fn unsure_is_modified(
426 fn unsure_is_modified(
428 repo: &Repo,
427 repo: &Repo,
429 manifest: &Manifest,
428 manifest: &Manifest,
430 hg_path: &HgPath,
429 hg_path: &HgPath,
431 ) -> Result<bool, HgError> {
430 ) -> Result<bool, HgError> {
432 let vfs = repo.working_directory_vfs();
431 let vfs = repo.working_directory_vfs();
433 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
432 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
434 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
433 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
435 let is_symlink = fs_metadata.file_type().is_symlink();
434 let is_symlink = fs_metadata.file_type().is_symlink();
436 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
435 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
437 // dirstate
436 // dirstate
438 let fs_flags = if is_symlink {
437 let fs_flags = if is_symlink {
439 Some(b'l')
438 Some(b'l')
440 } else if has_exec_bit(&fs_metadata) {
439 } else if has_exec_bit(&fs_metadata) {
441 Some(b'x')
440 Some(b'x')
442 } else {
441 } else {
443 None
442 None
444 };
443 };
445
444
446 let entry = manifest
445 let entry = manifest
447 .find_file(hg_path)?
446 .find_file(hg_path)?
448 .expect("ambgious file not in p1");
447 .expect("ambgious file not in p1");
449 if entry.flags != fs_flags {
448 if entry.flags != fs_flags {
450 return Ok(true);
449 return Ok(true);
451 }
450 }
452 let filelog = repo.filelog(hg_path)?;
451 let filelog = repo.filelog(hg_path)?;
453 let filelog_entry =
452 let filelog_entry =
454 filelog.data_for_node(entry.node_id()?).map_err(|_| {
453 filelog.data_for_node(entry.node_id()?).map_err(|_| {
455 HgError::corrupted("filelog missing node from manifest")
454 HgError::corrupted("filelog missing node from manifest")
456 })?;
455 })?;
457 let contents_in_p1 = filelog_entry.data()?;
456 let contents_in_p1 = filelog_entry.data()?;
458
457
459 let fs_contents = if is_symlink {
458 let fs_contents = if is_symlink {
460 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
459 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
461 } else {
460 } else {
462 vfs.read(fs_path)?
461 vfs.read(fs_path)?
463 };
462 };
464 Ok(contents_in_p1 != &*fs_contents)
463 Ok(contents_in_p1 != &*fs_contents)
465 }
464 }
@@ -1,49 +1,55 b''
1 // path utils module
1 // path utils module
2 //
2 //
3 // This software may be used and distributed according to the terms of the
3 // This software may be used and distributed according to the terms of the
4 // GNU General Public License version 2 or any later version.
4 // GNU General Public License version 2 or any later version.
5
5
6 use crate::error::CommandError;
7 use crate::ui::UiError;
8 use hg::errors::HgError;
6 use hg::errors::HgError;
9 use hg::repo::Repo;
7 use hg::repo::Repo;
10 use hg::utils::current_dir;
8 use hg::utils::current_dir;
11 use hg::utils::files::{get_bytes_from_path, relativize_path};
9 use hg::utils::files::{get_bytes_from_path, relativize_path};
12 use hg::utils::hg_path::HgPath;
10 use hg::utils::hg_path::HgPath;
13 use hg::utils::hg_path::HgPathBuf;
11 use hg::utils::hg_path::HgPathBuf;
14 use std::borrow::Cow;
12 use std::borrow::Cow;
15
13
16 pub fn relativize_paths(
14 pub struct RelativizePaths {
17 repo: &Repo,
15 repo_root: HgPathBuf,
18 paths: impl IntoIterator<Item = Result<impl AsRef<HgPath>, HgError>>,
16 cwd: HgPathBuf,
19 mut callback: impl FnMut(Cow<[u8]>) -> Result<(), UiError>,
17 outside_repo: bool,
20 ) -> Result<(), CommandError> {
18 }
19
20 impl RelativizePaths {
21 pub fn new(repo: &Repo) -> Result<Self, HgError> {
21 let cwd = current_dir()?;
22 let cwd = current_dir()?;
22 let repo_root = repo.working_directory_path();
23 let repo_root = repo.working_directory_path();
23 let repo_root = cwd.join(repo_root); // Make it absolute
24 let repo_root = cwd.join(repo_root); // Make it absolute
24 let repo_root_hgpath =
25 let repo_root_hgpath =
25 HgPathBuf::from(get_bytes_from_path(repo_root.to_owned()));
26 HgPathBuf::from(get_bytes_from_path(repo_root.to_owned()));
26 let outside_repo: bool;
27 let cwd_hgpath: HgPathBuf;
28
27
29 if let Ok(cwd_relative_to_repo) = cwd.strip_prefix(&repo_root) {
28 if let Ok(cwd_relative_to_repo) = cwd.strip_prefix(&repo_root) {
30 // The current directory is inside the repo, so we can work with
29 // The current directory is inside the repo, so we can work with
31 // relative paths
30 // relative paths
32 outside_repo = false;
31 Ok(Self {
33 cwd_hgpath =
32 repo_root: repo_root_hgpath,
34 HgPathBuf::from(get_bytes_from_path(cwd_relative_to_repo));
33 cwd: HgPathBuf::from(get_bytes_from_path(
34 cwd_relative_to_repo,
35 )),
36 outside_repo: false,
37 })
35 } else {
38 } else {
36 outside_repo = true;
39 Ok(Self {
37 cwd_hgpath = HgPathBuf::from(get_bytes_from_path(cwd));
40 repo_root: repo_root_hgpath,
41 cwd: HgPathBuf::from(get_bytes_from_path(cwd)),
42 outside_repo: true,
43 })
44 }
38 }
45 }
39
46
40 for file in paths {
47 pub fn relativize<'a>(&self, path: &'a HgPath) -> Cow<'a, [u8]> {
41 if outside_repo {
48 if self.outside_repo {
42 let file = repo_root_hgpath.join(file?.as_ref());
49 let joined = self.repo_root.join(path);
43 callback(relativize_path(&file, &cwd_hgpath))?;
50 Cow::Owned(relativize_path(&joined, &self.cwd).into_owned())
44 } else {
51 } else {
45 callback(relativize_path(file?.as_ref(), &cwd_hgpath))?;
52 relativize_path(path, &self.cwd)
46 }
53 }
47 }
54 }
48 Ok(())
49 }
55 }
General Comments 0
You need to be logged in to leave comments. Login now