##// END OF EJS Templates
rhg: add support for narrow clones and sparse checkouts...
Arseniy Alekseyev -
r49238:005ae1a3 default
parent child Browse files
Show More
@@ -0,0 +1,120 b''
1 #require rhg
2
3 $ NO_FALLBACK="env RHG_ON_UNSUPPORTED=abort"
4
5 Rhg works well when sparse working copy is enabled.
6
7 $ cd "$TESTTMP"
8 $ hg init repo-sparse
9 $ cd repo-sparse
10 $ cat > .hg/hgrc <<EOF
11 > [extensions]
12 > sparse=
13 > EOF
14
15 $ echo a > show
16 $ echo x > hide
17 $ mkdir dir1 dir2
18 $ echo x > dir1/x
19 $ echo y > dir1/y
20 $ echo z > dir2/z
21
22 $ hg ci -Aqm 'initial'
23 $ hg debugsparse --include 'show'
24 $ ls -A
25 .hg
26 show
27
28 $ tip=$(hg log -r . --template '{node}')
29 $ $NO_FALLBACK rhg files -r "$tip"
30 dir1/x
31 dir1/y
32 dir2/z
33 hide
34 show
35 $ $NO_FALLBACK rhg files
36 show
37
38 $ $NO_FALLBACK rhg cat -r "$tip" hide
39 x
40
41 $ cd ..
42
43 We support most things when narrow is enabled, too, with a couple of caveats.
44
45 $ . "$TESTDIR/narrow-library.sh"
46 $ real_hg=$RHG_FALLBACK_EXECUTABLE
47
48 $ cat >> $HGRCPATH <<EOF
49 > [extensions]
50 > narrow=
51 > EOF
52
53 $ hg clone --narrow ./repo-sparse repo-narrow --include dir1
54 requesting all changes
55 adding changesets
56 adding manifests
57 adding file changes
58 added 1 changesets with 2 changes to 2 files
59 new changesets 6d714a4a2998
60 updating to branch default
61 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
62
63 $ cd repo-narrow
64
65 $ $NO_FALLBACK rhg cat -r "$tip" dir1/x
66 x
67 $ "$real_hg" cat -r "$tip" dir1/x
68 x
69
70 TODO: bad error message
71
72 $ $NO_FALLBACK rhg cat -r "$tip" hide
73 abort: invalid revision identifier: 6d714a4a2998cbfd0620db44da58b749f6565d63
74 [255]
75 $ "$real_hg" cat -r "$tip" hide
76 [1]
77
78 A naive implementation of [rhg files] leaks the paths that are supposed to be
79 hidden by narrow, so we just fall back to hg.
80
81 $ $NO_FALLBACK rhg files -r "$tip"
82 unsupported feature: rhg files -r <rev> is not supported in narrow clones
83 [252]
84 $ "$real_hg" files -r "$tip"
85 dir1/x
86 dir1/y
87
88 Hg status needs to do some filtering based on narrow spec, so we don't
89 support it in rhg for narrow clones yet.
90
91 $ mkdir dir2
92 $ touch dir2/q
93 $ "$real_hg" status
94 $ $NO_FALLBACK rhg --config rhg.status=true status
95 unsupported feature: rhg status is not supported for sparse checkouts or narrow clones yet
96 [252]
97
98 Adding "orphaned" index files:
99
100 $ (cd ..; cp repo-sparse/.hg/store/data/hide.i repo-narrow/.hg/store/data/hide.i)
101 $ (cd ..; mkdir repo-narrow/.hg/store/data/dir2; cp repo-sparse/.hg/store/data/dir2/z.i repo-narrow/.hg/store/data/dir2/z.i)
102 $ "$real_hg" verify
103 checking changesets
104 checking manifests
105 crosschecking files in changesets and manifests
106 checking files
107 checked 1 changesets with 2 changes to 2 files
108
109 $ "$real_hg" files -r "$tip"
110 dir1/x
111 dir1/y
112
113 # TODO: even though [hg files] hides the orphaned dir2/z, [hg cat] still shows it.
114 # rhg has the same issue, but at least it's not specific to rhg.
115 # This is despite [hg verify] succeeding above.
116
117 $ $NO_FALLBACK rhg cat -r "$tip" dir2/z
118 z
119 $ "$real_hg" cat -r "$tip" dir2/z
120 z
@@ -1,409 +1,417 b''
1 use crate::changelog::Changelog;
1 use crate::changelog::Changelog;
2 use crate::config::{Config, ConfigError, ConfigParseError};
2 use crate::config::{Config, ConfigError, ConfigParseError};
3 use crate::dirstate::DirstateParents;
3 use crate::dirstate::DirstateParents;
4 use crate::dirstate_tree::dirstate_map::DirstateMap;
4 use crate::dirstate_tree::dirstate_map::DirstateMap;
5 use crate::dirstate_tree::owning::OwningDirstateMap;
5 use crate::dirstate_tree::owning::OwningDirstateMap;
6 use crate::errors::HgError;
6 use crate::errors::HgError;
7 use crate::errors::HgResultExt;
7 use crate::errors::HgResultExt;
8 use crate::exit_codes;
8 use crate::exit_codes;
9 use crate::manifest::{Manifest, Manifestlog};
9 use crate::manifest::{Manifest, Manifestlog};
10 use crate::revlog::filelog::Filelog;
10 use crate::revlog::filelog::Filelog;
11 use crate::revlog::revlog::RevlogError;
11 use crate::revlog::revlog::RevlogError;
12 use crate::utils::files::get_path_from_bytes;
12 use crate::utils::files::get_path_from_bytes;
13 use crate::utils::hg_path::HgPath;
13 use crate::utils::hg_path::HgPath;
14 use crate::utils::SliceExt;
14 use crate::utils::SliceExt;
15 use crate::vfs::{is_dir, is_file, Vfs};
15 use crate::vfs::{is_dir, is_file, Vfs};
16 use crate::{requirements, NodePrefix};
16 use crate::{requirements, NodePrefix};
17 use crate::{DirstateError, Revision};
17 use crate::{DirstateError, Revision};
18 use std::cell::{Cell, Ref, RefCell, RefMut};
18 use std::cell::{Cell, Ref, RefCell, RefMut};
19 use std::collections::HashSet;
19 use std::collections::HashSet;
20 use std::path::{Path, PathBuf};
20 use std::path::{Path, PathBuf};
21
21
22 /// A repository on disk
22 /// A repository on disk
23 pub struct Repo {
23 pub struct Repo {
24 working_directory: PathBuf,
24 working_directory: PathBuf,
25 dot_hg: PathBuf,
25 dot_hg: PathBuf,
26 store: PathBuf,
26 store: PathBuf,
27 requirements: HashSet<String>,
27 requirements: HashSet<String>,
28 config: Config,
28 config: Config,
29 // None means not known/initialized yet
29 // None means not known/initialized yet
30 dirstate_parents: Cell<Option<DirstateParents>>,
30 dirstate_parents: Cell<Option<DirstateParents>>,
31 dirstate_map: LazyCell<OwningDirstateMap, DirstateError>,
31 dirstate_map: LazyCell<OwningDirstateMap, DirstateError>,
32 changelog: LazyCell<Changelog, HgError>,
32 changelog: LazyCell<Changelog, HgError>,
33 manifestlog: LazyCell<Manifestlog, HgError>,
33 manifestlog: LazyCell<Manifestlog, HgError>,
34 }
34 }
35
35
36 #[derive(Debug, derive_more::From)]
36 #[derive(Debug, derive_more::From)]
37 pub enum RepoError {
37 pub enum RepoError {
38 NotFound {
38 NotFound {
39 at: PathBuf,
39 at: PathBuf,
40 },
40 },
41 #[from]
41 #[from]
42 ConfigParseError(ConfigParseError),
42 ConfigParseError(ConfigParseError),
43 #[from]
43 #[from]
44 Other(HgError),
44 Other(HgError),
45 }
45 }
46
46
47 impl From<ConfigError> for RepoError {
47 impl From<ConfigError> for RepoError {
48 fn from(error: ConfigError) -> Self {
48 fn from(error: ConfigError) -> Self {
49 match error {
49 match error {
50 ConfigError::Parse(error) => error.into(),
50 ConfigError::Parse(error) => error.into(),
51 ConfigError::Other(error) => error.into(),
51 ConfigError::Other(error) => error.into(),
52 }
52 }
53 }
53 }
54 }
54 }
55
55
56 impl Repo {
56 impl Repo {
57 /// tries to find nearest repository root in current working directory or
57 /// tries to find nearest repository root in current working directory or
58 /// its ancestors
58 /// its ancestors
59 pub fn find_repo_root() -> Result<PathBuf, RepoError> {
59 pub fn find_repo_root() -> Result<PathBuf, RepoError> {
60 let current_directory = crate::utils::current_dir()?;
60 let current_directory = crate::utils::current_dir()?;
61 // ancestors() is inclusive: it first yields `current_directory`
61 // ancestors() is inclusive: it first yields `current_directory`
62 // as-is.
62 // as-is.
63 for ancestor in current_directory.ancestors() {
63 for ancestor in current_directory.ancestors() {
64 if is_dir(ancestor.join(".hg"))? {
64 if is_dir(ancestor.join(".hg"))? {
65 return Ok(ancestor.to_path_buf());
65 return Ok(ancestor.to_path_buf());
66 }
66 }
67 }
67 }
68 return Err(RepoError::NotFound {
68 return Err(RepoError::NotFound {
69 at: current_directory,
69 at: current_directory,
70 });
70 });
71 }
71 }
72
72
73 /// Find a repository, either at the given path (which must contain a `.hg`
73 /// Find a repository, either at the given path (which must contain a `.hg`
74 /// sub-directory) or by searching the current directory and its
74 /// sub-directory) or by searching the current directory and its
75 /// ancestors.
75 /// ancestors.
76 ///
76 ///
77 /// A method with two very different "modes" like this usually a code smell
77 /// A method with two very different "modes" like this usually a code smell
78 /// to make two methods instead, but in this case an `Option` is what rhg
78 /// to make two methods instead, but in this case an `Option` is what rhg
79 /// sub-commands get from Clap for the `-R` / `--repository` CLI argument.
79 /// sub-commands get from Clap for the `-R` / `--repository` CLI argument.
80 /// Having two methods would just move that `if` to almost all callers.
80 /// Having two methods would just move that `if` to almost all callers.
81 pub fn find(
81 pub fn find(
82 config: &Config,
82 config: &Config,
83 explicit_path: Option<PathBuf>,
83 explicit_path: Option<PathBuf>,
84 ) -> Result<Self, RepoError> {
84 ) -> Result<Self, RepoError> {
85 if let Some(root) = explicit_path {
85 if let Some(root) = explicit_path {
86 if is_dir(root.join(".hg"))? {
86 if is_dir(root.join(".hg"))? {
87 Self::new_at_path(root.to_owned(), config)
87 Self::new_at_path(root.to_owned(), config)
88 } else if is_file(&root)? {
88 } else if is_file(&root)? {
89 Err(HgError::unsupported("bundle repository").into())
89 Err(HgError::unsupported("bundle repository").into())
90 } else {
90 } else {
91 Err(RepoError::NotFound {
91 Err(RepoError::NotFound {
92 at: root.to_owned(),
92 at: root.to_owned(),
93 })
93 })
94 }
94 }
95 } else {
95 } else {
96 let root = Self::find_repo_root()?;
96 let root = Self::find_repo_root()?;
97 Self::new_at_path(root, config)
97 Self::new_at_path(root, config)
98 }
98 }
99 }
99 }
100
100
101 /// To be called after checking that `.hg` is a sub-directory
101 /// To be called after checking that `.hg` is a sub-directory
102 fn new_at_path(
102 fn new_at_path(
103 working_directory: PathBuf,
103 working_directory: PathBuf,
104 config: &Config,
104 config: &Config,
105 ) -> Result<Self, RepoError> {
105 ) -> Result<Self, RepoError> {
106 let dot_hg = working_directory.join(".hg");
106 let dot_hg = working_directory.join(".hg");
107
107
108 let mut repo_config_files = Vec::new();
108 let mut repo_config_files = Vec::new();
109 repo_config_files.push(dot_hg.join("hgrc"));
109 repo_config_files.push(dot_hg.join("hgrc"));
110 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
110 repo_config_files.push(dot_hg.join("hgrc-not-shared"));
111
111
112 let hg_vfs = Vfs { base: &dot_hg };
112 let hg_vfs = Vfs { base: &dot_hg };
113 let mut reqs = requirements::load_if_exists(hg_vfs)?;
113 let mut reqs = requirements::load_if_exists(hg_vfs)?;
114 let relative =
114 let relative =
115 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
115 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
116 let shared =
116 let shared =
117 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
117 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
118
118
119 // From `mercurial/localrepo.py`:
119 // From `mercurial/localrepo.py`:
120 //
120 //
121 // if .hg/requires contains the sharesafe requirement, it means
121 // if .hg/requires contains the sharesafe requirement, it means
122 // there exists a `.hg/store/requires` too and we should read it
122 // there exists a `.hg/store/requires` too and we should read it
123 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
123 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
124 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
124 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
125 // is not present, refer checkrequirementscompat() for that
125 // is not present, refer checkrequirementscompat() for that
126 //
126 //
127 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
127 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
128 // repository was shared the old way. We check the share source
128 // repository was shared the old way. We check the share source
129 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
129 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
130 // current repository needs to be reshared
130 // current repository needs to be reshared
131 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
131 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
132
132
133 let store_path;
133 let store_path;
134 if !shared {
134 if !shared {
135 store_path = dot_hg.join("store");
135 store_path = dot_hg.join("store");
136 } else {
136 } else {
137 let bytes = hg_vfs.read("sharedpath")?;
137 let bytes = hg_vfs.read("sharedpath")?;
138 let mut shared_path =
138 let mut shared_path =
139 get_path_from_bytes(bytes.trim_end_matches(|b| b == b'\n'))
139 get_path_from_bytes(bytes.trim_end_matches(|b| b == b'\n'))
140 .to_owned();
140 .to_owned();
141 if relative {
141 if relative {
142 shared_path = dot_hg.join(shared_path)
142 shared_path = dot_hg.join(shared_path)
143 }
143 }
144 if !is_dir(&shared_path)? {
144 if !is_dir(&shared_path)? {
145 return Err(HgError::corrupted(format!(
145 return Err(HgError::corrupted(format!(
146 ".hg/sharedpath points to nonexistent directory {}",
146 ".hg/sharedpath points to nonexistent directory {}",
147 shared_path.display()
147 shared_path.display()
148 ))
148 ))
149 .into());
149 .into());
150 }
150 }
151
151
152 store_path = shared_path.join("store");
152 store_path = shared_path.join("store");
153
153
154 let source_is_share_safe =
154 let source_is_share_safe =
155 requirements::load(Vfs { base: &shared_path })?
155 requirements::load(Vfs { base: &shared_path })?
156 .contains(requirements::SHARESAFE_REQUIREMENT);
156 .contains(requirements::SHARESAFE_REQUIREMENT);
157
157
158 if share_safe && !source_is_share_safe {
158 if share_safe && !source_is_share_safe {
159 return Err(match config
159 return Err(match config
160 .get(b"share", b"safe-mismatch.source-not-safe")
160 .get(b"share", b"safe-mismatch.source-not-safe")
161 {
161 {
162 Some(b"abort") | None => HgError::abort(
162 Some(b"abort") | None => HgError::abort(
163 "abort: share source does not support share-safe requirement\n\
163 "abort: share source does not support share-safe requirement\n\
164 (see `hg help config.format.use-share-safe` for more information)",
164 (see `hg help config.format.use-share-safe` for more information)",
165 exit_codes::ABORT,
165 exit_codes::ABORT,
166 ),
166 ),
167 _ => HgError::unsupported("share-safe downgrade"),
167 _ => HgError::unsupported("share-safe downgrade"),
168 }
168 }
169 .into());
169 .into());
170 } else if source_is_share_safe && !share_safe {
170 } else if source_is_share_safe && !share_safe {
171 return Err(
171 return Err(
172 match config.get(b"share", b"safe-mismatch.source-safe") {
172 match config.get(b"share", b"safe-mismatch.source-safe") {
173 Some(b"abort") | None => HgError::abort(
173 Some(b"abort") | None => HgError::abort(
174 "abort: version mismatch: source uses share-safe \
174 "abort: version mismatch: source uses share-safe \
175 functionality while the current share does not\n\
175 functionality while the current share does not\n\
176 (see `hg help config.format.use-share-safe` for more information)",
176 (see `hg help config.format.use-share-safe` for more information)",
177 exit_codes::ABORT,
177 exit_codes::ABORT,
178 ),
178 ),
179 _ => HgError::unsupported("share-safe upgrade"),
179 _ => HgError::unsupported("share-safe upgrade"),
180 }
180 }
181 .into(),
181 .into(),
182 );
182 );
183 }
183 }
184
184
185 if share_safe {
185 if share_safe {
186 repo_config_files.insert(0, shared_path.join("hgrc"))
186 repo_config_files.insert(0, shared_path.join("hgrc"))
187 }
187 }
188 }
188 }
189 if share_safe {
189 if share_safe {
190 reqs.extend(requirements::load(Vfs { base: &store_path })?);
190 reqs.extend(requirements::load(Vfs { base: &store_path })?);
191 }
191 }
192
192
193 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
193 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
194 config.combine_with_repo(&repo_config_files)?
194 config.combine_with_repo(&repo_config_files)?
195 } else {
195 } else {
196 config.clone()
196 config.clone()
197 };
197 };
198
198
199 let repo = Self {
199 let repo = Self {
200 requirements: reqs,
200 requirements: reqs,
201 working_directory,
201 working_directory,
202 store: store_path,
202 store: store_path,
203 dot_hg,
203 dot_hg,
204 config: repo_config,
204 config: repo_config,
205 dirstate_parents: Cell::new(None),
205 dirstate_parents: Cell::new(None),
206 dirstate_map: LazyCell::new(Self::new_dirstate_map),
206 dirstate_map: LazyCell::new(Self::new_dirstate_map),
207 changelog: LazyCell::new(Changelog::open),
207 changelog: LazyCell::new(Changelog::open),
208 manifestlog: LazyCell::new(Manifestlog::open),
208 manifestlog: LazyCell::new(Manifestlog::open),
209 };
209 };
210
210
211 requirements::check(&repo)?;
211 requirements::check(&repo)?;
212
212
213 Ok(repo)
213 Ok(repo)
214 }
214 }
215
215
216 pub fn working_directory_path(&self) -> &Path {
216 pub fn working_directory_path(&self) -> &Path {
217 &self.working_directory
217 &self.working_directory
218 }
218 }
219
219
220 pub fn requirements(&self) -> &HashSet<String> {
220 pub fn requirements(&self) -> &HashSet<String> {
221 &self.requirements
221 &self.requirements
222 }
222 }
223
223
224 pub fn config(&self) -> &Config {
224 pub fn config(&self) -> &Config {
225 &self.config
225 &self.config
226 }
226 }
227
227
228 /// For accessing repository files (in `.hg`), except for the store
228 /// For accessing repository files (in `.hg`), except for the store
229 /// (`.hg/store`).
229 /// (`.hg/store`).
230 pub fn hg_vfs(&self) -> Vfs<'_> {
230 pub fn hg_vfs(&self) -> Vfs<'_> {
231 Vfs { base: &self.dot_hg }
231 Vfs { base: &self.dot_hg }
232 }
232 }
233
233
234 /// For accessing repository store files (in `.hg/store`)
234 /// For accessing repository store files (in `.hg/store`)
235 pub fn store_vfs(&self) -> Vfs<'_> {
235 pub fn store_vfs(&self) -> Vfs<'_> {
236 Vfs { base: &self.store }
236 Vfs { base: &self.store }
237 }
237 }
238
238
239 /// For accessing the working copy
239 /// For accessing the working copy
240 pub fn working_directory_vfs(&self) -> Vfs<'_> {
240 pub fn working_directory_vfs(&self) -> Vfs<'_> {
241 Vfs {
241 Vfs {
242 base: &self.working_directory,
242 base: &self.working_directory,
243 }
243 }
244 }
244 }
245
245
246 pub fn has_dirstate_v2(&self) -> bool {
246 pub fn has_dirstate_v2(&self) -> bool {
247 self.requirements
247 self.requirements
248 .contains(requirements::DIRSTATE_V2_REQUIREMENT)
248 .contains(requirements::DIRSTATE_V2_REQUIREMENT)
249 }
249 }
250
250
251 pub fn has_sparse(&self) -> bool {
252 self.requirements.contains(requirements::SPARSE_REQUIREMENT)
253 }
254
255 pub fn has_narrow(&self) -> bool {
256 self.requirements.contains(requirements::NARROW_REQUIREMENT)
257 }
258
251 fn dirstate_file_contents(&self) -> Result<Vec<u8>, HgError> {
259 fn dirstate_file_contents(&self) -> Result<Vec<u8>, HgError> {
252 Ok(self
260 Ok(self
253 .hg_vfs()
261 .hg_vfs()
254 .read("dirstate")
262 .read("dirstate")
255 .io_not_found_as_none()?
263 .io_not_found_as_none()?
256 .unwrap_or(Vec::new()))
264 .unwrap_or(Vec::new()))
257 }
265 }
258
266
259 pub fn dirstate_parents(&self) -> Result<DirstateParents, HgError> {
267 pub fn dirstate_parents(&self) -> Result<DirstateParents, HgError> {
260 if let Some(parents) = self.dirstate_parents.get() {
268 if let Some(parents) = self.dirstate_parents.get() {
261 return Ok(parents);
269 return Ok(parents);
262 }
270 }
263 let dirstate = self.dirstate_file_contents()?;
271 let dirstate = self.dirstate_file_contents()?;
264 let parents = if dirstate.is_empty() {
272 let parents = if dirstate.is_empty() {
265 DirstateParents::NULL
273 DirstateParents::NULL
266 } else if self.has_dirstate_v2() {
274 } else if self.has_dirstate_v2() {
267 crate::dirstate_tree::on_disk::read_docket(&dirstate)?.parents()
275 crate::dirstate_tree::on_disk::read_docket(&dirstate)?.parents()
268 } else {
276 } else {
269 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?
277 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?
270 .clone()
278 .clone()
271 };
279 };
272 self.dirstate_parents.set(Some(parents));
280 self.dirstate_parents.set(Some(parents));
273 Ok(parents)
281 Ok(parents)
274 }
282 }
275
283
276 fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> {
284 fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> {
277 let dirstate_file_contents = self.dirstate_file_contents()?;
285 let dirstate_file_contents = self.dirstate_file_contents()?;
278 if dirstate_file_contents.is_empty() {
286 if dirstate_file_contents.is_empty() {
279 self.dirstate_parents.set(Some(DirstateParents::NULL));
287 self.dirstate_parents.set(Some(DirstateParents::NULL));
280 Ok(OwningDirstateMap::new_empty(Vec::new()))
288 Ok(OwningDirstateMap::new_empty(Vec::new()))
281 } else if self.has_dirstate_v2() {
289 } else if self.has_dirstate_v2() {
282 let docket = crate::dirstate_tree::on_disk::read_docket(
290 let docket = crate::dirstate_tree::on_disk::read_docket(
283 &dirstate_file_contents,
291 &dirstate_file_contents,
284 )?;
292 )?;
285 self.dirstate_parents.set(Some(docket.parents()));
293 self.dirstate_parents.set(Some(docket.parents()));
286 let data_size = docket.data_size();
294 let data_size = docket.data_size();
287 let metadata = docket.tree_metadata();
295 let metadata = docket.tree_metadata();
288 let mut map = if let Some(data_mmap) = self
296 let mut map = if let Some(data_mmap) = self
289 .hg_vfs()
297 .hg_vfs()
290 .mmap_open(docket.data_filename())
298 .mmap_open(docket.data_filename())
291 .io_not_found_as_none()?
299 .io_not_found_as_none()?
292 {
300 {
293 OwningDirstateMap::new_empty(data_mmap)
301 OwningDirstateMap::new_empty(data_mmap)
294 } else {
302 } else {
295 OwningDirstateMap::new_empty(Vec::new())
303 OwningDirstateMap::new_empty(Vec::new())
296 };
304 };
297 let (on_disk, placeholder) = map.get_pair_mut();
305 let (on_disk, placeholder) = map.get_pair_mut();
298 *placeholder = DirstateMap::new_v2(on_disk, data_size, metadata)?;
306 *placeholder = DirstateMap::new_v2(on_disk, data_size, metadata)?;
299 Ok(map)
307 Ok(map)
300 } else {
308 } else {
301 let mut map = OwningDirstateMap::new_empty(dirstate_file_contents);
309 let mut map = OwningDirstateMap::new_empty(dirstate_file_contents);
302 let (on_disk, placeholder) = map.get_pair_mut();
310 let (on_disk, placeholder) = map.get_pair_mut();
303 let (inner, parents) = DirstateMap::new_v1(on_disk)?;
311 let (inner, parents) = DirstateMap::new_v1(on_disk)?;
304 self.dirstate_parents
312 self.dirstate_parents
305 .set(Some(parents.unwrap_or(DirstateParents::NULL)));
313 .set(Some(parents.unwrap_or(DirstateParents::NULL)));
306 *placeholder = inner;
314 *placeholder = inner;
307 Ok(map)
315 Ok(map)
308 }
316 }
309 }
317 }
310
318
311 pub fn dirstate_map(
319 pub fn dirstate_map(
312 &self,
320 &self,
313 ) -> Result<Ref<OwningDirstateMap>, DirstateError> {
321 ) -> Result<Ref<OwningDirstateMap>, DirstateError> {
314 self.dirstate_map.get_or_init(self)
322 self.dirstate_map.get_or_init(self)
315 }
323 }
316
324
317 pub fn dirstate_map_mut(
325 pub fn dirstate_map_mut(
318 &self,
326 &self,
319 ) -> Result<RefMut<OwningDirstateMap>, DirstateError> {
327 ) -> Result<RefMut<OwningDirstateMap>, DirstateError> {
320 self.dirstate_map.get_mut_or_init(self)
328 self.dirstate_map.get_mut_or_init(self)
321 }
329 }
322
330
323 pub fn changelog(&self) -> Result<Ref<Changelog>, HgError> {
331 pub fn changelog(&self) -> Result<Ref<Changelog>, HgError> {
324 self.changelog.get_or_init(self)
332 self.changelog.get_or_init(self)
325 }
333 }
326
334
327 pub fn changelog_mut(&self) -> Result<RefMut<Changelog>, HgError> {
335 pub fn changelog_mut(&self) -> Result<RefMut<Changelog>, HgError> {
328 self.changelog.get_mut_or_init(self)
336 self.changelog.get_mut_or_init(self)
329 }
337 }
330
338
331 pub fn manifestlog(&self) -> Result<Ref<Manifestlog>, HgError> {
339 pub fn manifestlog(&self) -> Result<Ref<Manifestlog>, HgError> {
332 self.manifestlog.get_or_init(self)
340 self.manifestlog.get_or_init(self)
333 }
341 }
334
342
335 pub fn manifestlog_mut(&self) -> Result<RefMut<Manifestlog>, HgError> {
343 pub fn manifestlog_mut(&self) -> Result<RefMut<Manifestlog>, HgError> {
336 self.manifestlog.get_mut_or_init(self)
344 self.manifestlog.get_mut_or_init(self)
337 }
345 }
338
346
339 /// Returns the manifest of the *changeset* with the given node ID
347 /// Returns the manifest of the *changeset* with the given node ID
340 pub fn manifest_for_node(
348 pub fn manifest_for_node(
341 &self,
349 &self,
342 node: impl Into<NodePrefix>,
350 node: impl Into<NodePrefix>,
343 ) -> Result<Manifest, RevlogError> {
351 ) -> Result<Manifest, RevlogError> {
344 self.manifestlog()?.data_for_node(
352 self.manifestlog()?.data_for_node(
345 self.changelog()?
353 self.changelog()?
346 .data_for_node(node.into())?
354 .data_for_node(node.into())?
347 .manifest_node()?
355 .manifest_node()?
348 .into(),
356 .into(),
349 )
357 )
350 }
358 }
351
359
352 /// Returns the manifest of the *changeset* with the given revision number
360 /// Returns the manifest of the *changeset* with the given revision number
353 pub fn manifest_for_rev(
361 pub fn manifest_for_rev(
354 &self,
362 &self,
355 revision: Revision,
363 revision: Revision,
356 ) -> Result<Manifest, RevlogError> {
364 ) -> Result<Manifest, RevlogError> {
357 self.manifestlog()?.data_for_node(
365 self.manifestlog()?.data_for_node(
358 self.changelog()?
366 self.changelog()?
359 .data_for_rev(revision)?
367 .data_for_rev(revision)?
360 .manifest_node()?
368 .manifest_node()?
361 .into(),
369 .into(),
362 )
370 )
363 }
371 }
364
372
365 pub fn filelog(&self, path: &HgPath) -> Result<Filelog, HgError> {
373 pub fn filelog(&self, path: &HgPath) -> Result<Filelog, HgError> {
366 Filelog::open(self, path)
374 Filelog::open(self, path)
367 }
375 }
368 }
376 }
369
377
370 /// Lazily-initialized component of `Repo` with interior mutability
378 /// Lazily-initialized component of `Repo` with interior mutability
371 ///
379 ///
372 /// This differs from `OnceCell` in that the value can still be "deinitialized"
380 /// This differs from `OnceCell` in that the value can still be "deinitialized"
373 /// later by setting its inner `Option` to `None`.
381 /// later by setting its inner `Option` to `None`.
374 struct LazyCell<T, E> {
382 struct LazyCell<T, E> {
375 value: RefCell<Option<T>>,
383 value: RefCell<Option<T>>,
376 // `Fn`s that don’t capture environment are zero-size, so this box does
384 // `Fn`s that don’t capture environment are zero-size, so this box does
377 // not allocate:
385 // not allocate:
378 init: Box<dyn Fn(&Repo) -> Result<T, E>>,
386 init: Box<dyn Fn(&Repo) -> Result<T, E>>,
379 }
387 }
380
388
381 impl<T, E> LazyCell<T, E> {
389 impl<T, E> LazyCell<T, E> {
382 fn new(init: impl Fn(&Repo) -> Result<T, E> + 'static) -> Self {
390 fn new(init: impl Fn(&Repo) -> Result<T, E> + 'static) -> Self {
383 Self {
391 Self {
384 value: RefCell::new(None),
392 value: RefCell::new(None),
385 init: Box::new(init),
393 init: Box::new(init),
386 }
394 }
387 }
395 }
388
396
389 fn get_or_init(&self, repo: &Repo) -> Result<Ref<T>, E> {
397 fn get_or_init(&self, repo: &Repo) -> Result<Ref<T>, E> {
390 let mut borrowed = self.value.borrow();
398 let mut borrowed = self.value.borrow();
391 if borrowed.is_none() {
399 if borrowed.is_none() {
392 drop(borrowed);
400 drop(borrowed);
393 // Only use `borrow_mut` if it is really needed to avoid panic in
401 // Only use `borrow_mut` if it is really needed to avoid panic in
394 // case there is another outstanding borrow but mutation is not
402 // case there is another outstanding borrow but mutation is not
395 // needed.
403 // needed.
396 *self.value.borrow_mut() = Some((self.init)(repo)?);
404 *self.value.borrow_mut() = Some((self.init)(repo)?);
397 borrowed = self.value.borrow()
405 borrowed = self.value.borrow()
398 }
406 }
399 Ok(Ref::map(borrowed, |option| option.as_ref().unwrap()))
407 Ok(Ref::map(borrowed, |option| option.as_ref().unwrap()))
400 }
408 }
401
409
402 pub fn get_mut_or_init(&self, repo: &Repo) -> Result<RefMut<T>, E> {
410 pub fn get_mut_or_init(&self, repo: &Repo) -> Result<RefMut<T>, E> {
403 let mut borrowed = self.value.borrow_mut();
411 let mut borrowed = self.value.borrow_mut();
404 if borrowed.is_none() {
412 if borrowed.is_none() {
405 *borrowed = Some((self.init)(repo)?);
413 *borrowed = Some((self.init)(repo)?);
406 }
414 }
407 Ok(RefMut::map(borrowed, |option| option.as_mut().unwrap()))
415 Ok(RefMut::map(borrowed, |option| option.as_mut().unwrap()))
408 }
416 }
409 }
417 }
@@ -1,157 +1,161 b''
1 use crate::errors::{HgError, HgResultExt};
1 use crate::errors::{HgError, HgResultExt};
2 use crate::repo::Repo;
2 use crate::repo::Repo;
3 use crate::utils::join_display;
3 use crate::utils::join_display;
4 use crate::vfs::Vfs;
4 use crate::vfs::Vfs;
5 use std::collections::HashSet;
5 use std::collections::HashSet;
6
6
7 fn parse(bytes: &[u8]) -> Result<HashSet<String>, HgError> {
7 fn parse(bytes: &[u8]) -> Result<HashSet<String>, HgError> {
8 // The Python code reading this file uses `str.splitlines`
8 // The Python code reading this file uses `str.splitlines`
9 // which looks for a number of line separators (even including a couple of
9 // which looks for a number of line separators (even including a couple of
10 // non-ASCII ones), but Python code writing it always uses `\n`.
10 // non-ASCII ones), but Python code writing it always uses `\n`.
11 let lines = bytes.split(|&byte| byte == b'\n');
11 let lines = bytes.split(|&byte| byte == b'\n');
12
12
13 lines
13 lines
14 .filter(|line| !line.is_empty())
14 .filter(|line| !line.is_empty())
15 .map(|line| {
15 .map(|line| {
16 // Python uses Unicode `str.isalnum` but feature names are all
16 // Python uses Unicode `str.isalnum` but feature names are all
17 // ASCII
17 // ASCII
18 if line[0].is_ascii_alphanumeric() && line.is_ascii() {
18 if line[0].is_ascii_alphanumeric() && line.is_ascii() {
19 Ok(String::from_utf8(line.into()).unwrap())
19 Ok(String::from_utf8(line.into()).unwrap())
20 } else {
20 } else {
21 Err(HgError::corrupted("parse error in 'requires' file"))
21 Err(HgError::corrupted("parse error in 'requires' file"))
22 }
22 }
23 })
23 })
24 .collect()
24 .collect()
25 }
25 }
26
26
27 pub(crate) fn load(hg_vfs: Vfs) -> Result<HashSet<String>, HgError> {
27 pub(crate) fn load(hg_vfs: Vfs) -> Result<HashSet<String>, HgError> {
28 parse(&hg_vfs.read("requires")?)
28 parse(&hg_vfs.read("requires")?)
29 }
29 }
30
30
31 pub(crate) fn load_if_exists(hg_vfs: Vfs) -> Result<HashSet<String>, HgError> {
31 pub(crate) fn load_if_exists(hg_vfs: Vfs) -> Result<HashSet<String>, HgError> {
32 if let Some(bytes) = hg_vfs.read("requires").io_not_found_as_none()? {
32 if let Some(bytes) = hg_vfs.read("requires").io_not_found_as_none()? {
33 parse(&bytes)
33 parse(&bytes)
34 } else {
34 } else {
35 // Treat a missing file the same as an empty file.
35 // Treat a missing file the same as an empty file.
36 // From `mercurial/localrepo.py`:
36 // From `mercurial/localrepo.py`:
37 // > requires file contains a newline-delimited list of
37 // > requires file contains a newline-delimited list of
38 // > features/capabilities the opener (us) must have in order to use
38 // > features/capabilities the opener (us) must have in order to use
39 // > the repository. This file was introduced in Mercurial 0.9.2,
39 // > the repository. This file was introduced in Mercurial 0.9.2,
40 // > which means very old repositories may not have one. We assume
40 // > which means very old repositories may not have one. We assume
41 // > a missing file translates to no requirements.
41 // > a missing file translates to no requirements.
42 Ok(HashSet::new())
42 Ok(HashSet::new())
43 }
43 }
44 }
44 }
45
45
46 pub(crate) fn check(repo: &Repo) -> Result<(), HgError> {
46 pub(crate) fn check(repo: &Repo) -> Result<(), HgError> {
47 let unknown: Vec<_> = repo
47 let unknown: Vec<_> = repo
48 .requirements()
48 .requirements()
49 .iter()
49 .iter()
50 .map(String::as_str)
50 .map(String::as_str)
51 // .filter(|feature| !ALL_SUPPORTED.contains(feature.as_str()))
51 // .filter(|feature| !ALL_SUPPORTED.contains(feature.as_str()))
52 .filter(|feature| {
52 .filter(|feature| {
53 !REQUIRED.contains(feature) && !SUPPORTED.contains(feature)
53 !REQUIRED.contains(feature) && !SUPPORTED.contains(feature)
54 })
54 })
55 .collect();
55 .collect();
56 if !unknown.is_empty() {
56 if !unknown.is_empty() {
57 return Err(HgError::unsupported(format!(
57 return Err(HgError::unsupported(format!(
58 "repository requires feature unknown to this Mercurial: {}",
58 "repository requires feature unknown to this Mercurial: {}",
59 join_display(&unknown, ", ")
59 join_display(&unknown, ", ")
60 )));
60 )));
61 }
61 }
62 let missing: Vec<_> = REQUIRED
62 let missing: Vec<_> = REQUIRED
63 .iter()
63 .iter()
64 .filter(|&&feature| !repo.requirements().contains(feature))
64 .filter(|&&feature| !repo.requirements().contains(feature))
65 .collect();
65 .collect();
66 if !missing.is_empty() {
66 if !missing.is_empty() {
67 return Err(HgError::unsupported(format!(
67 return Err(HgError::unsupported(format!(
68 "repository is missing feature required by this Mercurial: {}",
68 "repository is missing feature required by this Mercurial: {}",
69 join_display(&missing, ", ")
69 join_display(&missing, ", ")
70 )));
70 )));
71 }
71 }
72 Ok(())
72 Ok(())
73 }
73 }
74
74
75 /// rhg does not support repositories that are *missing* any of these features
75 /// rhg does not support repositories that are *missing* any of these features
76 const REQUIRED: &[&str] = &["revlogv1", "store", "fncache", "dotencode"];
76 const REQUIRED: &[&str] = &["revlogv1", "store", "fncache", "dotencode"];
77
77
78 /// rhg supports repository with or without these
78 /// rhg supports repository with or without these
79 const SUPPORTED: &[&str] = &[
79 const SUPPORTED: &[&str] = &[
80 "generaldelta",
80 "generaldelta",
81 SHARED_REQUIREMENT,
81 SHARED_REQUIREMENT,
82 SHARESAFE_REQUIREMENT,
82 SHARESAFE_REQUIREMENT,
83 SPARSEREVLOG_REQUIREMENT,
83 SPARSEREVLOG_REQUIREMENT,
84 RELATIVE_SHARED_REQUIREMENT,
84 RELATIVE_SHARED_REQUIREMENT,
85 REVLOG_COMPRESSION_ZSTD,
85 REVLOG_COMPRESSION_ZSTD,
86 DIRSTATE_V2_REQUIREMENT,
86 DIRSTATE_V2_REQUIREMENT,
87 // As of this writing everything rhg does is read-only.
87 // As of this writing everything rhg does is read-only.
88 // When it starts writing to the repository, it’ll need to either keep the
88 // When it starts writing to the repository, it’ll need to either keep the
89 // persistent nodemap up to date or remove this entry:
89 // persistent nodemap up to date or remove this entry:
90 NODEMAP_REQUIREMENT,
90 NODEMAP_REQUIREMENT,
91 // Not all commands support `sparse` and `narrow`. The commands that do
92 // not should opt out by checking `has_sparse` and `has_narrow`.
93 SPARSE_REQUIREMENT,
94 NARROW_REQUIREMENT,
91 ];
95 ];
92
96
93 // Copied from mercurial/requirements.py:
97 // Copied from mercurial/requirements.py:
94
98
95 pub(crate) const DIRSTATE_V2_REQUIREMENT: &str = "dirstate-v2";
99 pub(crate) const DIRSTATE_V2_REQUIREMENT: &str = "dirstate-v2";
96
100
97 /// When narrowing is finalized and no longer subject to format changes,
101 /// When narrowing is finalized and no longer subject to format changes,
98 /// we should move this to just "narrow" or similar.
102 /// we should move this to just "narrow" or similar.
99 #[allow(unused)]
103 #[allow(unused)]
100 pub(crate) const NARROW_REQUIREMENT: &str = "narrowhg-experimental";
104 pub(crate) const NARROW_REQUIREMENT: &str = "narrowhg-experimental";
101
105
102 /// Enables sparse working directory usage
106 /// Enables sparse working directory usage
103 #[allow(unused)]
107 #[allow(unused)]
104 pub(crate) const SPARSE_REQUIREMENT: &str = "exp-sparse";
108 pub(crate) const SPARSE_REQUIREMENT: &str = "exp-sparse";
105
109
106 /// Enables the internal phase which is used to hide changesets instead
110 /// Enables the internal phase which is used to hide changesets instead
107 /// of stripping them
111 /// of stripping them
108 #[allow(unused)]
112 #[allow(unused)]
109 pub(crate) const INTERNAL_PHASE_REQUIREMENT: &str = "internal-phase";
113 pub(crate) const INTERNAL_PHASE_REQUIREMENT: &str = "internal-phase";
110
114
111 /// Stores manifest in Tree structure
115 /// Stores manifest in Tree structure
112 #[allow(unused)]
116 #[allow(unused)]
113 pub(crate) const TREEMANIFEST_REQUIREMENT: &str = "treemanifest";
117 pub(crate) const TREEMANIFEST_REQUIREMENT: &str = "treemanifest";
114
118
115 /// Increment the sub-version when the revlog v2 format changes to lock out old
119 /// Increment the sub-version when the revlog v2 format changes to lock out old
116 /// clients.
120 /// clients.
117 #[allow(unused)]
121 #[allow(unused)]
118 pub(crate) const REVLOGV2_REQUIREMENT: &str = "exp-revlogv2.1";
122 pub(crate) const REVLOGV2_REQUIREMENT: &str = "exp-revlogv2.1";
119
123
120 /// A repository with the sparserevlog feature will have delta chains that
124 /// A repository with the sparserevlog feature will have delta chains that
121 /// can spread over a larger span. Sparse reading cuts these large spans into
125 /// can spread over a larger span. Sparse reading cuts these large spans into
122 /// pieces, so that each piece isn't too big.
126 /// pieces, so that each piece isn't too big.
123 /// Without the sparserevlog capability, reading from the repository could use
127 /// Without the sparserevlog capability, reading from the repository could use
124 /// huge amounts of memory, because the whole span would be read at once,
128 /// huge amounts of memory, because the whole span would be read at once,
125 /// including all the intermediate revisions that aren't pertinent for the
129 /// including all the intermediate revisions that aren't pertinent for the
126 /// chain. This is why once a repository has enabled sparse-read, it becomes
130 /// chain. This is why once a repository has enabled sparse-read, it becomes
127 /// required.
131 /// required.
128 #[allow(unused)]
132 #[allow(unused)]
129 pub(crate) const SPARSEREVLOG_REQUIREMENT: &str = "sparserevlog";
133 pub(crate) const SPARSEREVLOG_REQUIREMENT: &str = "sparserevlog";
130
134
131 /// A repository with the the copies-sidedata-changeset requirement will store
135 /// A repository with the the copies-sidedata-changeset requirement will store
132 /// copies related information in changeset's sidedata.
136 /// copies related information in changeset's sidedata.
133 #[allow(unused)]
137 #[allow(unused)]
134 pub(crate) const COPIESSDC_REQUIREMENT: &str = "exp-copies-sidedata-changeset";
138 pub(crate) const COPIESSDC_REQUIREMENT: &str = "exp-copies-sidedata-changeset";
135
139
136 /// The repository use persistent nodemap for the changelog and the manifest.
140 /// The repository use persistent nodemap for the changelog and the manifest.
137 #[allow(unused)]
141 #[allow(unused)]
138 pub(crate) const NODEMAP_REQUIREMENT: &str = "persistent-nodemap";
142 pub(crate) const NODEMAP_REQUIREMENT: &str = "persistent-nodemap";
139
143
140 /// Denotes that the current repository is a share
144 /// Denotes that the current repository is a share
141 #[allow(unused)]
145 #[allow(unused)]
142 pub(crate) const SHARED_REQUIREMENT: &str = "shared";
146 pub(crate) const SHARED_REQUIREMENT: &str = "shared";
143
147
144 /// Denotes that current repository is a share and the shared source path is
148 /// Denotes that current repository is a share and the shared source path is
145 /// relative to the current repository root path
149 /// relative to the current repository root path
146 #[allow(unused)]
150 #[allow(unused)]
147 pub(crate) const RELATIVE_SHARED_REQUIREMENT: &str = "relshared";
151 pub(crate) const RELATIVE_SHARED_REQUIREMENT: &str = "relshared";
148
152
149 /// A repository with share implemented safely. The repository has different
153 /// A repository with share implemented safely. The repository has different
150 /// store and working copy requirements i.e. both `.hg/requires` and
154 /// store and working copy requirements i.e. both `.hg/requires` and
151 /// `.hg/store/requires` are present.
155 /// `.hg/store/requires` are present.
152 #[allow(unused)]
156 #[allow(unused)]
153 pub(crate) const SHARESAFE_REQUIREMENT: &str = "share-safe";
157 pub(crate) const SHARESAFE_REQUIREMENT: &str = "share-safe";
154
158
155 /// A repository that use zstd compression inside its revlog
159 /// A repository that use zstd compression inside its revlog
156 #[allow(unused)]
160 #[allow(unused)]
157 pub(crate) const REVLOG_COMPRESSION_ZSTD: &str = "revlog-compression-zstd";
161 pub(crate) const REVLOG_COMPRESSION_ZSTD: &str = "revlog-compression-zstd";
@@ -1,72 +1,100 b''
1 use crate::error::CommandError;
1 use crate::error::CommandError;
2 use crate::ui::Ui;
2 use crate::ui::Ui;
3 use crate::ui::UiError;
3 use crate::ui::UiError;
4 use crate::utils::path_utils::relativize_paths;
4 use crate::utils::path_utils::relativize_paths;
5 use clap::Arg;
5 use clap::Arg;
6 use hg::errors::HgError;
6 use hg::errors::HgError;
7 use hg::operations::list_rev_tracked_files;
7 use hg::operations::list_rev_tracked_files;
8 use hg::operations::Dirstate;
8 use hg::operations::Dirstate;
9 use hg::repo::Repo;
9 use hg::repo::Repo;
10 use hg::utils::hg_path::HgPath;
10 use hg::utils::hg_path::HgPath;
11 use std::borrow::Cow;
11 use std::borrow::Cow;
12
12
13 pub const HELP_TEXT: &str = "
13 pub const HELP_TEXT: &str = "
14 List tracked files.
14 List tracked files.
15
15
16 Returns 0 on success.
16 Returns 0 on success.
17 ";
17 ";
18
18
19 pub fn args() -> clap::App<'static, 'static> {
19 pub fn args() -> clap::App<'static, 'static> {
20 clap::SubCommand::with_name("files")
20 clap::SubCommand::with_name("files")
21 .arg(
21 .arg(
22 Arg::with_name("rev")
22 Arg::with_name("rev")
23 .help("search the repository as it is in REV")
23 .help("search the repository as it is in REV")
24 .short("-r")
24 .short("-r")
25 .long("--revision")
25 .long("--revision")
26 .value_name("REV")
26 .value_name("REV")
27 .takes_value(true),
27 .takes_value(true),
28 )
28 )
29 .about(HELP_TEXT)
29 .about(HELP_TEXT)
30 }
30 }
31
31
32 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
32 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
33 let relative = invocation.config.get(b"ui", b"relative-paths");
33 let relative = invocation.config.get(b"ui", b"relative-paths");
34 if relative.is_some() {
34 if relative.is_some() {
35 return Err(CommandError::unsupported(
35 return Err(CommandError::unsupported(
36 "non-default ui.relative-paths",
36 "non-default ui.relative-paths",
37 ));
37 ));
38 }
38 }
39
39
40 let rev = invocation.subcommand_args.value_of("rev");
40 let rev = invocation.subcommand_args.value_of("rev");
41
41
42 let repo = invocation.repo?;
42 let repo = invocation.repo?;
43
44 // It seems better if this check is removed: this would correspond to
45 // automatically enabling the extension if the repo requires it.
46 // However we need this check to be in sync with vanilla hg so hg tests
47 // pass.
48 if repo.has_sparse()
49 && invocation.config.get(b"extensions", b"sparse").is_none()
50 {
51 return Err(CommandError::unsupported(
52 "repo is using sparse, but sparse extension is not enabled",
53 ));
54 }
55
43 if let Some(rev) = rev {
56 if let Some(rev) = rev {
57 if repo.has_narrow() {
58 return Err(CommandError::unsupported(
59 "rhg files -r <rev> is not supported in narrow clones",
60 ));
61 }
44 let files = list_rev_tracked_files(repo, rev).map_err(|e| (e, rev))?;
62 let files = list_rev_tracked_files(repo, rev).map_err(|e| (e, rev))?;
45 display_files(invocation.ui, repo, files.iter())
63 display_files(invocation.ui, repo, files.iter())
46 } else {
64 } else {
65 // The dirstate always reflects the sparse narrowspec, so if
66 // we only have sparse without narrow all is fine.
67 // If we have narrow, then [hg files] needs to check if
68 // the store narrowspec is in sync with the one of the dirstate,
69 // so we can't support that without explicit code.
70 if repo.has_narrow() {
71 return Err(CommandError::unsupported(
72 "rhg files is not supported in narrow clones",
73 ));
74 }
47 let distate = Dirstate::new(repo)?;
75 let distate = Dirstate::new(repo)?;
48 let files = distate.tracked_files()?;
76 let files = distate.tracked_files()?;
49 display_files(invocation.ui, repo, files.into_iter().map(Ok))
77 display_files(invocation.ui, repo, files.into_iter().map(Ok))
50 }
78 }
51 }
79 }
52
80
53 fn display_files<'a>(
81 fn display_files<'a>(
54 ui: &Ui,
82 ui: &Ui,
55 repo: &Repo,
83 repo: &Repo,
56 files: impl IntoIterator<Item = Result<&'a HgPath, HgError>>,
84 files: impl IntoIterator<Item = Result<&'a HgPath, HgError>>,
57 ) -> Result<(), CommandError> {
85 ) -> Result<(), CommandError> {
58 let mut stdout = ui.stdout_buffer();
86 let mut stdout = ui.stdout_buffer();
59 let mut any = false;
87 let mut any = false;
60
88
61 relativize_paths(repo, files, |path: Cow<[u8]>| -> Result<(), UiError> {
89 relativize_paths(repo, files, |path: Cow<[u8]>| -> Result<(), UiError> {
62 any = true;
90 any = true;
63 stdout.write_all(path.as_ref())?;
91 stdout.write_all(path.as_ref())?;
64 stdout.write_all(b"\n")
92 stdout.write_all(b"\n")
65 })?;
93 })?;
66 stdout.flush()?;
94 stdout.flush()?;
67 if any {
95 if any {
68 Ok(())
96 Ok(())
69 } else {
97 } else {
70 Err(CommandError::Unsuccessful)
98 Err(CommandError::Unsuccessful)
71 }
99 }
72 }
100 }
@@ -1,396 +1,403 b''
1 // status.rs
1 // status.rs
2 //
2 //
3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 use crate::error::CommandError;
8 use crate::error::CommandError;
9 use crate::ui::Ui;
9 use crate::ui::Ui;
10 use crate::utils::path_utils::relativize_paths;
10 use crate::utils::path_utils::relativize_paths;
11 use clap::{Arg, SubCommand};
11 use clap::{Arg, SubCommand};
12 use format_bytes::format_bytes;
12 use format_bytes::format_bytes;
13 use hg;
13 use hg;
14 use hg::config::Config;
14 use hg::config::Config;
15 use hg::dirstate::has_exec_bit;
15 use hg::dirstate::has_exec_bit;
16 use hg::errors::HgError;
16 use hg::errors::HgError;
17 use hg::manifest::Manifest;
17 use hg::manifest::Manifest;
18 use hg::matchers::AlwaysMatcher;
18 use hg::matchers::AlwaysMatcher;
19 use hg::repo::Repo;
19 use hg::repo::Repo;
20 use hg::utils::files::get_bytes_from_os_string;
20 use hg::utils::files::get_bytes_from_os_string;
21 use hg::utils::hg_path::{hg_path_to_os_string, HgPath};
21 use hg::utils::hg_path::{hg_path_to_os_string, HgPath};
22 use hg::{HgPathCow, StatusOptions};
22 use hg::{HgPathCow, StatusOptions};
23 use log::{info, warn};
23 use log::{info, warn};
24
24
25 pub const HELP_TEXT: &str = "
25 pub const HELP_TEXT: &str = "
26 Show changed files in the working directory
26 Show changed files in the working directory
27
27
28 This is a pure Rust version of `hg status`.
28 This is a pure Rust version of `hg status`.
29
29
30 Some options might be missing, check the list below.
30 Some options might be missing, check the list below.
31 ";
31 ";
32
32
33 pub fn args() -> clap::App<'static, 'static> {
33 pub fn args() -> clap::App<'static, 'static> {
34 SubCommand::with_name("status")
34 SubCommand::with_name("status")
35 .alias("st")
35 .alias("st")
36 .about(HELP_TEXT)
36 .about(HELP_TEXT)
37 .arg(
37 .arg(
38 Arg::with_name("all")
38 Arg::with_name("all")
39 .help("show status of all files")
39 .help("show status of all files")
40 .short("-A")
40 .short("-A")
41 .long("--all"),
41 .long("--all"),
42 )
42 )
43 .arg(
43 .arg(
44 Arg::with_name("modified")
44 Arg::with_name("modified")
45 .help("show only modified files")
45 .help("show only modified files")
46 .short("-m")
46 .short("-m")
47 .long("--modified"),
47 .long("--modified"),
48 )
48 )
49 .arg(
49 .arg(
50 Arg::with_name("added")
50 Arg::with_name("added")
51 .help("show only added files")
51 .help("show only added files")
52 .short("-a")
52 .short("-a")
53 .long("--added"),
53 .long("--added"),
54 )
54 )
55 .arg(
55 .arg(
56 Arg::with_name("removed")
56 Arg::with_name("removed")
57 .help("show only removed files")
57 .help("show only removed files")
58 .short("-r")
58 .short("-r")
59 .long("--removed"),
59 .long("--removed"),
60 )
60 )
61 .arg(
61 .arg(
62 Arg::with_name("clean")
62 Arg::with_name("clean")
63 .help("show only clean files")
63 .help("show only clean files")
64 .short("-c")
64 .short("-c")
65 .long("--clean"),
65 .long("--clean"),
66 )
66 )
67 .arg(
67 .arg(
68 Arg::with_name("deleted")
68 Arg::with_name("deleted")
69 .help("show only deleted files")
69 .help("show only deleted files")
70 .short("-d")
70 .short("-d")
71 .long("--deleted"),
71 .long("--deleted"),
72 )
72 )
73 .arg(
73 .arg(
74 Arg::with_name("unknown")
74 Arg::with_name("unknown")
75 .help("show only unknown (not tracked) files")
75 .help("show only unknown (not tracked) files")
76 .short("-u")
76 .short("-u")
77 .long("--unknown"),
77 .long("--unknown"),
78 )
78 )
79 .arg(
79 .arg(
80 Arg::with_name("ignored")
80 Arg::with_name("ignored")
81 .help("show only ignored files")
81 .help("show only ignored files")
82 .short("-i")
82 .short("-i")
83 .long("--ignored"),
83 .long("--ignored"),
84 )
84 )
85 .arg(
85 .arg(
86 Arg::with_name("no-status")
86 Arg::with_name("no-status")
87 .help("hide status prefix")
87 .help("hide status prefix")
88 .short("-n")
88 .short("-n")
89 .long("--no-status"),
89 .long("--no-status"),
90 )
90 )
91 }
91 }
92
92
93 /// Pure data type allowing the caller to specify file states to display
93 /// Pure data type allowing the caller to specify file states to display
94 #[derive(Copy, Clone, Debug)]
94 #[derive(Copy, Clone, Debug)]
95 pub struct DisplayStates {
95 pub struct DisplayStates {
96 pub modified: bool,
96 pub modified: bool,
97 pub added: bool,
97 pub added: bool,
98 pub removed: bool,
98 pub removed: bool,
99 pub clean: bool,
99 pub clean: bool,
100 pub deleted: bool,
100 pub deleted: bool,
101 pub unknown: bool,
101 pub unknown: bool,
102 pub ignored: bool,
102 pub ignored: bool,
103 }
103 }
104
104
105 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
105 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
106 modified: true,
106 modified: true,
107 added: true,
107 added: true,
108 removed: true,
108 removed: true,
109 clean: false,
109 clean: false,
110 deleted: true,
110 deleted: true,
111 unknown: true,
111 unknown: true,
112 ignored: false,
112 ignored: false,
113 };
113 };
114
114
115 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
115 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
116 modified: true,
116 modified: true,
117 added: true,
117 added: true,
118 removed: true,
118 removed: true,
119 clean: true,
119 clean: true,
120 deleted: true,
120 deleted: true,
121 unknown: true,
121 unknown: true,
122 ignored: true,
122 ignored: true,
123 };
123 };
124
124
125 impl DisplayStates {
125 impl DisplayStates {
126 pub fn is_empty(&self) -> bool {
126 pub fn is_empty(&self) -> bool {
127 !(self.modified
127 !(self.modified
128 || self.added
128 || self.added
129 || self.removed
129 || self.removed
130 || self.clean
130 || self.clean
131 || self.deleted
131 || self.deleted
132 || self.unknown
132 || self.unknown
133 || self.ignored)
133 || self.ignored)
134 }
134 }
135 }
135 }
136
136
137 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
137 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
138 let status_enabled_default = false;
138 let status_enabled_default = false;
139 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
139 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
140 if !status_enabled.unwrap_or(status_enabled_default) {
140 if !status_enabled.unwrap_or(status_enabled_default) {
141 return Err(CommandError::unsupported(
141 return Err(CommandError::unsupported(
142 "status is experimental in rhg (enable it with 'rhg.status = true' \
142 "status is experimental in rhg (enable it with 'rhg.status = true' \
143 or enable fallback with 'rhg.on-unsupported = fallback')"
143 or enable fallback with 'rhg.on-unsupported = fallback')"
144 ));
144 ));
145 }
145 }
146
146
147 // TODO: lift these limitations
147 // TODO: lift these limitations
148 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
148 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
149 return Err(CommandError::unsupported(
149 return Err(CommandError::unsupported(
150 "ui.tweakdefaults is not yet supported with rhg status",
150 "ui.tweakdefaults is not yet supported with rhg status",
151 ));
151 ));
152 }
152 }
153 if invocation.config.get_bool(b"ui", b"statuscopies")? {
153 if invocation.config.get_bool(b"ui", b"statuscopies")? {
154 return Err(CommandError::unsupported(
154 return Err(CommandError::unsupported(
155 "ui.statuscopies is not yet supported with rhg status",
155 "ui.statuscopies is not yet supported with rhg status",
156 ));
156 ));
157 }
157 }
158 if invocation
158 if invocation
159 .config
159 .config
160 .get(b"commands", b"status.terse")
160 .get(b"commands", b"status.terse")
161 .is_some()
161 .is_some()
162 {
162 {
163 return Err(CommandError::unsupported(
163 return Err(CommandError::unsupported(
164 "status.terse is not yet supported with rhg status",
164 "status.terse is not yet supported with rhg status",
165 ));
165 ));
166 }
166 }
167
167
168 let ui = invocation.ui;
168 let ui = invocation.ui;
169 let config = invocation.config;
169 let config = invocation.config;
170 let args = invocation.subcommand_args;
170 let args = invocation.subcommand_args;
171 let display_states = if args.is_present("all") {
171 let display_states = if args.is_present("all") {
172 // TODO when implementing `--quiet`: it excludes clean files
172 // TODO when implementing `--quiet`: it excludes clean files
173 // from `--all`
173 // from `--all`
174 ALL_DISPLAY_STATES
174 ALL_DISPLAY_STATES
175 } else {
175 } else {
176 let requested = DisplayStates {
176 let requested = DisplayStates {
177 modified: args.is_present("modified"),
177 modified: args.is_present("modified"),
178 added: args.is_present("added"),
178 added: args.is_present("added"),
179 removed: args.is_present("removed"),
179 removed: args.is_present("removed"),
180 clean: args.is_present("clean"),
180 clean: args.is_present("clean"),
181 deleted: args.is_present("deleted"),
181 deleted: args.is_present("deleted"),
182 unknown: args.is_present("unknown"),
182 unknown: args.is_present("unknown"),
183 ignored: args.is_present("ignored"),
183 ignored: args.is_present("ignored"),
184 };
184 };
185 if requested.is_empty() {
185 if requested.is_empty() {
186 DEFAULT_DISPLAY_STATES
186 DEFAULT_DISPLAY_STATES
187 } else {
187 } else {
188 requested
188 requested
189 }
189 }
190 };
190 };
191 let no_status = args.is_present("no-status");
191 let no_status = args.is_present("no-status");
192
192
193 let repo = invocation.repo?;
193 let repo = invocation.repo?;
194
195 if repo.has_sparse() || repo.has_narrow() {
196 return Err(CommandError::unsupported(
197 "rhg status is not supported for sparse checkouts or narrow clones yet"
198 ));
199 }
200
194 let mut dmap = repo.dirstate_map_mut()?;
201 let mut dmap = repo.dirstate_map_mut()?;
195
202
196 let options = StatusOptions {
203 let options = StatusOptions {
197 // we're currently supporting file systems with exec flags only
204 // we're currently supporting file systems with exec flags only
198 // anyway
205 // anyway
199 check_exec: true,
206 check_exec: true,
200 list_clean: display_states.clean,
207 list_clean: display_states.clean,
201 list_unknown: display_states.unknown,
208 list_unknown: display_states.unknown,
202 list_ignored: display_states.ignored,
209 list_ignored: display_states.ignored,
203 collect_traversed_dirs: false,
210 collect_traversed_dirs: false,
204 };
211 };
205 let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
212 let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
206 let (mut ds_status, pattern_warnings) = dmap.status(
213 let (mut ds_status, pattern_warnings) = dmap.status(
207 &AlwaysMatcher,
214 &AlwaysMatcher,
208 repo.working_directory_path().to_owned(),
215 repo.working_directory_path().to_owned(),
209 vec![ignore_file],
216 vec![ignore_file],
210 options,
217 options,
211 )?;
218 )?;
212 if !pattern_warnings.is_empty() {
219 if !pattern_warnings.is_empty() {
213 warn!("Pattern warnings: {:?}", &pattern_warnings);
220 warn!("Pattern warnings: {:?}", &pattern_warnings);
214 }
221 }
215
222
216 if !ds_status.bad.is_empty() {
223 if !ds_status.bad.is_empty() {
217 warn!("Bad matches {:?}", &(ds_status.bad))
224 warn!("Bad matches {:?}", &(ds_status.bad))
218 }
225 }
219 if !ds_status.unsure.is_empty() {
226 if !ds_status.unsure.is_empty() {
220 info!(
227 info!(
221 "Files to be rechecked by retrieval from filelog: {:?}",
228 "Files to be rechecked by retrieval from filelog: {:?}",
222 &ds_status.unsure
229 &ds_status.unsure
223 );
230 );
224 }
231 }
225 if !ds_status.unsure.is_empty()
232 if !ds_status.unsure.is_empty()
226 && (display_states.modified || display_states.clean)
233 && (display_states.modified || display_states.clean)
227 {
234 {
228 let p1 = repo.dirstate_parents()?.p1;
235 let p1 = repo.dirstate_parents()?.p1;
229 let manifest = repo.manifest_for_node(p1).map_err(|e| {
236 let manifest = repo.manifest_for_node(p1).map_err(|e| {
230 CommandError::from((e, &*format!("{:x}", p1.short())))
237 CommandError::from((e, &*format!("{:x}", p1.short())))
231 })?;
238 })?;
232 for to_check in ds_status.unsure {
239 for to_check in ds_status.unsure {
233 if unsure_is_modified(repo, &manifest, &to_check)? {
240 if unsure_is_modified(repo, &manifest, &to_check)? {
234 if display_states.modified {
241 if display_states.modified {
235 ds_status.modified.push(to_check);
242 ds_status.modified.push(to_check);
236 }
243 }
237 } else {
244 } else {
238 if display_states.clean {
245 if display_states.clean {
239 ds_status.clean.push(to_check);
246 ds_status.clean.push(to_check);
240 }
247 }
241 }
248 }
242 }
249 }
243 }
250 }
244 if display_states.modified {
251 if display_states.modified {
245 display_status_paths(
252 display_status_paths(
246 ui,
253 ui,
247 repo,
254 repo,
248 config,
255 config,
249 no_status,
256 no_status,
250 &mut ds_status.modified,
257 &mut ds_status.modified,
251 b"M",
258 b"M",
252 )?;
259 )?;
253 }
260 }
254 if display_states.added {
261 if display_states.added {
255 display_status_paths(
262 display_status_paths(
256 ui,
263 ui,
257 repo,
264 repo,
258 config,
265 config,
259 no_status,
266 no_status,
260 &mut ds_status.added,
267 &mut ds_status.added,
261 b"A",
268 b"A",
262 )?;
269 )?;
263 }
270 }
264 if display_states.removed {
271 if display_states.removed {
265 display_status_paths(
272 display_status_paths(
266 ui,
273 ui,
267 repo,
274 repo,
268 config,
275 config,
269 no_status,
276 no_status,
270 &mut ds_status.removed,
277 &mut ds_status.removed,
271 b"R",
278 b"R",
272 )?;
279 )?;
273 }
280 }
274 if display_states.deleted {
281 if display_states.deleted {
275 display_status_paths(
282 display_status_paths(
276 ui,
283 ui,
277 repo,
284 repo,
278 config,
285 config,
279 no_status,
286 no_status,
280 &mut ds_status.deleted,
287 &mut ds_status.deleted,
281 b"!",
288 b"!",
282 )?;
289 )?;
283 }
290 }
284 if display_states.unknown {
291 if display_states.unknown {
285 display_status_paths(
292 display_status_paths(
286 ui,
293 ui,
287 repo,
294 repo,
288 config,
295 config,
289 no_status,
296 no_status,
290 &mut ds_status.unknown,
297 &mut ds_status.unknown,
291 b"?",
298 b"?",
292 )?;
299 )?;
293 }
300 }
294 if display_states.ignored {
301 if display_states.ignored {
295 display_status_paths(
302 display_status_paths(
296 ui,
303 ui,
297 repo,
304 repo,
298 config,
305 config,
299 no_status,
306 no_status,
300 &mut ds_status.ignored,
307 &mut ds_status.ignored,
301 b"I",
308 b"I",
302 )?;
309 )?;
303 }
310 }
304 if display_states.clean {
311 if display_states.clean {
305 display_status_paths(
312 display_status_paths(
306 ui,
313 ui,
307 repo,
314 repo,
308 config,
315 config,
309 no_status,
316 no_status,
310 &mut ds_status.clean,
317 &mut ds_status.clean,
311 b"C",
318 b"C",
312 )?;
319 )?;
313 }
320 }
314 Ok(())
321 Ok(())
315 }
322 }
316
323
317 // Probably more elegant to use a Deref or Borrow trait rather than
324 // Probably more elegant to use a Deref or Borrow trait rather than
318 // harcode HgPathBuf, but probably not really useful at this point
325 // harcode HgPathBuf, but probably not really useful at this point
319 fn display_status_paths(
326 fn display_status_paths(
320 ui: &Ui,
327 ui: &Ui,
321 repo: &Repo,
328 repo: &Repo,
322 config: &Config,
329 config: &Config,
323 no_status: bool,
330 no_status: bool,
324 paths: &mut [HgPathCow],
331 paths: &mut [HgPathCow],
325 status_prefix: &[u8],
332 status_prefix: &[u8],
326 ) -> Result<(), CommandError> {
333 ) -> Result<(), CommandError> {
327 paths.sort_unstable();
334 paths.sort_unstable();
328 let mut relative: bool = config.get_bool(b"ui", b"relative-paths")?;
335 let mut relative: bool = config.get_bool(b"ui", b"relative-paths")?;
329 relative = config
336 relative = config
330 .get_option(b"commands", b"status.relative")?
337 .get_option(b"commands", b"status.relative")?
331 .unwrap_or(relative);
338 .unwrap_or(relative);
332 let print_path = |path: &[u8]| {
339 let print_path = |path: &[u8]| {
333 // TODO optim, probably lots of unneeded copies here, especially
340 // TODO optim, probably lots of unneeded copies here, especially
334 // if out stream is buffered
341 // if out stream is buffered
335 if no_status {
342 if no_status {
336 ui.write_stdout(&format_bytes!(b"{}\n", path))
343 ui.write_stdout(&format_bytes!(b"{}\n", path))
337 } else {
344 } else {
338 ui.write_stdout(&format_bytes!(b"{} {}\n", status_prefix, path))
345 ui.write_stdout(&format_bytes!(b"{} {}\n", status_prefix, path))
339 }
346 }
340 };
347 };
341
348
342 if relative && !ui.plain() {
349 if relative && !ui.plain() {
343 relativize_paths(repo, paths.iter().map(Ok), |path| {
350 relativize_paths(repo, paths.iter().map(Ok), |path| {
344 print_path(&path)
351 print_path(&path)
345 })?;
352 })?;
346 } else {
353 } else {
347 for path in paths {
354 for path in paths {
348 print_path(path.as_bytes())?
355 print_path(path.as_bytes())?
349 }
356 }
350 }
357 }
351 Ok(())
358 Ok(())
352 }
359 }
353
360
354 /// Check if a file is modified by comparing actual repo store and file system.
361 /// Check if a file is modified by comparing actual repo store and file system.
355 ///
362 ///
356 /// This meant to be used for those that the dirstate cannot resolve, due
363 /// This meant to be used for those that the dirstate cannot resolve, due
357 /// to time resolution limits.
364 /// to time resolution limits.
358 fn unsure_is_modified(
365 fn unsure_is_modified(
359 repo: &Repo,
366 repo: &Repo,
360 manifest: &Manifest,
367 manifest: &Manifest,
361 hg_path: &HgPath,
368 hg_path: &HgPath,
362 ) -> Result<bool, HgError> {
369 ) -> Result<bool, HgError> {
363 let vfs = repo.working_directory_vfs();
370 let vfs = repo.working_directory_vfs();
364 let fs_path = hg_path_to_os_string(hg_path).expect("HgPath conversion");
371 let fs_path = hg_path_to_os_string(hg_path).expect("HgPath conversion");
365 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
372 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
366 let is_symlink = fs_metadata.file_type().is_symlink();
373 let is_symlink = fs_metadata.file_type().is_symlink();
367 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
374 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
368 // dirstate
375 // dirstate
369 let fs_flags = if is_symlink {
376 let fs_flags = if is_symlink {
370 Some(b'l')
377 Some(b'l')
371 } else if has_exec_bit(&fs_metadata) {
378 } else if has_exec_bit(&fs_metadata) {
372 Some(b'x')
379 Some(b'x')
373 } else {
380 } else {
374 None
381 None
375 };
382 };
376
383
377 let entry = manifest
384 let entry = manifest
378 .find_file(hg_path)?
385 .find_file(hg_path)?
379 .expect("ambgious file not in p1");
386 .expect("ambgious file not in p1");
380 if entry.flags != fs_flags {
387 if entry.flags != fs_flags {
381 return Ok(true);
388 return Ok(true);
382 }
389 }
383 let filelog = repo.filelog(hg_path)?;
390 let filelog = repo.filelog(hg_path)?;
384 let filelog_entry =
391 let filelog_entry =
385 filelog.data_for_node(entry.node_id()?).map_err(|_| {
392 filelog.data_for_node(entry.node_id()?).map_err(|_| {
386 HgError::corrupted("filelog missing node from manifest")
393 HgError::corrupted("filelog missing node from manifest")
387 })?;
394 })?;
388 let contents_in_p1 = filelog_entry.data()?;
395 let contents_in_p1 = filelog_entry.data()?;
389
396
390 let fs_contents = if is_symlink {
397 let fs_contents = if is_symlink {
391 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
398 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
392 } else {
399 } else {
393 vfs.read(fs_path)?
400 vfs.read(fs_path)?
394 };
401 };
395 return Ok(contents_in_p1 != &*fs_contents);
402 return Ok(contents_in_p1 != &*fs_contents);
396 }
403 }
@@ -1,652 +1,653 b''
1 extern crate log;
1 extern crate log;
2 use crate::error::CommandError;
2 use crate::error::CommandError;
3 use crate::ui::Ui;
3 use crate::ui::Ui;
4 use clap::App;
4 use clap::App;
5 use clap::AppSettings;
5 use clap::AppSettings;
6 use clap::Arg;
6 use clap::Arg;
7 use clap::ArgMatches;
7 use clap::ArgMatches;
8 use format_bytes::{format_bytes, join};
8 use format_bytes::{format_bytes, join};
9 use hg::config::{Config, ConfigSource};
9 use hg::config::{Config, ConfigSource};
10 use hg::exit_codes;
10 use hg::exit_codes;
11 use hg::repo::{Repo, RepoError};
11 use hg::repo::{Repo, RepoError};
12 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
12 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
13 use hg::utils::SliceExt;
13 use hg::utils::SliceExt;
14 use std::ffi::OsString;
14 use std::ffi::OsString;
15 use std::path::PathBuf;
15 use std::path::PathBuf;
16 use std::process::Command;
16 use std::process::Command;
17
17
18 mod blackbox;
18 mod blackbox;
19 mod error;
19 mod error;
20 mod ui;
20 mod ui;
21 pub mod utils {
21 pub mod utils {
22 pub mod path_utils;
22 pub mod path_utils;
23 }
23 }
24
24
25 fn main_with_result(
25 fn main_with_result(
26 process_start_time: &blackbox::ProcessStartTime,
26 process_start_time: &blackbox::ProcessStartTime,
27 ui: &ui::Ui,
27 ui: &ui::Ui,
28 repo: Result<&Repo, &NoRepoInCwdError>,
28 repo: Result<&Repo, &NoRepoInCwdError>,
29 config: &Config,
29 config: &Config,
30 ) -> Result<(), CommandError> {
30 ) -> Result<(), CommandError> {
31 check_unsupported(config, ui)?;
31 check_unsupported(config, ui)?;
32
32
33 let app = App::new("rhg")
33 let app = App::new("rhg")
34 .global_setting(AppSettings::AllowInvalidUtf8)
34 .global_setting(AppSettings::AllowInvalidUtf8)
35 .global_setting(AppSettings::DisableVersion)
35 .global_setting(AppSettings::DisableVersion)
36 .setting(AppSettings::SubcommandRequired)
36 .setting(AppSettings::SubcommandRequired)
37 .setting(AppSettings::VersionlessSubcommands)
37 .setting(AppSettings::VersionlessSubcommands)
38 .arg(
38 .arg(
39 Arg::with_name("repository")
39 Arg::with_name("repository")
40 .help("repository root directory")
40 .help("repository root directory")
41 .short("-R")
41 .short("-R")
42 .long("--repository")
42 .long("--repository")
43 .value_name("REPO")
43 .value_name("REPO")
44 .takes_value(true)
44 .takes_value(true)
45 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
45 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
46 .global(true),
46 .global(true),
47 )
47 )
48 .arg(
48 .arg(
49 Arg::with_name("config")
49 Arg::with_name("config")
50 .help("set/override config option (use 'section.name=value')")
50 .help("set/override config option (use 'section.name=value')")
51 .long("--config")
51 .long("--config")
52 .value_name("CONFIG")
52 .value_name("CONFIG")
53 .takes_value(true)
53 .takes_value(true)
54 .global(true)
54 .global(true)
55 // Ok: `--config section.key1=val --config section.key2=val2`
55 // Ok: `--config section.key1=val --config section.key2=val2`
56 .multiple(true)
56 .multiple(true)
57 // Not ok: `--config section.key1=val section.key2=val2`
57 // Not ok: `--config section.key1=val section.key2=val2`
58 .number_of_values(1),
58 .number_of_values(1),
59 )
59 )
60 .arg(
60 .arg(
61 Arg::with_name("cwd")
61 Arg::with_name("cwd")
62 .help("change working directory")
62 .help("change working directory")
63 .long("--cwd")
63 .long("--cwd")
64 .value_name("DIR")
64 .value_name("DIR")
65 .takes_value(true)
65 .takes_value(true)
66 .global(true),
66 .global(true),
67 )
67 )
68 .version("0.0.1");
68 .version("0.0.1");
69 let app = add_subcommand_args(app);
69 let app = add_subcommand_args(app);
70
70
71 let matches = app.clone().get_matches_safe()?;
71 let matches = app.clone().get_matches_safe()?;
72
72
73 let (subcommand_name, subcommand_matches) = matches.subcommand();
73 let (subcommand_name, subcommand_matches) = matches.subcommand();
74
74
75 // Mercurial allows users to define "defaults" for commands, fallback
75 // Mercurial allows users to define "defaults" for commands, fallback
76 // if a default is detected for the current command
76 // if a default is detected for the current command
77 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
77 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
78 if defaults?.is_some() {
78 if defaults?.is_some() {
79 let msg = "`defaults` config set";
79 let msg = "`defaults` config set";
80 return Err(CommandError::unsupported(msg));
80 return Err(CommandError::unsupported(msg));
81 }
81 }
82
82
83 for prefix in ["pre", "post", "fail"].iter() {
83 for prefix in ["pre", "post", "fail"].iter() {
84 // Mercurial allows users to define generic hooks for commands,
84 // Mercurial allows users to define generic hooks for commands,
85 // fallback if any are detected
85 // fallback if any are detected
86 let item = format!("{}-{}", prefix, subcommand_name);
86 let item = format!("{}-{}", prefix, subcommand_name);
87 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
87 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
88 if hook_for_command.is_some() {
88 if hook_for_command.is_some() {
89 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
89 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
90 return Err(CommandError::unsupported(msg));
90 return Err(CommandError::unsupported(msg));
91 }
91 }
92 }
92 }
93 let run = subcommand_run_fn(subcommand_name)
93 let run = subcommand_run_fn(subcommand_name)
94 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
94 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
95 let subcommand_args = subcommand_matches
95 let subcommand_args = subcommand_matches
96 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
96 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
97
97
98 let invocation = CliInvocation {
98 let invocation = CliInvocation {
99 ui,
99 ui,
100 subcommand_args,
100 subcommand_args,
101 config,
101 config,
102 repo,
102 repo,
103 };
103 };
104
104
105 if let Ok(repo) = repo {
105 if let Ok(repo) = repo {
106 // We don't support subrepos, fallback if the subrepos file is present
106 // We don't support subrepos, fallback if the subrepos file is present
107 if repo.working_directory_vfs().join(".hgsub").exists() {
107 if repo.working_directory_vfs().join(".hgsub").exists() {
108 let msg = "subrepos (.hgsub is present)";
108 let msg = "subrepos (.hgsub is present)";
109 return Err(CommandError::unsupported(msg));
109 return Err(CommandError::unsupported(msg));
110 }
110 }
111 }
111 }
112
112
113 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
113 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
114 blackbox.log_command_start();
114 blackbox.log_command_start();
115 let result = run(&invocation);
115 let result = run(&invocation);
116 blackbox.log_command_end(exit_code(
116 blackbox.log_command_end(exit_code(
117 &result,
117 &result,
118 // TODO: show a warning or combine with original error if `get_bool`
118 // TODO: show a warning or combine with original error if `get_bool`
119 // returns an error
119 // returns an error
120 config
120 config
121 .get_bool(b"ui", b"detailed-exit-code")
121 .get_bool(b"ui", b"detailed-exit-code")
122 .unwrap_or(false),
122 .unwrap_or(false),
123 ));
123 ));
124 result
124 result
125 }
125 }
126
126
127 fn main() {
127 fn main() {
128 // Run this first, before we find out if the blackbox extension is even
128 // Run this first, before we find out if the blackbox extension is even
129 // enabled, in order to include everything in-between in the duration
129 // enabled, in order to include everything in-between in the duration
130 // measurements. Reading config files can be slow if they’re on NFS.
130 // measurements. Reading config files can be slow if they’re on NFS.
131 let process_start_time = blackbox::ProcessStartTime::now();
131 let process_start_time = blackbox::ProcessStartTime::now();
132
132
133 env_logger::init();
133 env_logger::init();
134 let ui = ui::Ui::new();
134 let ui = ui::Ui::new();
135
135
136 let early_args = EarlyArgs::parse(std::env::args_os());
136 let early_args = EarlyArgs::parse(std::env::args_os());
137
137
138 let initial_current_dir = early_args.cwd.map(|cwd| {
138 let initial_current_dir = early_args.cwd.map(|cwd| {
139 let cwd = get_path_from_bytes(&cwd);
139 let cwd = get_path_from_bytes(&cwd);
140 std::env::current_dir()
140 std::env::current_dir()
141 .and_then(|initial| {
141 .and_then(|initial| {
142 std::env::set_current_dir(cwd)?;
142 std::env::set_current_dir(cwd)?;
143 Ok(initial)
143 Ok(initial)
144 })
144 })
145 .unwrap_or_else(|error| {
145 .unwrap_or_else(|error| {
146 exit(
146 exit(
147 &None,
147 &None,
148 &ui,
148 &ui,
149 OnUnsupported::Abort,
149 OnUnsupported::Abort,
150 Err(CommandError::abort(format!(
150 Err(CommandError::abort(format!(
151 "abort: {}: '{}'",
151 "abort: {}: '{}'",
152 error,
152 error,
153 cwd.display()
153 cwd.display()
154 ))),
154 ))),
155 false,
155 false,
156 )
156 )
157 })
157 })
158 });
158 });
159
159
160 let mut non_repo_config =
160 let mut non_repo_config =
161 Config::load_non_repo().unwrap_or_else(|error| {
161 Config::load_non_repo().unwrap_or_else(|error| {
162 // Normally this is decided based on config, but we don’t have that
162 // Normally this is decided based on config, but we don’t have that
163 // available. As of this writing config loading never returns an
163 // available. As of this writing config loading never returns an
164 // "unsupported" error but that is not enforced by the type system.
164 // "unsupported" error but that is not enforced by the type system.
165 let on_unsupported = OnUnsupported::Abort;
165 let on_unsupported = OnUnsupported::Abort;
166
166
167 exit(
167 exit(
168 &initial_current_dir,
168 &initial_current_dir,
169 &ui,
169 &ui,
170 on_unsupported,
170 on_unsupported,
171 Err(error.into()),
171 Err(error.into()),
172 false,
172 false,
173 )
173 )
174 });
174 });
175
175
176 non_repo_config
176 non_repo_config
177 .load_cli_args_config(early_args.config)
177 .load_cli_args_config(early_args.config)
178 .unwrap_or_else(|error| {
178 .unwrap_or_else(|error| {
179 exit(
179 exit(
180 &initial_current_dir,
180 &initial_current_dir,
181 &ui,
181 &ui,
182 OnUnsupported::from_config(&non_repo_config),
182 OnUnsupported::from_config(&non_repo_config),
183 Err(error.into()),
183 Err(error.into()),
184 non_repo_config
184 non_repo_config
185 .get_bool(b"ui", b"detailed-exit-code")
185 .get_bool(b"ui", b"detailed-exit-code")
186 .unwrap_or(false),
186 .unwrap_or(false),
187 )
187 )
188 });
188 });
189
189
190 if let Some(repo_path_bytes) = &early_args.repo {
190 if let Some(repo_path_bytes) = &early_args.repo {
191 lazy_static::lazy_static! {
191 lazy_static::lazy_static! {
192 static ref SCHEME_RE: regex::bytes::Regex =
192 static ref SCHEME_RE: regex::bytes::Regex =
193 // Same as `_matchscheme` in `mercurial/util.py`
193 // Same as `_matchscheme` in `mercurial/util.py`
194 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
194 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
195 }
195 }
196 if SCHEME_RE.is_match(&repo_path_bytes) {
196 if SCHEME_RE.is_match(&repo_path_bytes) {
197 exit(
197 exit(
198 &initial_current_dir,
198 &initial_current_dir,
199 &ui,
199 &ui,
200 OnUnsupported::from_config(&non_repo_config),
200 OnUnsupported::from_config(&non_repo_config),
201 Err(CommandError::UnsupportedFeature {
201 Err(CommandError::UnsupportedFeature {
202 message: format_bytes!(
202 message: format_bytes!(
203 b"URL-like --repository {}",
203 b"URL-like --repository {}",
204 repo_path_bytes
204 repo_path_bytes
205 ),
205 ),
206 }),
206 }),
207 // TODO: show a warning or combine with original error if
207 // TODO: show a warning or combine with original error if
208 // `get_bool` returns an error
208 // `get_bool` returns an error
209 non_repo_config
209 non_repo_config
210 .get_bool(b"ui", b"detailed-exit-code")
210 .get_bool(b"ui", b"detailed-exit-code")
211 .unwrap_or(false),
211 .unwrap_or(false),
212 )
212 )
213 }
213 }
214 }
214 }
215 let repo_arg = early_args.repo.unwrap_or(Vec::new());
215 let repo_arg = early_args.repo.unwrap_or(Vec::new());
216 let repo_path: Option<PathBuf> = {
216 let repo_path: Option<PathBuf> = {
217 if repo_arg.is_empty() {
217 if repo_arg.is_empty() {
218 None
218 None
219 } else {
219 } else {
220 let local_config = {
220 let local_config = {
221 if std::env::var_os("HGRCSKIPREPO").is_none() {
221 if std::env::var_os("HGRCSKIPREPO").is_none() {
222 // TODO: handle errors from find_repo_root
222 // TODO: handle errors from find_repo_root
223 if let Ok(current_dir_path) = Repo::find_repo_root() {
223 if let Ok(current_dir_path) = Repo::find_repo_root() {
224 let config_files = vec![
224 let config_files = vec![
225 ConfigSource::AbsPath(
225 ConfigSource::AbsPath(
226 current_dir_path.join(".hg/hgrc"),
226 current_dir_path.join(".hg/hgrc"),
227 ),
227 ),
228 ConfigSource::AbsPath(
228 ConfigSource::AbsPath(
229 current_dir_path.join(".hg/hgrc-not-shared"),
229 current_dir_path.join(".hg/hgrc-not-shared"),
230 ),
230 ),
231 ];
231 ];
232 // TODO: handle errors from
232 // TODO: handle errors from
233 // `load_from_explicit_sources`
233 // `load_from_explicit_sources`
234 Config::load_from_explicit_sources(config_files).ok()
234 Config::load_from_explicit_sources(config_files).ok()
235 } else {
235 } else {
236 None
236 None
237 }
237 }
238 } else {
238 } else {
239 None
239 None
240 }
240 }
241 };
241 };
242
242
243 let non_repo_config_val = {
243 let non_repo_config_val = {
244 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
244 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
245 match &non_repo_val {
245 match &non_repo_val {
246 Some(val) if val.len() > 0 => home::home_dir()
246 Some(val) if val.len() > 0 => home::home_dir()
247 .unwrap_or_else(|| PathBuf::from("~"))
247 .unwrap_or_else(|| PathBuf::from("~"))
248 .join(get_path_from_bytes(val))
248 .join(get_path_from_bytes(val))
249 .canonicalize()
249 .canonicalize()
250 // TODO: handle error and make it similar to python
250 // TODO: handle error and make it similar to python
251 // implementation maybe?
251 // implementation maybe?
252 .ok(),
252 .ok(),
253 _ => None,
253 _ => None,
254 }
254 }
255 };
255 };
256
256
257 let config_val = match &local_config {
257 let config_val = match &local_config {
258 None => non_repo_config_val,
258 None => non_repo_config_val,
259 Some(val) => {
259 Some(val) => {
260 let local_config_val = val.get(b"paths", &repo_arg);
260 let local_config_val = val.get(b"paths", &repo_arg);
261 match &local_config_val {
261 match &local_config_val {
262 Some(val) if val.len() > 0 => {
262 Some(val) if val.len() > 0 => {
263 // presence of a local_config assures that
263 // presence of a local_config assures that
264 // current_dir
264 // current_dir
265 // wont result in an Error
265 // wont result in an Error
266 let canpath = hg::utils::current_dir()
266 let canpath = hg::utils::current_dir()
267 .unwrap()
267 .unwrap()
268 .join(get_path_from_bytes(val))
268 .join(get_path_from_bytes(val))
269 .canonicalize();
269 .canonicalize();
270 canpath.ok().or(non_repo_config_val)
270 canpath.ok().or(non_repo_config_val)
271 }
271 }
272 _ => non_repo_config_val,
272 _ => non_repo_config_val,
273 }
273 }
274 }
274 }
275 };
275 };
276 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
276 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
277 }
277 }
278 };
278 };
279
279
280 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
280 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
281 {
281 {
282 Ok(repo) => Ok(repo),
282 Ok(repo) => Ok(repo),
283 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
283 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
284 // Not finding a repo is not fatal yet, if `-R` was not given
284 // Not finding a repo is not fatal yet, if `-R` was not given
285 Err(NoRepoInCwdError { cwd: at })
285 Err(NoRepoInCwdError { cwd: at })
286 }
286 }
287 Err(error) => exit(
287 Err(error) => exit(
288 &initial_current_dir,
288 &initial_current_dir,
289 &ui,
289 &ui,
290 OnUnsupported::from_config(&non_repo_config),
290 OnUnsupported::from_config(&non_repo_config),
291 Err(error.into()),
291 Err(error.into()),
292 // TODO: show a warning or combine with original error if
292 // TODO: show a warning or combine with original error if
293 // `get_bool` returns an error
293 // `get_bool` returns an error
294 non_repo_config
294 non_repo_config
295 .get_bool(b"ui", b"detailed-exit-code")
295 .get_bool(b"ui", b"detailed-exit-code")
296 .unwrap_or(false),
296 .unwrap_or(false),
297 ),
297 ),
298 };
298 };
299
299
300 let config = if let Ok(repo) = &repo_result {
300 let config = if let Ok(repo) = &repo_result {
301 repo.config()
301 repo.config()
302 } else {
302 } else {
303 &non_repo_config
303 &non_repo_config
304 };
304 };
305 let on_unsupported = OnUnsupported::from_config(config);
305 let on_unsupported = OnUnsupported::from_config(config);
306
306
307 let result = main_with_result(
307 let result = main_with_result(
308 &process_start_time,
308 &process_start_time,
309 &ui,
309 &ui,
310 repo_result.as_ref(),
310 repo_result.as_ref(),
311 config,
311 config,
312 );
312 );
313 exit(
313 exit(
314 &initial_current_dir,
314 &initial_current_dir,
315 &ui,
315 &ui,
316 on_unsupported,
316 on_unsupported,
317 result,
317 result,
318 // TODO: show a warning or combine with original error if `get_bool`
318 // TODO: show a warning or combine with original error if `get_bool`
319 // returns an error
319 // returns an error
320 config
320 config
321 .get_bool(b"ui", b"detailed-exit-code")
321 .get_bool(b"ui", b"detailed-exit-code")
322 .unwrap_or(false),
322 .unwrap_or(false),
323 )
323 )
324 }
324 }
325
325
326 fn exit_code(
326 fn exit_code(
327 result: &Result<(), CommandError>,
327 result: &Result<(), CommandError>,
328 use_detailed_exit_code: bool,
328 use_detailed_exit_code: bool,
329 ) -> i32 {
329 ) -> i32 {
330 match result {
330 match result {
331 Ok(()) => exit_codes::OK,
331 Ok(()) => exit_codes::OK,
332 Err(CommandError::Abort {
332 Err(CommandError::Abort {
333 message: _,
333 message: _,
334 detailed_exit_code,
334 detailed_exit_code,
335 }) => {
335 }) => {
336 if use_detailed_exit_code {
336 if use_detailed_exit_code {
337 *detailed_exit_code
337 *detailed_exit_code
338 } else {
338 } else {
339 exit_codes::ABORT
339 exit_codes::ABORT
340 }
340 }
341 }
341 }
342 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
342 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
343
343
344 // Exit with a specific code and no error message to let a potential
344 // Exit with a specific code and no error message to let a potential
345 // wrapper script fallback to Python-based Mercurial.
345 // wrapper script fallback to Python-based Mercurial.
346 Err(CommandError::UnsupportedFeature { .. }) => {
346 Err(CommandError::UnsupportedFeature { .. }) => {
347 exit_codes::UNIMPLEMENTED
347 exit_codes::UNIMPLEMENTED
348 }
348 }
349 }
349 }
350 }
350 }
351
351
352 fn exit(
352 fn exit(
353 initial_current_dir: &Option<PathBuf>,
353 initial_current_dir: &Option<PathBuf>,
354 ui: &Ui,
354 ui: &Ui,
355 mut on_unsupported: OnUnsupported,
355 mut on_unsupported: OnUnsupported,
356 result: Result<(), CommandError>,
356 result: Result<(), CommandError>,
357 use_detailed_exit_code: bool,
357 use_detailed_exit_code: bool,
358 ) -> ! {
358 ) -> ! {
359 if let (
359 if let (
360 OnUnsupported::Fallback { executable },
360 OnUnsupported::Fallback { executable },
361 Err(CommandError::UnsupportedFeature { .. }),
361 Err(CommandError::UnsupportedFeature { .. }),
362 ) = (&on_unsupported, &result)
362 ) = (&on_unsupported, &result)
363 {
363 {
364 let mut args = std::env::args_os();
364 let mut args = std::env::args_os();
365 let executable = match executable {
365 let executable = match executable {
366 None => {
366 None => {
367 exit_no_fallback(
367 exit_no_fallback(
368 ui,
368 ui,
369 OnUnsupported::Abort,
369 OnUnsupported::Abort,
370 Err(CommandError::abort(
370 Err(CommandError::abort(
371 "abort: 'rhg.on-unsupported=fallback' without \
371 "abort: 'rhg.on-unsupported=fallback' without \
372 'rhg.fallback-executable' set.",
372 'rhg.fallback-executable' set.",
373 )),
373 )),
374 false,
374 false,
375 );
375 );
376 }
376 }
377 Some(executable) => executable,
377 Some(executable) => executable,
378 };
378 };
379 let executable_path = get_path_from_bytes(&executable);
379 let executable_path = get_path_from_bytes(&executable);
380 let this_executable = args.next().expect("exepcted argv[0] to exist");
380 let this_executable = args.next().expect("exepcted argv[0] to exist");
381 if executable_path == &PathBuf::from(this_executable) {
381 if executable_path == &PathBuf::from(this_executable) {
382 // Avoid spawning infinitely many processes until resource
382 // Avoid spawning infinitely many processes until resource
383 // exhaustion.
383 // exhaustion.
384 let _ = ui.write_stderr(&format_bytes!(
384 let _ = ui.write_stderr(&format_bytes!(
385 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
385 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
386 points to `rhg` itself.\n",
386 points to `rhg` itself.\n",
387 executable
387 executable
388 ));
388 ));
389 on_unsupported = OnUnsupported::Abort
389 on_unsupported = OnUnsupported::Abort
390 } else {
390 } else {
391 // `args` is now `argv[1..]` since we’ve already consumed
391 // `args` is now `argv[1..]` since we’ve already consumed
392 // `argv[0]`
392 // `argv[0]`
393 let mut command = Command::new(executable_path);
393 let mut command = Command::new(executable_path);
394 command.args(args);
394 command.args(args);
395 if let Some(initial) = initial_current_dir {
395 if let Some(initial) = initial_current_dir {
396 command.current_dir(initial);
396 command.current_dir(initial);
397 }
397 }
398 let result = command.status();
398 let result = command.status();
399 match result {
399 match result {
400 Ok(status) => std::process::exit(
400 Ok(status) => std::process::exit(
401 status.code().unwrap_or(exit_codes::ABORT),
401 status.code().unwrap_or(exit_codes::ABORT),
402 ),
402 ),
403 Err(error) => {
403 Err(error) => {
404 let _ = ui.write_stderr(&format_bytes!(
404 let _ = ui.write_stderr(&format_bytes!(
405 b"tried to fall back to a '{}' sub-process but got error {}\n",
405 b"tried to fall back to a '{}' sub-process but got error {}\n",
406 executable, format_bytes::Utf8(error)
406 executable, format_bytes::Utf8(error)
407 ));
407 ));
408 on_unsupported = OnUnsupported::Abort
408 on_unsupported = OnUnsupported::Abort
409 }
409 }
410 }
410 }
411 }
411 }
412 }
412 }
413 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
413 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
414 }
414 }
415
415
416 fn exit_no_fallback(
416 fn exit_no_fallback(
417 ui: &Ui,
417 ui: &Ui,
418 on_unsupported: OnUnsupported,
418 on_unsupported: OnUnsupported,
419 result: Result<(), CommandError>,
419 result: Result<(), CommandError>,
420 use_detailed_exit_code: bool,
420 use_detailed_exit_code: bool,
421 ) -> ! {
421 ) -> ! {
422 match &result {
422 match &result {
423 Ok(_) => {}
423 Ok(_) => {}
424 Err(CommandError::Unsuccessful) => {}
424 Err(CommandError::Unsuccessful) => {}
425 Err(CommandError::Abort {
425 Err(CommandError::Abort {
426 message,
426 message,
427 detailed_exit_code: _,
427 detailed_exit_code: _,
428 }) => {
428 }) => {
429 if !message.is_empty() {
429 if !message.is_empty() {
430 // Ignore errors when writing to stderr, we’re already exiting
430 // Ignore errors when writing to stderr, we’re already exiting
431 // with failure code so there’s not much more we can do.
431 // with failure code so there’s not much more we can do.
432 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
432 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
433 }
433 }
434 }
434 }
435 Err(CommandError::UnsupportedFeature { message }) => {
435 Err(CommandError::UnsupportedFeature { message }) => {
436 match on_unsupported {
436 match on_unsupported {
437 OnUnsupported::Abort => {
437 OnUnsupported::Abort => {
438 let _ = ui.write_stderr(&format_bytes!(
438 let _ = ui.write_stderr(&format_bytes!(
439 b"unsupported feature: {}\n",
439 b"unsupported feature: {}\n",
440 message
440 message
441 ));
441 ));
442 }
442 }
443 OnUnsupported::AbortSilent => {}
443 OnUnsupported::AbortSilent => {}
444 OnUnsupported::Fallback { .. } => unreachable!(),
444 OnUnsupported::Fallback { .. } => unreachable!(),
445 }
445 }
446 }
446 }
447 }
447 }
448 std::process::exit(exit_code(&result, use_detailed_exit_code))
448 std::process::exit(exit_code(&result, use_detailed_exit_code))
449 }
449 }
450
450
451 macro_rules! subcommands {
451 macro_rules! subcommands {
452 ($( $command: ident )+) => {
452 ($( $command: ident )+) => {
453 mod commands {
453 mod commands {
454 $(
454 $(
455 pub mod $command;
455 pub mod $command;
456 )+
456 )+
457 }
457 }
458
458
459 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
459 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
460 app
460 app
461 $(
461 $(
462 .subcommand(commands::$command::args())
462 .subcommand(commands::$command::args())
463 )+
463 )+
464 }
464 }
465
465
466 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
466 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
467
467
468 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
468 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
469 match name {
469 match name {
470 $(
470 $(
471 stringify!($command) => Some(commands::$command::run),
471 stringify!($command) => Some(commands::$command::run),
472 )+
472 )+
473 _ => None,
473 _ => None,
474 }
474 }
475 }
475 }
476 };
476 };
477 }
477 }
478
478
479 subcommands! {
479 subcommands! {
480 cat
480 cat
481 debugdata
481 debugdata
482 debugrequirements
482 debugrequirements
483 debugignorerhg
483 debugignorerhg
484 files
484 files
485 root
485 root
486 config
486 config
487 status
487 status
488 }
488 }
489
489
490 pub struct CliInvocation<'a> {
490 pub struct CliInvocation<'a> {
491 ui: &'a Ui,
491 ui: &'a Ui,
492 subcommand_args: &'a ArgMatches<'a>,
492 subcommand_args: &'a ArgMatches<'a>,
493 config: &'a Config,
493 config: &'a Config,
494 /// References inside `Result` is a bit peculiar but allow
494 /// References inside `Result` is a bit peculiar but allow
495 /// `invocation.repo?` to work out with `&CliInvocation` since this
495 /// `invocation.repo?` to work out with `&CliInvocation` since this
496 /// `Result` type is `Copy`.
496 /// `Result` type is `Copy`.
497 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
497 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
498 }
498 }
499
499
500 struct NoRepoInCwdError {
500 struct NoRepoInCwdError {
501 cwd: PathBuf,
501 cwd: PathBuf,
502 }
502 }
503
503
504 /// CLI arguments to be parsed "early" in order to be able to read
504 /// CLI arguments to be parsed "early" in order to be able to read
505 /// configuration before using Clap. Ideally we would also use Clap for this,
505 /// configuration before using Clap. Ideally we would also use Clap for this,
506 /// see <https://github.com/clap-rs/clap/discussions/2366>.
506 /// see <https://github.com/clap-rs/clap/discussions/2366>.
507 ///
507 ///
508 /// These arguments are still declared when we do use Clap later, so that Clap
508 /// These arguments are still declared when we do use Clap later, so that Clap
509 /// does not return an error for their presence.
509 /// does not return an error for their presence.
510 struct EarlyArgs {
510 struct EarlyArgs {
511 /// Values of all `--config` arguments. (Possibly none)
511 /// Values of all `--config` arguments. (Possibly none)
512 config: Vec<Vec<u8>>,
512 config: Vec<Vec<u8>>,
513 /// Value of the `-R` or `--repository` argument, if any.
513 /// Value of the `-R` or `--repository` argument, if any.
514 repo: Option<Vec<u8>>,
514 repo: Option<Vec<u8>>,
515 /// Value of the `--cwd` argument, if any.
515 /// Value of the `--cwd` argument, if any.
516 cwd: Option<Vec<u8>>,
516 cwd: Option<Vec<u8>>,
517 }
517 }
518
518
519 impl EarlyArgs {
519 impl EarlyArgs {
520 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
520 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
521 let mut args = args.into_iter().map(get_bytes_from_os_str);
521 let mut args = args.into_iter().map(get_bytes_from_os_str);
522 let mut config = Vec::new();
522 let mut config = Vec::new();
523 let mut repo = None;
523 let mut repo = None;
524 let mut cwd = None;
524 let mut cwd = None;
525 // Use `while let` instead of `for` so that we can also call
525 // Use `while let` instead of `for` so that we can also call
526 // `args.next()` inside the loop.
526 // `args.next()` inside the loop.
527 while let Some(arg) = args.next() {
527 while let Some(arg) = args.next() {
528 if arg == b"--config" {
528 if arg == b"--config" {
529 if let Some(value) = args.next() {
529 if let Some(value) = args.next() {
530 config.push(value)
530 config.push(value)
531 }
531 }
532 } else if let Some(value) = arg.drop_prefix(b"--config=") {
532 } else if let Some(value) = arg.drop_prefix(b"--config=") {
533 config.push(value.to_owned())
533 config.push(value.to_owned())
534 }
534 }
535
535
536 if arg == b"--cwd" {
536 if arg == b"--cwd" {
537 if let Some(value) = args.next() {
537 if let Some(value) = args.next() {
538 cwd = Some(value)
538 cwd = Some(value)
539 }
539 }
540 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
540 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
541 cwd = Some(value.to_owned())
541 cwd = Some(value.to_owned())
542 }
542 }
543
543
544 if arg == b"--repository" || arg == b"-R" {
544 if arg == b"--repository" || arg == b"-R" {
545 if let Some(value) = args.next() {
545 if let Some(value) = args.next() {
546 repo = Some(value)
546 repo = Some(value)
547 }
547 }
548 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
548 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
549 repo = Some(value.to_owned())
549 repo = Some(value.to_owned())
550 } else if let Some(value) = arg.drop_prefix(b"-R") {
550 } else if let Some(value) = arg.drop_prefix(b"-R") {
551 repo = Some(value.to_owned())
551 repo = Some(value.to_owned())
552 }
552 }
553 }
553 }
554 Self { config, repo, cwd }
554 Self { config, repo, cwd }
555 }
555 }
556 }
556 }
557
557
558 /// What to do when encountering some unsupported feature.
558 /// What to do when encountering some unsupported feature.
559 ///
559 ///
560 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
560 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
561 enum OnUnsupported {
561 enum OnUnsupported {
562 /// Print an error message describing what feature is not supported,
562 /// Print an error message describing what feature is not supported,
563 /// and exit with code 252.
563 /// and exit with code 252.
564 Abort,
564 Abort,
565 /// Silently exit with code 252.
565 /// Silently exit with code 252.
566 AbortSilent,
566 AbortSilent,
567 /// Try running a Python implementation
567 /// Try running a Python implementation
568 Fallback { executable: Option<Vec<u8>> },
568 Fallback { executable: Option<Vec<u8>> },
569 }
569 }
570
570
571 impl OnUnsupported {
571 impl OnUnsupported {
572 const DEFAULT: Self = OnUnsupported::Abort;
572 const DEFAULT: Self = OnUnsupported::Abort;
573
573
574 fn from_config(config: &Config) -> Self {
574 fn from_config(config: &Config) -> Self {
575 match config
575 match config
576 .get(b"rhg", b"on-unsupported")
576 .get(b"rhg", b"on-unsupported")
577 .map(|value| value.to_ascii_lowercase())
577 .map(|value| value.to_ascii_lowercase())
578 .as_deref()
578 .as_deref()
579 {
579 {
580 Some(b"abort") => OnUnsupported::Abort,
580 Some(b"abort") => OnUnsupported::Abort,
581 Some(b"abort-silent") => OnUnsupported::AbortSilent,
581 Some(b"abort-silent") => OnUnsupported::AbortSilent,
582 Some(b"fallback") => OnUnsupported::Fallback {
582 Some(b"fallback") => OnUnsupported::Fallback {
583 executable: config
583 executable: config
584 .get(b"rhg", b"fallback-executable")
584 .get(b"rhg", b"fallback-executable")
585 .map(|x| x.to_owned()),
585 .map(|x| x.to_owned()),
586 },
586 },
587 None => Self::DEFAULT,
587 None => Self::DEFAULT,
588 Some(_) => {
588 Some(_) => {
589 // TODO: warn about unknown config value
589 // TODO: warn about unknown config value
590 Self::DEFAULT
590 Self::DEFAULT
591 }
591 }
592 }
592 }
593 }
593 }
594 }
594 }
595
595
596 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"];
596 const SUPPORTED_EXTENSIONS: &[&[u8]] =
597 &[b"blackbox", b"share", b"sparse", b"narrow"];
597
598
598 fn check_extensions(config: &Config) -> Result<(), CommandError> {
599 fn check_extensions(config: &Config) -> Result<(), CommandError> {
599 let enabled = config.get_section_keys(b"extensions");
600 let enabled = config.get_section_keys(b"extensions");
600
601
601 let mut unsupported = enabled;
602 let mut unsupported = enabled;
602 for supported in SUPPORTED_EXTENSIONS {
603 for supported in SUPPORTED_EXTENSIONS {
603 unsupported.remove(supported);
604 unsupported.remove(supported);
604 }
605 }
605
606
606 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
607 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
607 {
608 {
608 for ignored in ignored_list {
609 for ignored in ignored_list {
609 unsupported.remove(ignored.as_slice());
610 unsupported.remove(ignored.as_slice());
610 }
611 }
611 }
612 }
612
613
613 if unsupported.is_empty() {
614 if unsupported.is_empty() {
614 Ok(())
615 Ok(())
615 } else {
616 } else {
616 Err(CommandError::UnsupportedFeature {
617 Err(CommandError::UnsupportedFeature {
617 message: format_bytes!(
618 message: format_bytes!(
618 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
619 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
619 join(unsupported, b", ")
620 join(unsupported, b", ")
620 ),
621 ),
621 })
622 })
622 }
623 }
623 }
624 }
624
625
625 fn check_unsupported(
626 fn check_unsupported(
626 config: &Config,
627 config: &Config,
627 ui: &ui::Ui,
628 ui: &ui::Ui,
628 ) -> Result<(), CommandError> {
629 ) -> Result<(), CommandError> {
629 check_extensions(config)?;
630 check_extensions(config)?;
630
631
631 if std::env::var_os("HG_PENDING").is_some() {
632 if std::env::var_os("HG_PENDING").is_some() {
632 // TODO: only if the value is `== repo.working_directory`?
633 // TODO: only if the value is `== repo.working_directory`?
633 // What about relative v.s. absolute paths?
634 // What about relative v.s. absolute paths?
634 Err(CommandError::unsupported("$HG_PENDING"))?
635 Err(CommandError::unsupported("$HG_PENDING"))?
635 }
636 }
636
637
637 if config.has_non_empty_section(b"encode") {
638 if config.has_non_empty_section(b"encode") {
638 Err(CommandError::unsupported("[encode] config"))?
639 Err(CommandError::unsupported("[encode] config"))?
639 }
640 }
640
641
641 if config.has_non_empty_section(b"decode") {
642 if config.has_non_empty_section(b"decode") {
642 Err(CommandError::unsupported("[decode] config"))?
643 Err(CommandError::unsupported("[decode] config"))?
643 }
644 }
644
645
645 if let Some(color) = config.get(b"ui", b"color") {
646 if let Some(color) = config.get(b"ui", b"color") {
646 if (color == b"always" || color == b"debug") && !ui.plain() {
647 if (color == b"always" || color == b"debug") && !ui.plain() {
647 Err(CommandError::unsupported("colored output"))?
648 Err(CommandError::unsupported("colored output"))?
648 }
649 }
649 }
650 }
650
651
651 Ok(())
652 Ok(())
652 }
653 }
General Comments 0
You need to be logged in to leave comments. Login now