##// END OF EJS Templates
rhg: Remove error message on unsupported CLI arguments...
Simon Sapin -
r47333:21d3b40b default
parent child Browse files
Show More
@@ -1,115 +1,123
1 1 use crate::ui::utf8_to_local;
2 2 use crate::ui::UiError;
3 3 use format_bytes::format_bytes;
4 4 use hg::config::{ConfigError, ConfigParseError};
5 5 use hg::errors::HgError;
6 6 use hg::repo::RepoError;
7 7 use hg::revlog::revlog::RevlogError;
8 8 use hg::utils::files::get_bytes_from_path;
9 9 use std::convert::From;
10 10
11 11 /// The kind of command error
12 12 #[derive(Debug)]
13 13 pub enum CommandError {
14 14 /// Exit with an error message and "standard" failure exit code.
15 15 Abort { message: Vec<u8> },
16 16
17 17 /// A mercurial capability as not been implemented.
18 18 ///
19 19 /// There is no error message printed in this case.
20 20 /// Instead, we exit with a specic status code and a wrapper script may
21 21 /// fallback to Python-based Mercurial.
22 22 Unimplemented,
23 23 }
24 24
25 25 impl CommandError {
26 26 pub fn abort(message: impl AsRef<str>) -> Self {
27 27 CommandError::Abort {
28 28 // TODO: bytes-based (instead of Unicode-based) formatting
29 29 // of error messages to handle non-UTF-8 filenames etc:
30 30 // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output
31 31 message: utf8_to_local(message.as_ref()).into(),
32 32 }
33 33 }
34 34 }
35 35
36 /// For now we don’t differenciate between invalid CLI args and valid for `hg`
37 /// but not supported yet by `rhg`.
38 impl From<clap::Error> for CommandError {
39 fn from(_: clap::Error) -> Self {
40 CommandError::Unimplemented
41 }
42 }
43
36 44 impl From<HgError> for CommandError {
37 45 fn from(error: HgError) -> Self {
38 46 match error {
39 47 HgError::UnsupportedFeature(_) => CommandError::Unimplemented,
40 48 _ => CommandError::abort(error.to_string()),
41 49 }
42 50 }
43 51 }
44 52
45 53 impl From<UiError> for CommandError {
46 54 fn from(_error: UiError) -> Self {
47 55 // If we already failed writing to stdout or stderr,
48 56 // writing an error message to stderr about it would be likely to fail
49 57 // too.
50 58 CommandError::abort("")
51 59 }
52 60 }
53 61
54 62 impl From<RepoError> for CommandError {
55 63 fn from(error: RepoError) -> Self {
56 64 match error {
57 65 RepoError::NotFound { at } => CommandError::Abort {
58 66 message: format_bytes!(
59 67 b"no repository found in '{}' (.hg not found)!",
60 68 get_bytes_from_path(at)
61 69 ),
62 70 },
63 71 RepoError::ConfigParseError(error) => error.into(),
64 72 RepoError::Other(error) => error.into(),
65 73 }
66 74 }
67 75 }
68 76
69 77 impl From<ConfigError> for CommandError {
70 78 fn from(error: ConfigError) -> Self {
71 79 match error {
72 80 ConfigError::Parse(error) => error.into(),
73 81 ConfigError::Other(error) => error.into(),
74 82 }
75 83 }
76 84 }
77 85
78 86 impl From<ConfigParseError> for CommandError {
79 87 fn from(error: ConfigParseError) -> Self {
80 88 let ConfigParseError {
81 89 origin,
82 90 line,
83 91 bytes,
84 92 } = error;
85 93 let line_message = if let Some(line_number) = line {
86 94 format_bytes!(b" at line {}", line_number.to_string().into_bytes())
87 95 } else {
88 96 Vec::new()
89 97 };
90 98 CommandError::Abort {
91 99 message: format_bytes!(
92 100 b"config parse error in {}{}: '{}'",
93 101 origin,
94 102 line_message,
95 103 bytes
96 104 ),
97 105 }
98 106 }
99 107 }
100 108
101 109 impl From<(RevlogError, &str)> for CommandError {
102 110 fn from((err, rev): (RevlogError, &str)) -> CommandError {
103 111 match err {
104 112 RevlogError::InvalidRevision => CommandError::abort(format!(
105 113 "invalid revision identifier {}",
106 114 rev
107 115 )),
108 116 RevlogError::AmbiguousPrefix => CommandError::abort(format!(
109 117 "ambiguous revision identifier {}",
110 118 rev
111 119 )),
112 120 RevlogError::Other(error) => error.into(),
113 121 }
114 122 }
115 123 }
@@ -1,138 +1,135
1 1 extern crate log;
2 2 use clap::App;
3 3 use clap::AppSettings;
4 4 use clap::Arg;
5 5 use clap::ArgMatches;
6 6 use format_bytes::format_bytes;
7 7 use std::path::Path;
8 8
9 9 mod error;
10 10 mod exitcode;
11 11 mod ui;
12 12 use error::CommandError;
13 13
14 14 fn add_global_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
15 15 app.arg(
16 16 Arg::with_name("repository")
17 17 .help("repository root directory")
18 18 .short("-R")
19 19 .long("--repository")
20 20 .value_name("REPO")
21 21 .takes_value(true),
22 22 )
23 23 .arg(
24 24 Arg::with_name("config")
25 25 .help("set/override config option (use 'section.name=value')")
26 26 .long("--config")
27 27 .value_name("CONFIG")
28 28 .takes_value(true)
29 29 // Ok: `--config section.key1=val --config section.key2=val2`
30 30 .multiple(true)
31 31 // Not ok: `--config section.key1=val section.key2=val2`
32 32 .number_of_values(1),
33 33 )
34 34 }
35 35
36 fn main() {
36 fn main_with_result(ui: &ui::Ui) -> Result<(), CommandError> {
37 37 env_logger::init();
38 38 let app = App::new("rhg")
39 39 .setting(AppSettings::AllowInvalidUtf8)
40 40 .setting(AppSettings::SubcommandRequired)
41 41 .setting(AppSettings::VersionlessSubcommands)
42 42 .version("0.0.1");
43 43 let app = add_global_args(app);
44 44 let app = add_subcommand_args(app);
45 45
46 let ui = ui::Ui::new();
47
48 let matches = app.clone().get_matches_safe().unwrap_or_else(|err| {
49 let _ = ui.writeln_stderr_str(&err.message);
50 std::process::exit(exitcode::UNIMPLEMENTED)
51 });
46 let matches = app.clone().get_matches_safe()?;
52 47
53 48 let (subcommand_name, subcommand_matches) = matches.subcommand();
54 49 let run = subcommand_run_fn(subcommand_name)
55 50 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
56 51 let args = subcommand_matches
57 52 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
58 53
59 54 // Global arguments can be in either based on e.g. `hg -R ./foo log` v.s.
60 55 // `hg log -R ./foo`
61 56 let value_of_global_arg =
62 57 |name| args.value_of_os(name).or_else(|| matches.value_of_os(name));
63 58 // For arguments where multiple occurences are allowed, return a
64 59 // possibly-iterator of all values.
65 60 let values_of_global_arg = |name: &str| {
66 61 let a = matches.values_of_os(name).into_iter().flatten();
67 62 let b = args.values_of_os(name).into_iter().flatten();
68 63 a.chain(b)
69 64 };
70 65
71 66 let repo_path = value_of_global_arg("repository").map(Path::new);
72 let result = (|| -> Result<(), CommandError> {
73 67 let config_args = values_of_global_arg("config")
74 68 // `get_bytes_from_path` works for OsStr the same as for Path
75 69 .map(hg::utils::files::get_bytes_from_path);
76 70 let config = hg::config::Config::load(config_args)?;
77 71 run(&ui, &config, repo_path, args)
78 })();
72 }
79 73
80 let exit_code = match result {
81 Ok(_) => exitcode::OK,
74 fn main() {
75 let ui = ui::Ui::new();
76
77 let exit_code = match main_with_result(&ui) {
78 Ok(()) => exitcode::OK,
82 79
83 80 // Exit with a specific code and no error message to let a potential
84 81 // wrapper script fallback to Python-based Mercurial.
85 82 Err(CommandError::Unimplemented) => exitcode::UNIMPLEMENTED,
86 83
87 84 Err(CommandError::Abort { message }) => {
88 85 if !message.is_empty() {
89 86 // Ignore errors when writing to stderr, we’re already exiting
90 87 // with failure code so there’s not much more we can do.
91 88 let _ =
92 89 ui.write_stderr(&format_bytes!(b"abort: {}\n", message));
93 90 }
94 91 exitcode::ABORT
95 92 }
96 93 };
97 94 std::process::exit(exit_code)
98 95 }
99 96
100 97 macro_rules! subcommands {
101 98 ($( $command: ident )+) => {
102 99 mod commands {
103 100 $(
104 101 pub mod $command;
105 102 )+
106 103 }
107 104
108 105 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
109 106 app
110 107 $(
111 108 .subcommand(add_global_args(commands::$command::args()))
112 109 )+
113 110 }
114 111
115 112 fn subcommand_run_fn(name: &str) -> Option<fn(
116 113 &ui::Ui,
117 114 &hg::config::Config,
118 115 Option<&Path>,
119 116 &ArgMatches,
120 117 ) -> Result<(), CommandError>> {
121 118 match name {
122 119 $(
123 120 stringify!($command) => Some(commands::$command::run),
124 121 )+
125 122 _ => None,
126 123 }
127 124 }
128 125 };
129 126 }
130 127
131 128 subcommands! {
132 129 cat
133 130 debugdata
134 131 debugrequirements
135 132 files
136 133 root
137 134 config
138 135 }
@@ -1,117 +1,112
1 1 use format_bytes::format_bytes;
2 2 use std::borrow::Cow;
3 3 use std::io;
4 4 use std::io::{ErrorKind, Write};
5 5
6 6 #[derive(Debug)]
7 7 pub struct Ui {
8 8 stdout: std::io::Stdout,
9 9 stderr: std::io::Stderr,
10 10 }
11 11
12 12 /// The kind of user interface error
13 13 pub enum UiError {
14 14 /// The standard output stream cannot be written to
15 15 StdoutError(io::Error),
16 16 /// The standard error stream cannot be written to
17 17 StderrError(io::Error),
18 18 }
19 19
20 20 /// The commandline user interface
21 21 impl Ui {
22 22 pub fn new() -> Self {
23 23 Ui {
24 24 stdout: std::io::stdout(),
25 25 stderr: std::io::stderr(),
26 26 }
27 27 }
28 28
29 29 /// Returns a buffered handle on stdout for faster batch printing
30 30 /// operations.
31 31 pub fn stdout_buffer(&self) -> StdoutBuffer<std::io::StdoutLock> {
32 32 StdoutBuffer::new(self.stdout.lock())
33 33 }
34 34
35 35 /// Write bytes to stdout
36 36 pub fn write_stdout(&self, bytes: &[u8]) -> Result<(), UiError> {
37 37 let mut stdout = self.stdout.lock();
38 38
39 39 stdout.write_all(bytes).or_else(handle_stdout_error)?;
40 40
41 41 stdout.flush().or_else(handle_stdout_error)
42 42 }
43 43
44 44 /// Write bytes to stderr
45 45 pub fn write_stderr(&self, bytes: &[u8]) -> Result<(), UiError> {
46 46 let mut stderr = self.stderr.lock();
47 47
48 48 stderr.write_all(bytes).or_else(handle_stderr_error)?;
49 49
50 50 stderr.flush().or_else(handle_stderr_error)
51 51 }
52
53 /// Write string line to stderr
54 pub fn writeln_stderr_str(&self, s: &str) -> Result<(), UiError> {
55 self.write_stderr(&format!("{}\n", s).as_bytes())
56 }
57 52 }
58 53
59 54 /// A buffered stdout writer for faster batch printing operations.
60 55 pub struct StdoutBuffer<W: Write> {
61 56 buf: io::BufWriter<W>,
62 57 }
63 58
64 59 impl<W: Write> StdoutBuffer<W> {
65 60 pub fn new(writer: W) -> Self {
66 61 let buf = io::BufWriter::new(writer);
67 62 Self { buf }
68 63 }
69 64
70 65 /// Write bytes to stdout buffer
71 66 pub fn write_all(&mut self, bytes: &[u8]) -> Result<(), UiError> {
72 67 self.buf.write_all(bytes).or_else(handle_stdout_error)
73 68 }
74 69
75 70 /// Flush bytes to stdout
76 71 pub fn flush(&mut self) -> Result<(), UiError> {
77 72 self.buf.flush().or_else(handle_stdout_error)
78 73 }
79 74 }
80 75
81 76 /// Sometimes writing to stdout is not possible, try writing to stderr to
82 77 /// signal that failure, otherwise just bail.
83 78 fn handle_stdout_error(error: io::Error) -> Result<(), UiError> {
84 79 if let ErrorKind::BrokenPipe = error.kind() {
85 80 // This makes `| head` work for example
86 81 return Ok(());
87 82 }
88 83 let mut stderr = io::stderr();
89 84
90 85 stderr
91 86 .write_all(&format_bytes!(
92 87 b"abort: {}\n",
93 88 error.to_string().as_bytes()
94 89 ))
95 90 .map_err(UiError::StderrError)?;
96 91
97 92 stderr.flush().map_err(UiError::StderrError)?;
98 93
99 94 Err(UiError::StdoutError(error))
100 95 }
101 96
102 97 /// Sometimes writing to stderr is not possible.
103 98 fn handle_stderr_error(error: io::Error) -> Result<(), UiError> {
104 99 // A broken pipe should not result in a error
105 100 // like with `| head` for example
106 101 if let ErrorKind::BrokenPipe = error.kind() {
107 102 return Ok(());
108 103 }
109 104 Err(UiError::StdoutError(error))
110 105 }
111 106
112 107 /// Encode rust strings according to the user system.
113 108 pub fn utf8_to_local(s: &str) -> Cow<[u8]> {
114 109 // TODO encode for the user's system //
115 110 let bytes = s.as_bytes();
116 111 Cow::Borrowed(bytes)
117 112 }
@@ -1,269 +1,263
1 1 #require rust
2 2
3 3 Define an rhg function that will only run if rhg exists
4 4 $ rhg() {
5 5 > if [ -f "$RUNTESTDIR/../rust/target/release/rhg" ]; then
6 6 > "$RUNTESTDIR/../rust/target/release/rhg" "$@"
7 7 > else
8 8 > echo "skipped: Cannot find rhg. Try to run cargo build in rust/rhg."
9 9 > exit 80
10 10 > fi
11 11 > }
12 12
13 13 Unimplemented command
14 14 $ rhg unimplemented-command
15 error: Found argument 'unimplemented-command' which wasn't expected, or isn't valid in this context
16
17 USAGE:
18 rhg [OPTIONS] <SUBCOMMAND>
19
20 For more information try --help
21 15 [252]
22 16
23 17 Finding root
24 18 $ rhg root
25 19 abort: no repository found in '$TESTTMP' (.hg not found)!
26 20 [255]
27 21
28 22 $ hg init repository
29 23 $ cd repository
30 24 $ rhg root
31 25 $TESTTMP/repository
32 26
33 27 Reading and setting configuration
34 28 $ echo "[ui]" >> $HGRCPATH
35 29 $ echo "username = user1" >> $HGRCPATH
36 30 $ rhg config ui.username
37 31 user1
38 32 $ echo "[ui]" >> .hg/hgrc
39 33 $ echo "username = user2" >> .hg/hgrc
40 34 $ rhg config ui.username
41 35 user2
42 36 $ rhg --config ui.username=user3 config ui.username
43 37 user3
44 38
45 39 Unwritable file descriptor
46 40 $ rhg root > /dev/full
47 41 abort: No space left on device (os error 28)
48 42 [255]
49 43
50 44 Deleted repository
51 45 $ rm -rf `pwd`
52 46 $ rhg root
53 47 abort: $ENOENT$: current directory
54 48 [255]
55 49
56 50 Listing tracked files
57 51 $ cd $TESTTMP
58 52 $ hg init repository
59 53 $ cd repository
60 54 $ for i in 1 2 3; do
61 55 > echo $i >> file$i
62 56 > hg add file$i
63 57 > done
64 58 > hg commit -m "commit $i" -q
65 59
66 60 Listing tracked files from root
67 61 $ rhg files
68 62 file1
69 63 file2
70 64 file3
71 65
72 66 Listing tracked files from subdirectory
73 67 $ mkdir -p path/to/directory
74 68 $ cd path/to/directory
75 69 $ rhg files
76 70 ../../../file1
77 71 ../../../file2
78 72 ../../../file3
79 73
80 74 Listing tracked files through broken pipe
81 75 $ rhg files | head -n 1
82 76 ../../../file1
83 77
84 78 Debuging data in inline index
85 79 $ cd $TESTTMP
86 80 $ rm -rf repository
87 81 $ hg init repository
88 82 $ cd repository
89 83 $ for i in 1 2 3 4 5 6; do
90 84 > echo $i >> file-$i
91 85 > hg add file-$i
92 86 > hg commit -m "Commit $i" -q
93 87 > done
94 88 $ rhg debugdata -c 2
95 89 8d0267cb034247ebfa5ee58ce59e22e57a492297
96 90 test
97 91 0 0
98 92 file-3
99 93
100 94 Commit 3 (no-eol)
101 95 $ rhg debugdata -m 2
102 96 file-1\x00b8e02f6433738021a065f94175c7cd23db5f05be (esc)
103 97 file-2\x005d9299349fc01ddd25d0070d149b124d8f10411e (esc)
104 98 file-3\x002661d26c649684b482d10f91960cc3db683c38b4 (esc)
105 99
106 100 Debuging with full node id
107 101 $ rhg debugdata -c `hg log -r 0 -T '{node}'`
108 102 d1d1c679d3053e8926061b6f45ca52009f011e3f
109 103 test
110 104 0 0
111 105 file-1
112 106
113 107 Commit 1 (no-eol)
114 108
115 109 Specifying revisions by changeset ID
116 110 $ hg log -T '{node}\n'
117 111 c6ad58c44207b6ff8a4fbbca7045a5edaa7e908b
118 112 d654274993d0149eecc3cc03214f598320211900
119 113 f646af7e96481d3a5470b695cf30ad8e3ab6c575
120 114 cf8b83f14ead62b374b6e91a0e9303b85dfd9ed7
121 115 91c6f6e73e39318534dc415ea4e8a09c99cd74d6
122 116 6ae9681c6d30389694d8701faf24b583cf3ccafe
123 117 $ rhg files -r cf8b83
124 118 file-1
125 119 file-2
126 120 file-3
127 121 $ rhg cat -r cf8b83 file-2
128 122 2
129 123 $ rhg cat -r c file-2
130 124 abort: ambiguous revision identifier c
131 125 [255]
132 126 $ rhg cat -r d file-2
133 127 2
134 128
135 129 Cat files
136 130 $ cd $TESTTMP
137 131 $ rm -rf repository
138 132 $ hg init repository
139 133 $ cd repository
140 134 $ echo "original content" > original
141 135 $ hg add original
142 136 $ hg commit -m "add original" original
143 137 $ rhg cat -r 0 original
144 138 original content
145 139 Cat copied file should not display copy metadata
146 140 $ hg copy original copy_of_original
147 141 $ hg commit -m "add copy of original"
148 142 $ rhg cat -r 1 copy_of_original
149 143 original content
150 144
151 145 Requirements
152 146 $ rhg debugrequirements
153 147 dotencode
154 148 fncache
155 149 generaldelta
156 150 revlogv1
157 151 sparserevlog
158 152 store
159 153
160 154 $ echo indoor-pool >> .hg/requires
161 155 $ rhg files
162 156 [252]
163 157
164 158 $ rhg cat -r 1 copy_of_original
165 159 [252]
166 160
167 161 $ rhg debugrequirements
168 162 [252]
169 163
170 164 $ echo -e '\xFF' >> .hg/requires
171 165 $ rhg debugrequirements
172 166 abort: corrupted repository: parse error in 'requires' file
173 167 [255]
174 168
175 169 Persistent nodemap
176 170 $ cd $TESTTMP
177 171 $ rm -rf repository
178 172 $ hg init repository
179 173 $ cd repository
180 174 $ rhg debugrequirements | grep nodemap
181 175 [1]
182 176 $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn"
183 177 $ hg id -r tip
184 178 c3ae8dec9fad tip
185 179 $ ls .hg/store/00changelog*
186 180 .hg/store/00changelog.d
187 181 .hg/store/00changelog.i
188 182 $ rhg files -r c3ae8dec9fad
189 183 of
190 184
191 185 $ cd $TESTTMP
192 186 $ rm -rf repository
193 187 $ hg --config format.use-persistent-nodemap=True init repository
194 188 $ cd repository
195 189 $ rhg debugrequirements | grep nodemap
196 190 persistent-nodemap
197 191 $ hg debugbuilddag .+5000 --overwritten-file --config "storage.revlog.nodemap.mode=warn"
198 192 $ hg id -r tip
199 193 c3ae8dec9fad tip
200 194 $ ls .hg/store/00changelog*
201 195 .hg/store/00changelog-*.nd (glob)
202 196 .hg/store/00changelog.d
203 197 .hg/store/00changelog.i
204 198 .hg/store/00changelog.n
205 199
206 200 Specifying revisions by changeset ID
207 201 $ rhg files -r c3ae8dec9fad
208 202 of
209 203 $ rhg cat -r c3ae8dec9fad of
210 204 r5000
211 205
212 206 Crate a shared repository
213 207
214 208 $ echo "[extensions]" >> $HGRCPATH
215 209 $ echo "share = " >> $HGRCPATH
216 210
217 211 $ cd $TESTTMP
218 212 $ hg init repo1
219 213 $ echo a > repo1/a
220 214 $ hg -R repo1 commit -A -m'init'
221 215 adding a
222 216
223 217 $ hg share repo1 repo2
224 218 updating working directory
225 219 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
226 220
227 221 And check that basic rhg commands work with sharing
228 222
229 223 $ rhg files -R repo2
230 224 repo2/a
231 225 $ rhg -R repo2 cat -r 0 repo2/a
232 226 a
233 227
234 228 Same with relative sharing
235 229
236 230 $ hg share repo2 repo3 --relative
237 231 updating working directory
238 232 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
239 233
240 234 $ rhg files -R repo3
241 235 repo3/a
242 236 $ rhg -R repo3 cat -r 0 repo3/a
243 237 a
244 238
245 239 Same with share-safe
246 240
247 241 $ echo "[format]" >> $HGRCPATH
248 242 $ echo "use-share-safe = True" >> $HGRCPATH
249 243
250 244 $ cd $TESTTMP
251 245 $ hg init repo4
252 246 $ cd repo4
253 247 $ echo a > a
254 248 $ hg commit -A -m'init'
255 249 adding a
256 250
257 251 $ cd ..
258 252 $ hg share repo4 repo5
259 253 updating working directory
260 254 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
261 255
262 256 And check that basic rhg commands work with sharing
263 257
264 258 $ cd repo5
265 259 $ rhg files
266 260 a
267 261 $ rhg cat -r 0 a
268 262 a
269 263
General Comments 0
You need to be logged in to leave comments. Login now