Show More
@@ -1,277 +1,284 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::requirements; |
|
3 | use crate::requirements; | |
4 | use crate::utils::files::get_path_from_bytes; |
|
4 | use crate::utils::files::get_path_from_bytes; | |
5 | use crate::utils::SliceExt; |
|
5 | use crate::utils::SliceExt; | |
6 | use memmap::{Mmap, MmapOptions}; |
|
6 | use memmap::{Mmap, MmapOptions}; | |
7 | use std::collections::HashSet; |
|
7 | use std::collections::HashSet; | |
8 | use std::path::{Path, PathBuf}; |
|
8 | use std::path::{Path, PathBuf}; | |
9 |
|
9 | |||
10 | /// A repository on disk |
|
10 | /// A repository on disk | |
11 | pub struct Repo { |
|
11 | pub struct Repo { | |
12 | working_directory: PathBuf, |
|
12 | working_directory: PathBuf, | |
13 | dot_hg: PathBuf, |
|
13 | dot_hg: PathBuf, | |
14 | store: PathBuf, |
|
14 | store: PathBuf, | |
15 | requirements: HashSet<String>, |
|
15 | requirements: HashSet<String>, | |
16 | config: Config, |
|
16 | config: Config, | |
17 | } |
|
17 | } | |
18 |
|
18 | |||
19 | #[derive(Debug, derive_more::From)] |
|
19 | #[derive(Debug, derive_more::From)] | |
20 | pub enum RepoError { |
|
20 | pub enum RepoError { | |
21 | NotFound { |
|
21 | NotFound { | |
22 | at: PathBuf, |
|
22 | at: PathBuf, | |
23 | }, |
|
23 | }, | |
24 | #[from] |
|
24 | #[from] | |
25 | ConfigParseError(ConfigParseError), |
|
25 | ConfigParseError(ConfigParseError), | |
26 | #[from] |
|
26 | #[from] | |
27 | Other(HgError), |
|
27 | Other(HgError), | |
28 | } |
|
28 | } | |
29 |
|
29 | |||
30 | impl From<ConfigError> for RepoError { |
|
30 | impl From<ConfigError> for RepoError { | |
31 | fn from(error: ConfigError) -> Self { |
|
31 | fn from(error: ConfigError) -> Self { | |
32 | match error { |
|
32 | match error { | |
33 | ConfigError::Parse(error) => error.into(), |
|
33 | ConfigError::Parse(error) => error.into(), | |
34 | ConfigError::Other(error) => error.into(), |
|
34 | ConfigError::Other(error) => error.into(), | |
35 | } |
|
35 | } | |
36 | } |
|
36 | } | |
37 | } |
|
37 | } | |
38 |
|
38 | |||
39 | /// Filesystem access abstraction for the contents of a given "base" diretory |
|
39 | /// Filesystem access abstraction for the contents of a given "base" diretory | |
40 | #[derive(Clone, Copy)] |
|
40 | #[derive(Clone, Copy)] | |
41 | pub struct Vfs<'a> { |
|
41 | pub struct Vfs<'a> { | |
42 | pub(crate) base: &'a Path, |
|
42 | pub(crate) base: &'a Path, | |
43 | } |
|
43 | } | |
44 |
|
44 | |||
45 | impl Repo { |
|
45 | impl Repo { | |
|
46 | /// tries to find nearest repository root in current working directory or | |||
|
47 | /// its ancestors | |||
|
48 | pub fn find_repo_root() -> Result<PathBuf, RepoError> { | |||
|
49 | let current_directory = crate::utils::current_dir()?; | |||
|
50 | // ancestors() is inclusive: it first yields `current_directory` | |||
|
51 | // as-is. | |||
|
52 | for ancestor in current_directory.ancestors() { | |||
|
53 | if ancestor.join(".hg").is_dir() { | |||
|
54 | return Ok(ancestor.to_path_buf()); | |||
|
55 | } | |||
|
56 | } | |||
|
57 | return Err(RepoError::NotFound { | |||
|
58 | at: current_directory, | |||
|
59 | }); | |||
|
60 | } | |||
|
61 | ||||
46 | /// Find a repository, either at the given path (which must contain a `.hg` |
|
62 | /// Find a repository, either at the given path (which must contain a `.hg` | |
47 | /// sub-directory) or by searching the current directory and its |
|
63 | /// sub-directory) or by searching the current directory and its | |
48 | /// ancestors. |
|
64 | /// ancestors. | |
49 | /// |
|
65 | /// | |
50 | /// A method with two very different "modes" like this usually a code smell |
|
66 | /// A method with two very different "modes" like this usually a code smell | |
51 | /// to make two methods instead, but in this case an `Option` is what rhg |
|
67 | /// to make two methods instead, but in this case an `Option` is what rhg | |
52 | /// sub-commands get from Clap for the `-R` / `--repository` CLI argument. |
|
68 | /// sub-commands get from Clap for the `-R` / `--repository` CLI argument. | |
53 | /// Having two methods would just move that `if` to almost all callers. |
|
69 | /// Having two methods would just move that `if` to almost all callers. | |
54 | pub fn find( |
|
70 | pub fn find( | |
55 | config: &Config, |
|
71 | config: &Config, | |
56 | explicit_path: Option<PathBuf>, |
|
72 | explicit_path: Option<PathBuf>, | |
57 | ) -> Result<Self, RepoError> { |
|
73 | ) -> Result<Self, RepoError> { | |
58 | if let Some(root) = explicit_path { |
|
74 | if let Some(root) = explicit_path { | |
59 | if root.join(".hg").is_dir() { |
|
75 | if root.join(".hg").is_dir() { | |
60 | Self::new_at_path(root.to_owned(), config) |
|
76 | Self::new_at_path(root.to_owned(), config) | |
61 | } else if root.is_file() { |
|
77 | } else if root.is_file() { | |
62 | Err(HgError::unsupported("bundle repository").into()) |
|
78 | Err(HgError::unsupported("bundle repository").into()) | |
63 | } else { |
|
79 | } else { | |
64 | Err(RepoError::NotFound { |
|
80 | Err(RepoError::NotFound { | |
65 | at: root.to_owned(), |
|
81 | at: root.to_owned(), | |
66 | }) |
|
82 | }) | |
67 | } |
|
83 | } | |
68 | } else { |
|
84 | } else { | |
69 | let current_directory = crate::utils::current_dir()?; |
|
85 | let root = Self::find_repo_root()?; | |
70 | // ancestors() is inclusive: it first yields `current_directory` |
|
86 | Self::new_at_path(root, config) | |
71 | // as-is. |
|
|||
72 | for ancestor in current_directory.ancestors() { |
|
|||
73 | if ancestor.join(".hg").is_dir() { |
|
|||
74 | return Self::new_at_path(ancestor.to_owned(), config); |
|
|||
75 | } |
|
|||
76 | } |
|
|||
77 | Err(RepoError::NotFound { |
|
|||
78 | at: current_directory, |
|
|||
79 | }) |
|
|||
80 | } |
|
87 | } | |
81 | } |
|
88 | } | |
82 |
|
89 | |||
83 | /// To be called after checking that `.hg` is a sub-directory |
|
90 | /// To be called after checking that `.hg` is a sub-directory | |
84 | fn new_at_path( |
|
91 | fn new_at_path( | |
85 | working_directory: PathBuf, |
|
92 | working_directory: PathBuf, | |
86 | config: &Config, |
|
93 | config: &Config, | |
87 | ) -> Result<Self, RepoError> { |
|
94 | ) -> Result<Self, RepoError> { | |
88 | let dot_hg = working_directory.join(".hg"); |
|
95 | let dot_hg = working_directory.join(".hg"); | |
89 |
|
96 | |||
90 | let mut repo_config_files = Vec::new(); |
|
97 | let mut repo_config_files = Vec::new(); | |
91 | repo_config_files.push(dot_hg.join("hgrc")); |
|
98 | repo_config_files.push(dot_hg.join("hgrc")); | |
92 | repo_config_files.push(dot_hg.join("hgrc-not-shared")); |
|
99 | repo_config_files.push(dot_hg.join("hgrc-not-shared")); | |
93 |
|
100 | |||
94 | let hg_vfs = Vfs { base: &dot_hg }; |
|
101 | let hg_vfs = Vfs { base: &dot_hg }; | |
95 | let mut reqs = requirements::load_if_exists(hg_vfs)?; |
|
102 | let mut reqs = requirements::load_if_exists(hg_vfs)?; | |
96 | let relative = |
|
103 | let relative = | |
97 | reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT); |
|
104 | reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT); | |
98 | let shared = |
|
105 | let shared = | |
99 | reqs.contains(requirements::SHARED_REQUIREMENT) || relative; |
|
106 | reqs.contains(requirements::SHARED_REQUIREMENT) || relative; | |
100 |
|
107 | |||
101 | // From `mercurial/localrepo.py`: |
|
108 | // From `mercurial/localrepo.py`: | |
102 | // |
|
109 | // | |
103 | // if .hg/requires contains the sharesafe requirement, it means |
|
110 | // if .hg/requires contains the sharesafe requirement, it means | |
104 | // there exists a `.hg/store/requires` too and we should read it |
|
111 | // there exists a `.hg/store/requires` too and we should read it | |
105 | // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement |
|
112 | // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement | |
106 | // is present. We never write SHARESAFE_REQUIREMENT for a repo if store |
|
113 | // is present. We never write SHARESAFE_REQUIREMENT for a repo if store | |
107 | // is not present, refer checkrequirementscompat() for that |
|
114 | // is not present, refer checkrequirementscompat() for that | |
108 | // |
|
115 | // | |
109 | // However, if SHARESAFE_REQUIREMENT is not present, it means that the |
|
116 | // However, if SHARESAFE_REQUIREMENT is not present, it means that the | |
110 | // repository was shared the old way. We check the share source |
|
117 | // repository was shared the old way. We check the share source | |
111 | // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the |
|
118 | // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the | |
112 | // current repository needs to be reshared |
|
119 | // current repository needs to be reshared | |
113 | let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT); |
|
120 | let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT); | |
114 |
|
121 | |||
115 | let store_path; |
|
122 | let store_path; | |
116 | if !shared { |
|
123 | if !shared { | |
117 | store_path = dot_hg.join("store"); |
|
124 | store_path = dot_hg.join("store"); | |
118 | } else { |
|
125 | } else { | |
119 | let bytes = hg_vfs.read("sharedpath")?; |
|
126 | let bytes = hg_vfs.read("sharedpath")?; | |
120 | let mut shared_path = |
|
127 | let mut shared_path = | |
121 | get_path_from_bytes(bytes.trim_end_newlines()).to_owned(); |
|
128 | get_path_from_bytes(bytes.trim_end_newlines()).to_owned(); | |
122 | if relative { |
|
129 | if relative { | |
123 | shared_path = dot_hg.join(shared_path) |
|
130 | shared_path = dot_hg.join(shared_path) | |
124 | } |
|
131 | } | |
125 | if !shared_path.is_dir() { |
|
132 | if !shared_path.is_dir() { | |
126 | return Err(HgError::corrupted(format!( |
|
133 | return Err(HgError::corrupted(format!( | |
127 | ".hg/sharedpath points to nonexistent directory {}", |
|
134 | ".hg/sharedpath points to nonexistent directory {}", | |
128 | shared_path.display() |
|
135 | shared_path.display() | |
129 | )) |
|
136 | )) | |
130 | .into()); |
|
137 | .into()); | |
131 | } |
|
138 | } | |
132 |
|
139 | |||
133 | store_path = shared_path.join("store"); |
|
140 | store_path = shared_path.join("store"); | |
134 |
|
141 | |||
135 | let source_is_share_safe = |
|
142 | let source_is_share_safe = | |
136 | requirements::load(Vfs { base: &shared_path })? |
|
143 | requirements::load(Vfs { base: &shared_path })? | |
137 | .contains(requirements::SHARESAFE_REQUIREMENT); |
|
144 | .contains(requirements::SHARESAFE_REQUIREMENT); | |
138 |
|
145 | |||
139 | if share_safe && !source_is_share_safe { |
|
146 | if share_safe && !source_is_share_safe { | |
140 | return Err(match config |
|
147 | return Err(match config | |
141 | .get(b"share", b"safe-mismatch.source-not-safe") |
|
148 | .get(b"share", b"safe-mismatch.source-not-safe") | |
142 | { |
|
149 | { | |
143 | Some(b"abort") | None => HgError::abort( |
|
150 | Some(b"abort") | None => HgError::abort( | |
144 | "abort: share source does not support share-safe requirement\n\ |
|
151 | "abort: share source does not support share-safe requirement\n\ | |
145 | (see `hg help config.format.use-share-safe` for more information)", |
|
152 | (see `hg help config.format.use-share-safe` for more information)", | |
146 | ), |
|
153 | ), | |
147 | _ => HgError::unsupported("share-safe downgrade"), |
|
154 | _ => HgError::unsupported("share-safe downgrade"), | |
148 | } |
|
155 | } | |
149 | .into()); |
|
156 | .into()); | |
150 | } else if source_is_share_safe && !share_safe { |
|
157 | } else if source_is_share_safe && !share_safe { | |
151 | return Err( |
|
158 | return Err( | |
152 | match config.get(b"share", b"safe-mismatch.source-safe") { |
|
159 | match config.get(b"share", b"safe-mismatch.source-safe") { | |
153 | Some(b"abort") | None => HgError::abort( |
|
160 | Some(b"abort") | None => HgError::abort( | |
154 | "abort: version mismatch: source uses share-safe \ |
|
161 | "abort: version mismatch: source uses share-safe \ | |
155 | functionality while the current share does not\n\ |
|
162 | functionality while the current share does not\n\ | |
156 | (see `hg help config.format.use-share-safe` for more information)", |
|
163 | (see `hg help config.format.use-share-safe` for more information)", | |
157 | ), |
|
164 | ), | |
158 | _ => HgError::unsupported("share-safe upgrade"), |
|
165 | _ => HgError::unsupported("share-safe upgrade"), | |
159 | } |
|
166 | } | |
160 | .into(), |
|
167 | .into(), | |
161 | ); |
|
168 | ); | |
162 | } |
|
169 | } | |
163 |
|
170 | |||
164 | if share_safe { |
|
171 | if share_safe { | |
165 | repo_config_files.insert(0, shared_path.join("hgrc")) |
|
172 | repo_config_files.insert(0, shared_path.join("hgrc")) | |
166 | } |
|
173 | } | |
167 | } |
|
174 | } | |
168 | if share_safe { |
|
175 | if share_safe { | |
169 | reqs.extend(requirements::load(Vfs { base: &store_path })?); |
|
176 | reqs.extend(requirements::load(Vfs { base: &store_path })?); | |
170 | } |
|
177 | } | |
171 |
|
178 | |||
172 | let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() { |
|
179 | let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() { | |
173 | config.combine_with_repo(&repo_config_files)? |
|
180 | config.combine_with_repo(&repo_config_files)? | |
174 | } else { |
|
181 | } else { | |
175 | config.clone() |
|
182 | config.clone() | |
176 | }; |
|
183 | }; | |
177 |
|
184 | |||
178 | let repo = Self { |
|
185 | let repo = Self { | |
179 | requirements: reqs, |
|
186 | requirements: reqs, | |
180 | working_directory, |
|
187 | working_directory, | |
181 | store: store_path, |
|
188 | store: store_path, | |
182 | dot_hg, |
|
189 | dot_hg, | |
183 | config: repo_config, |
|
190 | config: repo_config, | |
184 | }; |
|
191 | }; | |
185 |
|
192 | |||
186 | requirements::check(&repo)?; |
|
193 | requirements::check(&repo)?; | |
187 |
|
194 | |||
188 | Ok(repo) |
|
195 | Ok(repo) | |
189 | } |
|
196 | } | |
190 |
|
197 | |||
191 | pub fn working_directory_path(&self) -> &Path { |
|
198 | pub fn working_directory_path(&self) -> &Path { | |
192 | &self.working_directory |
|
199 | &self.working_directory | |
193 | } |
|
200 | } | |
194 |
|
201 | |||
195 | pub fn requirements(&self) -> &HashSet<String> { |
|
202 | pub fn requirements(&self) -> &HashSet<String> { | |
196 | &self.requirements |
|
203 | &self.requirements | |
197 | } |
|
204 | } | |
198 |
|
205 | |||
199 | pub fn config(&self) -> &Config { |
|
206 | pub fn config(&self) -> &Config { | |
200 | &self.config |
|
207 | &self.config | |
201 | } |
|
208 | } | |
202 |
|
209 | |||
203 | /// For accessing repository files (in `.hg`), except for the store |
|
210 | /// For accessing repository files (in `.hg`), except for the store | |
204 | /// (`.hg/store`). |
|
211 | /// (`.hg/store`). | |
205 | pub fn hg_vfs(&self) -> Vfs<'_> { |
|
212 | pub fn hg_vfs(&self) -> Vfs<'_> { | |
206 | Vfs { base: &self.dot_hg } |
|
213 | Vfs { base: &self.dot_hg } | |
207 | } |
|
214 | } | |
208 |
|
215 | |||
209 | /// For accessing repository store files (in `.hg/store`) |
|
216 | /// For accessing repository store files (in `.hg/store`) | |
210 | pub fn store_vfs(&self) -> Vfs<'_> { |
|
217 | pub fn store_vfs(&self) -> Vfs<'_> { | |
211 | Vfs { base: &self.store } |
|
218 | Vfs { base: &self.store } | |
212 | } |
|
219 | } | |
213 |
|
220 | |||
214 | /// For accessing the working copy |
|
221 | /// For accessing the working copy | |
215 | pub fn working_directory_vfs(&self) -> Vfs<'_> { |
|
222 | pub fn working_directory_vfs(&self) -> Vfs<'_> { | |
216 | Vfs { |
|
223 | Vfs { | |
217 | base: &self.working_directory, |
|
224 | base: &self.working_directory, | |
218 | } |
|
225 | } | |
219 | } |
|
226 | } | |
220 |
|
227 | |||
221 | pub fn has_dirstate_v2(&self) -> bool { |
|
228 | pub fn has_dirstate_v2(&self) -> bool { | |
222 | self.requirements |
|
229 | self.requirements | |
223 | .contains(requirements::DIRSTATE_V2_REQUIREMENT) |
|
230 | .contains(requirements::DIRSTATE_V2_REQUIREMENT) | |
224 | } |
|
231 | } | |
225 |
|
232 | |||
226 | pub fn dirstate_parents( |
|
233 | pub fn dirstate_parents( | |
227 | &self, |
|
234 | &self, | |
228 | ) -> Result<crate::dirstate::DirstateParents, HgError> { |
|
235 | ) -> Result<crate::dirstate::DirstateParents, HgError> { | |
229 | let dirstate = self.hg_vfs().mmap_open("dirstate")?; |
|
236 | let dirstate = self.hg_vfs().mmap_open("dirstate")?; | |
230 | if dirstate.is_empty() { |
|
237 | if dirstate.is_empty() { | |
231 | return Ok(crate::dirstate::DirstateParents::NULL); |
|
238 | return Ok(crate::dirstate::DirstateParents::NULL); | |
232 | } |
|
239 | } | |
233 | let parents = if self.has_dirstate_v2() { |
|
240 | let parents = if self.has_dirstate_v2() { | |
234 | crate::dirstate_tree::on_disk::parse_dirstate_parents(&dirstate)? |
|
241 | crate::dirstate_tree::on_disk::parse_dirstate_parents(&dirstate)? | |
235 | } else { |
|
242 | } else { | |
236 | crate::dirstate::parsers::parse_dirstate_parents(&dirstate)? |
|
243 | crate::dirstate::parsers::parse_dirstate_parents(&dirstate)? | |
237 | }; |
|
244 | }; | |
238 | Ok(parents.clone()) |
|
245 | Ok(parents.clone()) | |
239 | } |
|
246 | } | |
240 | } |
|
247 | } | |
241 |
|
248 | |||
242 | impl Vfs<'_> { |
|
249 | impl Vfs<'_> { | |
243 | pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf { |
|
250 | pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf { | |
244 | self.base.join(relative_path) |
|
251 | self.base.join(relative_path) | |
245 | } |
|
252 | } | |
246 |
|
253 | |||
247 | pub fn read( |
|
254 | pub fn read( | |
248 | &self, |
|
255 | &self, | |
249 | relative_path: impl AsRef<Path>, |
|
256 | relative_path: impl AsRef<Path>, | |
250 | ) -> Result<Vec<u8>, HgError> { |
|
257 | ) -> Result<Vec<u8>, HgError> { | |
251 | let path = self.join(relative_path); |
|
258 | let path = self.join(relative_path); | |
252 | std::fs::read(&path).when_reading_file(&path) |
|
259 | std::fs::read(&path).when_reading_file(&path) | |
253 | } |
|
260 | } | |
254 |
|
261 | |||
255 | pub fn mmap_open( |
|
262 | pub fn mmap_open( | |
256 | &self, |
|
263 | &self, | |
257 | relative_path: impl AsRef<Path>, |
|
264 | relative_path: impl AsRef<Path>, | |
258 | ) -> Result<Mmap, HgError> { |
|
265 | ) -> Result<Mmap, HgError> { | |
259 | let path = self.base.join(relative_path); |
|
266 | let path = self.base.join(relative_path); | |
260 | let file = std::fs::File::open(&path).when_reading_file(&path)?; |
|
267 | let file = std::fs::File::open(&path).when_reading_file(&path)?; | |
261 | // TODO: what are the safety requirements here? |
|
268 | // TODO: what are the safety requirements here? | |
262 | let mmap = unsafe { MmapOptions::new().map(&file) } |
|
269 | let mmap = unsafe { MmapOptions::new().map(&file) } | |
263 | .when_reading_file(&path)?; |
|
270 | .when_reading_file(&path)?; | |
264 | Ok(mmap) |
|
271 | Ok(mmap) | |
265 | } |
|
272 | } | |
266 |
|
273 | |||
267 | pub fn rename( |
|
274 | pub fn rename( | |
268 | &self, |
|
275 | &self, | |
269 | relative_from: impl AsRef<Path>, |
|
276 | relative_from: impl AsRef<Path>, | |
270 | relative_to: impl AsRef<Path>, |
|
277 | relative_to: impl AsRef<Path>, | |
271 | ) -> Result<(), HgError> { |
|
278 | ) -> Result<(), HgError> { | |
272 | let from = self.join(relative_from); |
|
279 | let from = self.join(relative_from); | |
273 | let to = self.join(relative_to); |
|
280 | let to = self.join(relative_to); | |
274 | std::fs::rename(&from, &to) |
|
281 | std::fs::rename(&from, &to) | |
275 | .with_context(|| IoErrorContext::RenamingFile { from, to }) |
|
282 | .with_context(|| IoErrorContext::RenamingFile { from, to }) | |
276 | } |
|
283 | } | |
277 | } |
|
284 | } |
@@ -1,575 +1,574 b'' | |||||
1 | extern crate log; |
|
1 | extern crate log; | |
2 | use crate::ui::Ui; |
|
2 | use crate::ui::Ui; | |
3 | use clap::App; |
|
3 | use clap::App; | |
4 | use clap::AppSettings; |
|
4 | use clap::AppSettings; | |
5 | use clap::Arg; |
|
5 | use clap::Arg; | |
6 | use clap::ArgMatches; |
|
6 | use clap::ArgMatches; | |
7 | use format_bytes::{format_bytes, join}; |
|
7 | use format_bytes::{format_bytes, join}; | |
8 | use hg::config::{Config, ConfigSource}; |
|
8 | use hg::config::{Config, ConfigSource}; | |
9 | use hg::repo::{Repo, RepoError}; |
|
9 | use hg::repo::{Repo, RepoError}; | |
10 | use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes}; |
|
10 | use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes}; | |
11 | use hg::utils::SliceExt; |
|
11 | use hg::utils::SliceExt; | |
12 | use std::ffi::OsString; |
|
12 | use std::ffi::OsString; | |
13 | use std::path::PathBuf; |
|
13 | use std::path::PathBuf; | |
14 | use std::process::Command; |
|
14 | use std::process::Command; | |
15 |
|
15 | |||
16 | mod blackbox; |
|
16 | mod blackbox; | |
17 | mod error; |
|
17 | mod error; | |
18 | mod exitcode; |
|
18 | mod exitcode; | |
19 | mod ui; |
|
19 | mod ui; | |
20 | use error::CommandError; |
|
20 | use error::CommandError; | |
21 |
|
21 | |||
22 | fn main_with_result( |
|
22 | fn main_with_result( | |
23 | process_start_time: &blackbox::ProcessStartTime, |
|
23 | process_start_time: &blackbox::ProcessStartTime, | |
24 | ui: &ui::Ui, |
|
24 | ui: &ui::Ui, | |
25 | repo: Result<&Repo, &NoRepoInCwdError>, |
|
25 | repo: Result<&Repo, &NoRepoInCwdError>, | |
26 | config: &Config, |
|
26 | config: &Config, | |
27 | ) -> Result<(), CommandError> { |
|
27 | ) -> Result<(), CommandError> { | |
28 | check_extensions(config)?; |
|
28 | check_extensions(config)?; | |
29 |
|
29 | |||
30 | let app = App::new("rhg") |
|
30 | let app = App::new("rhg") | |
31 | .global_setting(AppSettings::AllowInvalidUtf8) |
|
31 | .global_setting(AppSettings::AllowInvalidUtf8) | |
32 | .global_setting(AppSettings::DisableVersion) |
|
32 | .global_setting(AppSettings::DisableVersion) | |
33 | .setting(AppSettings::SubcommandRequired) |
|
33 | .setting(AppSettings::SubcommandRequired) | |
34 | .setting(AppSettings::VersionlessSubcommands) |
|
34 | .setting(AppSettings::VersionlessSubcommands) | |
35 | .arg( |
|
35 | .arg( | |
36 | Arg::with_name("repository") |
|
36 | Arg::with_name("repository") | |
37 | .help("repository root directory") |
|
37 | .help("repository root directory") | |
38 | .short("-R") |
|
38 | .short("-R") | |
39 | .long("--repository") |
|
39 | .long("--repository") | |
40 | .value_name("REPO") |
|
40 | .value_name("REPO") | |
41 | .takes_value(true) |
|
41 | .takes_value(true) | |
42 | // Both ok: `hg -R ./foo log` or `hg log -R ./foo` |
|
42 | // Both ok: `hg -R ./foo log` or `hg log -R ./foo` | |
43 | .global(true), |
|
43 | .global(true), | |
44 | ) |
|
44 | ) | |
45 | .arg( |
|
45 | .arg( | |
46 | Arg::with_name("config") |
|
46 | Arg::with_name("config") | |
47 | .help("set/override config option (use 'section.name=value')") |
|
47 | .help("set/override config option (use 'section.name=value')") | |
48 | .long("--config") |
|
48 | .long("--config") | |
49 | .value_name("CONFIG") |
|
49 | .value_name("CONFIG") | |
50 | .takes_value(true) |
|
50 | .takes_value(true) | |
51 | .global(true) |
|
51 | .global(true) | |
52 | // Ok: `--config section.key1=val --config section.key2=val2` |
|
52 | // Ok: `--config section.key1=val --config section.key2=val2` | |
53 | .multiple(true) |
|
53 | .multiple(true) | |
54 | // Not ok: `--config section.key1=val section.key2=val2` |
|
54 | // Not ok: `--config section.key1=val section.key2=val2` | |
55 | .number_of_values(1), |
|
55 | .number_of_values(1), | |
56 | ) |
|
56 | ) | |
57 | .arg( |
|
57 | .arg( | |
58 | Arg::with_name("cwd") |
|
58 | Arg::with_name("cwd") | |
59 | .help("change working directory") |
|
59 | .help("change working directory") | |
60 | .long("--cwd") |
|
60 | .long("--cwd") | |
61 | .value_name("DIR") |
|
61 | .value_name("DIR") | |
62 | .takes_value(true) |
|
62 | .takes_value(true) | |
63 | .global(true), |
|
63 | .global(true), | |
64 | ) |
|
64 | ) | |
65 | .version("0.0.1"); |
|
65 | .version("0.0.1"); | |
66 | let app = add_subcommand_args(app); |
|
66 | let app = add_subcommand_args(app); | |
67 |
|
67 | |||
68 | let matches = app.clone().get_matches_safe()?; |
|
68 | let matches = app.clone().get_matches_safe()?; | |
69 |
|
69 | |||
70 | let (subcommand_name, subcommand_matches) = matches.subcommand(); |
|
70 | let (subcommand_name, subcommand_matches) = matches.subcommand(); | |
71 | let run = subcommand_run_fn(subcommand_name) |
|
71 | let run = subcommand_run_fn(subcommand_name) | |
72 | .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired"); |
|
72 | .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired"); | |
73 | let subcommand_args = subcommand_matches |
|
73 | let subcommand_args = subcommand_matches | |
74 | .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired"); |
|
74 | .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired"); | |
75 |
|
75 | |||
76 | let invocation = CliInvocation { |
|
76 | let invocation = CliInvocation { | |
77 | ui, |
|
77 | ui, | |
78 | subcommand_args, |
|
78 | subcommand_args, | |
79 | config, |
|
79 | config, | |
80 | repo, |
|
80 | repo, | |
81 | }; |
|
81 | }; | |
82 | let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?; |
|
82 | let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?; | |
83 | blackbox.log_command_start(); |
|
83 | blackbox.log_command_start(); | |
84 | let result = run(&invocation); |
|
84 | let result = run(&invocation); | |
85 | blackbox.log_command_end(exit_code( |
|
85 | blackbox.log_command_end(exit_code( | |
86 | &result, |
|
86 | &result, | |
87 | // TODO: show a warning or combine with original error if `get_bool` |
|
87 | // TODO: show a warning or combine with original error if `get_bool` | |
88 | // returns an error |
|
88 | // returns an error | |
89 | config |
|
89 | config | |
90 | .get_bool(b"ui", b"detailed-exit-code") |
|
90 | .get_bool(b"ui", b"detailed-exit-code") | |
91 | .unwrap_or(false), |
|
91 | .unwrap_or(false), | |
92 | )); |
|
92 | )); | |
93 | result |
|
93 | result | |
94 | } |
|
94 | } | |
95 |
|
95 | |||
96 | fn main() { |
|
96 | fn main() { | |
97 | // Run this first, before we find out if the blackbox extension is even |
|
97 | // Run this first, before we find out if the blackbox extension is even | |
98 | // enabled, in order to include everything in-between in the duration |
|
98 | // enabled, in order to include everything in-between in the duration | |
99 | // measurements. Reading config files can be slow if theyβre on NFS. |
|
99 | // measurements. Reading config files can be slow if theyβre on NFS. | |
100 | let process_start_time = blackbox::ProcessStartTime::now(); |
|
100 | let process_start_time = blackbox::ProcessStartTime::now(); | |
101 |
|
101 | |||
102 | env_logger::init(); |
|
102 | env_logger::init(); | |
103 | let ui = ui::Ui::new(); |
|
103 | let ui = ui::Ui::new(); | |
104 |
|
104 | |||
105 | let early_args = EarlyArgs::parse(std::env::args_os()); |
|
105 | let early_args = EarlyArgs::parse(std::env::args_os()); | |
106 |
|
106 | |||
107 | let initial_current_dir = early_args.cwd.map(|cwd| { |
|
107 | let initial_current_dir = early_args.cwd.map(|cwd| { | |
108 | let cwd = get_path_from_bytes(&cwd); |
|
108 | let cwd = get_path_from_bytes(&cwd); | |
109 | std::env::current_dir() |
|
109 | std::env::current_dir() | |
110 | .and_then(|initial| { |
|
110 | .and_then(|initial| { | |
111 | std::env::set_current_dir(cwd)?; |
|
111 | std::env::set_current_dir(cwd)?; | |
112 | Ok(initial) |
|
112 | Ok(initial) | |
113 | }) |
|
113 | }) | |
114 | .unwrap_or_else(|error| { |
|
114 | .unwrap_or_else(|error| { | |
115 | exit( |
|
115 | exit( | |
116 | &None, |
|
116 | &None, | |
117 | &ui, |
|
117 | &ui, | |
118 | OnUnsupported::Abort, |
|
118 | OnUnsupported::Abort, | |
119 | Err(CommandError::abort(format!( |
|
119 | Err(CommandError::abort(format!( | |
120 | "abort: {}: '{}'", |
|
120 | "abort: {}: '{}'", | |
121 | error, |
|
121 | error, | |
122 | cwd.display() |
|
122 | cwd.display() | |
123 | ))), |
|
123 | ))), | |
124 | false, |
|
124 | false, | |
125 | ) |
|
125 | ) | |
126 | }) |
|
126 | }) | |
127 | }); |
|
127 | }); | |
128 |
|
128 | |||
129 | let non_repo_config = |
|
129 | let non_repo_config = | |
130 | Config::load(early_args.config).unwrap_or_else(|error| { |
|
130 | Config::load(early_args.config).unwrap_or_else(|error| { | |
131 | // Normally this is decided based on config, but we donβt have that |
|
131 | // Normally this is decided based on config, but we donβt have that | |
132 | // available. As of this writing config loading never returns an |
|
132 | // available. As of this writing config loading never returns an | |
133 | // "unsupported" error but that is not enforced by the type system. |
|
133 | // "unsupported" error but that is not enforced by the type system. | |
134 | let on_unsupported = OnUnsupported::Abort; |
|
134 | let on_unsupported = OnUnsupported::Abort; | |
135 |
|
135 | |||
136 | exit( |
|
136 | exit( | |
137 | &initial_current_dir, |
|
137 | &initial_current_dir, | |
138 | &ui, |
|
138 | &ui, | |
139 | on_unsupported, |
|
139 | on_unsupported, | |
140 | Err(error.into()), |
|
140 | Err(error.into()), | |
141 | false, |
|
141 | false, | |
142 | ) |
|
142 | ) | |
143 | }); |
|
143 | }); | |
144 |
|
144 | |||
145 | if let Some(repo_path_bytes) = &early_args.repo { |
|
145 | if let Some(repo_path_bytes) = &early_args.repo { | |
146 | lazy_static::lazy_static! { |
|
146 | lazy_static::lazy_static! { | |
147 | static ref SCHEME_RE: regex::bytes::Regex = |
|
147 | static ref SCHEME_RE: regex::bytes::Regex = | |
148 | // Same as `_matchscheme` in `mercurial/util.py` |
|
148 | // Same as `_matchscheme` in `mercurial/util.py` | |
149 | regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap(); |
|
149 | regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap(); | |
150 | } |
|
150 | } | |
151 | if SCHEME_RE.is_match(&repo_path_bytes) { |
|
151 | if SCHEME_RE.is_match(&repo_path_bytes) { | |
152 | exit( |
|
152 | exit( | |
153 | &initial_current_dir, |
|
153 | &initial_current_dir, | |
154 | &ui, |
|
154 | &ui, | |
155 | OnUnsupported::from_config(&ui, &non_repo_config), |
|
155 | OnUnsupported::from_config(&ui, &non_repo_config), | |
156 | Err(CommandError::UnsupportedFeature { |
|
156 | Err(CommandError::UnsupportedFeature { | |
157 | message: format_bytes!( |
|
157 | message: format_bytes!( | |
158 | b"URL-like --repository {}", |
|
158 | b"URL-like --repository {}", | |
159 | repo_path_bytes |
|
159 | repo_path_bytes | |
160 | ), |
|
160 | ), | |
161 | }), |
|
161 | }), | |
162 | // TODO: show a warning or combine with original error if |
|
162 | // TODO: show a warning or combine with original error if | |
163 | // `get_bool` returns an error |
|
163 | // `get_bool` returns an error | |
164 | non_repo_config |
|
164 | non_repo_config | |
165 | .get_bool(b"ui", b"detailed-exit-code") |
|
165 | .get_bool(b"ui", b"detailed-exit-code") | |
166 | .unwrap_or(false), |
|
166 | .unwrap_or(false), | |
167 | ) |
|
167 | ) | |
168 | } |
|
168 | } | |
169 | } |
|
169 | } | |
170 | let repo_arg = early_args.repo.unwrap_or(Vec::new()); |
|
170 | let repo_arg = early_args.repo.unwrap_or(Vec::new()); | |
171 | let repo_path: Option<PathBuf> = { |
|
171 | let repo_path: Option<PathBuf> = { | |
172 | if repo_arg.is_empty() { |
|
172 | if repo_arg.is_empty() { | |
173 | None |
|
173 | None | |
174 | } else { |
|
174 | } else { | |
175 | let local_config = { |
|
175 | let local_config = { | |
176 | if std::env::var_os("HGRCSKIPREPO").is_none() { |
|
176 | if std::env::var_os("HGRCSKIPREPO").is_none() { | |
177 | let current_dir = hg::utils::current_dir(); |
|
177 | // TODO: handle errors from find_repo_root | |
178 | // TODO: handle errors from current_dir |
|
178 | if let Ok(current_dir_path) = Repo::find_repo_root() { | |
179 | if let Ok(current_dir_path) = current_dir { |
|
|||
180 | let config_files = vec![ |
|
179 | let config_files = vec![ | |
181 | ConfigSource::AbsPath( |
|
180 | ConfigSource::AbsPath( | |
182 | current_dir_path.join(".hg/hgrc"), |
|
181 | current_dir_path.join(".hg/hgrc"), | |
183 | ), |
|
182 | ), | |
184 | ConfigSource::AbsPath( |
|
183 | ConfigSource::AbsPath( | |
185 | current_dir_path.join(".hg/hgrc-not-shared"), |
|
184 | current_dir_path.join(".hg/hgrc-not-shared"), | |
186 | ), |
|
185 | ), | |
187 | ]; |
|
186 | ]; | |
188 | // TODO: handle errors from |
|
187 | // TODO: handle errors from | |
189 | // `load_from_explicit_sources` |
|
188 | // `load_from_explicit_sources` | |
190 | Config::load_from_explicit_sources(config_files).ok() |
|
189 | Config::load_from_explicit_sources(config_files).ok() | |
191 | } else { |
|
190 | } else { | |
192 | None |
|
191 | None | |
193 | } |
|
192 | } | |
194 | } else { |
|
193 | } else { | |
195 | None |
|
194 | None | |
196 | } |
|
195 | } | |
197 | }; |
|
196 | }; | |
198 |
|
197 | |||
199 | let non_repo_config_val = { |
|
198 | let non_repo_config_val = { | |
200 | let non_repo_val = non_repo_config.get(b"paths", &repo_arg); |
|
199 | let non_repo_val = non_repo_config.get(b"paths", &repo_arg); | |
201 | match &non_repo_val { |
|
200 | match &non_repo_val { | |
202 | Some(val) if val.len() > 0 => home::home_dir() |
|
201 | Some(val) if val.len() > 0 => home::home_dir() | |
203 | .unwrap_or_else(|| PathBuf::from("~")) |
|
202 | .unwrap_or_else(|| PathBuf::from("~")) | |
204 | .join(get_path_from_bytes(val)) |
|
203 | .join(get_path_from_bytes(val)) | |
205 | .canonicalize() |
|
204 | .canonicalize() | |
206 | // TODO: handle error and make it similar to python |
|
205 | // TODO: handle error and make it similar to python | |
207 | // implementation maybe? |
|
206 | // implementation maybe? | |
208 | .ok(), |
|
207 | .ok(), | |
209 | _ => None, |
|
208 | _ => None, | |
210 | } |
|
209 | } | |
211 | }; |
|
210 | }; | |
212 |
|
211 | |||
213 | let config_val = match &local_config { |
|
212 | let config_val = match &local_config { | |
214 | None => non_repo_config_val, |
|
213 | None => non_repo_config_val, | |
215 | Some(val) => { |
|
214 | Some(val) => { | |
216 | let local_config_val = val.get(b"paths", &repo_arg); |
|
215 | let local_config_val = val.get(b"paths", &repo_arg); | |
217 | match &local_config_val { |
|
216 | match &local_config_val { | |
218 | Some(val) if val.len() > 0 => { |
|
217 | Some(val) if val.len() > 0 => { | |
219 | // presence of a local_config assures that |
|
218 | // presence of a local_config assures that | |
220 | // current_dir |
|
219 | // current_dir | |
221 | // wont result in an Error |
|
220 | // wont result in an Error | |
222 | let canpath = hg::utils::current_dir() |
|
221 | let canpath = hg::utils::current_dir() | |
223 | .unwrap() |
|
222 | .unwrap() | |
224 | .join(get_path_from_bytes(val)) |
|
223 | .join(get_path_from_bytes(val)) | |
225 | .canonicalize(); |
|
224 | .canonicalize(); | |
226 | canpath.ok().or(non_repo_config_val) |
|
225 | canpath.ok().or(non_repo_config_val) | |
227 | } |
|
226 | } | |
228 | _ => non_repo_config_val, |
|
227 | _ => non_repo_config_val, | |
229 | } |
|
228 | } | |
230 | } |
|
229 | } | |
231 | }; |
|
230 | }; | |
232 | config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf())) |
|
231 | config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf())) | |
233 | } |
|
232 | } | |
234 | }; |
|
233 | }; | |
235 |
|
234 | |||
236 | let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned()) |
|
235 | let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned()) | |
237 | { |
|
236 | { | |
238 | Ok(repo) => Ok(repo), |
|
237 | Ok(repo) => Ok(repo), | |
239 | Err(RepoError::NotFound { at }) if repo_path.is_none() => { |
|
238 | Err(RepoError::NotFound { at }) if repo_path.is_none() => { | |
240 | // Not finding a repo is not fatal yet, if `-R` was not given |
|
239 | // Not finding a repo is not fatal yet, if `-R` was not given | |
241 | Err(NoRepoInCwdError { cwd: at }) |
|
240 | Err(NoRepoInCwdError { cwd: at }) | |
242 | } |
|
241 | } | |
243 | Err(error) => exit( |
|
242 | Err(error) => exit( | |
244 | &initial_current_dir, |
|
243 | &initial_current_dir, | |
245 | &ui, |
|
244 | &ui, | |
246 | OnUnsupported::from_config(&ui, &non_repo_config), |
|
245 | OnUnsupported::from_config(&ui, &non_repo_config), | |
247 | Err(error.into()), |
|
246 | Err(error.into()), | |
248 | // TODO: show a warning or combine with original error if |
|
247 | // TODO: show a warning or combine with original error if | |
249 | // `get_bool` returns an error |
|
248 | // `get_bool` returns an error | |
250 | non_repo_config |
|
249 | non_repo_config | |
251 | .get_bool(b"ui", b"detailed-exit-code") |
|
250 | .get_bool(b"ui", b"detailed-exit-code") | |
252 | .unwrap_or(false), |
|
251 | .unwrap_or(false), | |
253 | ), |
|
252 | ), | |
254 | }; |
|
253 | }; | |
255 |
|
254 | |||
256 | let config = if let Ok(repo) = &repo_result { |
|
255 | let config = if let Ok(repo) = &repo_result { | |
257 | repo.config() |
|
256 | repo.config() | |
258 | } else { |
|
257 | } else { | |
259 | &non_repo_config |
|
258 | &non_repo_config | |
260 | }; |
|
259 | }; | |
261 | let on_unsupported = OnUnsupported::from_config(&ui, config); |
|
260 | let on_unsupported = OnUnsupported::from_config(&ui, config); | |
262 |
|
261 | |||
263 | let result = main_with_result( |
|
262 | let result = main_with_result( | |
264 | &process_start_time, |
|
263 | &process_start_time, | |
265 | &ui, |
|
264 | &ui, | |
266 | repo_result.as_ref(), |
|
265 | repo_result.as_ref(), | |
267 | config, |
|
266 | config, | |
268 | ); |
|
267 | ); | |
269 | exit( |
|
268 | exit( | |
270 | &initial_current_dir, |
|
269 | &initial_current_dir, | |
271 | &ui, |
|
270 | &ui, | |
272 | on_unsupported, |
|
271 | on_unsupported, | |
273 | result, |
|
272 | result, | |
274 | // TODO: show a warning or combine with original error if `get_bool` |
|
273 | // TODO: show a warning or combine with original error if `get_bool` | |
275 | // returns an error |
|
274 | // returns an error | |
276 | config |
|
275 | config | |
277 | .get_bool(b"ui", b"detailed-exit-code") |
|
276 | .get_bool(b"ui", b"detailed-exit-code") | |
278 | .unwrap_or(false), |
|
277 | .unwrap_or(false), | |
279 | ) |
|
278 | ) | |
280 | } |
|
279 | } | |
281 |
|
280 | |||
282 | fn exit_code( |
|
281 | fn exit_code( | |
283 | result: &Result<(), CommandError>, |
|
282 | result: &Result<(), CommandError>, | |
284 | use_detailed_exit_code: bool, |
|
283 | use_detailed_exit_code: bool, | |
285 | ) -> i32 { |
|
284 | ) -> i32 { | |
286 | match result { |
|
285 | match result { | |
287 | Ok(()) => exitcode::OK, |
|
286 | Ok(()) => exitcode::OK, | |
288 | Err(CommandError::Abort { |
|
287 | Err(CommandError::Abort { | |
289 | message: _, |
|
288 | message: _, | |
290 | detailed_exit_code, |
|
289 | detailed_exit_code, | |
291 | }) => { |
|
290 | }) => { | |
292 | if use_detailed_exit_code { |
|
291 | if use_detailed_exit_code { | |
293 | *detailed_exit_code |
|
292 | *detailed_exit_code | |
294 | } else { |
|
293 | } else { | |
295 | exitcode::ABORT |
|
294 | exitcode::ABORT | |
296 | } |
|
295 | } | |
297 | } |
|
296 | } | |
298 | Err(CommandError::Unsuccessful) => exitcode::UNSUCCESSFUL, |
|
297 | Err(CommandError::Unsuccessful) => exitcode::UNSUCCESSFUL, | |
299 |
|
298 | |||
300 | // Exit with a specific code and no error message to let a potential |
|
299 | // Exit with a specific code and no error message to let a potential | |
301 | // wrapper script fallback to Python-based Mercurial. |
|
300 | // wrapper script fallback to Python-based Mercurial. | |
302 | Err(CommandError::UnsupportedFeature { .. }) => { |
|
301 | Err(CommandError::UnsupportedFeature { .. }) => { | |
303 | exitcode::UNIMPLEMENTED |
|
302 | exitcode::UNIMPLEMENTED | |
304 | } |
|
303 | } | |
305 | } |
|
304 | } | |
306 | } |
|
305 | } | |
307 |
|
306 | |||
308 | fn exit( |
|
307 | fn exit( | |
309 | initial_current_dir: &Option<PathBuf>, |
|
308 | initial_current_dir: &Option<PathBuf>, | |
310 | ui: &Ui, |
|
309 | ui: &Ui, | |
311 | mut on_unsupported: OnUnsupported, |
|
310 | mut on_unsupported: OnUnsupported, | |
312 | result: Result<(), CommandError>, |
|
311 | result: Result<(), CommandError>, | |
313 | use_detailed_exit_code: bool, |
|
312 | use_detailed_exit_code: bool, | |
314 | ) -> ! { |
|
313 | ) -> ! { | |
315 | if let ( |
|
314 | if let ( | |
316 | OnUnsupported::Fallback { executable }, |
|
315 | OnUnsupported::Fallback { executable }, | |
317 | Err(CommandError::UnsupportedFeature { .. }), |
|
316 | Err(CommandError::UnsupportedFeature { .. }), | |
318 | ) = (&on_unsupported, &result) |
|
317 | ) = (&on_unsupported, &result) | |
319 | { |
|
318 | { | |
320 | let mut args = std::env::args_os(); |
|
319 | let mut args = std::env::args_os(); | |
321 | let executable_path = get_path_from_bytes(&executable); |
|
320 | let executable_path = get_path_from_bytes(&executable); | |
322 | let this_executable = args.next().expect("exepcted argv[0] to exist"); |
|
321 | let this_executable = args.next().expect("exepcted argv[0] to exist"); | |
323 | if executable_path == &PathBuf::from(this_executable) { |
|
322 | if executable_path == &PathBuf::from(this_executable) { | |
324 | // Avoid spawning infinitely many processes until resource |
|
323 | // Avoid spawning infinitely many processes until resource | |
325 | // exhaustion. |
|
324 | // exhaustion. | |
326 | let _ = ui.write_stderr(&format_bytes!( |
|
325 | let _ = ui.write_stderr(&format_bytes!( | |
327 | b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \ |
|
326 | b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \ | |
328 | points to `rhg` itself.\n", |
|
327 | points to `rhg` itself.\n", | |
329 | executable |
|
328 | executable | |
330 | )); |
|
329 | )); | |
331 | on_unsupported = OnUnsupported::Abort |
|
330 | on_unsupported = OnUnsupported::Abort | |
332 | } else { |
|
331 | } else { | |
333 | // `args` is now `argv[1..]` since weβve already consumed `argv[0]` |
|
332 | // `args` is now `argv[1..]` since weβve already consumed `argv[0]` | |
334 | let mut command = Command::new(executable_path); |
|
333 | let mut command = Command::new(executable_path); | |
335 | command.args(args); |
|
334 | command.args(args); | |
336 | if let Some(initial) = initial_current_dir { |
|
335 | if let Some(initial) = initial_current_dir { | |
337 | command.current_dir(initial); |
|
336 | command.current_dir(initial); | |
338 | } |
|
337 | } | |
339 | let result = command.status(); |
|
338 | let result = command.status(); | |
340 | match result { |
|
339 | match result { | |
341 | Ok(status) => std::process::exit( |
|
340 | Ok(status) => std::process::exit( | |
342 | status.code().unwrap_or(exitcode::ABORT), |
|
341 | status.code().unwrap_or(exitcode::ABORT), | |
343 | ), |
|
342 | ), | |
344 | Err(error) => { |
|
343 | Err(error) => { | |
345 | let _ = ui.write_stderr(&format_bytes!( |
|
344 | let _ = ui.write_stderr(&format_bytes!( | |
346 | b"tried to fall back to a '{}' sub-process but got error {}\n", |
|
345 | b"tried to fall back to a '{}' sub-process but got error {}\n", | |
347 | executable, format_bytes::Utf8(error) |
|
346 | executable, format_bytes::Utf8(error) | |
348 | )); |
|
347 | )); | |
349 | on_unsupported = OnUnsupported::Abort |
|
348 | on_unsupported = OnUnsupported::Abort | |
350 | } |
|
349 | } | |
351 | } |
|
350 | } | |
352 | } |
|
351 | } | |
353 | } |
|
352 | } | |
354 | exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code) |
|
353 | exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code) | |
355 | } |
|
354 | } | |
356 |
|
355 | |||
357 | fn exit_no_fallback( |
|
356 | fn exit_no_fallback( | |
358 | ui: &Ui, |
|
357 | ui: &Ui, | |
359 | on_unsupported: OnUnsupported, |
|
358 | on_unsupported: OnUnsupported, | |
360 | result: Result<(), CommandError>, |
|
359 | result: Result<(), CommandError>, | |
361 | use_detailed_exit_code: bool, |
|
360 | use_detailed_exit_code: bool, | |
362 | ) -> ! { |
|
361 | ) -> ! { | |
363 | match &result { |
|
362 | match &result { | |
364 | Ok(_) => {} |
|
363 | Ok(_) => {} | |
365 | Err(CommandError::Unsuccessful) => {} |
|
364 | Err(CommandError::Unsuccessful) => {} | |
366 | Err(CommandError::Abort { |
|
365 | Err(CommandError::Abort { | |
367 | message, |
|
366 | message, | |
368 | detailed_exit_code: _, |
|
367 | detailed_exit_code: _, | |
369 | }) => { |
|
368 | }) => { | |
370 | if !message.is_empty() { |
|
369 | if !message.is_empty() { | |
371 | // Ignore errors when writing to stderr, weβre already exiting |
|
370 | // Ignore errors when writing to stderr, weβre already exiting | |
372 | // with failure code so thereβs not much more we can do. |
|
371 | // with failure code so thereβs not much more we can do. | |
373 | let _ = ui.write_stderr(&format_bytes!(b"{}\n", message)); |
|
372 | let _ = ui.write_stderr(&format_bytes!(b"{}\n", message)); | |
374 | } |
|
373 | } | |
375 | } |
|
374 | } | |
376 | Err(CommandError::UnsupportedFeature { message }) => { |
|
375 | Err(CommandError::UnsupportedFeature { message }) => { | |
377 | match on_unsupported { |
|
376 | match on_unsupported { | |
378 | OnUnsupported::Abort => { |
|
377 | OnUnsupported::Abort => { | |
379 | let _ = ui.write_stderr(&format_bytes!( |
|
378 | let _ = ui.write_stderr(&format_bytes!( | |
380 | b"unsupported feature: {}\n", |
|
379 | b"unsupported feature: {}\n", | |
381 | message |
|
380 | message | |
382 | )); |
|
381 | )); | |
383 | } |
|
382 | } | |
384 | OnUnsupported::AbortSilent => {} |
|
383 | OnUnsupported::AbortSilent => {} | |
385 | OnUnsupported::Fallback { .. } => unreachable!(), |
|
384 | OnUnsupported::Fallback { .. } => unreachable!(), | |
386 | } |
|
385 | } | |
387 | } |
|
386 | } | |
388 | } |
|
387 | } | |
389 | std::process::exit(exit_code(&result, use_detailed_exit_code)) |
|
388 | std::process::exit(exit_code(&result, use_detailed_exit_code)) | |
390 | } |
|
389 | } | |
391 |
|
390 | |||
392 | macro_rules! subcommands { |
|
391 | macro_rules! subcommands { | |
393 | ($( $command: ident )+) => { |
|
392 | ($( $command: ident )+) => { | |
394 | mod commands { |
|
393 | mod commands { | |
395 | $( |
|
394 | $( | |
396 | pub mod $command; |
|
395 | pub mod $command; | |
397 | )+ |
|
396 | )+ | |
398 | } |
|
397 | } | |
399 |
|
398 | |||
400 | fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { |
|
399 | fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { | |
401 | app |
|
400 | app | |
402 | $( |
|
401 | $( | |
403 | .subcommand(commands::$command::args()) |
|
402 | .subcommand(commands::$command::args()) | |
404 | )+ |
|
403 | )+ | |
405 | } |
|
404 | } | |
406 |
|
405 | |||
407 | pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>; |
|
406 | pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>; | |
408 |
|
407 | |||
409 | fn subcommand_run_fn(name: &str) -> Option<RunFn> { |
|
408 | fn subcommand_run_fn(name: &str) -> Option<RunFn> { | |
410 | match name { |
|
409 | match name { | |
411 | $( |
|
410 | $( | |
412 | stringify!($command) => Some(commands::$command::run), |
|
411 | stringify!($command) => Some(commands::$command::run), | |
413 | )+ |
|
412 | )+ | |
414 | _ => None, |
|
413 | _ => None, | |
415 | } |
|
414 | } | |
416 | } |
|
415 | } | |
417 | }; |
|
416 | }; | |
418 | } |
|
417 | } | |
419 |
|
418 | |||
420 | subcommands! { |
|
419 | subcommands! { | |
421 | cat |
|
420 | cat | |
422 | debugdata |
|
421 | debugdata | |
423 | debugrequirements |
|
422 | debugrequirements | |
424 | files |
|
423 | files | |
425 | root |
|
424 | root | |
426 | config |
|
425 | config | |
427 | status |
|
426 | status | |
428 | } |
|
427 | } | |
429 |
|
428 | |||
430 | pub struct CliInvocation<'a> { |
|
429 | pub struct CliInvocation<'a> { | |
431 | ui: &'a Ui, |
|
430 | ui: &'a Ui, | |
432 | subcommand_args: &'a ArgMatches<'a>, |
|
431 | subcommand_args: &'a ArgMatches<'a>, | |
433 | config: &'a Config, |
|
432 | config: &'a Config, | |
434 | /// References inside `Result` is a bit peculiar but allow |
|
433 | /// References inside `Result` is a bit peculiar but allow | |
435 | /// `invocation.repo?` to work out with `&CliInvocation` since this |
|
434 | /// `invocation.repo?` to work out with `&CliInvocation` since this | |
436 | /// `Result` type is `Copy`. |
|
435 | /// `Result` type is `Copy`. | |
437 | repo: Result<&'a Repo, &'a NoRepoInCwdError>, |
|
436 | repo: Result<&'a Repo, &'a NoRepoInCwdError>, | |
438 | } |
|
437 | } | |
439 |
|
438 | |||
440 | struct NoRepoInCwdError { |
|
439 | struct NoRepoInCwdError { | |
441 | cwd: PathBuf, |
|
440 | cwd: PathBuf, | |
442 | } |
|
441 | } | |
443 |
|
442 | |||
444 | /// CLI arguments to be parsed "early" in order to be able to read |
|
443 | /// CLI arguments to be parsed "early" in order to be able to read | |
445 | /// configuration before using Clap. Ideally we would also use Clap for this, |
|
444 | /// configuration before using Clap. Ideally we would also use Clap for this, | |
446 | /// see <https://github.com/clap-rs/clap/discussions/2366>. |
|
445 | /// see <https://github.com/clap-rs/clap/discussions/2366>. | |
447 | /// |
|
446 | /// | |
448 | /// These arguments are still declared when we do use Clap later, so that Clap |
|
447 | /// These arguments are still declared when we do use Clap later, so that Clap | |
449 | /// does not return an error for their presence. |
|
448 | /// does not return an error for their presence. | |
450 | struct EarlyArgs { |
|
449 | struct EarlyArgs { | |
451 | /// Values of all `--config` arguments. (Possibly none) |
|
450 | /// Values of all `--config` arguments. (Possibly none) | |
452 | config: Vec<Vec<u8>>, |
|
451 | config: Vec<Vec<u8>>, | |
453 | /// Value of the `-R` or `--repository` argument, if any. |
|
452 | /// Value of the `-R` or `--repository` argument, if any. | |
454 | repo: Option<Vec<u8>>, |
|
453 | repo: Option<Vec<u8>>, | |
455 | /// Value of the `--cwd` argument, if any. |
|
454 | /// Value of the `--cwd` argument, if any. | |
456 | cwd: Option<Vec<u8>>, |
|
455 | cwd: Option<Vec<u8>>, | |
457 | } |
|
456 | } | |
458 |
|
457 | |||
459 | impl EarlyArgs { |
|
458 | impl EarlyArgs { | |
460 | fn parse(args: impl IntoIterator<Item = OsString>) -> Self { |
|
459 | fn parse(args: impl IntoIterator<Item = OsString>) -> Self { | |
461 | let mut args = args.into_iter().map(get_bytes_from_os_str); |
|
460 | let mut args = args.into_iter().map(get_bytes_from_os_str); | |
462 | let mut config = Vec::new(); |
|
461 | let mut config = Vec::new(); | |
463 | let mut repo = None; |
|
462 | let mut repo = None; | |
464 | let mut cwd = None; |
|
463 | let mut cwd = None; | |
465 | // Use `while let` instead of `for` so that we can also call |
|
464 | // Use `while let` instead of `for` so that we can also call | |
466 | // `args.next()` inside the loop. |
|
465 | // `args.next()` inside the loop. | |
467 | while let Some(arg) = args.next() { |
|
466 | while let Some(arg) = args.next() { | |
468 | if arg == b"--config" { |
|
467 | if arg == b"--config" { | |
469 | if let Some(value) = args.next() { |
|
468 | if let Some(value) = args.next() { | |
470 | config.push(value) |
|
469 | config.push(value) | |
471 | } |
|
470 | } | |
472 | } else if let Some(value) = arg.drop_prefix(b"--config=") { |
|
471 | } else if let Some(value) = arg.drop_prefix(b"--config=") { | |
473 | config.push(value.to_owned()) |
|
472 | config.push(value.to_owned()) | |
474 | } |
|
473 | } | |
475 |
|
474 | |||
476 | if arg == b"--cwd" { |
|
475 | if arg == b"--cwd" { | |
477 | if let Some(value) = args.next() { |
|
476 | if let Some(value) = args.next() { | |
478 | cwd = Some(value) |
|
477 | cwd = Some(value) | |
479 | } |
|
478 | } | |
480 | } else if let Some(value) = arg.drop_prefix(b"--cwd=") { |
|
479 | } else if let Some(value) = arg.drop_prefix(b"--cwd=") { | |
481 | cwd = Some(value.to_owned()) |
|
480 | cwd = Some(value.to_owned()) | |
482 | } |
|
481 | } | |
483 |
|
482 | |||
484 | if arg == b"--repository" || arg == b"-R" { |
|
483 | if arg == b"--repository" || arg == b"-R" { | |
485 | if let Some(value) = args.next() { |
|
484 | if let Some(value) = args.next() { | |
486 | repo = Some(value) |
|
485 | repo = Some(value) | |
487 | } |
|
486 | } | |
488 | } else if let Some(value) = arg.drop_prefix(b"--repository=") { |
|
487 | } else if let Some(value) = arg.drop_prefix(b"--repository=") { | |
489 | repo = Some(value.to_owned()) |
|
488 | repo = Some(value.to_owned()) | |
490 | } else if let Some(value) = arg.drop_prefix(b"-R") { |
|
489 | } else if let Some(value) = arg.drop_prefix(b"-R") { | |
491 | repo = Some(value.to_owned()) |
|
490 | repo = Some(value.to_owned()) | |
492 | } |
|
491 | } | |
493 | } |
|
492 | } | |
494 | Self { config, repo, cwd } |
|
493 | Self { config, repo, cwd } | |
495 | } |
|
494 | } | |
496 | } |
|
495 | } | |
497 |
|
496 | |||
498 | /// What to do when encountering some unsupported feature. |
|
497 | /// What to do when encountering some unsupported feature. | |
499 | /// |
|
498 | /// | |
500 | /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`. |
|
499 | /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`. | |
501 | enum OnUnsupported { |
|
500 | enum OnUnsupported { | |
502 | /// Print an error message describing what feature is not supported, |
|
501 | /// Print an error message describing what feature is not supported, | |
503 | /// and exit with code 252. |
|
502 | /// and exit with code 252. | |
504 | Abort, |
|
503 | Abort, | |
505 | /// Silently exit with code 252. |
|
504 | /// Silently exit with code 252. | |
506 | AbortSilent, |
|
505 | AbortSilent, | |
507 | /// Try running a Python implementation |
|
506 | /// Try running a Python implementation | |
508 | Fallback { executable: Vec<u8> }, |
|
507 | Fallback { executable: Vec<u8> }, | |
509 | } |
|
508 | } | |
510 |
|
509 | |||
511 | impl OnUnsupported { |
|
510 | impl OnUnsupported { | |
512 | const DEFAULT: Self = OnUnsupported::Abort; |
|
511 | const DEFAULT: Self = OnUnsupported::Abort; | |
513 |
|
512 | |||
514 | fn from_config(ui: &Ui, config: &Config) -> Self { |
|
513 | fn from_config(ui: &Ui, config: &Config) -> Self { | |
515 | match config |
|
514 | match config | |
516 | .get(b"rhg", b"on-unsupported") |
|
515 | .get(b"rhg", b"on-unsupported") | |
517 | .map(|value| value.to_ascii_lowercase()) |
|
516 | .map(|value| value.to_ascii_lowercase()) | |
518 | .as_deref() |
|
517 | .as_deref() | |
519 | { |
|
518 | { | |
520 | Some(b"abort") => OnUnsupported::Abort, |
|
519 | Some(b"abort") => OnUnsupported::Abort, | |
521 | Some(b"abort-silent") => OnUnsupported::AbortSilent, |
|
520 | Some(b"abort-silent") => OnUnsupported::AbortSilent, | |
522 | Some(b"fallback") => OnUnsupported::Fallback { |
|
521 | Some(b"fallback") => OnUnsupported::Fallback { | |
523 | executable: config |
|
522 | executable: config | |
524 | .get(b"rhg", b"fallback-executable") |
|
523 | .get(b"rhg", b"fallback-executable") | |
525 | .unwrap_or_else(|| { |
|
524 | .unwrap_or_else(|| { | |
526 | exit_no_fallback( |
|
525 | exit_no_fallback( | |
527 | ui, |
|
526 | ui, | |
528 | Self::Abort, |
|
527 | Self::Abort, | |
529 | Err(CommandError::abort( |
|
528 | Err(CommandError::abort( | |
530 | "abort: 'rhg.on-unsupported=fallback' without \ |
|
529 | "abort: 'rhg.on-unsupported=fallback' without \ | |
531 | 'rhg.fallback-executable' set." |
|
530 | 'rhg.fallback-executable' set." | |
532 | )), |
|
531 | )), | |
533 | false, |
|
532 | false, | |
534 | ) |
|
533 | ) | |
535 | }) |
|
534 | }) | |
536 | .to_owned(), |
|
535 | .to_owned(), | |
537 | }, |
|
536 | }, | |
538 | None => Self::DEFAULT, |
|
537 | None => Self::DEFAULT, | |
539 | Some(_) => { |
|
538 | Some(_) => { | |
540 | // TODO: warn about unknown config value |
|
539 | // TODO: warn about unknown config value | |
541 | Self::DEFAULT |
|
540 | Self::DEFAULT | |
542 | } |
|
541 | } | |
543 | } |
|
542 | } | |
544 | } |
|
543 | } | |
545 | } |
|
544 | } | |
546 |
|
545 | |||
547 | const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"]; |
|
546 | const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"]; | |
548 |
|
547 | |||
549 | fn check_extensions(config: &Config) -> Result<(), CommandError> { |
|
548 | fn check_extensions(config: &Config) -> Result<(), CommandError> { | |
550 | let enabled = config.get_section_keys(b"extensions"); |
|
549 | let enabled = config.get_section_keys(b"extensions"); | |
551 |
|
550 | |||
552 | let mut unsupported = enabled; |
|
551 | let mut unsupported = enabled; | |
553 | for supported in SUPPORTED_EXTENSIONS { |
|
552 | for supported in SUPPORTED_EXTENSIONS { | |
554 | unsupported.remove(supported); |
|
553 | unsupported.remove(supported); | |
555 | } |
|
554 | } | |
556 |
|
555 | |||
557 | if let Some(ignored_list) = |
|
556 | if let Some(ignored_list) = | |
558 | config.get_simple_list(b"rhg", b"ignored-extensions") |
|
557 | config.get_simple_list(b"rhg", b"ignored-extensions") | |
559 | { |
|
558 | { | |
560 | for ignored in ignored_list { |
|
559 | for ignored in ignored_list { | |
561 | unsupported.remove(ignored); |
|
560 | unsupported.remove(ignored); | |
562 | } |
|
561 | } | |
563 | } |
|
562 | } | |
564 |
|
563 | |||
565 | if unsupported.is_empty() { |
|
564 | if unsupported.is_empty() { | |
566 | Ok(()) |
|
565 | Ok(()) | |
567 | } else { |
|
566 | } else { | |
568 | Err(CommandError::UnsupportedFeature { |
|
567 | Err(CommandError::UnsupportedFeature { | |
569 | message: format_bytes!( |
|
568 | message: format_bytes!( | |
570 | b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)", |
|
569 | b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)", | |
571 | join(unsupported, b", ") |
|
570 | join(unsupported, b", ") | |
572 | ), |
|
571 | ), | |
573 | }) |
|
572 | }) | |
574 | } |
|
573 | } | |
575 | } |
|
574 | } |
General Comments 0
You need to be logged in to leave comments.
Login now