1use crate::config::YamlConfig;
2use crate::constants::{
3 DEFAULT_CHECK_LINES, REPORT_DATE_FORMAT, REPORT_SIMPLE_DATE_FORMAT, config_key, rmeta_action,
4 search_flag, section,
5};
6use crate::util::fuzzy;
7use crate::{error, info, usage};
8use chrono::{Local, NaiveDate};
9use colored::Colorize;
10use std::fs;
11use std::io::{Read, Seek, SeekFrom};
12use std::path::Path;
13use std::process::Command;
14
15const DATE_FORMAT: &str = REPORT_DATE_FORMAT;
16const SIMPLE_DATE_FORMAT: &str = REPORT_SIMPLE_DATE_FORMAT;
17
18pub fn handle_report(sub: &str, content: &[String], config: &mut YamlConfig) {
22 if content.is_empty() {
23 if sub == "reportctl" {
24 usage!(
25 "j reportctl new [date] | j reportctl sync [date] | j reportctl push | j reportctl pull | j reportctl set-url <url> | j reportctl open"
26 );
27 return;
28 }
29 handle_report_tui(config);
31 return;
32 }
33
34 let first = content[0].as_str();
35
36 if sub == "reportctl" {
38 match first {
39 f if f == rmeta_action::NEW => {
40 let date_str = content.get(1).map(|s| s.as_str());
41 handle_week_update(date_str, config);
42 }
43 f if f == rmeta_action::SYNC => {
44 let date_str = content.get(1).map(|s| s.as_str());
45 handle_sync(date_str, config);
46 }
47 f if f == rmeta_action::PUSH => {
48 let msg = content.get(1).map(|s| s.as_str());
49 handle_push(msg, config);
50 }
51 f if f == rmeta_action::PULL => {
52 handle_pull(config);
53 }
54 f if f == rmeta_action::SET_URL => {
55 let url = content.get(1).map(|s| s.as_str());
56 handle_set_url(url, config);
57 }
58 f if f == rmeta_action::OPEN => {
59 handle_open_report(config);
60 }
61 _ => {
62 error!(
63 "❌ 未知的元数据操作: {},可选: {}, {}, {}, {}, {}, {}",
64 first,
65 rmeta_action::NEW,
66 rmeta_action::SYNC,
67 rmeta_action::PUSH,
68 rmeta_action::PULL,
69 rmeta_action::SET_URL,
70 rmeta_action::OPEN
71 );
72 }
73 }
74 return;
75 }
76
77 let text = content.join(" ");
79 let text = text.trim().trim_matches('"').to_string();
80
81 if text.is_empty() {
82 error!("⚠️ 内容为空,无法写入");
83 return;
84 }
85
86 handle_daily_report(&text, config);
87}
88
89fn get_report_path(config: &YamlConfig) -> Option<String> {
91 let report_path = config.report_file_path();
92
93 if let Some(parent) = report_path.parent() {
95 let _ = fs::create_dir_all(parent);
96 }
97
98 if !report_path.exists() {
100 if let Err(e) = fs::write(&report_path, "") {
101 error!("❌ 创建日报文件失败: {}", e);
102 return None;
103 }
104 info!("📄 已自动创建日报文件: {:?}", report_path);
105 }
106
107 Some(report_path.to_string_lossy().to_string())
108}
109
110fn get_settings_json_path(report_path: &str) -> std::path::PathBuf {
112 Path::new(report_path)
113 .parent()
114 .unwrap()
115 .join("settings.json")
116}
117
118fn handle_report_tui(config: &mut YamlConfig) {
120 let report_path = match get_report_path(config) {
121 Some(p) => p,
122 None => return,
123 };
124
125 let config_path = get_settings_json_path(&report_path);
126 load_config_from_json_and_sync(&config_path, config);
127
128 let now = Local::now().date_naive();
130 let week_num = config
131 .get_property(section::REPORT, config_key::WEEK_NUM)
132 .and_then(|s| s.parse::<i32>().ok())
133 .unwrap_or(1);
134 let last_day_str = config
135 .get_property(section::REPORT, config_key::LAST_DAY)
136 .cloned()
137 .unwrap_or_default();
138 let last_day = parse_date(&last_day_str);
139
140 let context_lines = 3;
142 let report_file = Path::new(&report_path);
143 let last_lines = read_last_n_lines(report_file, context_lines);
144
145 let mut initial_lines: Vec<String> = last_lines.clone();
147
148 if let Some(last_day) = last_day {
150 if now > last_day {
151 let next_last_day = now + chrono::Duration::days(6);
152 let new_week_title = format!(
153 "# Week{}[{}-{}]",
154 week_num,
155 now.format(DATE_FORMAT),
156 next_last_day.format(DATE_FORMAT)
157 );
158 update_config_files(week_num + 1, &next_last_day, &config_path, config);
159 initial_lines.push(new_week_title);
161 }
162 }
163
164 let today_str = now.format(SIMPLE_DATE_FORMAT);
166 let date_prefix = format!("- 【{}】 ", today_str);
167 initial_lines.push(date_prefix);
168
169 match crate::tui::editor::open_multiline_editor_with_content("📝 编辑日报", &initial_lines)
171 {
172 Ok(Some(text)) => {
173 let original_context_count = last_lines.len();
176
177 replace_last_n_lines(report_file, original_context_count, &text);
179
180 info!("✅ 日报已写入:{}", report_path);
181 }
182 Ok(None) => {
183 info!("已取消编辑");
184 }
187 Err(e) => {
188 error!("❌ 编辑器启动失败: {}", e);
189 }
190 }
191}
192
193fn replace_last_n_lines(path: &Path, n: usize, new_content: &str) {
195 let content = match fs::read_to_string(path) {
196 Ok(c) => c,
197 Err(e) => {
198 error!("❌ 读取文件失败: {}", e);
199 return;
200 }
201 };
202
203 let all_lines: Vec<&str> = content.lines().collect();
204
205 let keep_count = if all_lines.len() > n {
207 all_lines.len() - n
208 } else {
209 0
210 };
211
212 let mut result = String::new();
213
214 for line in &all_lines[..keep_count] {
216 result.push_str(line);
217 result.push('\n');
218 }
219
220 result.push_str(new_content);
222
223 if !result.ends_with('\n') {
225 result.push('\n');
226 }
227
228 if let Err(e) = fs::write(path, &result) {
229 error!("❌ 写入文件失败: {}", e);
230 }
231}
232
233pub fn write_to_report(content: &str, config: &mut YamlConfig) -> bool {
237 let report_path = match get_report_path_silent(config) {
238 Some(p) => p,
239 None => return false,
240 };
241
242 let report_file = Path::new(&report_path);
243 let config_path = get_settings_json_path(&report_path);
244
245 load_config_from_json_silent(&config_path, config);
247
248 let now = Local::now().date_naive();
249
250 let week_num = config
251 .get_property(section::REPORT, config_key::WEEK_NUM)
252 .and_then(|s| s.parse::<i32>().ok())
253 .unwrap_or(1);
254
255 let last_day_str = config
256 .get_property(section::REPORT, config_key::LAST_DAY)
257 .cloned()
258 .unwrap_or_default();
259
260 let last_day = parse_date(&last_day_str);
261
262 match last_day {
263 Some(last_day) => {
264 if now > last_day {
265 let next_last_day = now + chrono::Duration::days(6);
266 let new_week_title = format!(
267 "# Week{}[{}-{}]\n",
268 week_num,
269 now.format(DATE_FORMAT),
270 next_last_day.format(DATE_FORMAT)
271 );
272 update_config_files_silent(week_num + 1, &next_last_day, &config_path, config);
273 append_to_file(report_file, &new_week_title);
274 }
275 }
276 None => {
277 return false;
278 }
279 }
280
281 let today_str = now.format(SIMPLE_DATE_FORMAT);
282 let log_entry = format!("- 【{}】 {}\n", today_str, content);
283 append_to_file(report_file, &log_entry);
284 true
285}
286
287fn get_report_path_silent(config: &YamlConfig) -> Option<String> {
289 let report_path = config.report_file_path();
290
291 if let Some(parent) = report_path.parent() {
292 let _ = fs::create_dir_all(parent);
293 }
294
295 if !report_path.exists() {
296 if fs::write(&report_path, "").is_err() {
297 return None;
298 }
299 }
300
301 Some(report_path.to_string_lossy().to_string())
302}
303
304fn update_config_files_silent(
306 week_num: i32,
307 last_day: &NaiveDate,
308 config_path: &Path,
309 config: &mut YamlConfig,
310) {
311 let last_day_str = last_day.format(DATE_FORMAT).to_string();
312
313 config.set_property(section::REPORT, config_key::WEEK_NUM, &week_num.to_string());
314 config.set_property(section::REPORT, config_key::LAST_DAY, &last_day_str);
315
316 if config_path.exists() {
317 let json = serde_json::json!({
318 "week_num": week_num,
319 "last_day": last_day_str
320 });
321 let _ = fs::write(config_path, json.to_string());
322 }
323}
324
325fn load_config_from_json_silent(config_path: &Path, config: &mut YamlConfig) {
327 if !config_path.exists() {
328 return;
329 }
330
331 if let Ok(content) = fs::read_to_string(config_path) {
332 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
333 let last_day = json.get("last_day").and_then(|v| v.as_str()).unwrap_or("");
334 let week_num = json.get("week_num").and_then(|v| v.as_i64()).unwrap_or(1);
335
336 if let Some(last_day_date) = parse_date(last_day) {
337 update_config_files_silent(week_num as i32, &last_day_date, config_path, config);
338 }
339 }
340 }
341}
342
343fn handle_daily_report(content: &str, config: &mut YamlConfig) {
345 let report_path = match get_report_path(config) {
346 Some(p) => p,
347 None => return,
348 };
349
350 info!("📂 日报文件路径:{}", report_path);
351
352 let report_file = Path::new(&report_path);
353 let config_path = get_settings_json_path(&report_path);
354
355 load_config_from_json_and_sync(&config_path, config);
356
357 let now = Local::now().date_naive();
358
359 let week_num = config
360 .get_property(section::REPORT, config_key::WEEK_NUM)
361 .and_then(|s| s.parse::<i32>().ok())
362 .unwrap_or(1);
363
364 let last_day_str = config
365 .get_property(section::REPORT, config_key::LAST_DAY)
366 .cloned()
367 .unwrap_or_default();
368
369 let last_day = parse_date(&last_day_str);
370
371 match last_day {
372 Some(last_day) => {
373 if now > last_day {
374 let next_last_day = now + chrono::Duration::days(6);
376 let new_week_title = format!(
377 "# Week{}[{}-{}]\n",
378 week_num,
379 now.format(DATE_FORMAT),
380 next_last_day.format(DATE_FORMAT)
381 );
382 update_config_files(week_num + 1, &next_last_day, &config_path, config);
383 append_to_file(report_file, &new_week_title);
384 }
385 }
386 None => {
387 error!("❌ 无法解析 last_day 日期: {}", last_day_str);
388 return;
389 }
390 }
391
392 let today_str = now.format(SIMPLE_DATE_FORMAT);
393 let log_entry = format!("- 【{}】 {}\n", today_str, content);
394 append_to_file(report_file, &log_entry);
395 info!("✅ 成功将内容写入:{}", report_path);
396}
397
398fn handle_week_update(date_str: Option<&str>, config: &mut YamlConfig) {
400 let report_path = match get_report_path(config) {
401 Some(p) => p,
402 None => return,
403 };
404
405 let config_path = get_settings_json_path(&report_path);
406
407 let week_num = config
408 .get_property(section::REPORT, config_key::WEEK_NUM)
409 .and_then(|s| s.parse::<i32>().ok())
410 .unwrap_or(1);
411
412 let last_day_str = date_str
413 .map(|s| s.to_string())
414 .or_else(|| {
415 config
416 .get_property(section::REPORT, config_key::LAST_DAY)
417 .cloned()
418 })
419 .unwrap_or_default();
420
421 match parse_date(&last_day_str) {
422 Some(last_day) => {
423 let next_last_day = last_day + chrono::Duration::days(7);
424 update_config_files(week_num + 1, &next_last_day, &config_path, config);
425 }
426 None => {
427 error!(
428 "❌ 更新周数失败,请检查日期字符串是否有误: {}",
429 last_day_str
430 );
431 }
432 }
433}
434
435fn handle_sync(date_str: Option<&str>, config: &mut YamlConfig) {
437 let report_path = match get_report_path(config) {
438 Some(p) => p,
439 None => return,
440 };
441
442 let config_path = get_settings_json_path(&report_path);
443
444 load_config_from_json_and_sync(&config_path, config);
445
446 let week_num = config
447 .get_property(section::REPORT, config_key::WEEK_NUM)
448 .and_then(|s| s.parse::<i32>().ok())
449 .unwrap_or(1);
450
451 let last_day_str = date_str
452 .map(|s| s.to_string())
453 .or_else(|| {
454 config
455 .get_property(section::REPORT, config_key::LAST_DAY)
456 .cloned()
457 })
458 .unwrap_or_default();
459
460 match parse_date(&last_day_str) {
461 Some(last_day) => {
462 update_config_files(week_num, &last_day, &config_path, config);
463 }
464 None => {
465 error!(
466 "❌ 更新周数失败,请检查日期字符串是否有误: {}",
467 last_day_str
468 );
469 }
470 }
471}
472
473fn update_config_files(
475 week_num: i32,
476 last_day: &NaiveDate,
477 config_path: &Path,
478 config: &mut YamlConfig,
479) {
480 let last_day_str = last_day.format(DATE_FORMAT).to_string();
481
482 config.set_property(section::REPORT, config_key::WEEK_NUM, &week_num.to_string());
484 config.set_property(section::REPORT, config_key::LAST_DAY, &last_day_str);
485 info!(
486 "✅ 更新YAML配置文件成功:周数 = {}, 周结束日期 = {}",
487 week_num, last_day_str
488 );
489
490 if config_path.exists() {
492 let json = serde_json::json!({
493 "week_num": week_num,
494 "last_day": last_day_str
495 });
496 match fs::write(config_path, json.to_string()) {
497 Ok(_) => info!(
498 "✅ 更新JSON配置文件成功:周数 = {}, 周结束日期 = {}",
499 week_num, last_day_str
500 ),
501 Err(e) => error!("❌ 更新JSON配置文件时出错: {}", e),
502 }
503 }
504}
505
506fn load_config_from_json_and_sync(config_path: &Path, config: &mut YamlConfig) {
508 if !config_path.exists() {
509 error!("❌ 日报配置文件不存在:{:?}", config_path);
510 return;
511 }
512
513 match fs::read_to_string(config_path) {
514 Ok(content) => {
515 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
516 let last_day = json.get("last_day").and_then(|v| v.as_str()).unwrap_or("");
517 let week_num = json.get("week_num").and_then(|v| v.as_i64()).unwrap_or(1);
518
519 info!(
520 "✅ 从日报配置文件中读取到:last_day = {}, week_num = {}",
521 last_day, week_num
522 );
523
524 if let Some(last_day_date) = parse_date(last_day) {
525 update_config_files(week_num as i32, &last_day_date, config_path, config);
526 }
527 } else {
528 error!("❌ 解析日报配置文件时出错");
529 }
530 }
531 Err(e) => error!("❌ 读取日报配置文件失败: {}", e),
532 }
533}
534
535fn parse_date(s: &str) -> Option<NaiveDate> {
536 NaiveDate::parse_from_str(s, DATE_FORMAT).ok()
537}
538
539fn append_to_file(path: &Path, content: &str) {
540 use std::fs::OpenOptions;
541 use std::io::Write;
542 match OpenOptions::new().create(true).append(true).open(path) {
543 Ok(mut f) => {
544 if let Err(e) = f.write_all(content.as_bytes()) {
545 error!("❌ 写入文件失败: {}", e);
546 }
547 }
548 Err(e) => error!("❌ 打开文件失败: {}", e),
549 }
550}
551
552fn handle_open_report(config: &YamlConfig) {
556 let report_path = match get_report_path(config) {
557 Some(p) => p,
558 None => return,
559 };
560
561 let path = Path::new(&report_path);
562 if !path.is_file() {
563 error!("❌ 日报文件不存在: {}", report_path);
564 return;
565 }
566
567 let content = match fs::read_to_string(path) {
569 Ok(c) => c,
570 Err(e) => {
571 error!("❌ 读取日报文件失败: {}", e);
572 return;
573 }
574 };
575
576 let lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
577
578 match crate::tui::editor::open_multiline_editor_with_content("📝 编辑日报文件", &lines)
580 {
581 Ok(Some(text)) => {
582 let mut result = text;
584 if !result.ends_with('\n') {
585 result.push('\n');
586 }
587 if let Err(e) = fs::write(path, &result) {
588 error!("❌ 写入日报文件失败: {}", e);
589 return;
590 }
591 info!("✅ 日报文件已保存:{}", report_path);
592 }
593 Ok(None) => {
594 info!("已取消编辑,文件未修改");
595 }
596 Err(e) => {
597 error!("❌ 编辑器启动失败: {}", e);
598 }
599 }
600}
601
602fn handle_set_url(url: Option<&str>, config: &mut YamlConfig) {
606 match url {
607 Some(u) if !u.is_empty() => {
608 let old = config
609 .get_property(section::REPORT, config_key::GIT_REPO)
610 .cloned();
611 config.set_property(section::REPORT, config_key::GIT_REPO, u);
612
613 if let Some(dir) = get_report_dir(config) {
615 let git_dir = Path::new(&dir).join(".git");
616 if git_dir.exists() {
617 sync_git_remote(config);
618 }
619 }
620
621 match old {
622 Some(old_url) if !old_url.is_empty() => {
623 info!("✅ git 仓库地址已更新: {} → {}", old_url, u);
624 }
625 _ => {
626 info!("✅ git 仓库地址已设置: {}", u);
627 }
628 }
629 }
630 _ => {
631 match config.get_property(section::REPORT, config_key::GIT_REPO) {
633 Some(url) if !url.is_empty() => {
634 info!("📦 当前 git 仓库地址: {}", url);
635 }
636 _ => {
637 info!("📦 尚未配置 git 仓库地址");
638 usage!("reportctl set-url <repo_url>");
639 }
640 }
641 }
642 }
643}
644
645fn get_report_dir(config: &YamlConfig) -> Option<String> {
649 let report_path = config.report_file_path();
650 report_path
651 .parent()
652 .map(|p| p.to_string_lossy().to_string())
653}
654
655fn run_git_in_report_dir(args: &[&str], config: &YamlConfig) -> Option<std::process::ExitStatus> {
657 let dir = match get_report_dir(config) {
658 Some(d) => d,
659 None => {
660 error!("❌ 无法确定日报目录");
661 return None;
662 }
663 };
664
665 let result = Command::new("git").args(args).current_dir(&dir).status();
666
667 match result {
668 Ok(status) => Some(status),
669 Err(e) => {
670 error!("💥 执行 git 命令失败: {}", e);
671 None
672 }
673 }
674}
675
676fn ensure_git_repo(config: &YamlConfig) -> bool {
678 let dir = match get_report_dir(config) {
679 Some(d) => d,
680 None => {
681 error!("❌ 无法确定日报目录");
682 return false;
683 }
684 };
685
686 let git_dir = Path::new(&dir).join(".git");
687 if git_dir.exists() {
688 sync_git_remote(config);
690 return true;
691 }
692
693 let git_repo = config.get_property(section::REPORT, config_key::GIT_REPO);
695 if git_repo.is_none() || git_repo.unwrap().is_empty() {
696 error!("❌ 尚未配置 git 仓库地址,请先执行: j reportctl set-url <repo_url>");
697 return false;
698 }
699 let repo_url = git_repo.unwrap().clone();
700
701 info!("📦 日报目录尚未初始化 git 仓库,正在初始化...");
702
703 if let Some(status) = run_git_in_report_dir(&["init", "-b", "main"], config) {
705 if !status.success() {
706 error!("❌ git init 失败");
707 return false;
708 }
709 } else {
710 return false;
711 }
712
713 if let Some(status) = run_git_in_report_dir(&["remote", "add", "origin", &repo_url], config) {
715 if !status.success() {
716 error!("❌ git remote add 失败");
717 return false;
718 }
719 } else {
720 return false;
721 }
722
723 info!("✅ git 仓库初始化完成,remote: {}", repo_url);
724 true
725}
726
727fn sync_git_remote(config: &YamlConfig) {
729 let git_repo = match config.get_property(section::REPORT, config_key::GIT_REPO) {
730 Some(url) if !url.is_empty() => url.clone(),
731 _ => return, };
733
734 let dir = match get_report_dir(config) {
736 Some(d) => d,
737 None => return,
738 };
739
740 let current_url = Command::new("git")
741 .args(["remote", "get-url", "origin"])
742 .current_dir(&dir)
743 .output();
744
745 match current_url {
746 Ok(output) if output.status.success() => {
747 let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
748 if url != git_repo {
749 let _ = run_git_in_report_dir(&["remote", "set-url", "origin", &git_repo], config);
751 info!("🔄 已同步 remote origin: {} → {}", url, git_repo);
752 }
753 }
754 _ => {
755 let _ = run_git_in_report_dir(&["remote", "add", "origin", &git_repo], config);
757 }
758 }
759}
760
761fn handle_push(commit_msg: Option<&str>, config: &YamlConfig) {
763 let git_repo = config.get_property(section::REPORT, config_key::GIT_REPO);
765 if git_repo.is_none() || git_repo.unwrap().is_empty() {
766 error!("❌ 尚未配置 git 仓库地址,请先执行: j reportctl set-url <repo_url>");
767 return;
768 }
769
770 if !ensure_git_repo(config) {
772 return;
773 }
774
775 let default_msg = format!("update report {}", Local::now().format("%Y-%m-%d %H:%M"));
776 let msg = commit_msg.unwrap_or(&default_msg);
777
778 info!("📤 正在推送周报到远程仓库...");
779
780 if let Some(status) = run_git_in_report_dir(&["add", "."], config) {
782 if !status.success() {
783 error!("❌ git add 失败");
784 return;
785 }
786 } else {
787 return;
788 }
789
790 if let Some(status) = run_git_in_report_dir(&["commit", "-m", msg], config) {
792 if !status.success() {
793 info!("ℹ️ git commit 返回非零退出码(可能没有新变更)");
795 }
796 } else {
797 return;
798 }
799
800 if let Some(status) = run_git_in_report_dir(&["push", "-u", "origin", "main"], config) {
802 if status.success() {
803 info!("✅ 周报已成功推送到远程仓库");
804 } else {
805 error!("❌ git push 失败,请检查网络连接和仓库权限");
806 }
807 }
808}
809
810fn handle_pull(config: &YamlConfig) {
812 let git_repo = config.get_property(section::REPORT, config_key::GIT_REPO);
814 if git_repo.is_none() || git_repo.unwrap().is_empty() {
815 error!("❌ 尚未配置 git 仓库地址,请先执行: j reportctl set-url <repo_url>");
816 return;
817 }
818
819 let dir = match get_report_dir(config) {
820 Some(d) => d,
821 None => {
822 error!("❌ 无法确定日报目录");
823 return;
824 }
825 };
826
827 let git_dir = Path::new(&dir).join(".git");
828
829 if !git_dir.exists() {
830 let repo_url = git_repo.unwrap().clone();
832 info!("📥 日报目录尚未初始化,正在从远程仓库克隆...");
833
834 let report_path = config.report_file_path();
836 let has_existing = report_path.exists()
837 && fs::metadata(&report_path)
838 .map(|m| m.len() > 0)
839 .unwrap_or(false);
840
841 if has_existing {
842 let backup_path = report_path.with_extension("md.bak");
844 if let Err(e) = fs::copy(&report_path, &backup_path) {
845 error!("⚠️ 备份现有日报文件失败: {}", e);
846 } else {
847 info!("📋 已备份现有日报到: {:?}", backup_path);
848 }
849 }
850
851 let temp_dir = Path::new(&dir).with_file_name(".report_clone_tmp");
854 let _ = fs::remove_dir_all(&temp_dir);
855
856 let result = Command::new("git")
857 .args([
858 "clone",
859 "-b",
860 "main",
861 &repo_url,
862 &temp_dir.to_string_lossy(),
863 ])
864 .status();
865
866 match result {
867 Ok(status) if status.success() => {
868 let _ = fs::remove_dir_all(&dir);
870 if let Err(e) = fs::rename(&temp_dir, &dir) {
871 error!("❌ 移动克隆仓库失败: {},临时目录: {:?}", e, temp_dir);
872 return;
873 }
874 info!("✅ 成功从远程仓库克隆周报");
875 }
876 Ok(_) => {
877 error!("❌ git clone 失败,请检查仓库地址和网络连接");
878 let _ = fs::remove_dir_all(&temp_dir);
879 }
880 Err(e) => {
881 error!("💥 执行 git clone 失败: {}", e);
882 let _ = fs::remove_dir_all(&temp_dir);
883 }
884 }
885 } else {
886 sync_git_remote(config);
888
889 let has_commits = Command::new("git")
891 .args(["rev-parse", "HEAD"])
892 .current_dir(&dir)
893 .output()
894 .map(|o| o.status.success())
895 .unwrap_or(false);
896
897 if !has_commits {
898 info!("📥 本地仓库尚无提交,正在从远程仓库拉取...");
900
901 let report_path = config.report_file_path();
903 if report_path.exists()
904 && fs::metadata(&report_path)
905 .map(|m| m.len() > 0)
906 .unwrap_or(false)
907 {
908 let backup_path = report_path.with_extension("md.bak");
909 let _ = fs::copy(&report_path, &backup_path);
910 info!("📋 已备份本地日报到: {:?}", backup_path);
911 }
912
913 if let Some(status) = run_git_in_report_dir(&["fetch", "origin", "main"], config) {
915 if !status.success() {
916 error!("❌ git fetch 失败,请检查网络连接和仓库地址");
917 return;
918 }
919 } else {
920 return;
921 }
922
923 if let Some(status) = run_git_in_report_dir(&["reset", "--hard", "origin/main"], config)
925 {
926 if status.success() {
927 info!("✅ 成功从远程仓库拉取周报");
928 } else {
929 error!("❌ git reset 失败");
930 }
931 }
932 } else {
933 info!("📥 正在从远程仓库拉取最新周报...");
935
936 let _ = run_git_in_report_dir(&["add", "-A"], config);
938 let stash_result = Command::new("git")
939 .args(["stash", "push", "-m", "auto-stash-before-pull"])
940 .current_dir(&dir)
941 .output();
942 let has_stash = match &stash_result {
943 Ok(output) => {
944 let msg = String::from_utf8_lossy(&output.stdout);
945 !msg.contains("No local changes")
946 }
947 Err(_) => false,
948 };
949
950 let pull_ok = if let Some(status) =
952 run_git_in_report_dir(&["pull", "origin", "main", "--rebase"], config)
953 {
954 if status.success() {
955 info!("✅ 周报已更新到最新版本");
956 true
957 } else {
958 error!("❌ git pull 失败,请检查网络连接或手动解决冲突");
959 false
960 }
961 } else {
962 false
963 };
964
965 if has_stash {
967 if let Some(status) = run_git_in_report_dir(&["stash", "pop"], config) {
968 if !status.success() && pull_ok {
969 info!("⚠️ stash pop 存在冲突,请手动合并本地修改(已保存在 git stash 中)");
970 }
971 }
972 }
973 }
974 }
975}
976
977pub fn handle_check(line_count: Option<&str>, config: &YamlConfig) {
981 if line_count == Some("open") {
983 handle_open_report(config);
984 return;
985 }
986
987 let num = match line_count {
988 Some(s) => match s.parse::<usize>() {
989 Ok(n) if n > 0 => n,
990 _ => {
991 error!("❌ 无效的行数参数: {},请输入正整数或 open", s);
992 return;
993 }
994 },
995 None => DEFAULT_CHECK_LINES,
996 };
997
998 let report_path = match get_report_path(config) {
999 Some(p) => p,
1000 None => return,
1001 };
1002
1003 info!("📂 正在读取周报文件路径: {}", report_path);
1004
1005 let path = Path::new(&report_path);
1006 if !path.is_file() {
1007 error!("❌ 文件不存在或不是有效文件: {}", report_path);
1008 return;
1009 }
1010
1011 let lines = read_last_n_lines(path, num);
1012 info!("📄 最近的 {} 行内容如下:", lines.len());
1013 let md_content = lines.join("\n");
1015 crate::md!("{}", md_content);
1016}
1017
1018pub fn handle_search(
1022 line_count: &str,
1023 target: &str,
1024 fuzzy_flag: Option<&str>,
1025 config: &YamlConfig,
1026) {
1027 let num = if line_count == "all" {
1028 usize::MAX
1029 } else {
1030 match line_count.parse::<usize>() {
1031 Ok(n) if n > 0 => n,
1032 _ => {
1033 error!("❌ 无效的行数参数: {},请输入正整数或 all", line_count);
1034 return;
1035 }
1036 }
1037 };
1038
1039 let report_path = match get_report_path(config) {
1040 Some(p) => p,
1041 None => return,
1042 };
1043
1044 info!("📂 正在读取周报文件路径: {}", report_path);
1045
1046 let path = Path::new(&report_path);
1047 if !path.is_file() {
1048 error!("❌ 文件不存在或不是有效文件: {}", report_path);
1049 return;
1050 }
1051
1052 let is_fuzzy =
1053 matches!(fuzzy_flag, Some(f) if f == search_flag::FUZZY_SHORT || f == search_flag::FUZZY);
1054 if is_fuzzy {
1055 info!("启用模糊匹配...");
1056 }
1057
1058 let lines = read_last_n_lines(path, num);
1059 info!("🔍 搜索目标关键字: {}", target.green());
1060
1061 let mut index = 0;
1062 for line in &lines {
1063 let matched = if is_fuzzy {
1064 fuzzy::fuzzy_match(line, target)
1065 } else {
1066 line.contains(target)
1067 };
1068
1069 if matched {
1070 index += 1;
1071 let highlighted = fuzzy::highlight_matches(line, target, is_fuzzy);
1072 info!("[{}] {}", index, highlighted);
1073 }
1074 }
1075
1076 if index == 0 {
1077 info!("nothing found 😢");
1078 }
1079}
1080
1081fn read_last_n_lines(path: &Path, n: usize) -> Vec<String> {
1083 let mut lines = Vec::new();
1084 let buffer_size: usize = 16384; let mut file = match fs::File::open(path) {
1087 Ok(f) => f,
1088 Err(e) => {
1089 error!("❌ 读取文件时发生错误: {}", e);
1090 return lines;
1091 }
1092 };
1093
1094 let file_len = match file.metadata() {
1095 Ok(m) => m.len() as usize,
1096 Err(_) => return lines,
1097 };
1098
1099 if file_len == 0 {
1100 return lines;
1101 }
1102
1103 if n == usize::MAX || file_len <= buffer_size * 2 {
1105 let mut content = String::new();
1106 let _ = file.seek(SeekFrom::Start(0));
1107 if file.read_to_string(&mut content).is_ok() {
1108 let all_lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
1109 if n >= all_lines.len() {
1110 return all_lines;
1111 }
1112 return all_lines[all_lines.len() - n..].to_vec();
1113 }
1114 return lines;
1115 }
1116
1117 let mut pointer = file_len;
1119 let mut remainder = Vec::new();
1120
1121 while pointer > 0 && lines.len() < n {
1122 let bytes_to_read = pointer.min(buffer_size);
1123 pointer -= bytes_to_read;
1124
1125 let _ = file.seek(SeekFrom::Start(pointer as u64));
1126 let mut buffer = vec![0u8; bytes_to_read];
1127 if file.read_exact(&mut buffer).is_err() {
1128 break;
1129 }
1130
1131 buffer.extend(remainder.drain(..));
1133
1134 let text = String::from_utf8_lossy(&buffer).to_string();
1136 let mut block_lines: Vec<&str> = text.split('\n').collect();
1137
1138 if pointer > 0 {
1140 remainder = block_lines.remove(0).as_bytes().to_vec();
1141 }
1142
1143 for line in block_lines.into_iter().rev() {
1144 if !line.is_empty() {
1145 lines.push(line.to_string());
1146 if lines.len() >= n {
1147 break;
1148 }
1149 }
1150 }
1151 }
1152
1153 if !remainder.is_empty() && lines.len() < n {
1155 let line = String::from_utf8_lossy(&remainder).to_string();
1156 if !line.is_empty() {
1157 lines.push(line);
1158 }
1159 }
1160
1161 lines.reverse();
1162 lines
1163}