##// END OF EJS Templates
rhg: Propagate permission errors when finding a repository...
Simon Sapin -
r48584:cf5f8da2 stable
parent child Browse files
Show More
@@ -1,194 +1,199 b''
1 1 use crate::config::ConfigValueParseError;
2 2 use crate::exit_codes;
3 3 use std::fmt;
4 4
5 5 /// Common error cases that can happen in many different APIs
6 6 #[derive(Debug, derive_more::From)]
7 7 pub enum HgError {
8 8 IoError {
9 9 error: std::io::Error,
10 10 context: IoErrorContext,
11 11 },
12 12
13 13 /// A file under `.hg/` normally only written by Mercurial is not in the
14 14 /// expected format. This indicates a bug in Mercurial, filesystem
15 15 /// corruption, or hardware failure.
16 16 ///
17 17 /// The given string is a short explanation for users, not intended to be
18 18 /// machine-readable.
19 19 CorruptedRepository(String),
20 20
21 21 /// The respository or requested operation involves a feature not
22 22 /// supported by the Rust implementation. Falling back to the Python
23 23 /// implementation may or may not work.
24 24 ///
25 25 /// The given string is a short explanation for users, not intended to be
26 26 /// machine-readable.
27 27 UnsupportedFeature(String),
28 28
29 29 /// Operation cannot proceed for some other reason.
30 30 ///
31 31 /// The message is a short explanation for users, not intended to be
32 32 /// machine-readable.
33 33 Abort {
34 34 message: String,
35 35 detailed_exit_code: exit_codes::ExitCode,
36 36 },
37 37
38 38 /// A configuration value is not in the expected syntax.
39 39 ///
40 40 /// These errors can happen in many places in the code because values are
41 41 /// parsed lazily as the file-level parser does not know the expected type
42 42 /// and syntax of each value.
43 43 #[from]
44 44 ConfigValueParseError(ConfigValueParseError),
45 45 }
46 46
47 47 /// Details about where an I/O error happened
48 48 #[derive(Debug)]
49 49 pub enum IoErrorContext {
50 /// `std::fs::metadata`
51 ReadingMetadata(std::path::PathBuf),
50 52 ReadingFile(std::path::PathBuf),
51 53 WritingFile(std::path::PathBuf),
52 54 RemovingFile(std::path::PathBuf),
53 55 RenamingFile {
54 56 from: std::path::PathBuf,
55 57 to: std::path::PathBuf,
56 58 },
57 59 /// `std::fs::canonicalize`
58 60 CanonicalizingPath(std::path::PathBuf),
59 61 /// `std::env::current_dir`
60 62 CurrentDir,
61 63 /// `std::env::current_exe`
62 64 CurrentExe,
63 65 }
64 66
65 67 impl HgError {
66 68 pub fn corrupted(explanation: impl Into<String>) -> Self {
67 69 // TODO: capture a backtrace here and keep it in the error value
68 70 // to aid debugging?
69 71 // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
70 72 HgError::CorruptedRepository(explanation.into())
71 73 }
72 74
73 75 pub fn unsupported(explanation: impl Into<String>) -> Self {
74 76 HgError::UnsupportedFeature(explanation.into())
75 77 }
76 78
77 79 pub fn abort(
78 80 explanation: impl Into<String>,
79 81 exit_code: exit_codes::ExitCode,
80 82 ) -> Self {
81 83 HgError::Abort {
82 84 message: explanation.into(),
83 85 detailed_exit_code: exit_code,
84 86 }
85 87 }
86 88 }
87 89
88 90 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
89 91 impl fmt::Display for HgError {
90 92 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
91 93 match self {
92 94 HgError::Abort { message, .. } => write!(f, "{}", message),
93 95 HgError::IoError { error, context } => {
94 96 write!(f, "abort: {}: {}", context, error)
95 97 }
96 98 HgError::CorruptedRepository(explanation) => {
97 99 write!(f, "abort: {}", explanation)
98 100 }
99 101 HgError::UnsupportedFeature(explanation) => {
100 102 write!(f, "unsupported feature: {}", explanation)
101 103 }
102 104 HgError::ConfigValueParseError(error) => error.fmt(f),
103 105 }
104 106 }
105 107 }
106 108
107 109 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
108 110 impl fmt::Display for IoErrorContext {
109 111 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
110 112 match self {
113 IoErrorContext::ReadingMetadata(path) => {
114 write!(f, "when reading metadata of {}", path.display())
115 }
111 116 IoErrorContext::ReadingFile(path) => {
112 117 write!(f, "when reading {}", path.display())
113 118 }
114 119 IoErrorContext::WritingFile(path) => {
115 120 write!(f, "when writing {}", path.display())
116 121 }
117 122 IoErrorContext::RemovingFile(path) => {
118 123 write!(f, "when removing {}", path.display())
119 124 }
120 125 IoErrorContext::RenamingFile { from, to } => write!(
121 126 f,
122 127 "when renaming {} to {}",
123 128 from.display(),
124 129 to.display()
125 130 ),
126 131 IoErrorContext::CanonicalizingPath(path) => {
127 132 write!(f, "when canonicalizing {}", path.display())
128 133 }
129 134 IoErrorContext::CurrentDir => {
130 135 write!(f, "error getting current working directory")
131 136 }
132 137 IoErrorContext::CurrentExe => {
133 138 write!(f, "error getting current executable")
134 139 }
135 140 }
136 141 }
137 142 }
138 143
139 144 pub trait IoResultExt<T> {
140 145 /// Annotate a possible I/O error as related to a reading a file at the
141 146 /// given path.
142 147 ///
143 148 /// This allows printing something like β€œFile not found when reading
144 149 /// example.txt” instead of just β€œFile not found”.
145 150 ///
146 151 /// Converts a `Result` with `std::io::Error` into one with `HgError`.
147 152 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError>;
148 153
149 154 fn with_context(
150 155 self,
151 156 context: impl FnOnce() -> IoErrorContext,
152 157 ) -> Result<T, HgError>;
153 158 }
154 159
155 160 impl<T> IoResultExt<T> for std::io::Result<T> {
156 161 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError> {
157 162 self.with_context(|| IoErrorContext::ReadingFile(path.to_owned()))
158 163 }
159 164
160 165 fn with_context(
161 166 self,
162 167 context: impl FnOnce() -> IoErrorContext,
163 168 ) -> Result<T, HgError> {
164 169 self.map_err(|error| HgError::IoError {
165 170 error,
166 171 context: context(),
167 172 })
168 173 }
169 174 }
170 175
171 176 pub trait HgResultExt<T> {
172 177 /// Handle missing files separately from other I/O error cases.
173 178 ///
174 179 /// Wraps the `Ok` type in an `Option`:
175 180 ///
176 181 /// * `Ok(x)` becomes `Ok(Some(x))`
177 182 /// * An I/O "not found" error becomes `Ok(None)`
178 183 /// * Other errors are unchanged
179 184 fn io_not_found_as_none(self) -> Result<Option<T>, HgError>;
180 185 }
181 186
182 187 impl<T> HgResultExt<T> for Result<T, HgError> {
183 188 fn io_not_found_as_none(self) -> Result<Option<T>, HgError> {
184 189 match self {
185 190 Ok(x) => Ok(Some(x)),
186 191 Err(HgError::IoError { error, .. })
187 192 if error.kind() == std::io::ErrorKind::NotFound =>
188 193 {
189 194 Ok(None)
190 195 }
191 196 Err(other_error) => Err(other_error),
192 197 }
193 198 }
194 199 }
@@ -1,288 +1,315 b''
1 1 use crate::config::{Config, ConfigError, ConfigParseError};
2 2 use crate::errors::{HgError, IoErrorContext, IoResultExt};
3 3 use crate::exit_codes;
4 4 use crate::requirements;
5 5 use crate::utils::files::get_path_from_bytes;
6 6 use crate::utils::SliceExt;
7 7 use memmap::{Mmap, MmapOptions};
8 8 use std::collections::HashSet;
9 use std::io::ErrorKind;
9 10 use std::path::{Path, PathBuf};
10 11
11 12 /// A repository on disk
12 13 pub struct Repo {
13 14 working_directory: PathBuf,
14 15 dot_hg: PathBuf,
15 16 store: PathBuf,
16 17 requirements: HashSet<String>,
17 18 config: Config,
18 19 }
19 20
20 21 #[derive(Debug, derive_more::From)]
21 22 pub enum RepoError {
22 23 NotFound {
23 24 at: PathBuf,
24 25 },
25 26 #[from]
26 27 ConfigParseError(ConfigParseError),
27 28 #[from]
28 29 Other(HgError),
29 30 }
30 31
31 32 impl From<ConfigError> for RepoError {
32 33 fn from(error: ConfigError) -> Self {
33 34 match error {
34 35 ConfigError::Parse(error) => error.into(),
35 36 ConfigError::Other(error) => error.into(),
36 37 }
37 38 }
38 39 }
39 40
40 41 /// Filesystem access abstraction for the contents of a given "base" diretory
41 42 #[derive(Clone, Copy)]
42 43 pub struct Vfs<'a> {
43 44 pub(crate) base: &'a Path,
44 45 }
45 46
46 47 impl Repo {
47 48 /// tries to find nearest repository root in current working directory or
48 49 /// its ancestors
49 50 pub fn find_repo_root() -> Result<PathBuf, RepoError> {
50 51 let current_directory = crate::utils::current_dir()?;
51 52 // ancestors() is inclusive: it first yields `current_directory`
52 53 // as-is.
53 54 for ancestor in current_directory.ancestors() {
54 if ancestor.join(".hg").is_dir() {
55 if is_dir(ancestor.join(".hg"))? {
55 56 return Ok(ancestor.to_path_buf());
56 57 }
57 58 }
58 59 return Err(RepoError::NotFound {
59 60 at: current_directory,
60 61 });
61 62 }
62 63
63 64 /// Find a repository, either at the given path (which must contain a `.hg`
64 65 /// sub-directory) or by searching the current directory and its
65 66 /// ancestors.
66 67 ///
67 68 /// A method with two very different "modes" like this usually a code smell
68 69 /// to make two methods instead, but in this case an `Option` is what rhg
69 70 /// sub-commands get from Clap for the `-R` / `--repository` CLI argument.
70 71 /// Having two methods would just move that `if` to almost all callers.
71 72 pub fn find(
72 73 config: &Config,
73 74 explicit_path: Option<PathBuf>,
74 75 ) -> Result<Self, RepoError> {
75 76 if let Some(root) = explicit_path {
76 if root.join(".hg").is_dir() {
77 if is_dir(root.join(".hg"))? {
77 78 Self::new_at_path(root.to_owned(), config)
78 } else if root.is_file() {
79 } else if is_file(&root)? {
79 80 Err(HgError::unsupported("bundle repository").into())
80 81 } else {
81 82 Err(RepoError::NotFound {
82 83 at: root.to_owned(),
83 84 })
84 85 }
85 86 } else {
86 87 let root = Self::find_repo_root()?;
87 88 Self::new_at_path(root, config)
88 89 }
89 90 }
90 91
91 92 /// To be called after checking that `.hg` is a sub-directory
92 93 fn new_at_path(
93 94 working_directory: PathBuf,
94 95 config: &Config,
95 96 ) -> Result<Self, RepoError> {
96 97 let dot_hg = working_directory.join(".hg");
97 98
98 99 let mut repo_config_files = Vec::new();
99 100 repo_config_files.push(dot_hg.join("hgrc"));
100 101 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
101 102
102 103 let hg_vfs = Vfs { base: &dot_hg };
103 104 let mut reqs = requirements::load_if_exists(hg_vfs)?;
104 105 let relative =
105 106 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
106 107 let shared =
107 108 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
108 109
109 110 // From `mercurial/localrepo.py`:
110 111 //
111 112 // if .hg/requires contains the sharesafe requirement, it means
112 113 // there exists a `.hg/store/requires` too and we should read it
113 114 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
114 115 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
115 116 // is not present, refer checkrequirementscompat() for that
116 117 //
117 118 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
118 119 // repository was shared the old way. We check the share source
119 120 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
120 121 // current repository needs to be reshared
121 122 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
122 123
123 124 let store_path;
124 125 if !shared {
125 126 store_path = dot_hg.join("store");
126 127 } else {
127 128 let bytes = hg_vfs.read("sharedpath")?;
128 129 let mut shared_path =
129 130 get_path_from_bytes(bytes.trim_end_newlines()).to_owned();
130 131 if relative {
131 132 shared_path = dot_hg.join(shared_path)
132 133 }
133 if !shared_path.is_dir() {
134 if !is_dir(&shared_path)? {
134 135 return Err(HgError::corrupted(format!(
135 136 ".hg/sharedpath points to nonexistent directory {}",
136 137 shared_path.display()
137 138 ))
138 139 .into());
139 140 }
140 141
141 142 store_path = shared_path.join("store");
142 143
143 144 let source_is_share_safe =
144 145 requirements::load(Vfs { base: &shared_path })?
145 146 .contains(requirements::SHARESAFE_REQUIREMENT);
146 147
147 148 if share_safe && !source_is_share_safe {
148 149 return Err(match config
149 150 .get(b"share", b"safe-mismatch.source-not-safe")
150 151 {
151 152 Some(b"abort") | None => HgError::abort(
152 153 "abort: share source does not support share-safe requirement\n\
153 154 (see `hg help config.format.use-share-safe` for more information)",
154 155 exit_codes::ABORT,
155 156 ),
156 157 _ => HgError::unsupported("share-safe downgrade"),
157 158 }
158 159 .into());
159 160 } else if source_is_share_safe && !share_safe {
160 161 return Err(
161 162 match config.get(b"share", b"safe-mismatch.source-safe") {
162 163 Some(b"abort") | None => HgError::abort(
163 164 "abort: version mismatch: source uses share-safe \
164 165 functionality while the current share does not\n\
165 166 (see `hg help config.format.use-share-safe` for more information)",
166 167 exit_codes::ABORT,
167 168 ),
168 169 _ => HgError::unsupported("share-safe upgrade"),
169 170 }
170 171 .into(),
171 172 );
172 173 }
173 174
174 175 if share_safe {
175 176 repo_config_files.insert(0, shared_path.join("hgrc"))
176 177 }
177 178 }
178 179 if share_safe {
179 180 reqs.extend(requirements::load(Vfs { base: &store_path })?);
180 181 }
181 182
182 183 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
183 184 config.combine_with_repo(&repo_config_files)?
184 185 } else {
185 186 config.clone()
186 187 };
187 188
188 189 let repo = Self {
189 190 requirements: reqs,
190 191 working_directory,
191 192 store: store_path,
192 193 dot_hg,
193 194 config: repo_config,
194 195 };
195 196
196 197 requirements::check(&repo)?;
197 198
198 199 Ok(repo)
199 200 }
200 201
201 202 pub fn working_directory_path(&self) -> &Path {
202 203 &self.working_directory
203 204 }
204 205
205 206 pub fn requirements(&self) -> &HashSet<String> {
206 207 &self.requirements
207 208 }
208 209
209 210 pub fn config(&self) -> &Config {
210 211 &self.config
211 212 }
212 213
213 214 /// For accessing repository files (in `.hg`), except for the store
214 215 /// (`.hg/store`).
215 216 pub fn hg_vfs(&self) -> Vfs<'_> {
216 217 Vfs { base: &self.dot_hg }
217 218 }
218 219
219 220 /// For accessing repository store files (in `.hg/store`)
220 221 pub fn store_vfs(&self) -> Vfs<'_> {
221 222 Vfs { base: &self.store }
222 223 }
223 224
224 225 /// For accessing the working copy
225 226 pub fn working_directory_vfs(&self) -> Vfs<'_> {
226 227 Vfs {
227 228 base: &self.working_directory,
228 229 }
229 230 }
230 231
231 232 pub fn has_dirstate_v2(&self) -> bool {
232 233 self.requirements
233 234 .contains(requirements::DIRSTATE_V2_REQUIREMENT)
234 235 }
235 236
236 237 pub fn dirstate_parents(
237 238 &self,
238 239 ) -> Result<crate::dirstate::DirstateParents, HgError> {
239 240 let dirstate = self.hg_vfs().mmap_open("dirstate")?;
240 241 if dirstate.is_empty() {
241 242 return Ok(crate::dirstate::DirstateParents::NULL);
242 243 }
243 244 let parents = if self.has_dirstate_v2() {
244 245 crate::dirstate_tree::on_disk::read_docket(&dirstate)?.parents()
245 246 } else {
246 247 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?
247 248 .clone()
248 249 };
249 250 Ok(parents)
250 251 }
251 252 }
252 253
253 254 impl Vfs<'_> {
254 255 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
255 256 self.base.join(relative_path)
256 257 }
257 258
258 259 pub fn read(
259 260 &self,
260 261 relative_path: impl AsRef<Path>,
261 262 ) -> Result<Vec<u8>, HgError> {
262 263 let path = self.join(relative_path);
263 264 std::fs::read(&path).when_reading_file(&path)
264 265 }
265 266
266 267 pub fn mmap_open(
267 268 &self,
268 269 relative_path: impl AsRef<Path>,
269 270 ) -> Result<Mmap, HgError> {
270 271 let path = self.base.join(relative_path);
271 272 let file = std::fs::File::open(&path).when_reading_file(&path)?;
272 273 // TODO: what are the safety requirements here?
273 274 let mmap = unsafe { MmapOptions::new().map(&file) }
274 275 .when_reading_file(&path)?;
275 276 Ok(mmap)
276 277 }
277 278
278 279 pub fn rename(
279 280 &self,
280 281 relative_from: impl AsRef<Path>,
281 282 relative_to: impl AsRef<Path>,
282 283 ) -> Result<(), HgError> {
283 284 let from = self.join(relative_from);
284 285 let to = self.join(relative_to);
285 286 std::fs::rename(&from, &to)
286 287 .with_context(|| IoErrorContext::RenamingFile { from, to })
287 288 }
288 289 }
290
291 fn fs_metadata(
292 path: impl AsRef<Path>,
293 ) -> Result<Option<std::fs::Metadata>, HgError> {
294 let path = path.as_ref();
295 match std::fs::metadata(path) {
296 Ok(meta) => Ok(Some(meta)),
297 Err(error) => match error.kind() {
298 // TODO: when we require a Rust version where `NotADirectory` is
299 // stable, invert this logic and return None for it and `NotFound`
300 // and propagate any other error.
301 ErrorKind::PermissionDenied => Err(error).with_context(|| {
302 IoErrorContext::ReadingMetadata(path.to_owned())
303 }),
304 _ => Ok(None),
305 },
306 }
307 }
308
309 fn is_dir(path: impl AsRef<Path>) -> Result<bool, HgError> {
310 Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_dir()))
311 }
312
313 fn is_file(path: impl AsRef<Path>) -> Result<bool, HgError> {
314 Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_file()))
315 }
General Comments 0
You need to be logged in to leave comments. Login now