##// END OF EJS Templates
rust-ui: refactor ui code for printing narrow/sparse warnings...
Raphaël Gomès -
r50876:364e7838 default
parent child Browse files
Show More
@@ -1,646 +1,601 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::{
10 format_pattern_file_warning, print_narrow_sparse_warnings, Ui,
11 };
10 use crate::utils::path_utils::RelativizePaths;
12 use crate::utils::path_utils::RelativizePaths;
11 use clap::Arg;
13 use clap::Arg;
12 use format_bytes::format_bytes;
14 use format_bytes::format_bytes;
13 use hg::config::Config;
15 use hg::config::Config;
14 use hg::dirstate::has_exec_bit;
16 use hg::dirstate::has_exec_bit;
15 use hg::dirstate::status::StatusPath;
17 use hg::dirstate::status::StatusPath;
16 use hg::dirstate::TruncatedTimestamp;
18 use hg::dirstate::TruncatedTimestamp;
17 use hg::errors::{HgError, IoResultExt};
19 use hg::errors::{HgError, IoResultExt};
18 use hg::lock::LockError;
20 use hg::lock::LockError;
19 use hg::manifest::Manifest;
21 use hg::manifest::Manifest;
20 use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
22 use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
21 use hg::repo::Repo;
23 use hg::repo::Repo;
22 use hg::utils::files::get_bytes_from_os_string;
24 use hg::utils::files::get_bytes_from_os_string;
23 use hg::utils::files::get_bytes_from_path;
24 use hg::utils::files::get_path_from_bytes;
25 use hg::utils::files::get_path_from_bytes;
25 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
26 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
26 use hg::DirstateStatus;
27 use hg::DirstateStatus;
27 use hg::PatternFileWarning;
28 use hg::PatternFileWarning;
28 use hg::StatusError;
29 use hg::StatusError;
29 use hg::StatusOptions;
30 use hg::StatusOptions;
30 use hg::{self, narrow, sparse};
31 use hg::{self, narrow, sparse};
31 use log::info;
32 use log::info;
32 use rayon::prelude::*;
33 use rayon::prelude::*;
33 use std::io;
34 use std::io;
34 use std::path::PathBuf;
35 use std::path::PathBuf;
35
36
36 pub const HELP_TEXT: &str = "
37 pub const HELP_TEXT: &str = "
37 Show changed files in the working directory
38 Show changed files in the working directory
38
39
39 This is a pure Rust version of `hg status`.
40 This is a pure Rust version of `hg status`.
40
41
41 Some options might be missing, check the list below.
42 Some options might be missing, check the list below.
42 ";
43 ";
43
44
44 pub fn args() -> clap::Command {
45 pub fn args() -> clap::Command {
45 clap::command!("status")
46 clap::command!("status")
46 .alias("st")
47 .alias("st")
47 .about(HELP_TEXT)
48 .about(HELP_TEXT)
48 .arg(
49 .arg(
49 Arg::new("all")
50 Arg::new("all")
50 .help("show status of all files")
51 .help("show status of all files")
51 .short('A')
52 .short('A')
52 .action(clap::ArgAction::SetTrue)
53 .action(clap::ArgAction::SetTrue)
53 .long("all"),
54 .long("all"),
54 )
55 )
55 .arg(
56 .arg(
56 Arg::new("modified")
57 Arg::new("modified")
57 .help("show only modified files")
58 .help("show only modified files")
58 .short('m')
59 .short('m')
59 .action(clap::ArgAction::SetTrue)
60 .action(clap::ArgAction::SetTrue)
60 .long("modified"),
61 .long("modified"),
61 )
62 )
62 .arg(
63 .arg(
63 Arg::new("added")
64 Arg::new("added")
64 .help("show only added files")
65 .help("show only added files")
65 .short('a')
66 .short('a')
66 .action(clap::ArgAction::SetTrue)
67 .action(clap::ArgAction::SetTrue)
67 .long("added"),
68 .long("added"),
68 )
69 )
69 .arg(
70 .arg(
70 Arg::new("removed")
71 Arg::new("removed")
71 .help("show only removed files")
72 .help("show only removed files")
72 .short('r')
73 .short('r')
73 .action(clap::ArgAction::SetTrue)
74 .action(clap::ArgAction::SetTrue)
74 .long("removed"),
75 .long("removed"),
75 )
76 )
76 .arg(
77 .arg(
77 Arg::new("clean")
78 Arg::new("clean")
78 .help("show only clean files")
79 .help("show only clean files")
79 .short('c')
80 .short('c')
80 .action(clap::ArgAction::SetTrue)
81 .action(clap::ArgAction::SetTrue)
81 .long("clean"),
82 .long("clean"),
82 )
83 )
83 .arg(
84 .arg(
84 Arg::new("deleted")
85 Arg::new("deleted")
85 .help("show only deleted files")
86 .help("show only deleted files")
86 .short('d')
87 .short('d')
87 .action(clap::ArgAction::SetTrue)
88 .action(clap::ArgAction::SetTrue)
88 .long("deleted"),
89 .long("deleted"),
89 )
90 )
90 .arg(
91 .arg(
91 Arg::new("unknown")
92 Arg::new("unknown")
92 .help("show only unknown (not tracked) files")
93 .help("show only unknown (not tracked) files")
93 .short('u')
94 .short('u')
94 .action(clap::ArgAction::SetTrue)
95 .action(clap::ArgAction::SetTrue)
95 .long("unknown"),
96 .long("unknown"),
96 )
97 )
97 .arg(
98 .arg(
98 Arg::new("ignored")
99 Arg::new("ignored")
99 .help("show only ignored files")
100 .help("show only ignored files")
100 .short('i')
101 .short('i')
101 .action(clap::ArgAction::SetTrue)
102 .action(clap::ArgAction::SetTrue)
102 .long("ignored"),
103 .long("ignored"),
103 )
104 )
104 .arg(
105 .arg(
105 Arg::new("copies")
106 Arg::new("copies")
106 .help("show source of copied files (DEFAULT: ui.statuscopies)")
107 .help("show source of copied files (DEFAULT: ui.statuscopies)")
107 .short('C')
108 .short('C')
108 .action(clap::ArgAction::SetTrue)
109 .action(clap::ArgAction::SetTrue)
109 .long("copies"),
110 .long("copies"),
110 )
111 )
111 .arg(
112 .arg(
112 Arg::new("no-status")
113 Arg::new("no-status")
113 .help("hide status prefix")
114 .help("hide status prefix")
114 .short('n')
115 .short('n')
115 .action(clap::ArgAction::SetTrue)
116 .action(clap::ArgAction::SetTrue)
116 .long("no-status"),
117 .long("no-status"),
117 )
118 )
118 .arg(
119 .arg(
119 Arg::new("verbose")
120 Arg::new("verbose")
120 .help("enable additional output")
121 .help("enable additional output")
121 .short('v')
122 .short('v')
122 .action(clap::ArgAction::SetTrue)
123 .action(clap::ArgAction::SetTrue)
123 .long("verbose"),
124 .long("verbose"),
124 )
125 )
125 }
126 }
126
127
127 /// Pure data type allowing the caller to specify file states to display
128 /// Pure data type allowing the caller to specify file states to display
128 #[derive(Copy, Clone, Debug)]
129 #[derive(Copy, Clone, Debug)]
129 pub struct DisplayStates {
130 pub struct DisplayStates {
130 pub modified: bool,
131 pub modified: bool,
131 pub added: bool,
132 pub added: bool,
132 pub removed: bool,
133 pub removed: bool,
133 pub clean: bool,
134 pub clean: bool,
134 pub deleted: bool,
135 pub deleted: bool,
135 pub unknown: bool,
136 pub unknown: bool,
136 pub ignored: bool,
137 pub ignored: bool,
137 }
138 }
138
139
139 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
140 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
140 modified: true,
141 modified: true,
141 added: true,
142 added: true,
142 removed: true,
143 removed: true,
143 clean: false,
144 clean: false,
144 deleted: true,
145 deleted: true,
145 unknown: true,
146 unknown: true,
146 ignored: false,
147 ignored: false,
147 };
148 };
148
149
149 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
150 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
150 modified: true,
151 modified: true,
151 added: true,
152 added: true,
152 removed: true,
153 removed: true,
153 clean: true,
154 clean: true,
154 deleted: true,
155 deleted: true,
155 unknown: true,
156 unknown: true,
156 ignored: true,
157 ignored: true,
157 };
158 };
158
159
159 impl DisplayStates {
160 impl DisplayStates {
160 pub fn is_empty(&self) -> bool {
161 pub fn is_empty(&self) -> bool {
161 !(self.modified
162 !(self.modified
162 || self.added
163 || self.added
163 || self.removed
164 || self.removed
164 || self.clean
165 || self.clean
165 || self.deleted
166 || self.deleted
166 || self.unknown
167 || self.unknown
167 || self.ignored)
168 || self.ignored)
168 }
169 }
169 }
170 }
170
171
171 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
172 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
172 Ok(repo.dirstate_parents()?.is_merge())
173 Ok(repo.dirstate_parents()?.is_merge())
173 }
174 }
174
175
175 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
176 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
176 // These are all the known values for the [fname] argument of
177 // These are all the known values for the [fname] argument of
177 // [addunfinished] function in [state.py]
178 // [addunfinished] function in [state.py]
178 let known_state_files: &[&str] = &[
179 let known_state_files: &[&str] = &[
179 "bisect.state",
180 "bisect.state",
180 "graftstate",
181 "graftstate",
181 "histedit-state",
182 "histedit-state",
182 "rebasestate",
183 "rebasestate",
183 "shelvedstate",
184 "shelvedstate",
184 "transplant/journal",
185 "transplant/journal",
185 "updatestate",
186 "updatestate",
186 ];
187 ];
187 if has_unfinished_merge(repo)? {
188 if has_unfinished_merge(repo)? {
188 return Ok(true);
189 return Ok(true);
189 };
190 };
190 for f in known_state_files {
191 for f in known_state_files {
191 if repo.hg_vfs().join(f).exists() {
192 if repo.hg_vfs().join(f).exists() {
192 return Ok(true);
193 return Ok(true);
193 }
194 }
194 }
195 }
195 Ok(false)
196 Ok(false)
196 }
197 }
197
198
198 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
199 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
199 // TODO: lift these limitations
200 // TODO: lift these limitations
200 if invocation
201 if invocation
201 .config
202 .config
202 .get(b"commands", b"status.terse")
203 .get(b"commands", b"status.terse")
203 .is_some()
204 .is_some()
204 {
205 {
205 return Err(CommandError::unsupported(
206 return Err(CommandError::unsupported(
206 "status.terse is not yet supported with rhg status",
207 "status.terse is not yet supported with rhg status",
207 ));
208 ));
208 }
209 }
209
210
210 let ui = invocation.ui;
211 let ui = invocation.ui;
211 let config = invocation.config;
212 let config = invocation.config;
212 let args = invocation.subcommand_args;
213 let args = invocation.subcommand_args;
213
214
214 // TODO add `!args.get_flag("print0") &&` when we support `print0`
215 // TODO add `!args.get_flag("print0") &&` when we support `print0`
215 let verbose = args.get_flag("verbose")
216 let verbose = args.get_flag("verbose")
216 || config.get_bool(b"ui", b"verbose")?
217 || config.get_bool(b"ui", b"verbose")?
217 || config.get_bool(b"commands", b"status.verbose")?;
218 || config.get_bool(b"commands", b"status.verbose")?;
218
219
219 let all = args.get_flag("all");
220 let all = args.get_flag("all");
220 let display_states = if all {
221 let display_states = if all {
221 // TODO when implementing `--quiet`: it excludes clean files
222 // TODO when implementing `--quiet`: it excludes clean files
222 // from `--all`
223 // from `--all`
223 ALL_DISPLAY_STATES
224 ALL_DISPLAY_STATES
224 } else {
225 } else {
225 let requested = DisplayStates {
226 let requested = DisplayStates {
226 modified: args.get_flag("modified"),
227 modified: args.get_flag("modified"),
227 added: args.get_flag("added"),
228 added: args.get_flag("added"),
228 removed: args.get_flag("removed"),
229 removed: args.get_flag("removed"),
229 clean: args.get_flag("clean"),
230 clean: args.get_flag("clean"),
230 deleted: args.get_flag("deleted"),
231 deleted: args.get_flag("deleted"),
231 unknown: args.get_flag("unknown"),
232 unknown: args.get_flag("unknown"),
232 ignored: args.get_flag("ignored"),
233 ignored: args.get_flag("ignored"),
233 };
234 };
234 if requested.is_empty() {
235 if requested.is_empty() {
235 DEFAULT_DISPLAY_STATES
236 DEFAULT_DISPLAY_STATES
236 } else {
237 } else {
237 requested
238 requested
238 }
239 }
239 };
240 };
240 let no_status = args.get_flag("no-status");
241 let no_status = args.get_flag("no-status");
241 let list_copies = all
242 let list_copies = all
242 || args.get_flag("copies")
243 || args.get_flag("copies")
243 || config.get_bool(b"ui", b"statuscopies")?;
244 || config.get_bool(b"ui", b"statuscopies")?;
244
245
245 let repo = invocation.repo?;
246 let repo = invocation.repo?;
246
247
247 if verbose && has_unfinished_state(repo)? {
248 if verbose && has_unfinished_state(repo)? {
248 return Err(CommandError::unsupported(
249 return Err(CommandError::unsupported(
249 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
250 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
250 ));
251 ));
251 }
252 }
252
253
253 let mut dmap = repo.dirstate_map_mut()?;
254 let mut dmap = repo.dirstate_map_mut()?;
254
255
255 let check_exec = hg::checkexec::check_exec(repo.working_directory_path());
256 let check_exec = hg::checkexec::check_exec(repo.working_directory_path());
256
257
257 let options = StatusOptions {
258 let options = StatusOptions {
258 check_exec,
259 check_exec,
259 list_clean: display_states.clean,
260 list_clean: display_states.clean,
260 list_unknown: display_states.unknown,
261 list_unknown: display_states.unknown,
261 list_ignored: display_states.ignored,
262 list_ignored: display_states.ignored,
262 list_copies,
263 list_copies,
263 collect_traversed_dirs: false,
264 collect_traversed_dirs: false,
264 };
265 };
265
266
266 type StatusResult<'a> =
267 type StatusResult<'a> =
267 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
268 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
268
269
269 let after_status = |res: StatusResult| -> Result<_, CommandError> {
270 let after_status = |res: StatusResult| -> Result<_, CommandError> {
270 let (mut ds_status, pattern_warnings) = res?;
271 let (mut ds_status, pattern_warnings) = res?;
271 for warning in pattern_warnings {
272 for warning in pattern_warnings {
272 ui.write_stderr(&print_pattern_file_warning(&warning, repo))?;
273 ui.write_stderr(&format_pattern_file_warning(&warning, repo))?;
273 }
274 }
274
275
275 for (path, error) in ds_status.bad {
276 for (path, error) in ds_status.bad {
276 let error = match error {
277 let error = match error {
277 hg::BadMatch::OsError(code) => {
278 hg::BadMatch::OsError(code) => {
278 std::io::Error::from_raw_os_error(code).to_string()
279 std::io::Error::from_raw_os_error(code).to_string()
279 }
280 }
280 hg::BadMatch::BadType(ty) => {
281 hg::BadMatch::BadType(ty) => {
281 format!("unsupported file type (type is {})", ty)
282 format!("unsupported file type (type is {})", ty)
282 }
283 }
283 };
284 };
284 ui.write_stderr(&format_bytes!(
285 ui.write_stderr(&format_bytes!(
285 b"{}: {}\n",
286 b"{}: {}\n",
286 path.as_bytes(),
287 path.as_bytes(),
287 error.as_bytes()
288 error.as_bytes()
288 ))?
289 ))?
289 }
290 }
290 if !ds_status.unsure.is_empty() {
291 if !ds_status.unsure.is_empty() {
291 info!(
292 info!(
292 "Files to be rechecked by retrieval from filelog: {:?}",
293 "Files to be rechecked by retrieval from filelog: {:?}",
293 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
294 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
294 );
295 );
295 }
296 }
296 let mut fixup = Vec::new();
297 let mut fixup = Vec::new();
297 if !ds_status.unsure.is_empty()
298 if !ds_status.unsure.is_empty()
298 && (display_states.modified || display_states.clean)
299 && (display_states.modified || display_states.clean)
299 {
300 {
300 let p1 = repo.dirstate_parents()?.p1;
301 let p1 = repo.dirstate_parents()?.p1;
301 let manifest = repo.manifest_for_node(p1).map_err(|e| {
302 let manifest = repo.manifest_for_node(p1).map_err(|e| {
302 CommandError::from((e, &*format!("{:x}", p1.short())))
303 CommandError::from((e, &*format!("{:x}", p1.short())))
303 })?;
304 })?;
304 let working_directory_vfs = repo.working_directory_vfs();
305 let working_directory_vfs = repo.working_directory_vfs();
305 let store_vfs = repo.store_vfs();
306 let store_vfs = repo.store_vfs();
306 let res: Vec<_> = ds_status
307 let res: Vec<_> = ds_status
307 .unsure
308 .unsure
308 .into_par_iter()
309 .into_par_iter()
309 .map(|to_check| {
310 .map(|to_check| {
310 unsure_is_modified(
311 unsure_is_modified(
311 working_directory_vfs,
312 working_directory_vfs,
312 store_vfs,
313 store_vfs,
313 check_exec,
314 check_exec,
314 &manifest,
315 &manifest,
315 &to_check.path,
316 &to_check.path,
316 )
317 )
317 .map(|modified| (to_check, modified))
318 .map(|modified| (to_check, modified))
318 })
319 })
319 .collect::<Result<_, _>>()?;
320 .collect::<Result<_, _>>()?;
320 for (status_path, is_modified) in res.into_iter() {
321 for (status_path, is_modified) in res.into_iter() {
321 if is_modified {
322 if is_modified {
322 if display_states.modified {
323 if display_states.modified {
323 ds_status.modified.push(status_path);
324 ds_status.modified.push(status_path);
324 }
325 }
325 } else {
326 } else {
326 if display_states.clean {
327 if display_states.clean {
327 ds_status.clean.push(status_path.clone());
328 ds_status.clean.push(status_path.clone());
328 }
329 }
329 fixup.push(status_path.path.into_owned())
330 fixup.push(status_path.path.into_owned())
330 }
331 }
331 }
332 }
332 }
333 }
333 let relative_paths = config
334 let relative_paths = config
334 .get_option(b"commands", b"status.relative")?
335 .get_option(b"commands", b"status.relative")?
335 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
336 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
336 let output = DisplayStatusPaths {
337 let output = DisplayStatusPaths {
337 ui,
338 ui,
338 no_status,
339 no_status,
339 relativize: if relative_paths {
340 relativize: if relative_paths {
340 Some(RelativizePaths::new(repo)?)
341 Some(RelativizePaths::new(repo)?)
341 } else {
342 } else {
342 None
343 None
343 },
344 },
344 };
345 };
345 if display_states.modified {
346 if display_states.modified {
346 output.display(b"M ", "status.modified", ds_status.modified)?;
347 output.display(b"M ", "status.modified", ds_status.modified)?;
347 }
348 }
348 if display_states.added {
349 if display_states.added {
349 output.display(b"A ", "status.added", ds_status.added)?;
350 output.display(b"A ", "status.added", ds_status.added)?;
350 }
351 }
351 if display_states.removed {
352 if display_states.removed {
352 output.display(b"R ", "status.removed", ds_status.removed)?;
353 output.display(b"R ", "status.removed", ds_status.removed)?;
353 }
354 }
354 if display_states.deleted {
355 if display_states.deleted {
355 output.display(b"! ", "status.deleted", ds_status.deleted)?;
356 output.display(b"! ", "status.deleted", ds_status.deleted)?;
356 }
357 }
357 if display_states.unknown {
358 if display_states.unknown {
358 output.display(b"? ", "status.unknown", ds_status.unknown)?;
359 output.display(b"? ", "status.unknown", ds_status.unknown)?;
359 }
360 }
360 if display_states.ignored {
361 if display_states.ignored {
361 output.display(b"I ", "status.ignored", ds_status.ignored)?;
362 output.display(b"I ", "status.ignored", ds_status.ignored)?;
362 }
363 }
363 if display_states.clean {
364 if display_states.clean {
364 output.display(b"C ", "status.clean", ds_status.clean)?;
365 output.display(b"C ", "status.clean", ds_status.clean)?;
365 }
366 }
366
367
367 let dirstate_write_needed = ds_status.dirty;
368 let dirstate_write_needed = ds_status.dirty;
368 let filesystem_time_at_status_start =
369 let filesystem_time_at_status_start =
369 ds_status.filesystem_time_at_status_start;
370 ds_status.filesystem_time_at_status_start;
370
371
371 Ok((
372 Ok((
372 fixup,
373 fixup,
373 dirstate_write_needed,
374 dirstate_write_needed,
374 filesystem_time_at_status_start,
375 filesystem_time_at_status_start,
375 ))
376 ))
376 };
377 };
377 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
378 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
378 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
379 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
379 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
380 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
380 (true, true) => {
381 (true, true) => {
381 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
382 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
382 }
383 }
383 (true, false) => narrow_matcher,
384 (true, false) => narrow_matcher,
384 (false, true) => sparse_matcher,
385 (false, true) => sparse_matcher,
385 (false, false) => Box::new(AlwaysMatcher),
386 (false, false) => Box::new(AlwaysMatcher),
386 };
387 };
387
388
388 for warning in narrow_warnings.into_iter().chain(sparse_warnings) {
389 print_narrow_sparse_warnings(
389 match &warning {
390 &narrow_warnings,
390 sparse::SparseWarning::RootWarning { context, line } => {
391 &sparse_warnings,
391 let msg = format_bytes!(
392 ui,
392 b"warning: {} profile cannot use paths \"
393 repo,
393 starting with /, ignoring {}\n",
394 )?;
394 context,
395 line
396 );
397 ui.write_stderr(&msg)?;
398 }
399 sparse::SparseWarning::ProfileNotFound { profile, rev } => {
400 let msg = format_bytes!(
401 b"warning: sparse profile '{}' not found \"
402 in rev {} - ignoring it\n",
403 profile,
404 rev
405 );
406 ui.write_stderr(&msg)?;
407 }
408 sparse::SparseWarning::Pattern(e) => {
409 ui.write_stderr(&print_pattern_file_warning(e, repo))?;
410 }
411 }
412 }
413 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
395 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
414 dmap.with_status(
396 dmap.with_status(
415 matcher.as_ref(),
397 matcher.as_ref(),
416 repo.working_directory_path().to_owned(),
398 repo.working_directory_path().to_owned(),
417 ignore_files(repo, config),
399 ignore_files(repo, config),
418 options,
400 options,
419 after_status,
401 after_status,
420 )?;
402 )?;
421
403
422 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
404 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
423 && !dirstate_write_needed
405 && !dirstate_write_needed
424 {
406 {
425 // Nothing to update
407 // Nothing to update
426 return Ok(());
408 return Ok(());
427 }
409 }
428
410
429 // Update the dirstate on disk if we can
411 // Update the dirstate on disk if we can
430 let with_lock_result =
412 let with_lock_result =
431 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
413 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
432 if let Some(mtime_boundary) = filesystem_time_at_status_start {
414 if let Some(mtime_boundary) = filesystem_time_at_status_start {
433 for hg_path in fixup {
415 for hg_path in fixup {
434 use std::os::unix::fs::MetadataExt;
416 use std::os::unix::fs::MetadataExt;
435 let fs_path = hg_path_to_path_buf(&hg_path)
417 let fs_path = hg_path_to_path_buf(&hg_path)
436 .expect("HgPath conversion");
418 .expect("HgPath conversion");
437 // Specifically do not reuse `fs_metadata` from
419 // Specifically do not reuse `fs_metadata` from
438 // `unsure_is_clean` which was needed before reading
420 // `unsure_is_clean` which was needed before reading
439 // contents. Here we access metadata again after reading
421 // contents. Here we access metadata again after reading
440 // content, in case it changed in the meantime.
422 // content, in case it changed in the meantime.
441 let fs_metadata = repo
423 let fs_metadata = repo
442 .working_directory_vfs()
424 .working_directory_vfs()
443 .symlink_metadata(&fs_path)?;
425 .symlink_metadata(&fs_path)?;
444 if let Some(mtime) =
426 if let Some(mtime) =
445 TruncatedTimestamp::for_reliable_mtime_of(
427 TruncatedTimestamp::for_reliable_mtime_of(
446 &fs_metadata,
428 &fs_metadata,
447 &mtime_boundary,
429 &mtime_boundary,
448 )
430 )
449 .when_reading_file(&fs_path)?
431 .when_reading_file(&fs_path)?
450 {
432 {
451 let mode = fs_metadata.mode();
433 let mode = fs_metadata.mode();
452 let size = fs_metadata.len();
434 let size = fs_metadata.len();
453 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
435 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
454 dirstate_write_needed = true
436 dirstate_write_needed = true
455 }
437 }
456 }
438 }
457 }
439 }
458 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
440 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
459 if dirstate_write_needed {
441 if dirstate_write_needed {
460 repo.write_dirstate()?
442 repo.write_dirstate()?
461 }
443 }
462 Ok(())
444 Ok(())
463 });
445 });
464 match with_lock_result {
446 match with_lock_result {
465 Ok(closure_result) => closure_result?,
447 Ok(closure_result) => closure_result?,
466 Err(LockError::AlreadyHeld) => {
448 Err(LockError::AlreadyHeld) => {
467 // Not updating the dirstate is not ideal but not critical:
449 // Not updating the dirstate is not ideal but not critical:
468 // don’t keep our caller waiting until some other Mercurial
450 // don’t keep our caller waiting until some other Mercurial
469 // process releases the lock.
451 // process releases the lock.
470 }
452 }
471 Err(LockError::Other(HgError::IoError { error, .. }))
453 Err(LockError::Other(HgError::IoError { error, .. }))
472 if error.kind() == io::ErrorKind::PermissionDenied =>
454 if error.kind() == io::ErrorKind::PermissionDenied =>
473 {
455 {
474 // `hg status` on a read-only repository is fine
456 // `hg status` on a read-only repository is fine
475 }
457 }
476 Err(LockError::Other(error)) => {
458 Err(LockError::Other(error)) => {
477 // Report other I/O errors
459 // Report other I/O errors
478 Err(error)?
460 Err(error)?
479 }
461 }
480 }
462 }
481 Ok(())
463 Ok(())
482 }
464 }
483
465
484 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
466 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
485 let mut ignore_files = Vec::new();
467 let mut ignore_files = Vec::new();
486 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
468 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
487 if repo_ignore.exists() {
469 if repo_ignore.exists() {
488 ignore_files.push(repo_ignore)
470 ignore_files.push(repo_ignore)
489 }
471 }
490 for (key, value) in config.iter_section(b"ui") {
472 for (key, value) in config.iter_section(b"ui") {
491 if key == b"ignore" || key.starts_with(b"ignore.") {
473 if key == b"ignore" || key.starts_with(b"ignore.") {
492 let path = get_path_from_bytes(value);
474 let path = get_path_from_bytes(value);
493 // TODO: expand "~/" and environment variable here, like Python
475 // TODO: expand "~/" and environment variable here, like Python
494 // does with `os.path.expanduser` and `os.path.expandvars`
476 // does with `os.path.expanduser` and `os.path.expandvars`
495
477
496 let joined = repo.working_directory_path().join(path);
478 let joined = repo.working_directory_path().join(path);
497 ignore_files.push(joined);
479 ignore_files.push(joined);
498 }
480 }
499 }
481 }
500 ignore_files
482 ignore_files
501 }
483 }
502
484
503 struct DisplayStatusPaths<'a> {
485 struct DisplayStatusPaths<'a> {
504 ui: &'a Ui,
486 ui: &'a Ui,
505 no_status: bool,
487 no_status: bool,
506 relativize: Option<RelativizePaths>,
488 relativize: Option<RelativizePaths>,
507 }
489 }
508
490
509 impl DisplayStatusPaths<'_> {
491 impl DisplayStatusPaths<'_> {
510 // Probably more elegant to use a Deref or Borrow trait rather than
492 // Probably more elegant to use a Deref or Borrow trait rather than
511 // harcode HgPathBuf, but probably not really useful at this point
493 // harcode HgPathBuf, but probably not really useful at this point
512 fn display(
494 fn display(
513 &self,
495 &self,
514 status_prefix: &[u8],
496 status_prefix: &[u8],
515 label: &'static str,
497 label: &'static str,
516 mut paths: Vec<StatusPath<'_>>,
498 mut paths: Vec<StatusPath<'_>>,
517 ) -> Result<(), CommandError> {
499 ) -> Result<(), CommandError> {
518 paths.sort_unstable();
500 paths.sort_unstable();
519 // TODO: get the stdout lock once for the whole loop
501 // TODO: get the stdout lock once for the whole loop
520 // instead of in each write
502 // instead of in each write
521 for StatusPath { path, copy_source } in paths {
503 for StatusPath { path, copy_source } in paths {
522 let relative;
504 let relative;
523 let path = if let Some(relativize) = &self.relativize {
505 let path = if let Some(relativize) = &self.relativize {
524 relative = relativize.relativize(&path);
506 relative = relativize.relativize(&path);
525 &*relative
507 &*relative
526 } else {
508 } else {
527 path.as_bytes()
509 path.as_bytes()
528 };
510 };
529 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
511 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
530 // in order to stream to stdout instead of allocating an
512 // in order to stream to stdout instead of allocating an
531 // itermediate `Vec<u8>`.
513 // itermediate `Vec<u8>`.
532 if !self.no_status {
514 if !self.no_status {
533 self.ui.write_stdout_labelled(status_prefix, label)?
515 self.ui.write_stdout_labelled(status_prefix, label)?
534 }
516 }
535 self.ui
517 self.ui
536 .write_stdout_labelled(&format_bytes!(b"{}\n", path), label)?;
518 .write_stdout_labelled(&format_bytes!(b"{}\n", path), label)?;
537 if let Some(source) = copy_source {
519 if let Some(source) = copy_source {
538 let label = "status.copied";
520 let label = "status.copied";
539 self.ui.write_stdout_labelled(
521 self.ui.write_stdout_labelled(
540 &format_bytes!(b" {}\n", source.as_bytes()),
522 &format_bytes!(b" {}\n", source.as_bytes()),
541 label,
523 label,
542 )?
524 )?
543 }
525 }
544 }
526 }
545 Ok(())
527 Ok(())
546 }
528 }
547 }
529 }
548
530
549 /// Check if a file is modified by comparing actual repo store and file system.
531 /// Check if a file is modified by comparing actual repo store and file system.
550 ///
532 ///
551 /// This meant to be used for those that the dirstate cannot resolve, due
533 /// This meant to be used for those that the dirstate cannot resolve, due
552 /// to time resolution limits.
534 /// to time resolution limits.
553 fn unsure_is_modified(
535 fn unsure_is_modified(
554 working_directory_vfs: hg::vfs::Vfs,
536 working_directory_vfs: hg::vfs::Vfs,
555 store_vfs: hg::vfs::Vfs,
537 store_vfs: hg::vfs::Vfs,
556 check_exec: bool,
538 check_exec: bool,
557 manifest: &Manifest,
539 manifest: &Manifest,
558 hg_path: &HgPath,
540 hg_path: &HgPath,
559 ) -> Result<bool, HgError> {
541 ) -> Result<bool, HgError> {
560 let vfs = working_directory_vfs;
542 let vfs = working_directory_vfs;
561 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
543 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
562 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
544 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
563 let is_symlink = fs_metadata.file_type().is_symlink();
545 let is_symlink = fs_metadata.file_type().is_symlink();
564
546
565 let entry = manifest
547 let entry = manifest
566 .find_by_path(hg_path)?
548 .find_by_path(hg_path)?
567 .expect("ambgious file not in p1");
549 .expect("ambgious file not in p1");
568
550
569 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
551 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
570 // dirstate
552 // dirstate
571 let fs_flags = if is_symlink {
553 let fs_flags = if is_symlink {
572 Some(b'l')
554 Some(b'l')
573 } else if check_exec && has_exec_bit(&fs_metadata) {
555 } else if check_exec && has_exec_bit(&fs_metadata) {
574 Some(b'x')
556 Some(b'x')
575 } else {
557 } else {
576 None
558 None
577 };
559 };
578
560
579 let entry_flags = if check_exec {
561 let entry_flags = if check_exec {
580 entry.flags
562 entry.flags
581 } else if entry.flags == Some(b'x') {
563 } else if entry.flags == Some(b'x') {
582 None
564 None
583 } else {
565 } else {
584 entry.flags
566 entry.flags
585 };
567 };
586
568
587 if entry_flags != fs_flags {
569 if entry_flags != fs_flags {
588 return Ok(true);
570 return Ok(true);
589 }
571 }
590 let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;
572 let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;
591 let fs_len = fs_metadata.len();
573 let fs_len = fs_metadata.len();
592 let file_node = entry.node_id()?;
574 let file_node = entry.node_id()?;
593 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
575 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
594 HgError::corrupted(format!(
576 HgError::corrupted(format!(
595 "filelog missing node {:?} from manifest",
577 "filelog missing node {:?} from manifest",
596 file_node
578 file_node
597 ))
579 ))
598 })?;
580 })?;
599 if filelog_entry.file_data_len_not_equal_to(fs_len) {
581 if filelog_entry.file_data_len_not_equal_to(fs_len) {
600 // No need to read file contents:
582 // No need to read file contents:
601 // it cannot be equal if it has a different length.
583 // it cannot be equal if it has a different length.
602 return Ok(true);
584 return Ok(true);
603 }
585 }
604
586
605 let p1_filelog_data = filelog_entry.data()?;
587 let p1_filelog_data = filelog_entry.data()?;
606 let p1_contents = p1_filelog_data.file_data()?;
588 let p1_contents = p1_filelog_data.file_data()?;
607 if p1_contents.len() as u64 != fs_len {
589 if p1_contents.len() as u64 != fs_len {
608 // No need to read file contents:
590 // No need to read file contents:
609 // it cannot be equal if it has a different length.
591 // it cannot be equal if it has a different length.
610 return Ok(true);
592 return Ok(true);
611 }
593 }
612
594
613 let fs_contents = if is_symlink {
595 let fs_contents = if is_symlink {
614 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
596 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
615 } else {
597 } else {
616 vfs.read(fs_path)?
598 vfs.read(fs_path)?
617 };
599 };
618 Ok(p1_contents != &*fs_contents)
600 Ok(p1_contents != &*fs_contents)
619 }
601 }
620
621 fn print_pattern_file_warning(
622 warning: &PatternFileWarning,
623 repo: &Repo,
624 ) -> Vec<u8> {
625 match warning {
626 PatternFileWarning::InvalidSyntax(path, syntax) => format_bytes!(
627 b"{}: ignoring invalid syntax '{}'\n",
628 get_bytes_from_path(path),
629 &*syntax
630 ),
631 PatternFileWarning::NoSuchFile(path) => {
632 let path = if let Ok(relative) =
633 path.strip_prefix(repo.working_directory_path())
634 {
635 relative
636 } else {
637 &*path
638 };
639 format_bytes!(
640 b"skipping unreadable pattern file '{}': \
641 No such file or directory\n",
642 get_bytes_from_path(path),
643 )
644 }
645 }
646 }
@@ -1,225 +1,295 b''
1 use crate::color::ColorConfig;
1 use crate::color::ColorConfig;
2 use crate::color::Effect;
2 use crate::color::Effect;
3 use crate::error::CommandError;
3 use format_bytes::format_bytes;
4 use format_bytes::format_bytes;
4 use format_bytes::write_bytes;
5 use format_bytes::write_bytes;
5 use hg::config::Config;
6 use hg::config::Config;
6 use hg::config::PlainInfo;
7 use hg::config::PlainInfo;
7 use hg::errors::HgError;
8 use hg::errors::HgError;
9 use hg::repo::Repo;
10 use hg::sparse;
11 use hg::utils::files::get_bytes_from_path;
12 use hg::PatternFileWarning;
8 use std::borrow::Cow;
13 use std::borrow::Cow;
9 use std::io;
14 use std::io;
10 use std::io::{ErrorKind, Write};
15 use std::io::{ErrorKind, Write};
11
16
12 pub struct Ui {
17 pub struct Ui {
13 stdout: std::io::Stdout,
18 stdout: std::io::Stdout,
14 stderr: std::io::Stderr,
19 stderr: std::io::Stderr,
15 colors: Option<ColorConfig>,
20 colors: Option<ColorConfig>,
16 }
21 }
17
22
18 /// The kind of user interface error
23 /// The kind of user interface error
19 pub enum UiError {
24 pub enum UiError {
20 /// The standard output stream cannot be written to
25 /// The standard output stream cannot be written to
21 StdoutError(io::Error),
26 StdoutError(io::Error),
22 /// The standard error stream cannot be written to
27 /// The standard error stream cannot be written to
23 StderrError(io::Error),
28 StderrError(io::Error),
24 }
29 }
25
30
26 /// The commandline user interface
31 /// The commandline user interface
27 impl Ui {
32 impl Ui {
28 pub fn new(config: &Config) -> Result<Self, HgError> {
33 pub fn new(config: &Config) -> Result<Self, HgError> {
29 Ok(Ui {
34 Ok(Ui {
30 // If using something else, also adapt `isatty()` below.
35 // If using something else, also adapt `isatty()` below.
31 stdout: std::io::stdout(),
36 stdout: std::io::stdout(),
32
37
33 stderr: std::io::stderr(),
38 stderr: std::io::stderr(),
34 colors: ColorConfig::new(config)?,
39 colors: ColorConfig::new(config)?,
35 })
40 })
36 }
41 }
37
42
38 /// Default to no color if color configuration errors.
43 /// Default to no color if color configuration errors.
39 ///
44 ///
40 /// Useful when we’re already handling another error.
45 /// Useful when we’re already handling another error.
41 pub fn new_infallible(config: &Config) -> Self {
46 pub fn new_infallible(config: &Config) -> Self {
42 Ui {
47 Ui {
43 // If using something else, also adapt `isatty()` below.
48 // If using something else, also adapt `isatty()` below.
44 stdout: std::io::stdout(),
49 stdout: std::io::stdout(),
45
50
46 stderr: std::io::stderr(),
51 stderr: std::io::stderr(),
47 colors: ColorConfig::new(config).unwrap_or(None),
52 colors: ColorConfig::new(config).unwrap_or(None),
48 }
53 }
49 }
54 }
50
55
51 /// Returns a buffered handle on stdout for faster batch printing
56 /// Returns a buffered handle on stdout for faster batch printing
52 /// operations.
57 /// operations.
53 pub fn stdout_buffer(&self) -> StdoutBuffer<std::io::StdoutLock> {
58 pub fn stdout_buffer(&self) -> StdoutBuffer<std::io::StdoutLock> {
54 StdoutBuffer::new(self.stdout.lock())
59 StdoutBuffer::new(self.stdout.lock())
55 }
60 }
56
61
57 /// Write bytes to stdout
62 /// Write bytes to stdout
58 pub fn write_stdout(&self, bytes: &[u8]) -> Result<(), UiError> {
63 pub fn write_stdout(&self, bytes: &[u8]) -> Result<(), UiError> {
59 let mut stdout = self.stdout.lock();
64 let mut stdout = self.stdout.lock();
60
65
61 stdout.write_all(bytes).or_else(handle_stdout_error)?;
66 stdout.write_all(bytes).or_else(handle_stdout_error)?;
62
67
63 stdout.flush().or_else(handle_stdout_error)
68 stdout.flush().or_else(handle_stdout_error)
64 }
69 }
65
70
66 /// Write bytes to stderr
71 /// Write bytes to stderr
67 pub fn write_stderr(&self, bytes: &[u8]) -> Result<(), UiError> {
72 pub fn write_stderr(&self, bytes: &[u8]) -> Result<(), UiError> {
68 let mut stderr = self.stderr.lock();
73 let mut stderr = self.stderr.lock();
69
74
70 stderr.write_all(bytes).or_else(handle_stderr_error)?;
75 stderr.write_all(bytes).or_else(handle_stderr_error)?;
71
76
72 stderr.flush().or_else(handle_stderr_error)
77 stderr.flush().or_else(handle_stderr_error)
73 }
78 }
74
79
75 /// Write bytes to stdout with the given label
80 /// Write bytes to stdout with the given label
76 ///
81 ///
77 /// Like the optional `label` parameter in `mercurial/ui.py`,
82 /// Like the optional `label` parameter in `mercurial/ui.py`,
78 /// this label influences the color used for this output.
83 /// this label influences the color used for this output.
79 pub fn write_stdout_labelled(
84 pub fn write_stdout_labelled(
80 &self,
85 &self,
81 bytes: &[u8],
86 bytes: &[u8],
82 label: &str,
87 label: &str,
83 ) -> Result<(), UiError> {
88 ) -> Result<(), UiError> {
84 if let Some(colors) = &self.colors {
89 if let Some(colors) = &self.colors {
85 if let Some(effects) = colors.styles.get(label.as_bytes()) {
90 if let Some(effects) = colors.styles.get(label.as_bytes()) {
86 if !effects.is_empty() {
91 if !effects.is_empty() {
87 return self
92 return self
88 .write_stdout_with_effects(bytes, effects)
93 .write_stdout_with_effects(bytes, effects)
89 .or_else(handle_stdout_error);
94 .or_else(handle_stdout_error);
90 }
95 }
91 }
96 }
92 }
97 }
93 self.write_stdout(bytes)
98 self.write_stdout(bytes)
94 }
99 }
95
100
96 fn write_stdout_with_effects(
101 fn write_stdout_with_effects(
97 &self,
102 &self,
98 bytes: &[u8],
103 bytes: &[u8],
99 effects: &[Effect],
104 effects: &[Effect],
100 ) -> io::Result<()> {
105 ) -> io::Result<()> {
101 let stdout = &mut self.stdout.lock();
106 let stdout = &mut self.stdout.lock();
102 let mut write_line = |line: &[u8], first: bool| {
107 let mut write_line = |line: &[u8], first: bool| {
103 // `line` does not include the newline delimiter
108 // `line` does not include the newline delimiter
104 if !first {
109 if !first {
105 stdout.write_all(b"\n")?;
110 stdout.write_all(b"\n")?;
106 }
111 }
107 if line.is_empty() {
112 if line.is_empty() {
108 return Ok(());
113 return Ok(());
109 }
114 }
110 /// 0x1B == 27 == 0o33
115 /// 0x1B == 27 == 0o33
111 const ASCII_ESCAPE: &[u8] = b"\x1b";
116 const ASCII_ESCAPE: &[u8] = b"\x1b";
112 write_bytes!(stdout, b"{}[0", ASCII_ESCAPE)?;
117 write_bytes!(stdout, b"{}[0", ASCII_ESCAPE)?;
113 for effect in effects {
118 for effect in effects {
114 write_bytes!(stdout, b";{}", effect)?;
119 write_bytes!(stdout, b";{}", effect)?;
115 }
120 }
116 write_bytes!(stdout, b"m")?;
121 write_bytes!(stdout, b"m")?;
117 stdout.write_all(line)?;
122 stdout.write_all(line)?;
118 write_bytes!(stdout, b"{}[0m", ASCII_ESCAPE)
123 write_bytes!(stdout, b"{}[0m", ASCII_ESCAPE)
119 };
124 };
120 let mut lines = bytes.split(|&byte| byte == b'\n');
125 let mut lines = bytes.split(|&byte| byte == b'\n');
121 if let Some(first) = lines.next() {
126 if let Some(first) = lines.next() {
122 write_line(first, true)?;
127 write_line(first, true)?;
123 for line in lines {
128 for line in lines {
124 write_line(line, false)?
129 write_line(line, false)?
125 }
130 }
126 }
131 }
127 stdout.flush()
132 stdout.flush()
128 }
133 }
129 }
134 }
130
135
131 // TODO: pass the PlainInfo to call sites directly and
136 // TODO: pass the PlainInfo to call sites directly and
132 // delete this function
137 // delete this function
133 pub fn plain(opt_feature: Option<&str>) -> bool {
138 pub fn plain(opt_feature: Option<&str>) -> bool {
134 let plain_info = PlainInfo::from_env();
139 let plain_info = PlainInfo::from_env();
135 match opt_feature {
140 match opt_feature {
136 None => plain_info.is_plain(),
141 None => plain_info.is_plain(),
137 Some(feature) => plain_info.is_feature_plain(feature),
142 Some(feature) => plain_info.is_feature_plain(feature),
138 }
143 }
139 }
144 }
140
145
141 /// A buffered stdout writer for faster batch printing operations.
146 /// A buffered stdout writer for faster batch printing operations.
142 pub struct StdoutBuffer<W: Write> {
147 pub struct StdoutBuffer<W: Write> {
143 buf: io::BufWriter<W>,
148 buf: io::BufWriter<W>,
144 }
149 }
145
150
146 impl<W: Write> StdoutBuffer<W> {
151 impl<W: Write> StdoutBuffer<W> {
147 pub fn new(writer: W) -> Self {
152 pub fn new(writer: W) -> Self {
148 let buf = io::BufWriter::new(writer);
153 let buf = io::BufWriter::new(writer);
149 Self { buf }
154 Self { buf }
150 }
155 }
151
156
152 /// Write bytes to stdout buffer
157 /// Write bytes to stdout buffer
153 pub fn write_all(&mut self, bytes: &[u8]) -> Result<(), UiError> {
158 pub fn write_all(&mut self, bytes: &[u8]) -> Result<(), UiError> {
154 self.buf.write_all(bytes).or_else(handle_stdout_error)
159 self.buf.write_all(bytes).or_else(handle_stdout_error)
155 }
160 }
156
161
157 /// Flush bytes to stdout
162 /// Flush bytes to stdout
158 pub fn flush(&mut self) -> Result<(), UiError> {
163 pub fn flush(&mut self) -> Result<(), UiError> {
159 self.buf.flush().or_else(handle_stdout_error)
164 self.buf.flush().or_else(handle_stdout_error)
160 }
165 }
161 }
166 }
162
167
163 /// Sometimes writing to stdout is not possible, try writing to stderr to
168 /// Sometimes writing to stdout is not possible, try writing to stderr to
164 /// signal that failure, otherwise just bail.
169 /// signal that failure, otherwise just bail.
165 fn handle_stdout_error(error: io::Error) -> Result<(), UiError> {
170 fn handle_stdout_error(error: io::Error) -> Result<(), UiError> {
166 if let ErrorKind::BrokenPipe = error.kind() {
171 if let ErrorKind::BrokenPipe = error.kind() {
167 // This makes `| head` work for example
172 // This makes `| head` work for example
168 return Ok(());
173 return Ok(());
169 }
174 }
170 let mut stderr = io::stderr();
175 let mut stderr = io::stderr();
171
176
172 stderr
177 stderr
173 .write_all(&format_bytes!(
178 .write_all(&format_bytes!(
174 b"abort: {}\n",
179 b"abort: {}\n",
175 error.to_string().as_bytes()
180 error.to_string().as_bytes()
176 ))
181 ))
177 .map_err(UiError::StderrError)?;
182 .map_err(UiError::StderrError)?;
178
183
179 stderr.flush().map_err(UiError::StderrError)?;
184 stderr.flush().map_err(UiError::StderrError)?;
180
185
181 Err(UiError::StdoutError(error))
186 Err(UiError::StdoutError(error))
182 }
187 }
183
188
184 /// Sometimes writing to stderr is not possible.
189 /// Sometimes writing to stderr is not possible.
185 fn handle_stderr_error(error: io::Error) -> Result<(), UiError> {
190 fn handle_stderr_error(error: io::Error) -> Result<(), UiError> {
186 // A broken pipe should not result in a error
191 // A broken pipe should not result in a error
187 // like with `| head` for example
192 // like with `| head` for example
188 if let ErrorKind::BrokenPipe = error.kind() {
193 if let ErrorKind::BrokenPipe = error.kind() {
189 return Ok(());
194 return Ok(());
190 }
195 }
191 Err(UiError::StdoutError(error))
196 Err(UiError::StdoutError(error))
192 }
197 }
193
198
194 /// Encode rust strings according to the user system.
199 /// Encode rust strings according to the user system.
195 pub fn utf8_to_local(s: &str) -> Cow<[u8]> {
200 pub fn utf8_to_local(s: &str) -> Cow<[u8]> {
196 // TODO encode for the user's system //
201 // TODO encode for the user's system //
197 let bytes = s.as_bytes();
202 let bytes = s.as_bytes();
198 Cow::Borrowed(bytes)
203 Cow::Borrowed(bytes)
199 }
204 }
200
205
201 /// Decode user system bytes to Rust string.
206 /// Decode user system bytes to Rust string.
202 pub fn local_to_utf8(s: &[u8]) -> Cow<str> {
207 pub fn local_to_utf8(s: &[u8]) -> Cow<str> {
203 // TODO decode from the user's system
208 // TODO decode from the user's system
204 String::from_utf8_lossy(s)
209 String::from_utf8_lossy(s)
205 }
210 }
206
211
207 /// Should formatted output be used?
212 /// Should formatted output be used?
208 ///
213 ///
209 /// Note: rhg does not have the formatter mechanism yet,
214 /// Note: rhg does not have the formatter mechanism yet,
210 /// but this is also used when deciding whether to use color.
215 /// but this is also used when deciding whether to use color.
211 pub fn formatted(config: &Config) -> Result<bool, HgError> {
216 pub fn formatted(config: &Config) -> Result<bool, HgError> {
212 if let Some(formatted) = config.get_option(b"ui", b"formatted")? {
217 if let Some(formatted) = config.get_option(b"ui", b"formatted")? {
213 Ok(formatted)
218 Ok(formatted)
214 } else {
219 } else {
215 isatty(config)
220 isatty(config)
216 }
221 }
217 }
222 }
218
223
219 fn isatty(config: &Config) -> Result<bool, HgError> {
224 fn isatty(config: &Config) -> Result<bool, HgError> {
220 Ok(if config.get_bool(b"ui", b"nontty")? {
225 Ok(if config.get_bool(b"ui", b"nontty")? {
221 false
226 false
222 } else {
227 } else {
223 atty::is(atty::Stream::Stdout)
228 atty::is(atty::Stream::Stdout)
224 })
229 })
225 }
230 }
231
232 /// Return the formatted bytestring corresponding to a pattern file warning,
233 /// as expected by the CLI.
234 pub(crate) fn format_pattern_file_warning(
235 warning: &PatternFileWarning,
236 repo: &Repo,
237 ) -> Vec<u8> {
238 match warning {
239 PatternFileWarning::InvalidSyntax(path, syntax) => format_bytes!(
240 b"{}: ignoring invalid syntax '{}'\n",
241 get_bytes_from_path(path),
242 &*syntax
243 ),
244 PatternFileWarning::NoSuchFile(path) => {
245 let path = if let Ok(relative) =
246 path.strip_prefix(repo.working_directory_path())
247 {
248 relative
249 } else {
250 &*path
251 };
252 format_bytes!(
253 b"skipping unreadable pattern file '{}': \
254 No such file or directory\n",
255 get_bytes_from_path(path),
256 )
257 }
258 }
259 }
260
261 /// Print with `Ui` the formatted bytestring corresponding to a
262 /// sparse/narrow warning, as expected by the CLI.
263 pub(crate) fn print_narrow_sparse_warnings(
264 narrow_warnings: &[sparse::SparseWarning],
265 sparse_warnings: &[sparse::SparseWarning],
266 ui: &Ui,
267 repo: &Repo,
268 ) -> Result<(), CommandError> {
269 for warning in narrow_warnings.iter().chain(sparse_warnings) {
270 match &warning {
271 sparse::SparseWarning::RootWarning { context, line } => {
272 let msg = format_bytes!(
273 b"warning: {} profile cannot use paths \"
274 starting with /, ignoring {}\n",
275 context,
276 line
277 );
278 ui.write_stderr(&msg)?;
279 }
280 sparse::SparseWarning::ProfileNotFound { profile, rev } => {
281 let msg = format_bytes!(
282 b"warning: sparse profile '{}' not found \"
283 in rev {} - ignoring it\n",
284 profile,
285 rev
286 );
287 ui.write_stderr(&msg)?;
288 }
289 sparse::SparseWarning::Pattern(e) => {
290 ui.write_stderr(&format_pattern_file_warning(e, repo))?;
291 }
292 }
293 }
294 Ok(())
295 }
General Comments 0
You need to be logged in to leave comments. Login now