Show More
@@ -1,121 +1,133 b'' | |||
|
1 | 1 | use std::fmt; |
|
2 | 2 | |
|
3 | 3 | /// Common error cases that can happen in many different APIs |
|
4 | 4 | #[derive(Debug)] |
|
5 | 5 | pub enum HgError { |
|
6 | 6 | IoError { |
|
7 | 7 | error: std::io::Error, |
|
8 | 8 | context: IoErrorContext, |
|
9 | 9 | }, |
|
10 | 10 | |
|
11 | /// A file under `.hg/` normally only written by Mercurial | |
|
11 | /// A file under `.hg/` normally only written by Mercurial is not in the | |
|
12 | /// expected format. This indicates a bug in Mercurial, filesystem | |
|
13 | /// corruption, or hardware failure. | |
|
12 | 14 | /// |
|
13 | 15 | /// The given string is a short explanation for users, not intended to be |
|
14 | 16 | /// machine-readable. |
|
15 | 17 | CorruptedRepository(String), |
|
16 | 18 | |
|
17 | 19 | /// The respository or requested operation involves a feature not |
|
18 | 20 | /// supported by the Rust implementation. Falling back to the Python |
|
19 | 21 | /// implementation may or may not work. |
|
20 | 22 | /// |
|
21 | 23 | /// The given string is a short explanation for users, not intended to be |
|
22 | 24 | /// machine-readable. |
|
23 | 25 | UnsupportedFeature(String), |
|
26 | ||
|
27 | /// Operation cannot proceed for some other reason. | |
|
28 | /// | |
|
29 | /// The given string is a short explanation for users, not intended to be | |
|
30 | /// machine-readable. | |
|
31 | Abort(String), | |
|
24 | 32 | } |
|
25 | 33 | |
|
26 | 34 | /// Details about where an I/O error happened |
|
27 | 35 | #[derive(Debug, derive_more::From)] |
|
28 | 36 | pub enum IoErrorContext { |
|
29 | 37 | /// A filesystem operation for the given file |
|
30 | 38 | #[from] |
|
31 | 39 | File(std::path::PathBuf), |
|
32 | 40 | /// `std::env::current_dir` |
|
33 | 41 | CurrentDir, |
|
34 | 42 | /// `std::env::current_exe` |
|
35 | 43 | CurrentExe, |
|
36 | 44 | } |
|
37 | 45 | |
|
38 | 46 | impl HgError { |
|
39 | 47 | pub fn corrupted(explanation: impl Into<String>) -> Self { |
|
40 | 48 | // TODO: capture a backtrace here and keep it in the error value |
|
41 | 49 | // to aid debugging? |
|
42 | 50 | // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html |
|
43 | 51 | HgError::CorruptedRepository(explanation.into()) |
|
44 | 52 | } |
|
45 | 53 | |
|
46 | 54 | pub fn unsupported(explanation: impl Into<String>) -> Self { |
|
47 | 55 | HgError::UnsupportedFeature(explanation.into()) |
|
48 | 56 | } |
|
57 | pub fn abort(explanation: impl Into<String>) -> Self { | |
|
58 | HgError::Abort(explanation.into()) | |
|
59 | } | |
|
49 | 60 | } |
|
50 | 61 | |
|
51 | 62 | // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly? |
|
52 | 63 | impl fmt::Display for HgError { |
|
53 | 64 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
|
54 | 65 | match self { |
|
55 | 66 | HgError::IoError { error, context } => { |
|
56 | 67 | write!(f, "{}: {}", error, context) |
|
57 | 68 | } |
|
58 | 69 | HgError::CorruptedRepository(explanation) => { |
|
59 | 70 | write!(f, "corrupted repository: {}", explanation) |
|
60 | 71 | } |
|
61 | 72 | HgError::UnsupportedFeature(explanation) => { |
|
62 | 73 | write!(f, "unsupported feature: {}", explanation) |
|
63 | 74 | } |
|
75 | HgError::Abort(explanation) => explanation.fmt(f), | |
|
64 | 76 | } |
|
65 | 77 | } |
|
66 | 78 | } |
|
67 | 79 | |
|
68 | 80 | // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly? |
|
69 | 81 | impl fmt::Display for IoErrorContext { |
|
70 | 82 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
|
71 | 83 | match self { |
|
72 | 84 | IoErrorContext::File(path) => path.display().fmt(f), |
|
73 | 85 | IoErrorContext::CurrentDir => f.write_str("current directory"), |
|
74 | 86 | IoErrorContext::CurrentExe => f.write_str("current executable"), |
|
75 | 87 | } |
|
76 | 88 | } |
|
77 | 89 | } |
|
78 | 90 | |
|
79 | 91 | pub trait IoResultExt<T> { |
|
80 | 92 | /// Annotate a possible I/O error as related to a file at the given path. |
|
81 | 93 | /// |
|
82 | 94 | /// This allows printing something like βFile not found: example.txtβ |
|
83 | 95 | /// instead of just βFile not foundβ. |
|
84 | 96 | /// |
|
85 | 97 | /// Converts a `Result` with `std::io::Error` into one with `HgError`. |
|
86 | 98 | fn for_file(self, path: &std::path::Path) -> Result<T, HgError>; |
|
87 | 99 | } |
|
88 | 100 | |
|
89 | 101 | impl<T> IoResultExt<T> for std::io::Result<T> { |
|
90 | 102 | fn for_file(self, path: &std::path::Path) -> Result<T, HgError> { |
|
91 | 103 | self.map_err(|error| HgError::IoError { |
|
92 | 104 | error, |
|
93 | 105 | context: IoErrorContext::File(path.to_owned()), |
|
94 | 106 | }) |
|
95 | 107 | } |
|
96 | 108 | } |
|
97 | 109 | |
|
98 | 110 | pub trait HgResultExt<T> { |
|
99 | 111 | /// Handle missing files separately from other I/O error cases. |
|
100 | 112 | /// |
|
101 | 113 | /// Wraps the `Ok` type in an `Option`: |
|
102 | 114 | /// |
|
103 | 115 | /// * `Ok(x)` becomes `Ok(Some(x))` |
|
104 | 116 | /// * An I/O "not found" error becomes `Ok(None)` |
|
105 | 117 | /// * Other errors are unchanged |
|
106 | 118 | fn io_not_found_as_none(self) -> Result<Option<T>, HgError>; |
|
107 | 119 | } |
|
108 | 120 | |
|
109 | 121 | impl<T> HgResultExt<T> for Result<T, HgError> { |
|
110 | 122 | fn io_not_found_as_none(self) -> Result<Option<T>, HgError> { |
|
111 | 123 | match self { |
|
112 | 124 | Ok(x) => Ok(Some(x)), |
|
113 | 125 | Err(HgError::IoError { error, .. }) |
|
114 | 126 | if error.kind() == std::io::ErrorKind::NotFound => |
|
115 | 127 | { |
|
116 | 128 | Ok(None) |
|
117 | 129 | } |
|
118 | 130 | Err(other_error) => Err(other_error), |
|
119 | 131 | } |
|
120 | 132 | } |
|
121 | 133 | } |
@@ -1,171 +1,186 b'' | |||
|
1 | 1 | use crate::config::Config; |
|
2 | 2 | use crate::errors::{HgError, IoResultExt}; |
|
3 | 3 | use crate::requirements; |
|
4 | 4 | use crate::utils::files::get_path_from_bytes; |
|
5 | 5 | use memmap::{Mmap, MmapOptions}; |
|
6 | 6 | use std::collections::HashSet; |
|
7 | 7 | use std::path::{Path, PathBuf}; |
|
8 | 8 | |
|
9 | 9 | /// A repository on disk |
|
10 | 10 | pub struct Repo { |
|
11 | 11 | working_directory: PathBuf, |
|
12 | 12 | dot_hg: PathBuf, |
|
13 | 13 | store: PathBuf, |
|
14 | 14 | requirements: HashSet<String>, |
|
15 | 15 | } |
|
16 | 16 | |
|
17 | 17 | #[derive(Debug, derive_more::From)] |
|
18 | 18 | pub enum RepoFindError { |
|
19 | 19 | NotFoundInCurrentDirectoryOrAncestors { |
|
20 | 20 | current_directory: PathBuf, |
|
21 | 21 | }, |
|
22 | 22 | #[from] |
|
23 | 23 | Other(HgError), |
|
24 | 24 | } |
|
25 | 25 | |
|
26 | 26 | /// Filesystem access abstraction for the contents of a given "base" diretory |
|
27 | 27 | #[derive(Clone, Copy)] |
|
28 | 28 | pub(crate) struct Vfs<'a> { |
|
29 | 29 | base: &'a Path, |
|
30 | 30 | } |
|
31 | 31 | |
|
32 | 32 | impl Repo { |
|
33 | 33 | /// Search the current directory and its ancestores for a repository: |
|
34 | 34 | /// a working directory that contains a `.hg` sub-directory. |
|
35 |
pub fn find( |
|
|
35 | pub fn find(config: &Config) -> Result<Self, RepoFindError> { | |
|
36 | 36 | let current_directory = crate::utils::current_dir()?; |
|
37 | 37 | // ancestors() is inclusive: it first yields `current_directory` as-is. |
|
38 | 38 | for ancestor in current_directory.ancestors() { |
|
39 | 39 | if ancestor.join(".hg").is_dir() { |
|
40 | return Ok(Self::new_at_path(ancestor.to_owned())?); | |
|
40 | return Ok(Self::new_at_path(ancestor.to_owned(), config)?); | |
|
41 | 41 | } |
|
42 | 42 | } |
|
43 | 43 | Err(RepoFindError::NotFoundInCurrentDirectoryOrAncestors { |
|
44 | 44 | current_directory, |
|
45 | 45 | }) |
|
46 | 46 | } |
|
47 | 47 | |
|
48 | 48 | /// To be called after checking that `.hg` is a sub-directory |
|
49 | fn new_at_path(working_directory: PathBuf) -> Result<Self, HgError> { | |
|
49 | fn new_at_path( | |
|
50 | working_directory: PathBuf, | |
|
51 | config: &Config, | |
|
52 | ) -> Result<Self, HgError> { | |
|
50 | 53 | let dot_hg = working_directory.join(".hg"); |
|
51 | 54 | |
|
52 | 55 | let hg_vfs = Vfs { base: &dot_hg }; |
|
53 | 56 | let mut reqs = requirements::load_if_exists(hg_vfs)?; |
|
54 | 57 | let relative = |
|
55 | 58 | reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT); |
|
56 | 59 | let shared = |
|
57 | 60 | reqs.contains(requirements::SHARED_REQUIREMENT) || relative; |
|
58 | 61 | |
|
59 | 62 | // From `mercurial/localrepo.py`: |
|
60 | 63 | // |
|
61 | 64 | // if .hg/requires contains the sharesafe requirement, it means |
|
62 | 65 | // there exists a `.hg/store/requires` too and we should read it |
|
63 | 66 | // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement |
|
64 | 67 | // is present. We never write SHARESAFE_REQUIREMENT for a repo if store |
|
65 | 68 | // is not present, refer checkrequirementscompat() for that |
|
66 | 69 | // |
|
67 | 70 | // However, if SHARESAFE_REQUIREMENT is not present, it means that the |
|
68 | 71 | // repository was shared the old way. We check the share source |
|
69 | 72 | // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the |
|
70 | 73 | // current repository needs to be reshared |
|
71 | 74 | let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT); |
|
72 | 75 | |
|
73 | 76 | let store_path; |
|
74 | 77 | if !shared { |
|
75 | 78 | store_path = dot_hg.join("store"); |
|
76 | 79 | if share_safe { |
|
77 | 80 | reqs.extend(requirements::load(Vfs { base: &store_path })?); |
|
78 | 81 | } |
|
79 | 82 | } else { |
|
80 | 83 | let bytes = hg_vfs.read("sharedpath")?; |
|
81 | 84 | let mut shared_path = get_path_from_bytes(&bytes).to_owned(); |
|
82 | 85 | if relative { |
|
83 | 86 | shared_path = dot_hg.join(shared_path) |
|
84 | 87 | } |
|
85 | 88 | if !shared_path.is_dir() { |
|
86 | 89 | return Err(HgError::corrupted(format!( |
|
87 | 90 | ".hg/sharedpath points to nonexistent directory {}", |
|
88 | 91 | shared_path.display() |
|
89 | 92 | ))); |
|
90 | 93 | } |
|
91 | 94 | |
|
92 | 95 | store_path = shared_path.join("store"); |
|
93 | 96 | |
|
94 | 97 | let source_is_share_safe = |
|
95 | 98 | requirements::load(Vfs { base: &shared_path })? |
|
96 | 99 | .contains(requirements::SHARESAFE_REQUIREMENT); |
|
97 | 100 | |
|
98 | // TODO: support for `share.safe-mismatch.*` config | |
|
99 | 101 | if share_safe && !source_is_share_safe { |
|
100 | return Err(HgError::unsupported("share-safe downgrade")); | |
|
102 | return Err(match config.get(b"safe-mismatch", b"source-not-safe") { | |
|
103 | Some(b"abort") | None => HgError::abort( | |
|
104 | "share source does not support share-safe requirement" | |
|
105 | ), | |
|
106 | _ => HgError::unsupported("share-safe downgrade") | |
|
107 | }); | |
|
101 | 108 | } else if source_is_share_safe && !share_safe { |
|
102 | return Err(HgError::unsupported("share-safe upgrade")); | |
|
109 | return Err( | |
|
110 | match config.get(b"safe-mismatch", b"source-safe") { | |
|
111 | Some(b"abort") | None => HgError::abort( | |
|
112 | "version mismatch: source uses share-safe \ | |
|
113 | functionality while the current share does not", | |
|
114 | ), | |
|
115 | _ => HgError::unsupported("share-safe upgrade"), | |
|
116 | }, | |
|
117 | ); | |
|
103 | 118 | } |
|
104 | 119 | } |
|
105 | 120 | |
|
106 | 121 | let repo = Self { |
|
107 | 122 | requirements: reqs, |
|
108 | 123 | working_directory, |
|
109 | 124 | store: store_path, |
|
110 | 125 | dot_hg, |
|
111 | 126 | }; |
|
112 | 127 | |
|
113 | 128 | requirements::check(&repo)?; |
|
114 | 129 | |
|
115 | 130 | Ok(repo) |
|
116 | 131 | } |
|
117 | 132 | |
|
118 | 133 | pub fn working_directory_path(&self) -> &Path { |
|
119 | 134 | &self.working_directory |
|
120 | 135 | } |
|
121 | 136 | |
|
122 | 137 | pub fn requirements(&self) -> &HashSet<String> { |
|
123 | 138 | &self.requirements |
|
124 | 139 | } |
|
125 | 140 | |
|
126 | 141 | /// For accessing repository files (in `.hg`), except for the store |
|
127 | 142 | /// (`.hg/store`). |
|
128 | 143 | pub(crate) fn hg_vfs(&self) -> Vfs<'_> { |
|
129 | 144 | Vfs { base: &self.dot_hg } |
|
130 | 145 | } |
|
131 | 146 | |
|
132 | 147 | /// For accessing repository store files (in `.hg/store`) |
|
133 | 148 | pub(crate) fn store_vfs(&self) -> Vfs<'_> { |
|
134 | 149 | Vfs { base: &self.store } |
|
135 | 150 | } |
|
136 | 151 | |
|
137 | 152 | /// For accessing the working copy |
|
138 | 153 | |
|
139 | 154 | // The undescore prefix silences the "never used" warning. Remove before |
|
140 | 155 | // using. |
|
141 | 156 | pub(crate) fn _working_directory_vfs(&self) -> Vfs<'_> { |
|
142 | 157 | Vfs { |
|
143 | 158 | base: &self.working_directory, |
|
144 | 159 | } |
|
145 | 160 | } |
|
146 | 161 | } |
|
147 | 162 | |
|
148 | 163 | impl Vfs<'_> { |
|
149 | 164 | pub(crate) fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf { |
|
150 | 165 | self.base.join(relative_path) |
|
151 | 166 | } |
|
152 | 167 | |
|
153 | 168 | pub(crate) fn read( |
|
154 | 169 | &self, |
|
155 | 170 | relative_path: impl AsRef<Path>, |
|
156 | 171 | ) -> Result<Vec<u8>, HgError> { |
|
157 | 172 | let path = self.join(relative_path); |
|
158 | 173 | std::fs::read(&path).for_file(&path) |
|
159 | 174 | } |
|
160 | 175 | |
|
161 | 176 | pub(crate) fn mmap_open( |
|
162 | 177 | &self, |
|
163 | 178 | relative_path: impl AsRef<Path>, |
|
164 | 179 | ) -> Result<Mmap, HgError> { |
|
165 | 180 | let path = self.base.join(relative_path); |
|
166 | 181 | let file = std::fs::File::open(&path).for_file(&path)?; |
|
167 | 182 | // TODO: what are the safety requirements here? |
|
168 | 183 | let mmap = unsafe { MmapOptions::new().map(&file) }.for_file(&path)?; |
|
169 | 184 | Ok(mmap) |
|
170 | 185 | } |
|
171 | 186 | } |
General Comments 0
You need to be logged in to leave comments.
Login now