@anysphere/file-service 0.0.0-bff0125b → 0.0.0-c2b75b32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml CHANGED
@@ -20,6 +20,8 @@ prost = "0.11.9"
20
20
  tracing = "0.1.37"
21
21
  tracing-subscriber = "0.3.17"
22
22
  tracing-appender = "0.2.2"
23
+ binaryornot = "1.0.0"
24
+ dunce = "1.0.1"
23
25
 
24
26
  [build-dependencies]
25
27
  napi-build = "2.0.1"
package/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  /* auto-generated by NAPI-RS */
5
5
 
6
6
  export class MerkleClient {
7
- constructor(rootDirectory: string)
7
+ constructor(absoluteRootDirectory: string)
8
8
  init(): Promise<void>
9
9
  computeMerkleTree(): Promise<void>
10
10
  updateFile(filePath: string): Promise<void>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anysphere/file-service",
3
- "version": "0.0.0-bff0125b",
3
+ "version": "0.0.0-c2b75b32",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "napi": {
@@ -9,7 +9,8 @@
9
9
  "additional": [
10
10
  "aarch64-apple-darwin",
11
11
  "aarch64-pc-windows-msvc",
12
- "universal-apple-darwin"
12
+ "universal-apple-darwin",
13
+ "aarch64-unknown-linux-gnu"
13
14
  ]
14
15
  }
15
16
  },
@@ -35,11 +36,12 @@
35
36
  "version": "napi version"
36
37
  },
37
38
  "optionalDependencies": {
38
- "@anysphere/file-service-win32-x64-msvc": "0.0.0-bff0125b",
39
- "@anysphere/file-service-darwin-x64": "0.0.0-bff0125b",
40
- "@anysphere/file-service-linux-x64-gnu": "0.0.0-bff0125b",
41
- "@anysphere/file-service-darwin-arm64": "0.0.0-bff0125b",
42
- "@anysphere/file-service-win32-arm64-msvc": "0.0.0-bff0125b",
43
- "@anysphere/file-service-darwin-universal": "0.0.0-bff0125b"
39
+ "@anysphere/file-service-win32-x64-msvc": "0.0.0-c2b75b32",
40
+ "@anysphere/file-service-darwin-x64": "0.0.0-c2b75b32",
41
+ "@anysphere/file-service-linux-x64-gnu": "0.0.0-c2b75b32",
42
+ "@anysphere/file-service-darwin-arm64": "0.0.0-c2b75b32",
43
+ "@anysphere/file-service-win32-arm64-msvc": "0.0.0-c2b75b32",
44
+ "@anysphere/file-service-darwin-universal": "0.0.0-c2b75b32",
45
+ "@anysphere/file-service-linux-arm64-gnu": "0.0.0-c2b75b32"
44
46
  }
45
47
  }
