Skip to main content

gitstack/
git.rs

1use std::time::SystemTime;
2
3use anyhow::{Context, Result};
4use chrono::{Local, TimeZone};
5use git2::{Commit, Diff, Repository, Signature, Sort, Status, StatusOptions};
6
7use crate::event::{GitEvent, GitEventKind};
8
9/// 現在のHEADのハッシュを取得
10pub fn get_head_hash() -> Result<String> {
11    let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
12    get_head_hash_from_repo(&repo)
13}
14
15/// RepositoryからHEADのハッシュを取得
16pub fn get_head_hash_from_repo(repo: &Repository) -> Result<String> {
17    let head = repo.head().context("HEADが見つかりません")?;
18    let oid = head.target().context("HEADのターゲットが見つかりません")?;
19    Ok(oid.to_string())
20}
21
22/// .git/indexファイルの更新時刻を取得(ワーキングツリー変更検知用)
23pub fn get_index_mtime() -> Result<SystemTime> {
24    let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
25    get_index_mtime_from_repo(&repo)
26}
27
28/// Repositoryから.git/indexの更新時刻を取得
29pub fn get_index_mtime_from_repo(repo: &Repository) -> Result<SystemTime> {
30    let git_dir = repo.path();
31    let index_path = git_dir.join("index");
32    let metadata = std::fs::metadata(&index_path).context("indexファイルが見つかりません")?;
33    metadata.modified().context("更新時刻を取得できません")
34}
35
36/// git config user.name を取得
37pub fn get_user_name() -> Option<String> {
38    let repo = Repository::discover(".").ok()?;
39    get_user_name_from_repo(&repo)
40}
41
42/// Repositoryからgit config user.nameを取得
43pub fn get_user_name_from_repo(repo: &Repository) -> Option<String> {
44    let config = repo.config().ok()?;
45    config.get_string("user.name").ok()
46}
47
48/// ファイルのステータス種別
49#[derive(Debug, Clone, PartialEq)]
50pub enum FileStatusKind {
51    /// ステージ済み(新規追加)
52    StagedNew,
53    /// ステージ済み(変更)
54    StagedModified,
55    /// ステージ済み(削除)
56    StagedDeleted,
57    /// 未ステージ(変更)
58    Modified,
59    /// 未ステージ(削除)
60    Deleted,
61    /// 未追跡
62    Untracked,
63}
64
65/// ファイルステータス
66#[derive(Debug, Clone)]
67pub struct FileStatus {
68    /// ファイルパス
69    pub path: String,
70    /// ステータス種別
71    pub kind: FileStatusKind,
72}
73
74/// コミットのファイル変更統計
75#[derive(Debug, Clone, Default)]
76#[allow(dead_code)]
77pub struct DiffStats {
78    pub files_changed: usize,
79    pub insertions: usize,
80    pub deletions: usize,
81}
82
83/// コミットのdiff統計を取得
84fn get_commit_diff_stats(repo: &Repository, commit: &Commit) -> DiffStats {
85    let tree = match commit.tree() {
86        Ok(t) => t,
87        Err(_) => return DiffStats::default(),
88    };
89
90    let parent_tree = commit.parent(0).ok().and_then(|p| p.tree().ok());
91
92    let diff: Diff = match repo.diff_tree_to_tree(parent_tree.as_ref(), Some(&tree), None) {
93        Ok(d) => d,
94        Err(_) => return DiffStats::default(),
95    };
96
97    match diff.stats() {
98        Ok(stats) => DiffStats {
99            files_changed: stats.files_changed(),
100            insertions: stats.insertions(),
101            deletions: stats.deletions(),
102        },
103        Err(_) => DiffStats::default(),
104    }
105}
106
107/// リポジトリ情報
108#[derive(Debug, Clone)]
109pub struct RepoInfo {
110    /// リポジトリ名
111    pub name: String,
112    /// 現在のブランチ名
113    pub branch: String,
114}
115
116impl RepoInfo {
117    /// カレントディレクトリからリポジトリ情報を取得
118    pub fn from_current_dir() -> Result<Self> {
119        let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
120        Self::from_repo(&repo)
121    }
122
123    /// Repositoryからリポジトリ情報を取得
124    pub fn from_repo(repo: &Repository) -> Result<Self> {
125        let name = repo
126            .workdir()
127            .and_then(|p| p.file_name())
128            .and_then(|n| n.to_str())
129            .unwrap_or("unknown")
130            .to_string();
131
132        let branch = repo
133            .head()
134            .ok()
135            .and_then(|h| h.shorthand().map(|s| s.to_string()))
136            .unwrap_or_else(|| "HEAD".to_string());
137
138        Ok(Self { name, branch })
139    }
140}
141
142/// ローカルブランチ一覧を取得
143pub fn list_branches() -> Result<Vec<String>> {
144    let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
145    list_branches_from_repo(&repo)
146}
147
148/// Repositoryからローカルブランチ一覧を取得
149pub fn list_branches_from_repo(repo: &Repository) -> Result<Vec<String>> {
150    let branches = repo.branches(Some(git2::BranchType::Local))?;
151    let mut branch_names: Vec<String> = branches
152        .filter_map(|b| b.ok())
153        .filter_map(|(branch, _)| branch.name().ok().flatten().map(|s| s.to_string()))
154        .collect();
155    branch_names.sort();
156    Ok(branch_names)
157}
158
159/// ブランチを切り替え
160pub fn checkout_branch(branch_name: &str) -> Result<()> {
161    let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
162    checkout_branch_in_repo(&repo, branch_name)
163}
164
165/// Repositoryでブランチを切り替え
166pub fn checkout_branch_in_repo(repo: &Repository, branch_name: &str) -> Result<()> {
167    let obj = repo
168        .revparse_single(&format!("refs/heads/{}", branch_name))
169        .context("ブランチが見つかりません")?;
170    repo.checkout_tree(&obj, None)?;
171    repo.set_head(&format!("refs/heads/{}", branch_name))?;
172    Ok(())
173}
174
175/// ワーキングディレクトリのステータスを取得
176pub fn get_status() -> Result<Vec<FileStatus>> {
177    let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
178    get_status_from_repo(&repo)
179}
180
181/// Repositoryからワーキングディレクトリのステータスを取得
182pub fn get_status_from_repo(repo: &Repository) -> Result<Vec<FileStatus>> {
183    let mut opts = StatusOptions::new();
184    opts.include_untracked(true)
185        .recurse_untracked_dirs(true)
186        .include_ignored(false);
187
188    let statuses = repo.statuses(Some(&mut opts))?;
189    let mut result = Vec::new();
190
191    for entry in statuses.iter() {
192        let path = entry.path().unwrap_or("").to_string();
193        let status = entry.status();
194
195        // ステージ済みファイル
196        if status.contains(Status::INDEX_NEW) {
197            result.push(FileStatus {
198                path: path.clone(),
199                kind: FileStatusKind::StagedNew,
200            });
201        } else if status.contains(Status::INDEX_MODIFIED) {
202            result.push(FileStatus {
203                path: path.clone(),
204                kind: FileStatusKind::StagedModified,
205            });
206        } else if status.contains(Status::INDEX_DELETED) {
207            result.push(FileStatus {
208                path: path.clone(),
209                kind: FileStatusKind::StagedDeleted,
210            });
211        }
212
213        // 未ステージファイル(ステージ済みと重複する場合もある)
214        if status.contains(Status::WT_MODIFIED) {
215            result.push(FileStatus {
216                path: path.clone(),
217                kind: FileStatusKind::Modified,
218            });
219        } else if status.contains(Status::WT_DELETED) {
220            result.push(FileStatus {
221                path: path.clone(),
222                kind: FileStatusKind::Deleted,
223            });
224        } else if status.contains(Status::WT_NEW) {
225            result.push(FileStatus {
226                path,
227                kind: FileStatusKind::Untracked,
228            });
229        }
230    }
231
232    Ok(result)
233}
234
235/// ファイルをステージング
236pub fn stage_file(path: &str) -> Result<()> {
237    let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
238    stage_file_in_repo(&repo, path)
239}
240
241/// Repositoryでファイルをステージング
242pub fn stage_file_in_repo(repo: &Repository, path: &str) -> Result<()> {
243    let mut index = repo.index()?;
244    let workdir = repo
245        .workdir()
246        .context("ワーキングディレクトリが見つかりません")?;
247    let full_path = workdir.join(path);
248
249    if full_path.exists() {
250        index.add_path(std::path::Path::new(path))?;
251    } else {
252        // ファイルが削除された場合
253        index.remove_path(std::path::Path::new(path))?;
254    }
255    index.write()?;
256    Ok(())
257}
258
259/// ファイルをアンステージング
260pub fn unstage_file(path: &str) -> Result<()> {
261    let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
262    unstage_file_in_repo(&repo, path)
263}
264
265/// Repositoryでファイルをアンステージング
266pub fn unstage_file_in_repo(repo: &Repository, path: &str) -> Result<()> {
267    let head = repo.head()?.peel_to_commit()?;
268    repo.reset_default(Some(&head.into_object()), [std::path::Path::new(path)])?;
269    Ok(())
270}
271
272/// コミットを作成
273pub fn create_commit(message: &str) -> Result<()> {
274    let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
275    create_commit_in_repo(&repo, message)
276}
277
278/// Repositoryでコミットを作成
279pub fn create_commit_in_repo(repo: &Repository, message: &str) -> Result<()> {
280    let sig = Signature::now("gitstack", "gitstack@local")?;
281    let mut index = repo.index()?;
282    let tree_id = index.write_tree()?;
283    let tree = repo.find_tree(tree_id)?;
284    let parent = repo.head()?.peel_to_commit()?;
285
286    repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent])?;
287    Ok(())
288}
289
290/// リモートにプッシュ
291pub fn push() -> Result<()> {
292    let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
293    push_in_repo(&repo)
294}
295
296/// Repositoryでリモートにプッシュ
297pub fn push_in_repo(repo: &Repository) -> Result<()> {
298    let head = repo.head()?;
299    let branch_name = head.shorthand().context("ブランチ名が取得できません")?;
300
301    let mut remote = repo
302        .find_remote("origin")
303        .context("originリモートが見つかりません")?;
304    let refspec = format!("refs/heads/{}:refs/heads/{}", branch_name, branch_name);
305
306    remote.push(&[&refspec], None)?;
307    Ok(())
308}
309
310/// リモートからフェッチ
311pub fn fetch_remote() -> Result<()> {
312    let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
313    fetch_remote_in_repo(&repo)
314}
315
316/// パスを指定してリモートからフェッチ(バックグラウンドスレッド用)
317pub fn fetch_remote_at_path(repo_path: &std::path::Path) -> Result<()> {
318    let repo = Repository::open(repo_path).context("リポジトリを開けません")?;
319    fetch_remote_in_repo(&repo)
320}
321
322/// Repositoryでリモートからフェッチ
323pub fn fetch_remote_in_repo(repo: &Repository) -> Result<()> {
324    let mut remote = repo
325        .find_remote("origin")
326        .context("originリモートが見つかりません")?;
327
328    // デフォルトのrefspecでフェッチ
329    remote.fetch(&[] as &[&str], None, None)?;
330    Ok(())
331}
332
333/// ステージ済みファイルがあるかどうか
334pub fn has_staged_files() -> Result<bool> {
335    let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
336    has_staged_files_in_repo(&repo)
337}
338
339/// Repositoryでステージ済みファイルがあるかどうか
340pub fn has_staged_files_in_repo(repo: &Repository) -> Result<bool> {
341    let statuses = get_status_from_repo(repo)?;
342    Ok(statuses.iter().any(|s| {
343        matches!(
344            s.kind,
345            FileStatusKind::StagedNew
346                | FileStatusKind::StagedModified
347                | FileStatusKind::StagedDeleted
348        )
349    }))
350}
351
352/// コミット履歴をGitEventとして取得
353pub fn load_events(limit: usize) -> Result<Vec<GitEvent>> {
354    let repo = Repository::discover(".").context("Gitリポジトリが見つかりません")?;
355    load_events_from_repo(&repo, limit)
356}
357
358/// Repositoryからコミット履歴をGitEventとして取得
359pub fn load_events_from_repo(repo: &Repository, limit: usize) -> Result<Vec<GitEvent>> {
360    let mut revwalk = repo.revwalk()?;
361    // TIME | TOPOLOGICAL で、同じ秒内のコミットもトポロジカル順で正しくソート
362    revwalk.set_sorting(Sort::TIME | Sort::TOPOLOGICAL)?;
363    revwalk.push_head()?;
364
365    let mut events = Vec::new();
366
367    for oid in revwalk.take(limit) {
368        let oid = oid?;
369        let commit = repo.find_commit(oid)?;
370
371        let short_hash = oid.to_string()[..7].to_string();
372        let message = commit
373            .message()
374            .unwrap_or("")
375            .lines()
376            .next()
377            .unwrap_or("")
378            .to_string();
379        let author = commit.author().name().unwrap_or("unknown").to_string();
380        let timestamp = Local
381            .timestamp_opt(commit.time().seconds(), 0)
382            .single()
383            .unwrap_or_else(Local::now);
384
385        // マージコミットかどうかを判定(親が2つ以上)
386        let kind = if commit.parent_count() > 1 {
387            GitEventKind::Merge
388        } else {
389            GitEventKind::Commit
390        };
391
392        // diff統計を取得
393        let diff_stats = get_commit_diff_stats(repo, &commit);
394
395        let event = match kind {
396            GitEventKind::Merge => GitEvent::merge(short_hash, message, author, timestamp),
397            _ => GitEvent::commit(
398                short_hash,
399                message,
400                author,
401                timestamp,
402                diff_stats.insertions,
403                diff_stats.deletions,
404            ),
405        };
406
407        events.push(event);
408    }
409
410    Ok(events)
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416    use std::fs;
417    use std::path::Path;
418    use tempfile::TempDir;
419
420    fn init_test_repo() -> (TempDir, Repository) {
421        let temp_dir = TempDir::new().expect("Failed to create temp dir");
422        let repo = Repository::init(temp_dir.path()).expect("Failed to init repo");
423
424        // 初期コミットを作成
425        let sig = git2::Signature::now("Test Author", "test@example.com").unwrap();
426        let tree_id = {
427            let mut index = repo.index().unwrap();
428            let test_file = temp_dir.path().join("test.txt");
429            fs::write(&test_file, "test content").unwrap();
430            index.add_path(Path::new("test.txt")).unwrap();
431            index.write().unwrap();
432            index.write_tree().unwrap()
433        };
434        {
435            let tree = repo.find_tree(tree_id).unwrap();
436            repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
437                .unwrap();
438        }
439
440        (temp_dir, repo)
441    }
442
443    #[test]
444    fn test_repo_info_from_repo_gets_name() {
445        let (_temp_dir, repo) = init_test_repo();
446        let info = RepoInfo::from_repo(&repo).unwrap();
447        assert!(!info.name.is_empty());
448    }
449
450    #[test]
451    fn test_repo_info_from_repo_gets_branch() {
452        let (_temp_dir, repo) = init_test_repo();
453        let info = RepoInfo::from_repo(&repo).unwrap();
454        // 初期ブランチはmasterまたはmain
455        assert!(info.branch == "master" || info.branch == "main");
456    }
457
458    #[test]
459    fn test_load_events_from_repo_returns_events() {
460        let (_temp_dir, repo) = init_test_repo();
461        let events = load_events_from_repo(&repo, 10).unwrap();
462        assert!(!events.is_empty());
463    }
464
465    #[test]
466    fn test_load_events_from_repo_first_event_is_initial_commit() {
467        let (_temp_dir, repo) = init_test_repo();
468        let events = load_events_from_repo(&repo, 10).unwrap();
469        assert_eq!(events[0].message, "Initial commit");
470    }
471
472    #[test]
473    fn test_load_events_from_repo_respects_limit() {
474        let (temp_dir, repo) = init_test_repo();
475
476        // 追加のコミットを作成
477        let sig = git2::Signature::now("Test Author", "test@example.com").unwrap();
478        for i in 1..=5 {
479            let test_file = temp_dir.path().join(format!("file{}.txt", i));
480            fs::write(&test_file, format!("content {}", i)).unwrap();
481            let mut index = repo.index().unwrap();
482            index
483                .add_path(Path::new(&format!("file{}.txt", i)))
484                .unwrap();
485            index.write().unwrap();
486            let tree_id = index.write_tree().unwrap();
487            let tree = repo.find_tree(tree_id).unwrap();
488            let parent = repo.head().unwrap().peel_to_commit().unwrap();
489            repo.commit(
490                Some("HEAD"),
491                &sig,
492                &sig,
493                &format!("Commit {}", i),
494                &tree,
495                &[&parent],
496            )
497            .unwrap();
498        }
499
500        let events = load_events_from_repo(&repo, 3).unwrap();
501        assert_eq!(events.len(), 3);
502    }
503
504    #[test]
505    fn test_load_events_from_repo_returns_commits_in_order() {
506        let (temp_dir, repo) = init_test_repo();
507
508        // 追加のコミットを作成
509        let sig = git2::Signature::now("Test Author", "test@example.com").unwrap();
510        for i in 1..=3 {
511            let test_file = temp_dir.path().join(format!("file{}.txt", i));
512            fs::write(&test_file, format!("content {}", i)).unwrap();
513            let mut index = repo.index().unwrap();
514            index
515                .add_path(Path::new(&format!("file{}.txt", i)))
516                .unwrap();
517            index.write().unwrap();
518            let tree_id = index.write_tree().unwrap();
519            let tree = repo.find_tree(tree_id).unwrap();
520            let parent = repo.head().unwrap().peel_to_commit().unwrap();
521            repo.commit(
522                Some("HEAD"),
523                &sig,
524                &sig,
525                &format!("Commit {}", i),
526                &tree,
527                &[&parent],
528            )
529            .unwrap();
530        }
531
532        let events = load_events_from_repo(&repo, 10).unwrap();
533        // 4つのコミットがある
534        assert_eq!(events.len(), 4);
535        // 最新のコミット(Commit 3)が含まれている
536        assert!(events.iter().any(|e| e.message == "Commit 3"));
537        // Initial commitが含まれている
538        assert!(events.iter().any(|e| e.message == "Initial commit"));
539    }
540
541    #[test]
542    fn test_load_events_from_repo_event_has_short_hash() {
543        let (_temp_dir, repo) = init_test_repo();
544        let events = load_events_from_repo(&repo, 10).unwrap();
545        assert_eq!(events[0].short_hash.len(), 7);
546    }
547
548    #[test]
549    fn test_load_events_from_repo_event_has_author() {
550        let (_temp_dir, repo) = init_test_repo();
551        let events = load_events_from_repo(&repo, 10).unwrap();
552        assert_eq!(events[0].author, "Test Author");
553    }
554
555    #[test]
556    fn test_load_events_from_repo_event_has_file_stats() {
557        let (_temp_dir, repo) = init_test_repo();
558        let events = load_events_from_repo(&repo, 10).unwrap();
559        // Initial commitは1ファイル追加(test content = 12文字 + 改行なし)
560        assert!(events[0].files_added > 0);
561    }
562
563    #[test]
564    fn test_get_commit_diff_stats_returns_stats() {
565        let (_temp_dir, repo) = init_test_repo();
566        let commit = repo.head().unwrap().peel_to_commit().unwrap();
567        let stats = get_commit_diff_stats(&repo, &commit);
568        // Initial commitは1ファイル、1行追加
569        assert!(stats.files_changed > 0 || stats.insertions > 0);
570    }
571
572    #[test]
573    fn test_list_branches_from_repo_returns_branches() {
574        let (_temp_dir, repo) = init_test_repo();
575        let branches = list_branches_from_repo(&repo).unwrap();
576        assert!(!branches.is_empty());
577    }
578
579    #[test]
580    fn test_list_branches_from_repo_includes_current_branch() {
581        let (_temp_dir, repo) = init_test_repo();
582        let branches = list_branches_from_repo(&repo).unwrap();
583        // 初期ブランチはmasterまたはmain
584        assert!(branches.contains(&"master".to_string()) || branches.contains(&"main".to_string()));
585    }
586
587    #[test]
588    fn test_checkout_branch_in_repo_switches_branch() {
589        let (_temp_dir, repo) = init_test_repo();
590
591        // 新しいブランチを作成
592        {
593            let head = repo.head().unwrap().peel_to_commit().unwrap();
594            repo.branch("test-branch", &head, false).unwrap();
595        }
596
597        // ブランチを切り替え
598        checkout_branch_in_repo(&repo, "test-branch").unwrap();
599
600        // 現在のブランチを確認
601        let info = RepoInfo::from_repo(&repo).unwrap();
602        assert_eq!(info.branch, "test-branch");
603    }
604
605    #[test]
606    fn test_get_status_from_repo_empty_on_clean() {
607        let (_temp_dir, repo) = init_test_repo();
608        let statuses = get_status_from_repo(&repo).unwrap();
609        assert!(statuses.is_empty());
610    }
611
612    #[test]
613    fn test_get_status_from_repo_detects_modified() {
614        let (temp_dir, repo) = init_test_repo();
615        fs::write(temp_dir.path().join("test.txt"), "modified content").unwrap();
616
617        let statuses = get_status_from_repo(&repo).unwrap();
618        assert!(statuses.iter().any(|s| s.kind == FileStatusKind::Modified));
619    }
620
621    #[test]
622    fn test_get_status_from_repo_detects_untracked() {
623        let (temp_dir, repo) = init_test_repo();
624        fs::write(temp_dir.path().join("new_file.txt"), "new content").unwrap();
625
626        let statuses = get_status_from_repo(&repo).unwrap();
627        assert!(statuses.iter().any(|s| s.kind == FileStatusKind::Untracked));
628    }
629
630    #[test]
631    fn test_stage_file_in_repo_stages_file() {
632        let (temp_dir, repo) = init_test_repo();
633        fs::write(temp_dir.path().join("new_file.txt"), "new content").unwrap();
634
635        stage_file_in_repo(&repo, "new_file.txt").unwrap();
636
637        let statuses = get_status_from_repo(&repo).unwrap();
638        assert!(statuses.iter().any(|s| s.kind == FileStatusKind::StagedNew));
639    }
640
641    #[test]
642    fn test_unstage_file_in_repo_unstages_file() {
643        let (temp_dir, repo) = init_test_repo();
644        fs::write(temp_dir.path().join("new_file.txt"), "new content").unwrap();
645
646        stage_file_in_repo(&repo, "new_file.txt").unwrap();
647        unstage_file_in_repo(&repo, "new_file.txt").unwrap();
648
649        let statuses = get_status_from_repo(&repo).unwrap();
650        assert!(!statuses.iter().any(|s| s.kind == FileStatusKind::StagedNew));
651        assert!(statuses.iter().any(|s| s.kind == FileStatusKind::Untracked));
652    }
653
654    #[test]
655    fn test_create_commit_in_repo_creates_commit() {
656        let (temp_dir, repo) = init_test_repo();
657        fs::write(temp_dir.path().join("new_file.txt"), "new content").unwrap();
658        stage_file_in_repo(&repo, "new_file.txt").unwrap();
659
660        create_commit_in_repo(&repo, "Test commit").unwrap();
661
662        let events = load_events_from_repo(&repo, 10).unwrap();
663        assert!(events.iter().any(|e| e.message == "Test commit"));
664    }
665
666    #[test]
667    fn test_has_staged_files_in_repo_returns_false_when_empty() {
668        let (_temp_dir, repo) = init_test_repo();
669        assert!(!has_staged_files_in_repo(&repo).unwrap());
670    }
671
672    #[test]
673    fn test_has_staged_files_in_repo_returns_true_when_staged() {
674        let (temp_dir, repo) = init_test_repo();
675        fs::write(temp_dir.path().join("new_file.txt"), "new content").unwrap();
676        stage_file_in_repo(&repo, "new_file.txt").unwrap();
677
678        assert!(has_staged_files_in_repo(&repo).unwrap());
679    }
680}