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