Skip to main content

j_cli/command/
report.rs

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
18// ========== report 命令 ==========
19
20/// 处理 report 命令: j report <content...> 或 j reportctl new [date] / j reportctl sync [date]
21pub 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        // report 无参数:打开 TUI 多行编辑器(预填历史 + 日期前缀,NORMAL 模式)
30        handle_report_tui(config);
31        return;
32    }
33
34    let first = content[0].as_str();
35
36    // 元数据操作
37    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    // 常规日报写入
78    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
89/// 获取日报文件路径(统一入口,自动创建目录和文件)
90fn get_report_path(config: &YamlConfig) -> Option<String> {
91    let report_path = config.report_file_path();
92
93    // 确保父目录存在
94    if let Some(parent) = report_path.parent() {
95        let _ = fs::create_dir_all(parent);
96    }
97
98    // 如果文件不存在则自动创建空文件
99    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
110/// 获取日报工作目录下的 settings.json 路径
111fn 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
118/// TUI 模式日报编辑:预加载历史 + 日期前缀,NORMAL 模式进入
119fn 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    // 检查是否需要新开一周(与 handle_daily_report 相同逻辑)
129    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    // 先读取文件最后 3 行作为历史上下文(在任何写入之前读取)
141    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    // 拼接编辑器初始内容:历史行 + (可选的新周标题) + 日期前缀行
146    let mut initial_lines: Vec<String> = last_lines.clone();
147
148    // 检查是否需要新开一周 → 只更新配置,不写入文件;新周标题放入编辑器
149    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            // 新周标题放入编辑器初始内容,不提前写入文件
160            initial_lines.push(new_week_title);
161        }
162    }
163
164    // 构造日期前缀行
165    let today_str = now.format(SIMPLE_DATE_FORMAT);
166    let date_prefix = format!("- 【{}】 ", today_str);
167    initial_lines.push(date_prefix);
168
169    // 打开带初始内容的编辑器(NORMAL 模式)
170    match crate::tui::editor::open_multiline_editor_with_content("📝 编辑日报", &initial_lines)
171    {
172        Ok(Some(text)) => {
173            // 用户提交了内容
174            // 计算原始上下文有多少行(用于替换)
175            let original_context_count = last_lines.len();
176
177            // 从文件中去掉最后 N 行,再写入编辑器的全部内容
178            replace_last_n_lines(report_file, original_context_count, &text);
179
180            info!("✅ 日报已写入:{}", report_path);
181        }
182        Ok(None) => {
183            info!("已取消编辑");
184            // 文件未做任何修改(新周标题也没有写入)
185            // 配置文件中的 week_num/last_day 可能已更新,但下次进入时 now <= last_day 不会重复生成
186        }
187        Err(e) => {
188            error!("❌ 编辑器启动失败: {}", e);
189        }
190    }
191}
192
193/// 替换文件最后 N 行为新内容
194fn 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    // 保留前面的行(去掉最后 n 行)
206    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    // 写入保留的行
215    for line in &all_lines[..keep_count] {
216        result.push_str(line);
217        result.push('\n');
218    }
219
220    // 追加编辑器的内容
221    result.push_str(new_content);
222
223    // 确保文件以换行结尾
224    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
233/// 写入日报
234fn handle_daily_report(content: &str, config: &mut YamlConfig) {
235    let report_path = match get_report_path(config) {
236        Some(p) => p,
237        None => return,
238    };
239
240    info!("📂 日报文件路径:{}", report_path);
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_and_sync(&config_path, config);
246
247    let now = Local::now().date_naive();
248
249    let week_num = config
250        .get_property(section::REPORT, config_key::WEEK_NUM)
251        .and_then(|s| s.parse::<i32>().ok())
252        .unwrap_or(1);
253
254    let last_day_str = config
255        .get_property(section::REPORT, config_key::LAST_DAY)
256        .cloned()
257        .unwrap_or_default();
258
259    let last_day = parse_date(&last_day_str);
260
261    match last_day {
262        Some(last_day) => {
263            if now > last_day {
264                // 进入新的一周
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(week_num + 1, &next_last_day, &config_path, config);
273                append_to_file(report_file, &new_week_title);
274            }
275        }
276        None => {
277            error!("❌ 无法解析 last_day 日期: {}", last_day_str);
278            return;
279        }
280    }
281
282    let today_str = now.format(SIMPLE_DATE_FORMAT);
283    let log_entry = format!("- 【{}】 {}\n", today_str, content);
284    append_to_file(report_file, &log_entry);
285    info!("✅ 成功将内容写入:{}", report_path);
286}
287
288/// 处理 reportctl new 命令:开启新的一周
289fn handle_week_update(date_str: Option<&str>, config: &mut YamlConfig) {
290    let report_path = match get_report_path(config) {
291        Some(p) => p,
292        None => return,
293    };
294
295    let config_path = get_settings_json_path(&report_path);
296
297    let week_num = config
298        .get_property(section::REPORT, config_key::WEEK_NUM)
299        .and_then(|s| s.parse::<i32>().ok())
300        .unwrap_or(1);
301
302    let last_day_str = date_str
303        .map(|s| s.to_string())
304        .or_else(|| {
305            config
306                .get_property(section::REPORT, config_key::LAST_DAY)
307                .cloned()
308        })
309        .unwrap_or_default();
310
311    match parse_date(&last_day_str) {
312        Some(last_day) => {
313            let next_last_day = last_day + chrono::Duration::days(7);
314            update_config_files(week_num + 1, &next_last_day, &config_path, config);
315        }
316        None => {
317            error!(
318                "❌ 更新周数失败,请检查日期字符串是否有误: {}",
319                last_day_str
320            );
321        }
322    }
323}
324
325/// 处理 reportctl sync 命令:同步周数和日期
326fn handle_sync(date_str: Option<&str>, config: &mut YamlConfig) {
327    let report_path = match get_report_path(config) {
328        Some(p) => p,
329        None => return,
330    };
331
332    let config_path = get_settings_json_path(&report_path);
333
334    load_config_from_json_and_sync(&config_path, config);
335
336    let week_num = config
337        .get_property(section::REPORT, config_key::WEEK_NUM)
338        .and_then(|s| s.parse::<i32>().ok())
339        .unwrap_or(1);
340
341    let last_day_str = date_str
342        .map(|s| s.to_string())
343        .or_else(|| {
344            config
345                .get_property(section::REPORT, config_key::LAST_DAY)
346                .cloned()
347        })
348        .unwrap_or_default();
349
350    match parse_date(&last_day_str) {
351        Some(last_day) => {
352            update_config_files(week_num, &last_day, &config_path, config);
353        }
354        None => {
355            error!(
356                "❌ 更新周数失败,请检查日期字符串是否有误: {}",
357                last_day_str
358            );
359        }
360    }
361}
362
363/// 更新配置文件(YAML + JSON)
364fn update_config_files(
365    week_num: i32,
366    last_day: &NaiveDate,
367    config_path: &Path,
368    config: &mut YamlConfig,
369) {
370    let last_day_str = last_day.format(DATE_FORMAT).to_string();
371
372    // 更新 YAML 配置
373    config.set_property(section::REPORT, config_key::WEEK_NUM, &week_num.to_string());
374    config.set_property(section::REPORT, config_key::LAST_DAY, &last_day_str);
375    info!(
376        "✅ 更新YAML配置文件成功:周数 = {}, 周结束日期 = {}",
377        week_num, last_day_str
378    );
379
380    // 更新 JSON 配置
381    if config_path.exists() {
382        let json = serde_json::json!({
383            "week_num": week_num,
384            "last_day": last_day_str
385        });
386        match fs::write(config_path, json.to_string()) {
387            Ok(_) => info!(
388                "✅ 更新JSON配置文件成功:周数 = {}, 周结束日期 = {}",
389                week_num, last_day_str
390            ),
391            Err(e) => error!("❌ 更新JSON配置文件时出错: {}", e),
392        }
393    }
394}
395
396/// 从 JSON 配置文件读取并同步到 YAML
397fn load_config_from_json_and_sync(config_path: &Path, config: &mut YamlConfig) {
398    if !config_path.exists() {
399        error!("❌ 日报配置文件不存在:{:?}", config_path);
400        return;
401    }
402
403    match fs::read_to_string(config_path) {
404        Ok(content) => {
405            if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
406                let last_day = json.get("last_day").and_then(|v| v.as_str()).unwrap_or("");
407                let week_num = json.get("week_num").and_then(|v| v.as_i64()).unwrap_or(1);
408
409                info!(
410                    "✅ 从日报配置文件中读取到:last_day = {}, week_num = {}",
411                    last_day, week_num
412                );
413
414                if let Some(last_day_date) = parse_date(last_day) {
415                    update_config_files(week_num as i32, &last_day_date, config_path, config);
416                }
417            } else {
418                error!("❌ 解析日报配置文件时出错");
419            }
420        }
421        Err(e) => error!("❌ 读取日报配置文件失败: {}", e),
422    }
423}
424
425fn parse_date(s: &str) -> Option<NaiveDate> {
426    NaiveDate::parse_from_str(s, DATE_FORMAT).ok()
427}
428
429fn append_to_file(path: &Path, content: &str) {
430    use std::fs::OpenOptions;
431    use std::io::Write;
432    match OpenOptions::new().create(true).append(true).open(path) {
433        Ok(mut f) => {
434            if let Err(e) = f.write_all(content.as_bytes()) {
435                error!("❌ 写入文件失败: {}", e);
436            }
437        }
438        Err(e) => error!("❌ 打开文件失败: {}", e),
439    }
440}
441
442// ========== open 命令 ==========
443
444/// 处理 reportctl open 命令:用内置 TUI 编辑器打开日报文件,自由编辑全文
445fn handle_open_report(config: &YamlConfig) {
446    let report_path = match get_report_path(config) {
447        Some(p) => p,
448        None => return,
449    };
450
451    let path = Path::new(&report_path);
452    if !path.is_file() {
453        error!("❌ 日报文件不存在: {}", report_path);
454        return;
455    }
456
457    // 读取文件全部内容
458    let content = match fs::read_to_string(path) {
459        Ok(c) => c,
460        Err(e) => {
461            error!("❌ 读取日报文件失败: {}", e);
462            return;
463        }
464    };
465
466    let lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
467
468    // 用 TUI 编辑器打开全文(NORMAL 模式)
469    match crate::tui::editor::open_multiline_editor_with_content("📝 编辑日报文件", &lines)
470    {
471        Ok(Some(text)) => {
472            // 用户提交了内容,整体回写文件
473            let mut result = text;
474            if !result.ends_with('\n') {
475                result.push('\n');
476            }
477            if let Err(e) = fs::write(path, &result) {
478                error!("❌ 写入日报文件失败: {}", e);
479                return;
480            }
481            info!("✅ 日报文件已保存:{}", report_path);
482        }
483        Ok(None) => {
484            info!("已取消编辑,文件未修改");
485        }
486        Err(e) => {
487            error!("❌ 编辑器启动失败: {}", e);
488        }
489    }
490}
491
492// ========== set-url 命令 ==========
493
494/// 处理 reportctl set-url 命令:设置 git 仓库地址
495fn handle_set_url(url: Option<&str>, config: &mut YamlConfig) {
496    match url {
497        Some(u) if !u.is_empty() => {
498            let old = config
499                .get_property(section::REPORT, config_key::GIT_REPO)
500                .cloned();
501            config.set_property(section::REPORT, config_key::GIT_REPO, u);
502
503            // 如果日报目录已有 .git,同步更新 remote origin
504            if let Some(dir) = get_report_dir(config) {
505                let git_dir = Path::new(&dir).join(".git");
506                if git_dir.exists() {
507                    sync_git_remote(config);
508                }
509            }
510
511            match old {
512                Some(old_url) if !old_url.is_empty() => {
513                    info!("✅ git 仓库地址已更新: {} → {}", old_url, u);
514                }
515                _ => {
516                    info!("✅ git 仓库地址已设置: {}", u);
517                }
518            }
519        }
520        _ => {
521            // 无参数时显示当前配置
522            match config.get_property(section::REPORT, config_key::GIT_REPO) {
523                Some(url) if !url.is_empty() => {
524                    info!("📦 当前 git 仓库地址: {}", url);
525                }
526                _ => {
527                    info!("📦 尚未配置 git 仓库地址");
528                    usage!("reportctl set-url <repo_url>");
529                }
530            }
531        }
532    }
533}
534
535// ========== push / pull 命令 ==========
536
537/// 获取日报目录(report 文件所在的目录)
538fn get_report_dir(config: &YamlConfig) -> Option<String> {
539    let report_path = config.report_file_path();
540    report_path
541        .parent()
542        .map(|p| p.to_string_lossy().to_string())
543}
544
545/// 在日报目录下执行 git 命令
546fn run_git_in_report_dir(args: &[&str], config: &YamlConfig) -> Option<std::process::ExitStatus> {
547    let dir = match get_report_dir(config) {
548        Some(d) => d,
549        None => {
550            error!("❌ 无法确定日报目录");
551            return None;
552        }
553    };
554
555    let result = Command::new("git").args(args).current_dir(&dir).status();
556
557    match result {
558        Ok(status) => Some(status),
559        Err(e) => {
560            error!("💥 执行 git 命令失败: {}", e);
561            None
562        }
563    }
564}
565
566/// 检查日报目录是否已初始化 git 仓库,如果没有则初始化并配置 remote
567fn ensure_git_repo(config: &YamlConfig) -> bool {
568    let dir = match get_report_dir(config) {
569        Some(d) => d,
570        None => {
571            error!("❌ 无法确定日报目录");
572            return false;
573        }
574    };
575
576    let git_dir = Path::new(&dir).join(".git");
577    if git_dir.exists() {
578        // 已初始化,同步 remote URL(防止 set-url 后 remote 不一致)
579        sync_git_remote(config);
580        return true;
581    }
582
583    // 检查是否有配置 git_repo
584    let git_repo = config.get_property(section::REPORT, config_key::GIT_REPO);
585    if git_repo.is_none() || git_repo.unwrap().is_empty() {
586        error!("❌ 尚未配置 git 仓库地址,请先执行: j reportctl set-url <repo_url>");
587        return false;
588    }
589    let repo_url = git_repo.unwrap().clone();
590
591    info!("📦 日报目录尚未初始化 git 仓库,正在初始化...");
592
593    // git init -b main
594    if let Some(status) = run_git_in_report_dir(&["init", "-b", "main"], config) {
595        if !status.success() {
596            error!("❌ git init 失败");
597            return false;
598        }
599    } else {
600        return false;
601    }
602
603    // git remote add origin <repo_url>
604    if let Some(status) = run_git_in_report_dir(&["remote", "add", "origin", &repo_url], config) {
605        if !status.success() {
606            error!("❌ git remote add 失败");
607            return false;
608        }
609    } else {
610        return false;
611    }
612
613    info!("✅ git 仓库初始化完成,remote: {}", repo_url);
614    true
615}
616
617/// 同步 git remote origin URL 与配置文件中的 git_repo 保持一致
618fn sync_git_remote(config: &YamlConfig) {
619    let git_repo = match config.get_property(section::REPORT, config_key::GIT_REPO) {
620        Some(url) if !url.is_empty() => url.clone(),
621        _ => return, // 没有配置就不同步
622    };
623
624    // 获取当前 remote origin url
625    let dir = match get_report_dir(config) {
626        Some(d) => d,
627        None => return,
628    };
629
630    let current_url = Command::new("git")
631        .args(["remote", "get-url", "origin"])
632        .current_dir(&dir)
633        .output();
634
635    match current_url {
636        Ok(output) if output.status.success() => {
637            let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
638            if url != git_repo {
639                // URL 不一致,更新 remote
640                let _ = run_git_in_report_dir(&["remote", "set-url", "origin", &git_repo], config);
641                info!("🔄 已同步 remote origin: {} → {}", url, git_repo);
642            }
643        }
644        _ => {
645            // 没有 origin remote,添加一个
646            let _ = run_git_in_report_dir(&["remote", "add", "origin", &git_repo], config);
647        }
648    }
649}
650
651/// 处理 reportctl push 命令:推送周报到远程仓库
652fn handle_push(commit_msg: Option<&str>, config: &YamlConfig) {
653    // 检查 git_repo 配置
654    let git_repo = config.get_property(section::REPORT, config_key::GIT_REPO);
655    if git_repo.is_none() || git_repo.unwrap().is_empty() {
656        error!("❌ 尚未配置 git 仓库地址,请先执行: j reportctl set-url <repo_url>");
657        return;
658    }
659
660    // 确保 git 仓库已初始化
661    if !ensure_git_repo(config) {
662        return;
663    }
664
665    let default_msg = format!("update report {}", Local::now().format("%Y-%m-%d %H:%M"));
666    let msg = commit_msg.unwrap_or(&default_msg);
667
668    info!("📤 正在推送周报到远程仓库...");
669
670    // git add .
671    if let Some(status) = run_git_in_report_dir(&["add", "."], config) {
672        if !status.success() {
673            error!("❌ git add 失败");
674            return;
675        }
676    } else {
677        return;
678    }
679
680    // git commit -m "<msg>"
681    if let Some(status) = run_git_in_report_dir(&["commit", "-m", msg], config) {
682        if !status.success() {
683            // commit 可能因为没有变更而失败,这不一定是错误
684            info!("ℹ️ git commit 返回非零退出码(可能没有新变更)");
685        }
686    } else {
687        return;
688    }
689
690    // git push origin main
691    if let Some(status) = run_git_in_report_dir(&["push", "-u", "origin", "main"], config) {
692        if status.success() {
693            info!("✅ 周报已成功推送到远程仓库");
694        } else {
695            error!("❌ git push 失败,请检查网络连接和仓库权限");
696        }
697    }
698}
699
700/// 处理 reportctl pull 命令:从远程仓库拉取周报
701fn handle_pull(config: &YamlConfig) {
702    // 检查 git_repo 配置
703    let git_repo = config.get_property(section::REPORT, config_key::GIT_REPO);
704    if git_repo.is_none() || git_repo.unwrap().is_empty() {
705        error!("❌ 尚未配置 git 仓库地址,请先执行: j reportctl set-url <repo_url>");
706        return;
707    }
708
709    let dir = match get_report_dir(config) {
710        Some(d) => d,
711        None => {
712            error!("❌ 无法确定日报目录");
713            return;
714        }
715    };
716
717    let git_dir = Path::new(&dir).join(".git");
718
719    if !git_dir.exists() {
720        // 日报目录不是 git 仓库,尝试 clone
721        let repo_url = git_repo.unwrap().clone();
722        info!("📥 日报目录尚未初始化,正在从远程仓库克隆...");
723
724        // 先备份已有文件(如果有的话)
725        let report_path = config.report_file_path();
726        let has_existing = report_path.exists()
727            && fs::metadata(&report_path)
728                .map(|m| m.len() > 0)
729                .unwrap_or(false);
730
731        if has_existing {
732            // 备份现有文件
733            let backup_path = report_path.with_extension("md.bak");
734            if let Err(e) = fs::copy(&report_path, &backup_path) {
735                error!("⚠️ 备份现有日报文件失败: {}", e);
736            } else {
737                info!("📋 已备份现有日报到: {:?}", backup_path);
738            }
739        }
740
741        // 清空目录内容后 clone
742        // 使用 git clone 到一个临时目录再移动
743        let temp_dir = Path::new(&dir).with_file_name(".report_clone_tmp");
744        let _ = fs::remove_dir_all(&temp_dir);
745
746        let result = Command::new("git")
747            .args([
748                "clone",
749                "-b",
750                "main",
751                &repo_url,
752                &temp_dir.to_string_lossy(),
753            ])
754            .status();
755
756        match result {
757            Ok(status) if status.success() => {
758                // 将 clone 出来的内容移到 report 目录
759                let _ = fs::remove_dir_all(&dir);
760                if let Err(e) = fs::rename(&temp_dir, &dir) {
761                    error!("❌ 移动克隆仓库失败: {},临时目录: {:?}", e, temp_dir);
762                    return;
763                }
764                info!("✅ 成功从远程仓库克隆周报");
765            }
766            Ok(_) => {
767                error!("❌ git clone 失败,请检查仓库地址和网络连接");
768                let _ = fs::remove_dir_all(&temp_dir);
769            }
770            Err(e) => {
771                error!("💥 执行 git clone 失败: {}", e);
772                let _ = fs::remove_dir_all(&temp_dir);
773            }
774        }
775    } else {
776        // 已经是 git 仓库,先同步 remote URL
777        sync_git_remote(config);
778
779        // 检测是否是空仓库(unborn branch,没有任何 commit)
780        let has_commits = Command::new("git")
781            .args(["rev-parse", "HEAD"])
782            .current_dir(&dir)
783            .output()
784            .map(|o| o.status.success())
785            .unwrap_or(false);
786
787        if !has_commits {
788            // 空仓库(git init 后未 commit),通过 fetch + checkout 来拉取
789            info!("📥 本地仓库尚无提交,正在从远程仓库拉取...");
790
791            // 备份本地已有的未跟踪文件
792            let report_path = config.report_file_path();
793            if report_path.exists()
794                && fs::metadata(&report_path)
795                    .map(|m| m.len() > 0)
796                    .unwrap_or(false)
797            {
798                let backup_path = report_path.with_extension("md.bak");
799                let _ = fs::copy(&report_path, &backup_path);
800                info!("📋 已备份本地日报到: {:?}", backup_path);
801            }
802
803            // git fetch origin main
804            if let Some(status) = run_git_in_report_dir(&["fetch", "origin", "main"], config) {
805                if !status.success() {
806                    error!("❌ git fetch 失败,请检查网络连接和仓库地址");
807                    return;
808                }
809            } else {
810                return;
811            }
812
813            // git reset --hard origin/main(强制用远程覆盖本地)
814            if let Some(status) = run_git_in_report_dir(&["reset", "--hard", "origin/main"], config)
815            {
816                if status.success() {
817                    info!("✅ 成功从远程仓库拉取周报");
818                } else {
819                    error!("❌ git reset 失败");
820                }
821            }
822        } else {
823            // 正常仓库,先 stash 再 pull
824            info!("📥 正在从远程仓库拉取最新周报...");
825
826            // 先暂存本地未跟踪/修改的文件,防止 pull 时冲突
827            let _ = run_git_in_report_dir(&["add", "-A"], config);
828            let stash_result = Command::new("git")
829                .args(["stash", "push", "-m", "auto-stash-before-pull"])
830                .current_dir(&dir)
831                .output();
832            let has_stash = match &stash_result {
833                Ok(output) => {
834                    let msg = String::from_utf8_lossy(&output.stdout);
835                    !msg.contains("No local changes")
836                }
837                Err(_) => false,
838            };
839
840            // 执行 pull
841            let pull_ok = if let Some(status) =
842                run_git_in_report_dir(&["pull", "origin", "main", "--rebase"], config)
843            {
844                if status.success() {
845                    info!("✅ 周报已更新到最新版本");
846                    true
847                } else {
848                    error!("❌ git pull 失败,请检查网络连接或手动解决冲突");
849                    false
850                }
851            } else {
852                false
853            };
854
855            // 恢复 stash
856            if has_stash {
857                if let Some(status) = run_git_in_report_dir(&["stash", "pop"], config) {
858                    if !status.success() && pull_ok {
859                        info!("⚠️ stash pop 存在冲突,请手动合并本地修改(已保存在 git stash 中)");
860                    }
861                }
862            }
863        }
864    }
865}
866
867// ========== check 命令 ==========
868
869/// 处理 check 命令: j check [line_count]
870pub fn handle_check(line_count: Option<&str>, config: &YamlConfig) {
871    // 检查是否是 open 子命令
872    if line_count == Some("open") {
873        handle_open_report(config);
874        return;
875    }
876
877    let num = match line_count {
878        Some(s) => match s.parse::<usize>() {
879            Ok(n) if n > 0 => n,
880            _ => {
881                error!("❌ 无效的行数参数: {},请输入正整数或 open", s);
882                return;
883            }
884        },
885        None => DEFAULT_CHECK_LINES,
886    };
887
888    let report_path = match get_report_path(config) {
889        Some(p) => p,
890        None => return,
891    };
892
893    info!("📂 正在读取周报文件路径: {}", report_path);
894
895    let path = Path::new(&report_path);
896    if !path.is_file() {
897        error!("❌ 文件不存在或不是有效文件: {}", report_path);
898        return;
899    }
900
901    let lines = read_last_n_lines(path, num);
902    info!("📄 最近的 {} 行内容如下:", lines.len());
903    // 周报本身就是 Markdown 格式,使用 termimad 渲染
904    let md_content = lines.join("\n");
905    crate::md!("{}", md_content);
906}
907
908// ========== search 命令 ==========
909
910/// 处理 search 命令: j search <line_count|all> <target> [-f|-fuzzy]
911pub fn handle_search(
912    line_count: &str,
913    target: &str,
914    fuzzy_flag: Option<&str>,
915    config: &YamlConfig,
916) {
917    let num = if line_count == "all" {
918        usize::MAX
919    } else {
920        match line_count.parse::<usize>() {
921            Ok(n) if n > 0 => n,
922            _ => {
923                error!("❌ 无效的行数参数: {},请输入正整数或 all", line_count);
924                return;
925            }
926        }
927    };
928
929    let report_path = match get_report_path(config) {
930        Some(p) => p,
931        None => return,
932    };
933
934    info!("📂 正在读取周报文件路径: {}", report_path);
935
936    let path = Path::new(&report_path);
937    if !path.is_file() {
938        error!("❌ 文件不存在或不是有效文件: {}", report_path);
939        return;
940    }
941
942    let is_fuzzy =
943        matches!(fuzzy_flag, Some(f) if f == search_flag::FUZZY_SHORT || f == search_flag::FUZZY);
944    if is_fuzzy {
945        info!("启用模糊匹配...");
946    }
947
948    let lines = read_last_n_lines(path, num);
949    info!("🔍 搜索目标关键字: {}", target.green());
950
951    let mut index = 0;
952    for line in &lines {
953        let matched = if is_fuzzy {
954            fuzzy::fuzzy_match(line, target)
955        } else {
956            line.contains(target)
957        };
958
959        if matched {
960            index += 1;
961            let highlighted = fuzzy::highlight_matches(line, target, is_fuzzy);
962            info!("[{}] {}", index, highlighted);
963        }
964    }
965
966    if index == 0 {
967        info!("nothing found 😢");
968    }
969}
970
971/// 从文件尾部读取最后 N 行(高效实现,不需要读取整个文件)
972fn read_last_n_lines(path: &Path, n: usize) -> Vec<String> {
973    let mut lines = Vec::new();
974    let buffer_size: usize = 16384; // 16KB
975
976    let mut file = match fs::File::open(path) {
977        Ok(f) => f,
978        Err(e) => {
979            error!("❌ 读取文件时发生错误: {}", e);
980            return lines;
981        }
982    };
983
984    let file_len = match file.metadata() {
985        Ok(m) => m.len() as usize,
986        Err(_) => return lines,
987    };
988
989    if file_len == 0 {
990        return lines;
991    }
992
993    // 对于较小的文件或者需要读取全部内容的情况,直接全部读取
994    if n == usize::MAX || file_len <= buffer_size * 2 {
995        let mut content = String::new();
996        let _ = file.seek(SeekFrom::Start(0));
997        if file.read_to_string(&mut content).is_ok() {
998            let all_lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
999            if n >= all_lines.len() {
1000                return all_lines;
1001            }
1002            return all_lines[all_lines.len() - n..].to_vec();
1003        }
1004        return lines;
1005    }
1006
1007    // 从文件尾部逐块读取
1008    let mut pointer = file_len;
1009    let mut remainder = Vec::new();
1010
1011    while pointer > 0 && lines.len() < n {
1012        let bytes_to_read = pointer.min(buffer_size);
1013        pointer -= bytes_to_read;
1014
1015        let _ = file.seek(SeekFrom::Start(pointer as u64));
1016        let mut buffer = vec![0u8; bytes_to_read];
1017        if file.read_exact(&mut buffer).is_err() {
1018            break;
1019        }
1020
1021        // 将 remainder(上次剩余的不完整行)追加到这个块的末尾
1022        buffer.extend(remainder.drain(..));
1023
1024        // 从后向前按行分割
1025        let text = String::from_utf8_lossy(&buffer).to_string();
1026        let mut block_lines: Vec<&str> = text.split('\n').collect();
1027
1028        // 第一行可能是不完整的(跨块)
1029        if pointer > 0 {
1030            remainder = block_lines.remove(0).as_bytes().to_vec();
1031        }
1032
1033        for line in block_lines.into_iter().rev() {
1034            if !line.is_empty() {
1035                lines.push(line.to_string());
1036                if lines.len() >= n {
1037                    break;
1038                }
1039            }
1040        }
1041    }
1042
1043    // 处理文件最开头的那行
1044    if !remainder.is_empty() && lines.len() < n {
1045        let line = String::from_utf8_lossy(&remainder).to_string();
1046        if !line.is_empty() {
1047            lines.push(line);
1048        }
1049    }
1050
1051    lines.reverse();
1052    lines
1053}