##// END OF EJS Templates
rhg: Propogate manifest parse errors instead of panicking...
Simon Sapin -
r49165:10c32e1b default
parent child Browse files
Show More
@@ -1,115 +1,119 b''
1 // list_tracked_files.rs
1 // list_tracked_files.rs
2 //
2 //
3 // Copyright 2020 Antoine Cezar <antoine.cezar@octobus.net>
3 // Copyright 2020 Antoine Cezar <antoine.cezar@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::repo::Repo;
8 use crate::repo::Repo;
9 use crate::revlog::revlog::RevlogError;
9 use crate::revlog::revlog::RevlogError;
10 use crate::revlog::Node;
10 use crate::revlog::Node;
11
11
12 use crate::utils::hg_path::HgPath;
12 use crate::utils::hg_path::HgPath;
13
13
14 use crate::errors::HgError;
14 use itertools::put_back;
15 use itertools::put_back;
15 use itertools::PutBack;
16 use itertools::PutBack;
16 use std::cmp::Ordering;
17 use std::cmp::Ordering;
17
18
18 pub struct CatOutput<'a> {
19 pub struct CatOutput<'a> {
19 /// Whether any file in the manifest matched the paths given as CLI
20 /// Whether any file in the manifest matched the paths given as CLI
20 /// arguments
21 /// arguments
21 pub found_any: bool,
22 pub found_any: bool,
22 /// The contents of matching files, in manifest order
23 /// The contents of matching files, in manifest order
23 pub results: Vec<(&'a HgPath, Vec<u8>)>,
24 pub results: Vec<(&'a HgPath, Vec<u8>)>,
24 /// Which of the CLI arguments did not match any manifest file
25 /// Which of the CLI arguments did not match any manifest file
25 pub missing: Vec<&'a HgPath>,
26 pub missing: Vec<&'a HgPath>,
26 /// The node ID that the given revset was resolved to
27 /// The node ID that the given revset was resolved to
27 pub node: Node,
28 pub node: Node,
28 }
29 }
29
30
30 // Find an item in an iterator over a sorted collection.
31 // Find an item in an iterator over a sorted collection.
31 fn find_item<'a, 'b, 'c, D, I: Iterator<Item = (&'a HgPath, D)>>(
32 fn find_item<'a, D, I: Iterator<Item = Result<(&'a HgPath, D), HgError>>>(
32 i: &mut PutBack<I>,
33 i: &mut PutBack<I>,
33 needle: &'b HgPath,
34 needle: &HgPath,
34 ) -> Option<D> {
35 ) -> Result<Option<D>, HgError> {
35 loop {
36 loop {
36 match i.next() {
37 match i.next() {
37 None => return None,
38 None => return Ok(None),
38 Some(val) => match needle.as_bytes().cmp(val.0.as_bytes()) {
39 Some(result) => {
39 Ordering::Less => {
40 let (path, value) = result?;
40 i.put_back(val);
41 match needle.as_bytes().cmp(path.as_bytes()) {
41 return None;
42 Ordering::Less => {
43 i.put_back(Ok((path, value)));
44 return Ok(None);
45 }
46 Ordering::Greater => continue,
47 Ordering::Equal => return Ok(Some(value)),
42 }
48 }
43 Ordering::Greater => continue,
49 }
44 Ordering::Equal => return Some(val.1),
45 },
46 }
50 }
47 }
51 }
48 }
52 }
49
53
50 fn find_files_in_manifest<
54 fn find_files_in_manifest<
51 'manifest,
55 'manifest,
52 'query,
56 'query,
53 Data,
57 Data,
54 Manifest: Iterator<Item = (&'manifest HgPath, Data)>,
58 Manifest: Iterator<Item = Result<(&'manifest HgPath, Data), HgError>>,
55 Query: Iterator<Item = &'query HgPath>,
59 Query: Iterator<Item = &'query HgPath>,
56 >(
60 >(
57 manifest: Manifest,
61 manifest: Manifest,
58 query: Query,
62 query: Query,
59 ) -> (Vec<(&'query HgPath, Data)>, Vec<&'query HgPath>) {
63 ) -> Result<(Vec<(&'query HgPath, Data)>, Vec<&'query HgPath>), HgError> {
60 let mut manifest = put_back(manifest);
64 let mut manifest = put_back(manifest);
61 let mut res = vec![];
65 let mut res = vec![];
62 let mut missing = vec![];
66 let mut missing = vec![];
63
67
64 for file in query {
68 for file in query {
65 match find_item(&mut manifest, file) {
69 match find_item(&mut manifest, file)? {
66 None => missing.push(file),
70 None => missing.push(file),
67 Some(item) => res.push((file, item)),
71 Some(item) => res.push((file, item)),
68 }
72 }
69 }
73 }
70 return (res, missing);
74 return Ok((res, missing));
71 }
75 }
72
76
73 /// Output the given revision of files
77 /// Output the given revision of files
74 ///
78 ///
75 /// * `root`: Repository root
79 /// * `root`: Repository root
76 /// * `rev`: The revision to cat the files from.
80 /// * `rev`: The revision to cat the files from.
77 /// * `files`: The files to output.
81 /// * `files`: The files to output.
78 pub fn cat<'a>(
82 pub fn cat<'a>(
79 repo: &Repo,
83 repo: &Repo,
80 revset: &str,
84 revset: &str,
81 mut files: Vec<&'a HgPath>,
85 mut files: Vec<&'a HgPath>,
82 ) -> Result<CatOutput<'a>, RevlogError> {
86 ) -> Result<CatOutput<'a>, RevlogError> {
83 let rev = crate::revset::resolve_single(revset, repo)?;
87 let rev = crate::revset::resolve_single(revset, repo)?;
84 let manifest = repo.manifest_for_rev(rev)?;
88 let manifest = repo.manifest_for_rev(rev)?;
85 let node = *repo
89 let node = *repo
86 .changelog()?
90 .changelog()?
87 .node_from_rev(rev)
91 .node_from_rev(rev)
88 .expect("should succeed when repo.manifest did");
92 .expect("should succeed when repo.manifest did");
89 let mut results: Vec<(&'a HgPath, Vec<u8>)> = vec![];
93 let mut results: Vec<(&'a HgPath, Vec<u8>)> = vec![];
90 let mut found_any = false;
94 let mut found_any = false;
91
95
92 files.sort_unstable();
96 files.sort_unstable();
93
97
94 let (found, missing) = find_files_in_manifest(
98 let (found, missing) = find_files_in_manifest(
95 manifest.files_with_nodes(),
99 manifest.files_with_nodes(),
96 files.into_iter().map(|f| f.as_ref()),
100 files.into_iter().map(|f| f.as_ref()),
97 );
101 )?;
98
102
99 for (file_path, node_bytes) in found {
103 for (file_path, node_bytes) in found {
100 found_any = true;
104 found_any = true;
101 let file_log = repo.filelog(file_path)?;
105 let file_log = repo.filelog(file_path)?;
102 let file_node = Node::from_hex_for_repo(node_bytes)?;
106 let file_node = Node::from_hex_for_repo(node_bytes)?;
103 results.push((
107 results.push((
104 file_path,
108 file_path,
105 file_log.data_for_node(file_node)?.into_data()?,
109 file_log.data_for_node(file_node)?.into_data()?,
106 ));
110 ));
107 }
111 }
108
112
109 Ok(CatOutput {
113 Ok(CatOutput {
110 found_any,
114 found_any,
111 results,
115 results,
112 missing,
116 missing,
113 node,
117 node,
114 })
118 })
115 }
119 }
@@ -1,82 +1,82 b''
1 // list_tracked_files.rs
1 // list_tracked_files.rs
2 //
2 //
3 // Copyright 2020 Antoine Cezar <antoine.cezar@octobus.net>
3 // Copyright 2020 Antoine Cezar <antoine.cezar@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::dirstate::parsers::parse_dirstate_entries;
8 use crate::dirstate::parsers::parse_dirstate_entries;
9 use crate::dirstate_tree::on_disk::{for_each_tracked_path, read_docket};
9 use crate::dirstate_tree::on_disk::{for_each_tracked_path, read_docket};
10 use crate::errors::HgError;
10 use crate::errors::HgError;
11 use crate::repo::Repo;
11 use crate::repo::Repo;
12 use crate::revlog::manifest::Manifest;
12 use crate::revlog::manifest::Manifest;
13 use crate::revlog::revlog::RevlogError;
13 use crate::revlog::revlog::RevlogError;
14 use crate::utils::hg_path::HgPath;
14 use crate::utils::hg_path::HgPath;
15 use crate::DirstateError;
15 use crate::DirstateError;
16 use rayon::prelude::*;
16 use rayon::prelude::*;
17
17
18 /// List files under Mercurial control in the working directory
18 /// List files under Mercurial control in the working directory
19 /// by reading the dirstate
19 /// by reading the dirstate
20 pub struct Dirstate {
20 pub struct Dirstate {
21 /// The `dirstate` content.
21 /// The `dirstate` content.
22 content: Vec<u8>,
22 content: Vec<u8>,
23 v2_metadata: Option<Vec<u8>>,
23 v2_metadata: Option<Vec<u8>>,
24 }
24 }
25
25
26 impl Dirstate {
26 impl Dirstate {
27 pub fn new(repo: &Repo) -> Result<Self, HgError> {
27 pub fn new(repo: &Repo) -> Result<Self, HgError> {
28 let mut content = repo.hg_vfs().read("dirstate")?;
28 let mut content = repo.hg_vfs().read("dirstate")?;
29 let v2_metadata = if repo.has_dirstate_v2() {
29 let v2_metadata = if repo.has_dirstate_v2() {
30 let docket = read_docket(&content)?;
30 let docket = read_docket(&content)?;
31 let meta = docket.tree_metadata().to_vec();
31 let meta = docket.tree_metadata().to_vec();
32 content = repo.hg_vfs().read(docket.data_filename())?;
32 content = repo.hg_vfs().read(docket.data_filename())?;
33 Some(meta)
33 Some(meta)
34 } else {
34 } else {
35 None
35 None
36 };
36 };
37 Ok(Self {
37 Ok(Self {
38 content,
38 content,
39 v2_metadata,
39 v2_metadata,
40 })
40 })
41 }
41 }
42
42
43 pub fn tracked_files(&self) -> Result<Vec<&HgPath>, DirstateError> {
43 pub fn tracked_files(&self) -> Result<Vec<&HgPath>, DirstateError> {
44 let mut files = Vec::new();
44 let mut files = Vec::new();
45 if !self.content.is_empty() {
45 if !self.content.is_empty() {
46 if let Some(meta) = &self.v2_metadata {
46 if let Some(meta) = &self.v2_metadata {
47 for_each_tracked_path(&self.content, meta, |path| {
47 for_each_tracked_path(&self.content, meta, |path| {
48 files.push(path)
48 files.push(path)
49 })?
49 })?
50 } else {
50 } else {
51 let _parents = parse_dirstate_entries(
51 let _parents = parse_dirstate_entries(
52 &self.content,
52 &self.content,
53 |path, entry, _copy_source| {
53 |path, entry, _copy_source| {
54 if entry.state().is_tracked() {
54 if entry.state().is_tracked() {
55 files.push(path)
55 files.push(path)
56 }
56 }
57 Ok(())
57 Ok(())
58 },
58 },
59 )?;
59 )?;
60 }
60 }
61 }
61 }
62 files.par_sort_unstable();
62 files.par_sort_unstable();
63 Ok(files)
63 Ok(files)
64 }
64 }
65 }
65 }
66
66
67 /// List files under Mercurial control at a given revision.
67 /// List files under Mercurial control at a given revision.
68 pub fn list_rev_tracked_files(
68 pub fn list_rev_tracked_files(
69 repo: &Repo,
69 repo: &Repo,
70 revset: &str,
70 revset: &str,
71 ) -> Result<FilesForRev, RevlogError> {
71 ) -> Result<FilesForRev, RevlogError> {
72 let rev = crate::revset::resolve_single(revset, repo)?;
72 let rev = crate::revset::resolve_single(revset, repo)?;
73 Ok(FilesForRev(repo.manifest_for_rev(rev)?))
73 Ok(FilesForRev(repo.manifest_for_rev(rev)?))
74 }
74 }
75
75
76 pub struct FilesForRev(Manifest);
76 pub struct FilesForRev(Manifest);
77
77
78 impl FilesForRev {
78 impl FilesForRev {
79 pub fn iter(&self) -> impl Iterator<Item = &HgPath> {
79 pub fn iter(&self) -> impl Iterator<Item = Result<&HgPath, HgError>> {
80 self.0.files()
80 self.0.files()
81 }
81 }
82 }
82 }
@@ -1,101 +1,104 b''
1 use crate::errors::HgError;
1 use crate::errors::HgError;
2 use crate::repo::Repo;
2 use crate::repo::Repo;
3 use crate::revlog::revlog::{Revlog, RevlogError};
3 use crate::revlog::revlog::{Revlog, RevlogError};
4 use crate::revlog::Revision;
4 use crate::revlog::Revision;
5 use crate::revlog::{Node, NodePrefix};
5 use crate::revlog::{Node, NodePrefix};
6 use crate::utils::hg_path::HgPath;
6 use crate::utils::hg_path::HgPath;
7
7
8 /// A specialized `Revlog` to work with `manifest` data format.
8 /// A specialized `Revlog` to work with `manifest` data format.
9 pub struct Manifestlog {
9 pub struct Manifestlog {
10 /// The generic `revlog` format.
10 /// The generic `revlog` format.
11 revlog: Revlog,
11 revlog: Revlog,
12 }
12 }
13
13
14 impl Manifestlog {
14 impl Manifestlog {
15 /// Open the `manifest` of a repository given by its root.
15 /// Open the `manifest` of a repository given by its root.
16 pub fn open(repo: &Repo) -> Result<Self, HgError> {
16 pub fn open(repo: &Repo) -> Result<Self, HgError> {
17 let revlog = Revlog::open(repo, "00manifest.i", None)?;
17 let revlog = Revlog::open(repo, "00manifest.i", None)?;
18 Ok(Self { revlog })
18 Ok(Self { revlog })
19 }
19 }
20
20
21 /// Return the `Manifest` for the given node ID.
21 /// Return the `Manifest` for the given node ID.
22 ///
22 ///
23 /// Note: this is a node ID in the manifestlog, typically found through
23 /// Note: this is a node ID in the manifestlog, typically found through
24 /// `ChangelogEntry::manifest_node`. It is *not* the node ID of any
24 /// `ChangelogEntry::manifest_node`. It is *not* the node ID of any
25 /// changeset.
25 /// changeset.
26 ///
26 ///
27 /// See also `Repo::manifest_for_node`
27 /// See also `Repo::manifest_for_node`
28 pub fn data_for_node(
28 pub fn data_for_node(
29 &self,
29 &self,
30 node: NodePrefix,
30 node: NodePrefix,
31 ) -> Result<Manifest, RevlogError> {
31 ) -> Result<Manifest, RevlogError> {
32 let rev = self.revlog.rev_from_node(node)?;
32 let rev = self.revlog.rev_from_node(node)?;
33 self.data_for_rev(rev)
33 self.data_for_rev(rev)
34 }
34 }
35
35
36 /// Return the `Manifest` of a given revision number.
36 /// Return the `Manifest` of a given revision number.
37 ///
37 ///
38 /// Note: this is a revision number in the manifestlog, *not* of any
38 /// Note: this is a revision number in the manifestlog, *not* of any
39 /// changeset.
39 /// changeset.
40 ///
40 ///
41 /// See also `Repo::manifest_for_rev`
41 /// See also `Repo::manifest_for_rev`
42 pub fn data_for_rev(
42 pub fn data_for_rev(
43 &self,
43 &self,
44 rev: Revision,
44 rev: Revision,
45 ) -> Result<Manifest, RevlogError> {
45 ) -> Result<Manifest, RevlogError> {
46 let bytes = self.revlog.get_rev_data(rev)?;
46 let bytes = self.revlog.get_rev_data(rev)?;
47 Ok(Manifest { bytes })
47 Ok(Manifest { bytes })
48 }
48 }
49 }
49 }
50
50
51 /// `Manifestlog` entry which knows how to interpret the `manifest` data bytes.
51 /// `Manifestlog` entry which knows how to interpret the `manifest` data bytes.
52 #[derive(Debug)]
52 #[derive(Debug)]
53 pub struct Manifest {
53 pub struct Manifest {
54 bytes: Vec<u8>,
54 bytes: Vec<u8>,
55 }
55 }
56
56
57 impl Manifest {
57 impl Manifest {
58 /// Return an iterator over the lines of the entry.
58 /// Return an iterator over the lines of the entry.
59 pub fn lines(&self) -> impl Iterator<Item = &[u8]> {
59 pub fn lines(&self) -> impl Iterator<Item = &[u8]> {
60 self.bytes
60 self.bytes
61 .split(|b| b == &b'\n')
61 .split(|b| b == &b'\n')
62 .filter(|line| !line.is_empty())
62 .filter(|line| !line.is_empty())
63 }
63 }
64
64
65 /// Return an iterator over the files of the entry.
65 /// Return an iterator over the files of the entry.
66 pub fn files(&self) -> impl Iterator<Item = &HgPath> {
66 pub fn files(&self) -> impl Iterator<Item = Result<&HgPath, HgError>> {
67 self.lines().filter(|line| !line.is_empty()).map(|line| {
67 self.lines().filter(|line| !line.is_empty()).map(|line| {
68 let pos = line
68 let pos =
69 .iter()
69 line.iter().position(|x| x == &b'\0').ok_or_else(|| {
70 .position(|x| x == &b'\0')
70 HgError::corrupted("manifest line should contain \\0")
71 .expect("manifest line should contain \\0");
71 })?;
72 HgPath::new(&line[..pos])
72 Ok(HgPath::new(&line[..pos]))
73 })
73 })
74 }
74 }
75
75
76 /// Return an iterator over the files of the entry.
76 /// Return an iterator over the files of the entry.
77 pub fn files_with_nodes(&self) -> impl Iterator<Item = (&HgPath, &[u8])> {
77 pub fn files_with_nodes(
78 &self,
79 ) -> impl Iterator<Item = Result<(&HgPath, &[u8]), HgError>> {
78 self.lines().filter(|line| !line.is_empty()).map(|line| {
80 self.lines().filter(|line| !line.is_empty()).map(|line| {
79 let pos = line
81 let pos =
80 .iter()
82 line.iter().position(|x| x == &b'\0').ok_or_else(|| {
81 .position(|x| x == &b'\0')
83 HgError::corrupted("manifest line should contain \\0")
82 .expect("manifest line should contain \\0");
84 })?;
83 let hash_start = pos + 1;
85 let hash_start = pos + 1;
84 let hash_end = hash_start + 40;
86 let hash_end = hash_start + 40;
85 (HgPath::new(&line[..pos]), &line[hash_start..hash_end])
87 Ok((HgPath::new(&line[..pos]), &line[hash_start..hash_end]))
86 })
88 })
87 }
89 }
88
90
89 /// If the given path is in this manifest, return its filelog node ID
91 /// If the given path is in this manifest, return its filelog node ID
90 pub fn find_file(&self, path: &HgPath) -> Result<Option<Node>, HgError> {
92 pub fn find_file(&self, path: &HgPath) -> Result<Option<Node>, HgError> {
91 // TODO: use binary search instead of linear scan. This may involve
93 // TODO: use binary search instead of linear scan. This may involve
92 // building (and caching) an index of the byte indicex of each manifest
94 // building (and caching) an index of the byte indicex of each manifest
93 // line.
95 // line.
94 for (manifest_path, node) in self.files_with_nodes() {
96 for entry in self.files_with_nodes() {
97 let (manifest_path, node) = entry?;
95 if manifest_path == path {
98 if manifest_path == path {
96 return Ok(Some(Node::from_hex_for_repo(node)?));
99 return Ok(Some(Node::from_hex_for_repo(node)?));
97 }
100 }
98 }
101 }
99 Ok(None)
102 Ok(None)
100 }
103 }
101 }
104 }
@@ -1,71 +1,72 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::operations::list_rev_tracked_files;
7 use hg::operations::list_rev_tracked_files;
7 use hg::operations::Dirstate;
8 use hg::operations::Dirstate;
8 use hg::repo::Repo;
9 use hg::repo::Repo;
9 use hg::utils::hg_path::HgPath;
10 use hg::utils::hg_path::HgPath;
10 use std::borrow::Cow;
11 use std::borrow::Cow;
11
12
12 pub const HELP_TEXT: &str = "
13 pub const HELP_TEXT: &str = "
13 List tracked files.
14 List tracked files.
14
15
15 Returns 0 on success.
16 Returns 0 on success.
16 ";
17 ";
17
18
18 pub fn args() -> clap::App<'static, 'static> {
19 pub fn args() -> clap::App<'static, 'static> {
19 clap::SubCommand::with_name("files")
20 clap::SubCommand::with_name("files")
20 .arg(
21 .arg(
21 Arg::with_name("rev")
22 Arg::with_name("rev")
22 .help("search the repository as it is in REV")
23 .help("search the repository as it is in REV")
23 .short("-r")
24 .short("-r")
24 .long("--revision")
25 .long("--revision")
25 .value_name("REV")
26 .value_name("REV")
26 .takes_value(true),
27 .takes_value(true),
27 )
28 )
28 .about(HELP_TEXT)
29 .about(HELP_TEXT)
29 }
30 }
30
31
31 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
32 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
32 let relative = invocation.config.get(b"ui", b"relative-paths");
33 let relative = invocation.config.get(b"ui", b"relative-paths");
33 if relative.is_some() {
34 if relative.is_some() {
34 return Err(CommandError::unsupported(
35 return Err(CommandError::unsupported(
35 "non-default ui.relative-paths",
36 "non-default ui.relative-paths",
36 ));
37 ));
37 }
38 }
38
39
39 let rev = invocation.subcommand_args.value_of("rev");
40 let rev = invocation.subcommand_args.value_of("rev");
40
41
41 let repo = invocation.repo?;
42 let repo = invocation.repo?;
42 if let Some(rev) = rev {
43 if let Some(rev) = rev {
43 let files = list_rev_tracked_files(repo, rev).map_err(|e| (e, rev))?;
44 let files = list_rev_tracked_files(repo, rev).map_err(|e| (e, rev))?;
44 display_files(invocation.ui, repo, files.iter())
45 display_files(invocation.ui, repo, files.iter())
45 } else {
46 } else {
46 let distate = Dirstate::new(repo)?;
47 let distate = Dirstate::new(repo)?;
47 let files = distate.tracked_files()?;
48 let files = distate.tracked_files()?;
48 display_files(invocation.ui, repo, files)
49 display_files(invocation.ui, repo, files.into_iter().map(Ok))
49 }
50 }
50 }
51 }
51
52
52 fn display_files<'a>(
53 fn display_files<'a>(
53 ui: &Ui,
54 ui: &Ui,
54 repo: &Repo,
55 repo: &Repo,
55 files: impl IntoIterator<Item = &'a HgPath>,
56 files: impl IntoIterator<Item = Result<&'a HgPath, HgError>>,
56 ) -> Result<(), CommandError> {
57 ) -> Result<(), CommandError> {
57 let mut stdout = ui.stdout_buffer();
58 let mut stdout = ui.stdout_buffer();
58 let mut any = false;
59 let mut any = false;
59
60
60 relativize_paths(repo, files, |path: Cow<[u8]>| -> Result<(), UiError> {
61 relativize_paths(repo, files, |path: Cow<[u8]>| -> Result<(), UiError> {
61 any = true;
62 any = true;
62 stdout.write_all(path.as_ref())?;
63 stdout.write_all(path.as_ref())?;
63 stdout.write_all(b"\n")
64 stdout.write_all(b"\n")
64 })?;
65 })?;
65 stdout.flush()?;
66 stdout.flush()?;
66 if any {
67 if any {
67 Ok(())
68 Ok(())
68 } else {
69 } else {
69 Err(CommandError::Unsuccessful)
70 Err(CommandError::Unsuccessful)
70 }
71 }
71 }
72 }
@@ -1,324 +1,324 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, UiError};
9 use crate::ui::{Ui, UiError};
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 hg;
12 use hg;
13 use hg::config::Config;
13 use hg::config::Config;
14 use hg::dirstate::TruncatedTimestamp;
14 use hg::dirstate::TruncatedTimestamp;
15 use hg::errors::HgError;
15 use hg::errors::HgError;
16 use hg::manifest::Manifest;
16 use hg::manifest::Manifest;
17 use hg::matchers::AlwaysMatcher;
17 use hg::matchers::AlwaysMatcher;
18 use hg::repo::Repo;
18 use hg::repo::Repo;
19 use hg::utils::hg_path::{hg_path_to_os_string, HgPath};
19 use hg::utils::hg_path::{hg_path_to_os_string, HgPath};
20 use hg::{HgPathCow, StatusOptions};
20 use hg::{HgPathCow, StatusOptions};
21 use log::{info, warn};
21 use log::{info, warn};
22 use std::borrow::Cow;
22 use std::borrow::Cow;
23
23
24 pub const HELP_TEXT: &str = "
24 pub const HELP_TEXT: &str = "
25 Show changed files in the working directory
25 Show changed files in the working directory
26
26
27 This is a pure Rust version of `hg status`.
27 This is a pure Rust version of `hg status`.
28
28
29 Some options might be missing, check the list below.
29 Some options might be missing, check the list below.
30 ";
30 ";
31
31
32 pub fn args() -> clap::App<'static, 'static> {
32 pub fn args() -> clap::App<'static, 'static> {
33 SubCommand::with_name("status")
33 SubCommand::with_name("status")
34 .alias("st")
34 .alias("st")
35 .about(HELP_TEXT)
35 .about(HELP_TEXT)
36 .arg(
36 .arg(
37 Arg::with_name("all")
37 Arg::with_name("all")
38 .help("show status of all files")
38 .help("show status of all files")
39 .short("-A")
39 .short("-A")
40 .long("--all"),
40 .long("--all"),
41 )
41 )
42 .arg(
42 .arg(
43 Arg::with_name("modified")
43 Arg::with_name("modified")
44 .help("show only modified files")
44 .help("show only modified files")
45 .short("-m")
45 .short("-m")
46 .long("--modified"),
46 .long("--modified"),
47 )
47 )
48 .arg(
48 .arg(
49 Arg::with_name("added")
49 Arg::with_name("added")
50 .help("show only added files")
50 .help("show only added files")
51 .short("-a")
51 .short("-a")
52 .long("--added"),
52 .long("--added"),
53 )
53 )
54 .arg(
54 .arg(
55 Arg::with_name("removed")
55 Arg::with_name("removed")
56 .help("show only removed files")
56 .help("show only removed files")
57 .short("-r")
57 .short("-r")
58 .long("--removed"),
58 .long("--removed"),
59 )
59 )
60 .arg(
60 .arg(
61 Arg::with_name("clean")
61 Arg::with_name("clean")
62 .help("show only clean files")
62 .help("show only clean files")
63 .short("-c")
63 .short("-c")
64 .long("--clean"),
64 .long("--clean"),
65 )
65 )
66 .arg(
66 .arg(
67 Arg::with_name("deleted")
67 Arg::with_name("deleted")
68 .help("show only deleted files")
68 .help("show only deleted files")
69 .short("-d")
69 .short("-d")
70 .long("--deleted"),
70 .long("--deleted"),
71 )
71 )
72 .arg(
72 .arg(
73 Arg::with_name("unknown")
73 Arg::with_name("unknown")
74 .help("show only unknown (not tracked) files")
74 .help("show only unknown (not tracked) files")
75 .short("-u")
75 .short("-u")
76 .long("--unknown"),
76 .long("--unknown"),
77 )
77 )
78 .arg(
78 .arg(
79 Arg::with_name("ignored")
79 Arg::with_name("ignored")
80 .help("show only ignored files")
80 .help("show only ignored files")
81 .short("-i")
81 .short("-i")
82 .long("--ignored"),
82 .long("--ignored"),
83 )
83 )
84 }
84 }
85
85
86 /// Pure data type allowing the caller to specify file states to display
86 /// Pure data type allowing the caller to specify file states to display
87 #[derive(Copy, Clone, Debug)]
87 #[derive(Copy, Clone, Debug)]
88 pub struct DisplayStates {
88 pub struct DisplayStates {
89 pub modified: bool,
89 pub modified: bool,
90 pub added: bool,
90 pub added: bool,
91 pub removed: bool,
91 pub removed: bool,
92 pub clean: bool,
92 pub clean: bool,
93 pub deleted: bool,
93 pub deleted: bool,
94 pub unknown: bool,
94 pub unknown: bool,
95 pub ignored: bool,
95 pub ignored: bool,
96 }
96 }
97
97
98 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
98 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
99 modified: true,
99 modified: true,
100 added: true,
100 added: true,
101 removed: true,
101 removed: true,
102 clean: false,
102 clean: false,
103 deleted: true,
103 deleted: true,
104 unknown: true,
104 unknown: true,
105 ignored: false,
105 ignored: false,
106 };
106 };
107
107
108 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
108 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
109 modified: true,
109 modified: true,
110 added: true,
110 added: true,
111 removed: true,
111 removed: true,
112 clean: true,
112 clean: true,
113 deleted: true,
113 deleted: true,
114 unknown: true,
114 unknown: true,
115 ignored: true,
115 ignored: true,
116 };
116 };
117
117
118 impl DisplayStates {
118 impl DisplayStates {
119 pub fn is_empty(&self) -> bool {
119 pub fn is_empty(&self) -> bool {
120 !(self.modified
120 !(self.modified
121 || self.added
121 || self.added
122 || self.removed
122 || self.removed
123 || self.clean
123 || self.clean
124 || self.deleted
124 || self.deleted
125 || self.unknown
125 || self.unknown
126 || self.ignored)
126 || self.ignored)
127 }
127 }
128 }
128 }
129
129
130 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
130 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
131 let status_enabled_default = false;
131 let status_enabled_default = false;
132 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
132 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
133 if !status_enabled.unwrap_or(status_enabled_default) {
133 if !status_enabled.unwrap_or(status_enabled_default) {
134 return Err(CommandError::unsupported(
134 return Err(CommandError::unsupported(
135 "status is experimental in rhg (enable it with 'rhg.status = true' \
135 "status is experimental in rhg (enable it with 'rhg.status = true' \
136 or enable fallback with 'rhg.on-unsupported = fallback')"
136 or enable fallback with 'rhg.on-unsupported = fallback')"
137 ));
137 ));
138 }
138 }
139
139
140 // TODO: lift these limitations
140 // TODO: lift these limitations
141 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
141 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
142 return Err(CommandError::unsupported(
142 return Err(CommandError::unsupported(
143 "ui.tweakdefaults is not yet supported with rhg status",
143 "ui.tweakdefaults is not yet supported with rhg status",
144 ));
144 ));
145 }
145 }
146 if invocation.config.get_bool(b"ui", b"statuscopies")? {
146 if invocation.config.get_bool(b"ui", b"statuscopies")? {
147 return Err(CommandError::unsupported(
147 return Err(CommandError::unsupported(
148 "ui.statuscopies is not yet supported with rhg status",
148 "ui.statuscopies is not yet supported with rhg status",
149 ));
149 ));
150 }
150 }
151 if invocation
151 if invocation
152 .config
152 .config
153 .get(b"commands", b"status.terse")
153 .get(b"commands", b"status.terse")
154 .is_some()
154 .is_some()
155 {
155 {
156 return Err(CommandError::unsupported(
156 return Err(CommandError::unsupported(
157 "status.terse is not yet supported with rhg status",
157 "status.terse is not yet supported with rhg status",
158 ));
158 ));
159 }
159 }
160
160
161 let ui = invocation.ui;
161 let ui = invocation.ui;
162 let config = invocation.config;
162 let config = invocation.config;
163 let args = invocation.subcommand_args;
163 let args = invocation.subcommand_args;
164 let display_states = if args.is_present("all") {
164 let display_states = if args.is_present("all") {
165 // TODO when implementing `--quiet`: it excludes clean files
165 // TODO when implementing `--quiet`: it excludes clean files
166 // from `--all`
166 // from `--all`
167 ALL_DISPLAY_STATES
167 ALL_DISPLAY_STATES
168 } else {
168 } else {
169 let requested = DisplayStates {
169 let requested = DisplayStates {
170 modified: args.is_present("modified"),
170 modified: args.is_present("modified"),
171 added: args.is_present("added"),
171 added: args.is_present("added"),
172 removed: args.is_present("removed"),
172 removed: args.is_present("removed"),
173 clean: args.is_present("clean"),
173 clean: args.is_present("clean"),
174 deleted: args.is_present("deleted"),
174 deleted: args.is_present("deleted"),
175 unknown: args.is_present("unknown"),
175 unknown: args.is_present("unknown"),
176 ignored: args.is_present("ignored"),
176 ignored: args.is_present("ignored"),
177 };
177 };
178 if requested.is_empty() {
178 if requested.is_empty() {
179 DEFAULT_DISPLAY_STATES
179 DEFAULT_DISPLAY_STATES
180 } else {
180 } else {
181 requested
181 requested
182 }
182 }
183 };
183 };
184
184
185 let repo = invocation.repo?;
185 let repo = invocation.repo?;
186 let mut dmap = repo.dirstate_map_mut()?;
186 let mut dmap = repo.dirstate_map_mut()?;
187
187
188 let options = StatusOptions {
188 let options = StatusOptions {
189 // TODO should be provided by the dirstate parsing and
189 // TODO should be provided by the dirstate parsing and
190 // hence be stored on dmap. Using a value that assumes we aren't
190 // hence be stored on dmap. Using a value that assumes we aren't
191 // below the time resolution granularity of the FS and the
191 // below the time resolution granularity of the FS and the
192 // dirstate.
192 // dirstate.
193 last_normal_time: TruncatedTimestamp::new_truncate(0, 0),
193 last_normal_time: TruncatedTimestamp::new_truncate(0, 0),
194 // we're currently supporting file systems with exec flags only
194 // we're currently supporting file systems with exec flags only
195 // anyway
195 // anyway
196 check_exec: true,
196 check_exec: true,
197 list_clean: display_states.clean,
197 list_clean: display_states.clean,
198 list_unknown: display_states.unknown,
198 list_unknown: display_states.unknown,
199 list_ignored: display_states.ignored,
199 list_ignored: display_states.ignored,
200 collect_traversed_dirs: false,
200 collect_traversed_dirs: false,
201 };
201 };
202 let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
202 let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
203 let (mut ds_status, pattern_warnings) = dmap.status(
203 let (mut ds_status, pattern_warnings) = dmap.status(
204 &AlwaysMatcher,
204 &AlwaysMatcher,
205 repo.working_directory_path().to_owned(),
205 repo.working_directory_path().to_owned(),
206 vec![ignore_file],
206 vec![ignore_file],
207 options,
207 options,
208 )?;
208 )?;
209 if !pattern_warnings.is_empty() {
209 if !pattern_warnings.is_empty() {
210 warn!("Pattern warnings: {:?}", &pattern_warnings);
210 warn!("Pattern warnings: {:?}", &pattern_warnings);
211 }
211 }
212
212
213 if !ds_status.bad.is_empty() {
213 if !ds_status.bad.is_empty() {
214 warn!("Bad matches {:?}", &(ds_status.bad))
214 warn!("Bad matches {:?}", &(ds_status.bad))
215 }
215 }
216 if !ds_status.unsure.is_empty() {
216 if !ds_status.unsure.is_empty() {
217 info!(
217 info!(
218 "Files to be rechecked by retrieval from filelog: {:?}",
218 "Files to be rechecked by retrieval from filelog: {:?}",
219 &ds_status.unsure
219 &ds_status.unsure
220 );
220 );
221 }
221 }
222 if !ds_status.unsure.is_empty()
222 if !ds_status.unsure.is_empty()
223 && (display_states.modified || display_states.clean)
223 && (display_states.modified || display_states.clean)
224 {
224 {
225 let p1 = repo.dirstate_parents()?.p1;
225 let p1 = repo.dirstate_parents()?.p1;
226 let manifest = repo.manifest_for_node(p1).map_err(|e| {
226 let manifest = repo.manifest_for_node(p1).map_err(|e| {
227 CommandError::from((e, &*format!("{:x}", p1.short())))
227 CommandError::from((e, &*format!("{:x}", p1.short())))
228 })?;
228 })?;
229 for to_check in ds_status.unsure {
229 for to_check in ds_status.unsure {
230 if cat_file_is_modified(repo, &manifest, &to_check)? {
230 if cat_file_is_modified(repo, &manifest, &to_check)? {
231 if display_states.modified {
231 if display_states.modified {
232 ds_status.modified.push(to_check);
232 ds_status.modified.push(to_check);
233 }
233 }
234 } else {
234 } else {
235 if display_states.clean {
235 if display_states.clean {
236 ds_status.clean.push(to_check);
236 ds_status.clean.push(to_check);
237 }
237 }
238 }
238 }
239 }
239 }
240 }
240 }
241 if display_states.modified {
241 if display_states.modified {
242 display_status_paths(ui, repo, config, &mut ds_status.modified, b"M")?;
242 display_status_paths(ui, repo, config, &mut ds_status.modified, b"M")?;
243 }
243 }
244 if display_states.added {
244 if display_states.added {
245 display_status_paths(ui, repo, config, &mut ds_status.added, b"A")?;
245 display_status_paths(ui, repo, config, &mut ds_status.added, b"A")?;
246 }
246 }
247 if display_states.removed {
247 if display_states.removed {
248 display_status_paths(ui, repo, config, &mut ds_status.removed, b"R")?;
248 display_status_paths(ui, repo, config, &mut ds_status.removed, b"R")?;
249 }
249 }
250 if display_states.deleted {
250 if display_states.deleted {
251 display_status_paths(ui, repo, config, &mut ds_status.deleted, b"!")?;
251 display_status_paths(ui, repo, config, &mut ds_status.deleted, b"!")?;
252 }
252 }
253 if display_states.unknown {
253 if display_states.unknown {
254 display_status_paths(ui, repo, config, &mut ds_status.unknown, b"?")?;
254 display_status_paths(ui, repo, config, &mut ds_status.unknown, b"?")?;
255 }
255 }
256 if display_states.ignored {
256 if display_states.ignored {
257 display_status_paths(ui, repo, config, &mut ds_status.ignored, b"I")?;
257 display_status_paths(ui, repo, config, &mut ds_status.ignored, b"I")?;
258 }
258 }
259 if display_states.clean {
259 if display_states.clean {
260 display_status_paths(ui, repo, config, &mut ds_status.clean, b"C")?;
260 display_status_paths(ui, repo, config, &mut ds_status.clean, b"C")?;
261 }
261 }
262 Ok(())
262 Ok(())
263 }
263 }
264
264
265 // Probably more elegant to use a Deref or Borrow trait rather than
265 // Probably more elegant to use a Deref or Borrow trait rather than
266 // harcode HgPathBuf, but probably not really useful at this point
266 // harcode HgPathBuf, but probably not really useful at this point
267 fn display_status_paths(
267 fn display_status_paths(
268 ui: &Ui,
268 ui: &Ui,
269 repo: &Repo,
269 repo: &Repo,
270 config: &Config,
270 config: &Config,
271 paths: &mut [HgPathCow],
271 paths: &mut [HgPathCow],
272 status_prefix: &[u8],
272 status_prefix: &[u8],
273 ) -> Result<(), CommandError> {
273 ) -> Result<(), CommandError> {
274 paths.sort_unstable();
274 paths.sort_unstable();
275 let mut relative: bool = config.get_bool(b"ui", b"relative-paths")?;
275 let mut relative: bool = config.get_bool(b"ui", b"relative-paths")?;
276 relative = config
276 relative = config
277 .get_option(b"commands", b"status.relative")?
277 .get_option(b"commands", b"status.relative")?
278 .unwrap_or(relative);
278 .unwrap_or(relative);
279 if relative && !ui.plain() {
279 if relative && !ui.plain() {
280 relativize_paths(
280 relativize_paths(
281 repo,
281 repo,
282 paths,
282 paths.iter().map(Ok),
283 |path: Cow<[u8]>| -> Result<(), UiError> {
283 |path: Cow<[u8]>| -> Result<(), UiError> {
284 ui.write_stdout(
284 ui.write_stdout(
285 &[status_prefix, b" ", path.as_ref(), b"\n"].concat(),
285 &[status_prefix, b" ", path.as_ref(), b"\n"].concat(),
286 )
286 )
287 },
287 },
288 )?;
288 )?;
289 } else {
289 } else {
290 for path in paths {
290 for path in paths {
291 // Same TODO as in commands::root
291 // Same TODO as in commands::root
292 let bytes: &[u8] = path.as_bytes();
292 let bytes: &[u8] = path.as_bytes();
293 // TODO optim, probably lots of unneeded copies here, especially
293 // TODO optim, probably lots of unneeded copies here, especially
294 // if out stream is buffered
294 // if out stream is buffered
295 ui.write_stdout(&[status_prefix, b" ", bytes, b"\n"].concat())?;
295 ui.write_stdout(&[status_prefix, b" ", bytes, b"\n"].concat())?;
296 }
296 }
297 }
297 }
298 Ok(())
298 Ok(())
299 }
299 }
300
300
301 /// Check if a file is modified by comparing actual repo store and file system.
301 /// Check if a file is modified by comparing actual repo store and file system.
302 ///
302 ///
303 /// This meant to be used for those that the dirstate cannot resolve, due
303 /// This meant to be used for those that the dirstate cannot resolve, due
304 /// to time resolution limits.
304 /// to time resolution limits.
305 ///
305 ///
306 /// TODO: detect permission bits and similar metadata modifications
306 /// TODO: detect permission bits and similar metadata modifications
307 fn cat_file_is_modified(
307 fn cat_file_is_modified(
308 repo: &Repo,
308 repo: &Repo,
309 manifest: &Manifest,
309 manifest: &Manifest,
310 hg_path: &HgPath,
310 hg_path: &HgPath,
311 ) -> Result<bool, HgError> {
311 ) -> Result<bool, HgError> {
312 let file_node = manifest
312 let file_node = manifest
313 .find_file(hg_path)?
313 .find_file(hg_path)?
314 .expect("ambgious file not in p1");
314 .expect("ambgious file not in p1");
315 let filelog = repo.filelog(hg_path)?;
315 let filelog = repo.filelog(hg_path)?;
316 let filelog_entry = filelog.data_for_node(file_node).map_err(|_| {
316 let filelog_entry = filelog.data_for_node(file_node).map_err(|_| {
317 HgError::corrupted("filelog missing node from manifest")
317 HgError::corrupted("filelog missing node from manifest")
318 })?;
318 })?;
319 let contents_in_p1 = filelog_entry.data()?;
319 let contents_in_p1 = filelog_entry.data()?;
320
320
321 let fs_path = hg_path_to_os_string(hg_path).expect("HgPath conversion");
321 let fs_path = hg_path_to_os_string(hg_path).expect("HgPath conversion");
322 let fs_contents = repo.working_directory_vfs().read(fs_path)?;
322 let fs_contents = repo.working_directory_vfs().read(fs_path)?;
323 return Ok(contents_in_p1 != &*fs_contents);
323 return Ok(contents_in_p1 != &*fs_contents);
324 }
324 }
@@ -1,48 +1,49 b''
1 // path utils module
1 // path utils module
2 //
2 //
3 // This software may be used and distributed according to the terms of the
3 // This software may be used and distributed according to the terms of the
4 // GNU General Public License version 2 or any later version.
4 // GNU General Public License version 2 or any later version.
5
5
6 use crate::error::CommandError;
6 use crate::error::CommandError;
7 use crate::ui::UiError;
7 use crate::ui::UiError;
8 use hg::errors::HgError;
8 use hg::repo::Repo;
9 use hg::repo::Repo;
9 use hg::utils::current_dir;
10 use hg::utils::current_dir;
10 use hg::utils::files::{get_bytes_from_path, relativize_path};
11 use hg::utils::files::{get_bytes_from_path, relativize_path};
11 use hg::utils::hg_path::HgPath;
12 use hg::utils::hg_path::HgPath;
12 use hg::utils::hg_path::HgPathBuf;
13 use hg::utils::hg_path::HgPathBuf;
13 use std::borrow::Cow;
14 use std::borrow::Cow;
14
15
15 pub fn relativize_paths(
16 pub fn relativize_paths(
16 repo: &Repo,
17 repo: &Repo,
17 paths: impl IntoIterator<Item = impl AsRef<HgPath>>,
18 paths: impl IntoIterator<Item = Result<impl AsRef<HgPath>, HgError>>,
18 mut callback: impl FnMut(Cow<[u8]>) -> Result<(), UiError>,
19 mut callback: impl FnMut(Cow<[u8]>) -> Result<(), UiError>,
19 ) -> Result<(), CommandError> {
20 ) -> Result<(), CommandError> {
20 let cwd = current_dir()?;
21 let cwd = current_dir()?;
21 let repo_root = repo.working_directory_path();
22 let repo_root = repo.working_directory_path();
22 let repo_root = cwd.join(repo_root); // Make it absolute
23 let repo_root = cwd.join(repo_root); // Make it absolute
23 let repo_root_hgpath =
24 let repo_root_hgpath =
24 HgPathBuf::from(get_bytes_from_path(repo_root.to_owned()));
25 HgPathBuf::from(get_bytes_from_path(repo_root.to_owned()));
25 let outside_repo: bool;
26 let outside_repo: bool;
26 let cwd_hgpath: HgPathBuf;
27 let cwd_hgpath: HgPathBuf;
27
28
28 if let Ok(cwd_relative_to_repo) = cwd.strip_prefix(&repo_root) {
29 if let Ok(cwd_relative_to_repo) = cwd.strip_prefix(&repo_root) {
29 // The current directory is inside the repo, so we can work with
30 // The current directory is inside the repo, so we can work with
30 // relative paths
31 // relative paths
31 outside_repo = false;
32 outside_repo = false;
32 cwd_hgpath =
33 cwd_hgpath =
33 HgPathBuf::from(get_bytes_from_path(cwd_relative_to_repo));
34 HgPathBuf::from(get_bytes_from_path(cwd_relative_to_repo));
34 } else {
35 } else {
35 outside_repo = true;
36 outside_repo = true;
36 cwd_hgpath = HgPathBuf::from(get_bytes_from_path(cwd));
37 cwd_hgpath = HgPathBuf::from(get_bytes_from_path(cwd));
37 }
38 }
38
39
39 for file in paths {
40 for file in paths {
40 if outside_repo {
41 if outside_repo {
41 let file = repo_root_hgpath.join(file.as_ref());
42 let file = repo_root_hgpath.join(file?.as_ref());
42 callback(relativize_path(&file, &cwd_hgpath))?;
43 callback(relativize_path(&file, &cwd_hgpath))?;
43 } else {
44 } else {
44 callback(relativize_path(file.as_ref(), &cwd_hgpath))?;
45 callback(relativize_path(file?.as_ref(), &cwd_hgpath))?;
45 }
46 }
46 }
47 }
47 Ok(())
48 Ok(())
48 }
49 }
General Comments 0
You need to be logged in to leave comments. Login now