Skip to content

Commit f8ccbbf

Browse files
Merge pull request #105 from unhappychoice/feature/github-repo-support
feat: comprehensive loading system improvements with GitHub repository support
2 parents 5881873 + 4cfedac commit f8ccbbf

23 files changed

+1917
-1216
lines changed

Cargo.lock

Lines changed: 415 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ once_cell = "1.19"
5858
rayon = "1.8"
5959
open = "5.0"
6060
urlencoding = "2.1"
61+
git2 = "0.18"
62+
dirs = "5.0"
6163

6264
[dev-dependencies]
6365
tempfile = "3.8"

src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ pub enum GitTypeError {
2828

2929
#[error("Walk directory error: {0}")]
3030
WalkDirError(#[from] walkdir::Error),
31+
32+
#[error("Repository clone error: {0}")]
33+
RepositoryCloneError(#[from] git2::Error),
34+
35+
#[error("Invalid repository format: {0}")]
36+
InvalidRepositoryFormat(String),
3137
}
3238

3339
pub type Result<T> = std::result::Result<T, GitTypeError>;

src/extractor/challenge_converter.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1-
use super::CodeChunk;
1+
use super::{CodeChunk, ProgressReporter};
22
use crate::game::Challenge;
33
use uuid::Uuid;
44

5+
struct NoOpProgressReporter;
6+
7+
impl ProgressReporter for NoOpProgressReporter {
8+
fn set_step(&self, _step_type: crate::game::models::loading_steps::StepType) {}
9+
fn set_progress(&self, _progress: f64) {}
10+
fn set_current_file(&self, _file: Option<String>) {}
11+
fn set_file_counts(&self, _processed: usize, _total: usize) {}
12+
}
13+
514
pub struct ChallengeConverter;
615

716
impl Default for ChallengeConverter {
@@ -27,9 +36,23 @@ impl ChallengeConverter {
2736
}
2837

2938
pub fn convert_chunks_to_challenges(&self, chunks: Vec<CodeChunk>) -> Vec<Challenge> {
39+
self.convert_chunks_to_challenges_with_progress(chunks, &NoOpProgressReporter)
40+
}
41+
42+
pub fn convert_chunks_to_challenges_with_progress(
43+
&self,
44+
chunks: Vec<CodeChunk>,
45+
progress: &dyn ProgressReporter,
46+
) -> Vec<Challenge> {
3047
let mut all_challenges = Vec::new();
48+
let total_chunks = chunks.len();
49+
50+
for (chunk_index, chunk) in chunks.iter().enumerate() {
51+
// Update progress
52+
let chunk_progress = chunk_index as f64 / total_chunks as f64;
53+
progress.set_progress(chunk_progress);
54+
progress.set_file_counts(chunk_index, total_chunks);
3155

32-
for chunk in &chunks {
3356
// Generate challenges for Easy (~100), Normal (~200), Hard (~500), Wild (full chunks) only
3457
let difficulties = [
3558
super::super::game::stage_builder::DifficultyLevel::Easy,
@@ -44,6 +67,10 @@ impl ChallengeConverter {
4467
}
4568
}
4669

70+
// Final progress update
71+
progress.set_progress(1.0);
72+
progress.set_file_counts(total_chunks, total_chunks);
73+
4774
// Zen challenges are now handled separately in main.rs
4875
all_challenges
4976
}

src/extractor/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ pub mod git_info;
44
pub mod models;
55
pub mod parser;
66
pub mod parsers;
7-
pub mod progress;
87
pub mod repository_loader;
98

9+
pub use crate::game::screens::loading_screen::{NoOpProgressReporter, ProgressReporter};
1010
pub use challenge_converter::ChallengeConverter;
1111
pub use git_info::{GitInfoExtractor, GitRepositoryInfo};
1212
pub use models::{ChunkType, CodeChunk, ExtractionOptions, Language};
1313
pub use parser::CodeExtractor;
14-
pub use progress::{ConsoleProgressReporter, NoOpProgressReporter, ProgressReporter};
1514
pub use repository_loader::RepositoryLoader;

src/extractor/parser.rs

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,53 @@ impl CodeExtractor {
2121
self.extract_chunks_with_progress(repo_path, _options, &NoOpProgressReporter)
2222
}
2323

24-
pub fn extract_chunks_with_progress<P: ProgressReporter + ?Sized>(
24+
pub fn extract_chunks_with_progress<P: ProgressReporter>(
2525
&mut self,
2626
repo_path: &Path,
2727
_options: ExtractionOptions,
2828
progress: &P,
2929
) -> Result<Vec<CodeChunk>> {
30-
progress.set_phase("Scanning repository".to_string());
30+
progress.set_step(crate::game::models::loading_steps::StepType::Scanning);
3131

32-
// Use ignore crate to respect .gitignore files
32+
// First pass: count total files to process
33+
let walker_count = WalkBuilder::new(repo_path)
34+
.hidden(false) // Include hidden files
35+
.git_ignore(true) // Respect .gitignore
36+
.git_global(true) // Respect global gitignore
37+
.git_exclude(true) // Respect .git/info/exclude
38+
.build();
39+
40+
let mut total_files_to_process = 0;
41+
42+
for entry in walker_count {
43+
let entry =
44+
entry.map_err(|e| GitTypeError::ExtractionFailed(format!("Walk error: {}", e)))?;
45+
let path = entry.path();
46+
47+
if !path.is_file() {
48+
continue;
49+
}
50+
51+
if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
52+
if let Some(_language) = Language::from_extension(extension) {
53+
if Self::should_process_file_static(path, &_options) {
54+
total_files_to_process += 1;
55+
}
56+
}
57+
}
58+
}
59+
60+
// Second pass: actually collect files with proper progress
3361
let walker = WalkBuilder::new(repo_path)
3462
.hidden(false) // Include hidden files
3563
.git_ignore(true) // Respect .gitignore
3664
.git_global(true) // Respect global gitignore
3765
.git_exclude(true) // Respect .git/info/exclude
3866
.build();
3967

40-
// Collect all files to process first to get total count
4168
let mut files_to_process = Vec::new();
69+
let mut processed_count = 0;
70+
4271
for entry in walker {
4372
let entry =
4473
entry.map_err(|e| GitTypeError::ExtractionFailed(format!("Walk error: {}", e)))?;
@@ -52,13 +81,22 @@ impl CodeExtractor {
5281
if let Some(language) = Language::from_extension(extension) {
5382
if Self::should_process_file_static(path, &_options) {
5483
files_to_process.push((path.to_path_buf(), language));
84+
processed_count += 1;
85+
86+
// Update progress with known total
87+
if processed_count % 10 == 0 || processed_count == total_files_to_process {
88+
progress.set_file_counts(processed_count, total_files_to_process);
89+
}
5590
}
5691
}
5792
}
5893
}
5994

6095
let total_files = files_to_process.len();
61-
progress.set_phase("Parsing AST".to_string());
96+
progress.set_file_counts(total_files, total_files);
97+
progress.set_progress(1.0); // Scanning completed
98+
99+
progress.set_step(crate::game::models::loading_steps::StepType::Extracting);
62100

63101
// Process files in parallel with better progress tracking
64102
// Split files into smaller chunks for better progress visibility
@@ -68,7 +106,7 @@ impl CodeExtractor {
68106

69107
for chunk in files_to_process.chunks(chunk_size) {
70108
// Process this chunk in parallel
71-
let chunk_results: Result<Vec<Vec<CodeChunk>>> = chunk
109+
let chunk_results: Vec<Result<Vec<CodeChunk>>> = chunk
72110
.par_iter()
73111
.map(|(path, language)| Self::extract_from_file_static(path, *language, &_options))
74112
.collect();
@@ -77,19 +115,29 @@ impl CodeExtractor {
77115
processed_files += chunk.len();
78116
progress.set_file_counts(processed_files, total_files);
79117

80-
// Update spinner for each chunk to show progress
81-
progress.update_spinner();
118+
// Progress updates are now cheap - LoadingScreen controls rendering
82119

83-
// Collect results
84-
let chunk_results = chunk_results?;
85-
for file_chunks in chunk_results {
86-
all_chunks.extend(file_chunks);
120+
// Collect results, skip failed files but continue processing
121+
for (i, result) in chunk_results.into_iter().enumerate() {
122+
match result {
123+
Ok(file_chunks) => {
124+
all_chunks.extend(file_chunks);
125+
}
126+
Err(e) => {
127+
let file_path = &chunk[i].0;
128+
eprintln!(
129+
"Warning: Failed to extract from file {:?}: {}",
130+
file_path, e
131+
);
132+
// Continue processing other files instead of crashing
133+
}
134+
}
87135
}
88136
}
89137

90138
progress.set_file_counts(total_files, total_files);
91139
progress.set_current_file(None);
92-
progress.set_phase("Finalizing".to_string());
140+
progress.set_step(crate::game::models::loading_steps::StepType::Finalizing);
93141

94142
Ok(all_chunks)
95143
}

src/extractor/progress.rs

Lines changed: 0 additions & 117 deletions
This file was deleted.

src/extractor/repository_loader.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl RepositoryLoader {
3636
)
3737
}
3838

39-
pub fn load_challenges_from_repository_with_progress<P: ProgressReporter + ?Sized>(
39+
pub fn load_challenges_from_repository_with_progress<P: ProgressReporter>(
4040
&mut self,
4141
repo_path: &Path,
4242
options: Option<ExtractionOptions>,
@@ -58,15 +58,17 @@ impl RepositoryLoader {
5858
return Err(GitTypeError::NoSupportedFiles);
5959
}
6060

61-
progress.set_phase("Generating challenges".to_string());
61+
progress.set_step(crate::game::models::loading_steps::StepType::Generating);
6262
// Expand chunks into multiple challenges across difficulties
63-
let challenges = self.converter.convert_chunks_to_challenges(chunks);
63+
let challenges = self
64+
.converter
65+
.convert_chunks_to_challenges_with_progress(chunks, progress);
6466

65-
progress.set_phase("Finalizing".to_string());
67+
progress.set_step(crate::game::models::loading_steps::StepType::Finalizing);
6668
Ok(challenges)
6769
}
6870

69-
pub fn load_challenges_with_difficulty<P: ProgressReporter + ?Sized>(
71+
pub fn load_challenges_with_difficulty<P: ProgressReporter>(
7072
&mut self,
7173
repo_path: &Path,
7274
options: Option<ExtractionOptions>,
@@ -81,10 +83,10 @@ impl RepositoryLoader {
8183

8284
match difficulty {
8385
DifficultyLevel::Zen => {
84-
progress.set_phase("Loading whole files".to_string());
86+
progress.set_step(crate::game::models::loading_steps::StepType::Generating);
8587
let file_paths = self.collect_source_files(repo_path)?;
8688
let challenges = self.converter.convert_whole_files_to_challenges(file_paths);
87-
progress.set_phase("Finalizing".to_string());
89+
progress.set_step(crate::game::models::loading_steps::StepType::Finalizing);
8890
Ok(challenges)
8991
}
9092
_ => {
@@ -99,12 +101,12 @@ impl RepositoryLoader {
99101
return Err(GitTypeError::NoSupportedFiles);
100102
}
101103

102-
progress.set_phase("Generating challenges".to_string());
104+
progress.set_step(crate::game::models::loading_steps::StepType::Generating);
103105
let challenges = self
104106
.converter
105107
.convert_with_difficulty_split(chunks, difficulty);
106108

107-
progress.set_phase("Finalizing".to_string());
109+
progress.set_step(crate::game::models::loading_steps::StepType::Finalizing);
108110
Ok(challenges)
109111
}
110112
}

0 commit comments

Comments
 (0)