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]] = |
|
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