package/src/file_utils.rs CHANGED
@@ -62,7 +62,7 @@ pub fn is_good_file(file_path: &Path) -> Result<(), Error> {
62
62
  Some(extension) => match extension.to_str() {
63
63
  Some(ext_str) => {
64
64
  if bad_extensions.contains(&ext_str) {
65
- return Err(anyhow::anyhow!("File is not a valid UTF-8 string"));
65
+ return Err(anyhow::anyhow!("Binary file excluded from indexing."));
66
66
  }
67
67
  }
68
68
  None => {
@@ -88,10 +88,12 @@ pub fn is_good_file(file_path: &Path) -> Result<(), Error> {
88
88
  Ok(())
89
89
  }
90
90
 
91
+ // use binaryornot::is_binary;
92
+ // use anyhow::Context;
91
93
  // implement the buffer above:
92
94
  pub async fn is_good_file_runtime_check(
93
95
  file_path: &Path,
94
- buffer: &[u8],
96
+ _buffer: &[u8],
95
97
  ) -> Result<(), Error> {
96
98
  match get_file_size(file_path).await {
97
99
  Ok(size) if size > 2 * 1024 * 1024 => {
@@ -101,13 +103,10 @@ pub async fn is_good_file_runtime_check(
101
103
  _ => {}
102
104
  }
103
105
 
104
- for &byte in buffer.iter().take(2048) {
105
- if byte.is_ascii() {
106
- continue;
107
- } else {
108
- return Err(anyhow::anyhow!("File is not a valid UTF-8 string"));
109
- }
110
- }
106
+ // if is_binary(file_path).context("Failed to check if file is binary")? {
107
+ // return Err(anyhow::anyhow!("File is binary"));
108
+ // }
109
+
111
110
  Ok(())
112
111
  }
113
112
 
package/src/git_utils.rs CHANGED
@@ -1,7 +1,7 @@
1
1
  use std::collections::HashSet;
2
2
  use std::process::Command;
3
3
 
4
- pub fn list_ignored_files(
4
+ pub fn list_ignored_files_and_directories(
5
5
  workspace_root_path: &str,
6
6
  should_return_absolute_paths: bool,
7
7
  ) -> Result<HashSet<String>, Box<dyn std::error::Error>> {
@@ -14,12 +14,14 @@ pub fn list_ignored_files(
14
14
  "--others",
15
15
  "--ignored",
16
16
  "--exclude-standard",
17
+ "--directory",
18
+ "--no-empty-directory"
17
19
  ],
18
20
  // FIXME(sualeh): this is super sketchy and might totally break in like a bazillion ways. i dont like it.
19
21
  vec![
20
22
  "sh",
21
23
  "-c",
22
- "git submodule foreach --quiet 'git ls-files --others --ignored --exclude-standard | sed \"s|^|$path/|\"'",
24
+ "git submodule foreach --quiet 'git ls-files --others --ignored --exclude-standard --directory --no-empty-directory | sed \"s|^|$path/|\"'",
23
25
  ],
24
26
  ];
25
27
 
@@ -133,7 +135,8 @@ mod tests {
133
135
  fn test_no_ignored_files() {
134
136
  let dir = tempfile::tempdir().unwrap();
135
137
  let gitignored_files =
136
- list_ignored_files(dir.path().to_str().unwrap(), false).unwrap();
138
+ list_ignored_files_and_directories(dir.path().to_str().unwrap(), false)
139
+ .unwrap();
137
140
  Command::new("git")
138
141
  .args(&["init"])
139
142
  .current_dir(dir.path())
@@ -160,7 +163,8 @@ mod tests {
160
163
  .output()
161
164
  .unwrap();
162
165
  let gitignored_files =
163
- list_ignored_files(dir.path().to_str().unwrap(), false).unwrap();
166
+ list_ignored_files_and_directories(dir.path().to_str().unwrap(), false)
167
+ .unwrap();
164
168
  println!(
165
169
  "ignored files for test_one_ignored_file: {:?}",
166
170
  gitignored_files
@@ -190,7 +194,8 @@ mod tests {
190
194
  .output()
191
195
  .unwrap();
192
196
  let gitignored_files =
193
- list_ignored_files(dir.path().to_str().unwrap(), false).unwrap();
197
+ list_ignored_files_and_directories(dir.path().to_str().unwrap(), false)
198
+ .unwrap();
194
199
  println!(
195
200
  "ignored files for test_multiple_ignored_files: {:?}",
196
201
  gitignored_files
@@ -254,7 +259,8 @@ mod tests {
254
259
  println!("git submodule add output: {:?}", o);
255
260
 
256
261
  let gitignored_files =
257
- list_ignored_files(dir.path().to_str().unwrap(), false).unwrap();
262
+ list_ignored_files_and_directories(dir.path().to_str().unwrap(), false)
263
+ .unwrap();
258
264
  println!(
259
265
  "ignored files for test_git_submodule_ignored_files: {:?}",
260
266
  gitignored_files
@@ -265,7 +271,8 @@ mod tests {
265
271
 
266
272
  #[test]
267
273
  fn test_multiple_ignored_files_in_current_dir() {
268
- let gitignored_files = list_ignored_files(".", false).unwrap();
274
+ let gitignored_files =
275
+ list_ignored_files_and_directories(".", false).unwrap();
269
276
  assert!(gitignored_files.len() > 1);
270
277
 
271
278
  // print a sample of the ignored files
package/src/lib.rs CHANGED
@@ -10,6 +10,7 @@ use merkle_tree::{LocalConstruction, MerkleTree};
10
10
  use tracing::{info, Level};
11
11
  use tracing_appender::rolling::{RollingFileAppender, Rotation};
12
12
  use tracing_subscriber::fmt;
13
+ use anyhow::Context;
13
14
 
14
15
  #[macro_use]
15
16
  extern crate napi_derive;
@@ -17,7 +18,7 @@ extern crate napi_derive;
17
18
  #[napi]
18
19
  pub struct MerkleClient {
19
20
  tree: MerkleTree,
20
- root_directory: String,
21
+ absolute_root_directory: String,
21
22
  _guard: tracing_appender::non_blocking::WorkerGuard,
22
23
  }
23
24
 
@@ -40,12 +41,22 @@ pub fn init_logger() -> tracing_appender::non_blocking::WorkerGuard {
40
41
  #[napi]
41
42
  impl MerkleClient {
42
43
  #[napi(constructor)]
43
- pub fn new(root_directory: String) -> MerkleClient {
44
+ pub fn new(absolute_root_directory: String) -> MerkleClient {
44
45
  let _guard = init_logger();
45
46
 
47
+ // let canonical_root_directory = std::path::Path::new(&absolute_root_directory);
48
+ // use dunce::canonicalize;
49
+ // let canonical_root_directory = match dunce::canonicalize(&canonical_root_directory) {
50
+ // Ok(path) => path.to_str().unwrap_or(&absolute_root_directory).to_string().to_lowercase(),
51
+ // Err(e) => {
52
+ // info!("Error in canonicalizing path: path: {:?}, error {:?}", canonical_root_directory, e);
53
+ // absolute_root_directory
54
+ // }
55
+ // };
56
+
46
57
  MerkleClient {
47
58
  tree: MerkleTree::empty_tree(),
48
- root_directory,
59
+ absolute_root_directory,
49
60
  _guard,
50
61
  }
51
62
  }
@@ -55,6 +66,7 @@ impl MerkleClient {
55
66
  // 1. compute the merkle tree
56
67
  // 2. update the backend
57
68
  // 3. sync with the remote
69
+ info!("Merkle tree compute started!");
58
70
  unsafe {
59
71
  self.compute_merkle_tree().await?;
60
72
  }
@@ -71,18 +83,7 @@ impl MerkleClient {
71
83
  &mut self,
72
84
  ) -> Result<(), napi::Error> {
73
85
  let t =
74
- MerkleTree::construct_merkle_tree(self.root_directory.clone()).await;
75
-
76
- let files = self.tree.get_all_files().await;
77
-
78
- match files {
79
- Ok(files) => {
80
- info!("files: {:?}", files);
81
- }
82
- Err(e) => {
83
- info!("Error in get_all_files: {:?}", e);
84
- }
85
- }
86
+ MerkleTree::construct_merkle_tree(self.absolute_root_directory.clone()).await;
86
87
 
87
88
  match t {
88
89
  Ok(tree) => {
@@ -111,19 +112,35 @@ impl MerkleClient {
111
112
  &self,
112
113
  relative_path: String,
113
114
  ) -> Result<String, napi::Error> {
114
- info!("relative_path: {:?}", relative_path);
115
- let absolute_path =
116
- std::path::Path::new(&self.root_directory).join(relative_path);
117
- let canonical_path = absolute_path.canonicalize().unwrap();
118
115
 
119
- info!("canonical_path: {:?}", canonical_path);
120
- let hash = self.tree.get_subtree_hash(canonical_path).await;
116
+ let relative_path_without_leading_slash = match relative_path.strip_prefix('.') {
117
+ Some(path) => path.strip_prefix(std::path::MAIN_SEPARATOR).unwrap_or(""),
118
+ None => relative_path.as_str(),
119
+ };
120
+
121
+ let absolute_path = if !relative_path_without_leading_slash.is_empty() {
122
+ std::path::Path::new(&self.absolute_root_directory).join(relative_path_without_leading_slash)
123
+ } else {
124
+ std::path::Path::new(&self.absolute_root_directory).to_path_buf()
125
+ };
126
+
127
+ let absolute_path_string = match absolute_path.to_str() {
128
+ Some(path) => path.to_string(),
129
+ None => {
130
+ return Err(napi::Error::new(
131
+ napi::Status::Unknown,
132
+ format!("some string error")
133
+ ))
134
+ }
135
+ };
136
+
137
+ let hash = self.tree.get_subtree_hash(absolute_path_string.as_str()).await;
121
138
 
122
139
  match hash {
123
140
  Ok(hash) => Ok(hash),
124
141
  Err(e) => Err(napi::Error::new(
125
142
  napi::Status::Unknown,
126
- format!("Error in get_subtree_hash: {:?}", e),
143
+ format!("Error in get_subtree_hash. \nRelative path: {:?}, \nAbsolute path: {:?}, \nRoot directory: {:?}\nError: {:?}", &relative_path, absolute_path, self.absolute_root_directory, e)
127
144
  )),
128
145
  }
129
146
  }
@@ -136,7 +153,7 @@ impl MerkleClient {
136
153
  Ok(num) => Ok(num),
137
154
  Err(e) => Err(napi::Error::new(
138
155
  napi::Status::Unknown,
139
- format!("Error in get_num_embeddable_files: {:?}", e),
156
+ format!("Error in get_num_embeddable_files: {:?}", e)
140
157
  )),
141
158
  }
142
159
  }
@@ -145,7 +162,7 @@ impl MerkleClient {
145
162
  &self,
146
163
  relative_path: String,
147
164
  ) -> Result<i32, napi::Error> {
148
- let absolute_path = std::path::Path::new(&self.root_directory)
165
+ let absolute_path = std::path::Path::new(&self.absolute_root_directory)
149
166
  .join(relative_path)
150
167
  .canonicalize()?;
151
168
 
@@ -158,7 +175,7 @@ impl MerkleClient {
158
175
  Ok(num) => Ok(num),
159
176
  Err(e) => Err(napi::Error::new(
160
177
  napi::Status::Unknown,
161
- format!("Error in get_num_embeddable_files_in_subtree: {:?}", e),
178
+ format!("Error in get_num_embeddable_files_in_subtree: {:?}", e)
162
179
  )),
163
180
  }
164
181
  }
@@ -171,7 +188,7 @@ impl MerkleClient {
171
188
  Ok(files) => Ok(files),
172
189
  Err(e) => Err(napi::Error::new(
173
190
  napi::Status::Unknown,
174
- format!("Error in get_all_files: {:?}", e),
191
+ format!("Error in get_all_files: {:?}", e)
175
192
  )),
176
193
  }
177
194
  }
@@ -181,10 +198,12 @@ impl MerkleClient {
181
198
  &self,
182
199
  absolute_file_path: String,
183
200
  ) -> Result<Vec<String>, napi::Error> {
184
- let absolute_path_str = absolute_file_path.as_str();
201
+ // let absolute_path = absolute_file_path.to_lowercase();
202
+ // let absolute_path_str = absolute_path.as_str();
203
+
185
204
  let files = self
186
205
  .tree
187
- .get_all_dir_files_to_embed(absolute_path_str)
206
+ .get_all_dir_files_to_embed(absolute_file_path.as_str())
188
207
  .await;
189
208
 
190
209
  match files {
@@ -209,11 +228,7 @@ impl MerkleClient {
209
228
  // TODO(sualeh): we should assert that the path is ascending up to the path.
210
229
 
211
230
  let ret = vec![file];
212
- info!("file: {:?}", ret);
213
-
214
231
  let ret = ret.into_iter().chain(path.into_iter()).collect::<Vec<_>>();
215
- info!("ret to js: {:?}", ret);
216
-
217
232
  Ok(ret)
218
233
  }
219
234
  Err(e) => Err(napi::Error::new(
@@ -229,8 +244,9 @@ impl MerkleClient {
229
244
  &self,
230
245
  absolute_file_path: String,
231
246
  ) -> Result<Vec<String>, napi::Error> {
232
- let absolute_path_str = absolute_file_path.as_str();
233
- let spline = self.tree.get_spline(absolute_path_str).await;
247
+ // let absolute_path = absolute_file_path.to_lowercase();
248
+ // let absolute_path_str = absolute_path.as_str();
249
+ let spline = self.tree.get_spline(absolute_file_path.as_str()).await;
234
250
 
235
251
  match spline {
236
252
  Ok(spline) => Ok(spline),
@@ -259,6 +275,6 @@ impl MerkleClient {
259
275
 
260
276
  #[napi]
261
277
  pub fn update_root_directory(&mut self, root_directory: String) {
262
- self.root_directory = root_directory;
278
+ self.absolute_root_directory = root_directory;
263
279
  }
264
280
  }
@@ -38,20 +38,19 @@ impl LocalConstruction for MerkleTree {
38
38
  }
39
39
 
40
40
  // 1. get all the gitignored files
41
- let git_ignored_files = match git_utils::list_ignored_files(
42
- absolute_path_to_root_directory.as_str(),
43
- true,
44
- ) {
45
- Ok(git_ignored) => git_ignored,
46
- Err(_e) => HashSet::new(),
47
- };
48
-
49
- tracing::info!("git_ignored_files: {:?}", git_ignored_files);
41
+ let git_ignored_files_and_dirs =
42
+ match git_utils::list_ignored_files_and_directories(
43
+ absolute_path_to_root_directory.as_str(),
44
+ true,
45
+ ) {
46
+ Ok(git_ignored) => git_ignored,
47
+ Err(_e) => HashSet::new(),
48
+ };
50
49
 
51
50
  let root_node = MerkleNode::new(
52
51
  path,
53
52
  None,
54
- &git_ignored_files,
53
+ &git_ignored_files_and_dirs,
55
54
  absolute_path_to_root_directory.as_str(),
56
55
  )
57
56
  .await;
@@ -60,7 +59,7 @@ impl LocalConstruction for MerkleTree {
60
59
  files: BTreeMap::new(),
61
60
  root_path: absolute_path_to_root_directory,
62
61
  cursor: None,
63
- git_ignored_files,
62
+ git_ignored_files_and_dirs: git_ignored_files_and_dirs,
64
63
  };
65
64
 
66
65
  // we now iterate over all the nodes and add them to the hashmap
@@ -73,6 +72,7 @@ impl LocalConstruction for MerkleTree {
73
72
  let node_reader = node.read().await;
74
73
  match &node_reader.node_type {
75
74
  NodeType::Branch(n) => {
75
+ tracing::info!("Branch: {:?}", n.0);
76
76
  let children = &n.1;
77
77
  files.insert(n.0.clone(), File { node: node.clone() });
78
78
  for child in children {
@@ -81,6 +81,13 @@ impl LocalConstruction for MerkleTree {
81
81
  }
82
82
  NodeType::File(file_name) => {
83
83
  let f = File { node: node.clone() };
84
+
85
+ // i dont reallly like this :(((
86
+ // let canonical_file_name = match dunce::canonicalize(file_name) {
87
+ // Ok(path) => path.to_str().unwrap_or(file_name).to_string(),
88
+ // Err(_) => file_name.clone(),
89
+ // };
90
+
84
91
  files.insert(file_name.clone(), f);
85
92
  }
86
93
  NodeType::ErrorNode(_) => {
@@ -92,6 +99,9 @@ impl LocalConstruction for MerkleTree {
92
99
 
93
100
  add_nodes_to_hashmap(&mt.root, &mut mt.files).await;
94
101
 
102
+ tracing::info!("Merkle tree compute finished!");
103
+ tracing::info!("Merkle tree: {}", mt);
104
+
95
105
  Ok(mt)
96
106
  }
97
107
 
@@ -4,6 +4,7 @@ use super::file_utils;
4
4
  use sha2::Digest;
5
5
  use std::collections::{BTreeMap, HashSet};
6
6
  use std::path::PathBuf;
7
+ use std::vec;
7
8
  use std::{fs, path::Path, sync::Arc};
8
9
  use tokio::sync::RwLock;
9
10
  use tonic::async_trait;
@@ -18,7 +19,7 @@ pub struct MerkleTree {
18
19
  root: MerkleNodePtr,
19
20
  files: BTreeMap<String, File>,
20
21
  cursor: Option<usize>,
21
- git_ignored_files: HashSet<String>,
22
+ git_ignored_files_and_dirs: HashSet<String>,
22
23
  }
23
24
 
24
25
  #[derive(Debug)]
@@ -95,33 +96,34 @@ impl MerkleTree {
95
96
  files: BTreeMap::new(),
96
97
  root_path: "".to_string(),
97
98
  cursor: None,
98
- git_ignored_files: HashSet::new(),
99
+ git_ignored_files_and_dirs: HashSet::new(),
99
100
  }
100
101
  }
101
102
 
102
103
  pub async fn get_subtree_hash(
103
104
  &self,
104
- absolute_path: PathBuf,
105
+ absolute_path: &str,
105
106
  ) -> Result<String, anyhow::Error> {
106
- let abs_string = match absolute_path.to_str() {
107
- Some(s) => s.to_string(),
108
- None => {
109
- return Err(anyhow::anyhow!(
110
- "get_subtree_hash: Failed to convert path to string"
111
- ))
112
- }
113
- };
114
-
115
- let node = match self.files.get(&abs_string) {
107
+ let node = match self.files.get(absolute_path) {
116
108
  Some(file) => file.node.clone(),
117
109
  None => {
118
- return Err(anyhow::anyhow!("Could not find file in tree!"));
110
+ let all_files: Vec<String> = self.files.keys().cloned().collect();
111
+ return Err(anyhow::anyhow!(
112
+ "Could not find file in tree! Looking for: {}. All files: {:?}",
113
+ absolute_path,
114
+ all_files
115
+ ));
119
116
  }
120
117
  };
121
118
 
122
119
  let node_reader = node.read().await;
123
120
  let node_hash = node_reader.hash.clone();
124
121
 
122
+ info!(
123
+ "get_subtree_hash for path: {}, node_hash: {}",
124
+ absolute_path, node_hash
125
+ );
126
+
125
127
  Ok(node_hash)
126
128
  }
127
129
 
@@ -397,7 +399,7 @@ impl MerkleTree {
397
399
  let new_node = MerkleNode::new(
398
400
  file_path.clone(),
399
401
  Some(ancestor.clone()),
400
- &self.git_ignored_files,
402
+ &self.git_ignored_files_and_dirs,
401
403
  &absolute_root_path.as_str(),
402
404
  )
403
405
  .await;
@@ -414,7 +416,7 @@ impl MerkleTree {
414
416
  let first_child = MerkleNode::new(
415
417
  first_child_path.clone(),
416
418
  Some(ancestor.clone()),
417
- &self.git_ignored_files,
419
+ &self.git_ignored_files_and_dirs,
418
420
  &absolute_root_path.as_str(),
419
421
  )
420
422
  .await;
@@ -790,22 +792,9 @@ impl MerkleNode {
790
792
  )));
791
793
  }
792
794
 
793
- // check if the directory is git ignored
794
- let is_git_ignored =
795
- match git_utils::is_git_ignored(absolute_root_path, path_str.as_str())
796
- .await
797
- {
798
- Ok(is_git_ignored) => is_git_ignored,
799
- Err(e) => {
800
- return Arc::new(RwLock::new(MerkleNode::empty_node(
801
- Some(absolute_file_or_directory),
802
- Some(e.to_string()),
803
- )));
804
- }
805
- };
795
+ let is_git_ignored_dir = ignored_files.contains(&path_str);
806
796
 
807
- if is_git_ignored && !bypass_git {
808
- // println!("skipping directory: {}", path_str);
797
+ if is_git_ignored_dir && !bypass_git {
809
798
  return Arc::new(RwLock::new(MerkleNode::empty_node(
810
799
  Some(absolute_file_or_directory),
811
800
  Some("Directory is git ignored!".to_string()),
@@ -979,15 +968,51 @@ impl MerkleNode {
979
968
 
980
969
  async fn compute_branch_hash(children: &[MerkleNodePtr]) -> String {
981
970
  let mut hasher = sha2::Sha256::new();
971
+ let mut names_and_hashes = vec![];
972
+ let mut non_zero_children = 0;
973
+
982
974
  for child in children {
983
975
  // check if it is an error node
984
976
  let child_reader = child.read().await;
985
- if let NodeType::ErrorNode(_) = &child_reader.node_type {
977
+
978
+ match &child_reader.node_type {
979
+ NodeType::File(file_name) => {
980
+ non_zero_children += 1;
981
+ names_and_hashes.push((file_name.clone(), child_reader.hash.clone()));
982
+ }
983
+ NodeType::Branch((file_name, _)) => {
984
+ let hash = child_reader.hash.clone();
985
+ if hash == "" {
986
+ continue;
987
+ }
988
+
989
+ non_zero_children += 1;
990
+ names_and_hashes.push((file_name.clone(), hash));
991
+ }
992
+ NodeType::ErrorNode(_) => {
993
+ continue;
994
+ }
995
+ }
996
+ }
997
+
998
+ // sort the list of names and hashes by the hashes!!
999
+ names_and_hashes
1000
+ .sort_by(|a, b| a.1.to_lowercase().cmp(&b.1.to_lowercase()));
1001
+
1002
+ for (name, hash) in names_and_hashes {
1003
+ if hash == "" {
986
1004
  continue;
987
1005
  }
1006
+ info!("name: {}, hash: {}", name, hash);
1007
+ hasher.update(hash);
1008
+ }
988
1009
 
989
- hasher.update(child_reader.hash.as_bytes());
1010
+ if non_zero_children == 0 {
1011
+ // this means that the branch is empty.
1012
+ // we should return an empty string.
1013
+ return "".to_string();
990
1014
  }
1015
+
991
1016
  let result = hasher.finalize();
992
1017
  format!("{:x}", result)
993
1018
  }