Skip to content

Commit affbc63

Browse files
committed
chore: 更新 10 个文件
1 parent b9a6421 commit affbc63

10 files changed

Lines changed: 276 additions & 7 deletions

File tree

src-tauri/src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
pub mod repo;
22
pub mod commit;
3+
pub mod stash;
34

45
pub use repo::{scan_repositories, get_repo_status, get_branch_info, get_commit_history, get_local_branches, switch_branch, publish_branch, push_branch, get_git_username, delete_branch, rename_branch, create_branch, get_file_diff, merge_branch};
56
pub use commit::{stage_files, unstage_files, stage_all, unstage_all, commit, revoke_latest_commit, batch_commit, generate_commit_message, review_code};
7+
pub use stash::{get_stash_list, stash_save, stash_apply, stash_pop, stash_drop};

src-tauri/src/commands/stash.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use crate::domain::StashInfo;
2+
use crate::error::{AppError, Result};
3+
use git2::{Repository, StashApplyOptions, StashFlags};
4+
5+
#[tauri::command]
6+
pub async fn get_stash_list(path: String) -> std::result::Result<Vec<StashInfo>, String> {
7+
get_stash_list_impl(&path).map_err(|e: AppError| e.to_string())
8+
}
9+
10+
fn get_stash_list_impl(path: &str) -> Result<Vec<StashInfo>> {
11+
let mut repo = Repository::open(path)?;
12+
let mut stashes = Vec::new();
13+
14+
repo.stash_foreach(|index, message, id| {
15+
stashes.push(StashInfo {
16+
index,
17+
message: message.to_string(),
18+
id: id.to_string(),
19+
});
20+
true
21+
})?;
22+
23+
Ok(stashes)
24+
}
25+
26+
#[tauri::command]
27+
pub async fn stash_save(
28+
path: String,
29+
message: Option<String>,
30+
include_untracked: bool,
31+
) -> std::result::Result<(), String> {
32+
stash_save_impl(&path, message.as_deref(), include_untracked).map_err(|e: AppError| e.to_string())
33+
}
34+
35+
fn stash_save_impl(path: &str, message: Option<&str>, include_untracked: bool) -> Result<()> {
36+
let mut repo = Repository::open(path)?;
37+
let signature = repo.signature()?;
38+
39+
let mut flags = StashFlags::DEFAULT;
40+
if include_untracked {
41+
flags |= StashFlags::INCLUDE_UNTRACKED;
42+
}
43+
44+
repo.stash_save(&signature, message.unwrap_or(""), Some(flags))?;
45+
Ok(())
46+
}
47+
48+
#[tauri::command]
49+
pub async fn stash_apply(path: String, index: usize) -> std::result::Result<(), String> {
50+
stash_apply_impl(&path, index).map_err(|e: AppError| e.to_string())
51+
}
52+
53+
fn stash_apply_impl(path: &str, index: usize) -> Result<()> {
54+
let mut repo = Repository::open(path)?;
55+
let mut opts = StashApplyOptions::new();
56+
repo.stash_apply(index, Some(&mut opts))?;
57+
Ok(())
58+
}
59+
60+
#[tauri::command]
61+
pub async fn stash_pop(path: String, index: usize) -> std::result::Result<(), String> {
62+
stash_pop_impl(&path, index).map_err(|e: AppError| e.to_string())
63+
}
64+
65+
fn stash_pop_impl(path: &str, index: usize) -> Result<()> {
66+
let mut repo = Repository::open(path)?;
67+
let mut opts = StashApplyOptions::new();
68+
repo.stash_pop(index, Some(&mut opts))?;
69+
Ok(())
70+
}
71+
72+
#[tauri::command]
73+
pub async fn stash_drop(path: String, index: usize) -> std::result::Result<(), String> {
74+
stash_drop_impl(&path, index).map_err(|e: AppError| e.to_string())
75+
}
76+
77+
fn stash_drop_impl(path: &str, index: usize) -> Result<()> {
78+
let mut repo = Repository::open(path)?;
79+
repo.stash_drop(index)?;
80+
Ok(())
81+
}

src-tauri/src/domain/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
pub mod repository;
22
pub mod status;
33

4-
pub use repository::{BranchInfo, BatchCommitResult, BatchFailure, CommitInfo, LocalBranch, RepositoryInfo};
4+
pub use repository::{BranchInfo, BatchCommitResult, BatchFailure, CommitInfo, LocalBranch, RepositoryInfo, StashInfo};
55
pub use status::{CommitSuggestion, CommitType, RepoStatus, StatusItem};

src-tauri/src/domain/repository.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,11 @@ pub struct BatchFailure {
5858
pub path: String,
5959
pub error: String,
6060
}
61+
62+
#[derive(Debug, Clone, Serialize, Deserialize)]
63+
#[serde(rename_all = "camelCase")]
64+
pub struct StashInfo {
65+
pub index: usize,
66+
pub message: String,
67+
pub id: String,
68+
}

src-tauri/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ pub fn run() {
3636
batch_commit,
3737
generate_commit_message,
3838
review_code,
39+
// Stash commands
40+
get_stash_list,
41+
stash_save,
42+
stash_apply,
43+
stash_pop,
44+
stash_drop,
3945
])
4046
.run(tauri::generate_context!())
4147
.expect("error while running tauri application");

src/components/CommitPanel.tsx

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useRepoStore } from '../store/repoStore';
22
import { useSettingsStore } from '../store/settingsStore';
33
import { Button } from './ui/Button';
4-
import { Sparkles, Bot, X } from 'lucide-react';
4+
import { Sparkles, Bot, X, Archive } from 'lucide-react';
55
import { useState } from 'react';
66
import Markdown from 'react-markdown';
77
import { cn } from '../lib/utils';
@@ -12,7 +12,7 @@ interface CommitPanelProps {
1212
}
1313

1414
export function CommitPanel({ repoPath, mode }: CommitPanelProps) {
15-
const { currentStatus, commit, batchCommit, generateCommitMessage, reviewCode } = useRepoStore();
15+
const { currentStatus, commit, batchCommit, generateCommitMessage, reviewCode, stashSave } = useRepoStore();
1616
const { commitLanguage } = useSettingsStore();
1717
const [message, setMessage] = useState('');
1818
const [isCommitting, setIsCommitting] = useState(false);
@@ -76,6 +76,22 @@ export function CommitPanel({ repoPath, mode }: CommitPanelProps) {
7676
}
7777
};
7878

79+
const [isStashing, setIsStashing] = useState(false);
80+
const handleStash = async () => {
81+
const paths = Array.isArray(repoPath) ? repoPath : [repoPath];
82+
const path = paths[0];
83+
84+
setIsStashing(true);
85+
try {
86+
await stashSave(path, message || undefined, true);
87+
setMessage('');
88+
} catch (e) {
89+
console.error('Stash failed:', e);
90+
} finally {
91+
setIsStashing(false);
92+
}
93+
};
94+
7995
const getPlaceholder = () => {
8096
if (commitLanguage === 'zh') {
8197
return 'feat: 添加新功能';
@@ -177,9 +193,9 @@ export function CommitPanel({ repoPath, mode }: CommitPanelProps) {
177193
</div>
178194
</div>
179195

180-
<div className="flex flex-col gap-2">
196+
<div className="flex gap-2">
181197
<Button
182-
className="w-full h-9 font-medium text-xs rounded-lg shadow-sm active:scale-[0.99] transition-all duration-200"
198+
className="flex-1 h-9 font-medium text-xs rounded-lg shadow-sm active:scale-[0.99] transition-all duration-200"
183199
onClick={handleCommit}
184200
disabled={!hasStaged || !message.trim() || isCommitting}
185201
>
@@ -192,6 +208,22 @@ export function CommitPanel({ repoPath, mode }: CommitPanelProps) {
192208
mode === 'single' ? '确认提交' : `批量提交 (${Array.isArray(repoPath) ? repoPath.length : 1} 个项目)`
193209
)}
194210
</Button>
211+
212+
{mode === 'single' && (
213+
<Button
214+
variant="outline"
215+
className="h-9 px-3 font-medium text-xs rounded-lg shadow-sm active:scale-[0.99] transition-all duration-200 border-border/60 hover:bg-secondary/50"
216+
onClick={handleStash}
217+
disabled={isStashing || (!currentStatus?.unstaged.length && !currentStatus?.untracked.length && !currentStatus?.staged.length)}
218+
title="贮存当前更改"
219+
>
220+
{isStashing ? (
221+
<div className="w-3 h-3 border-2 border-primary/30 border-t-primary rounded-full animate-spin" />
222+
) : (
223+
<Archive className="w-4 h-4" />
224+
)}
225+
</Button>
226+
)}
195227
</div>
196228
</div>
197229
</div>

src/components/RepoView.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { FileList } from './FileList';
55
import { CommitPanel } from './CommitPanel';
66
import { BranchSelector } from './BranchSelector';
77
import { DiffView } from './DiffView';
8-
import { AlertCircle, Upload, RotateCcw, GitCommit, Download, GitGraph, Clock, FileDiff } from 'lucide-react';
8+
import { StashPanel } from './StashPanel';
9+
import { AlertCircle, Upload, RotateCcw, GitCommit, Download, GitGraph, Clock, FileDiff, Archive } from 'lucide-react';
910
import { Badge } from './ui/Badge';
1011
import { Button } from './ui/Button';
1112
import { useState, useEffect } from 'react';
@@ -19,7 +20,7 @@ interface RepoViewProps {
1920
repoPath: string;
2021
}
2122

22-
type ViewMode = 'changes' | 'history';
23+
type ViewMode = 'changes' | 'history' | 'stashes';
2324

2425
export function RepoView({ repoPath }: RepoViewProps) {
2526
const {
@@ -216,6 +217,18 @@ export function RepoView({ repoPath }: RepoViewProps) {
216217
<Clock className="w-3.5 h-3.5" />
217218
历史
218219
</button>
220+
<button
221+
onClick={() => setViewMode('stashes')}
222+
className={cn(
223+
"flex items-center gap-1.5 px-3 py-1 text-xs font-medium rounded-md transition-all",
224+
viewMode === 'stashes'
225+
? "bg-background shadow-sm text-foreground"
226+
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
227+
)}
228+
>
229+
<Archive className="w-3.5 h-3.5" />
230+
贮存
231+
</button>
219232
</div>
220233

221234
<div className="flex items-center gap-3">
@@ -392,6 +405,13 @@ export function RepoView({ repoPath }: RepoViewProps) {
392405
</div>
393406
)}
394407

408+
{/* Stashes View */}
409+
{viewMode === 'stashes' && (
410+
<div className="absolute inset-0 animate-in fade-in zoom-in-95 duration-200">
411+
<StashPanel />
412+
</div>
413+
)}
414+
395415
</div>
396416
</div>
397417
);

src/components/StashPanel.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { useRepoStore } from '../store/repoStore';
2+
import { Button } from './ui/Button';
3+
import { Card } from './ui/Card';
4+
import { Archive, Play, CornerUpLeft, Trash2 } from 'lucide-react';
5+
6+
export const StashPanel: React.FC = () => {
7+
const { selectedRepoPath, stashes, stashApply, stashPop, stashDrop } = useRepoStore();
8+
9+
if (!selectedRepoPath) return null;
10+
11+
return (
12+
<div className="flex flex-col h-full bg-background/50 backdrop-blur-sm border-l border-border/50">
13+
<div className="p-4 border-b border-border/40 flex items-center gap-2">
14+
<Archive className="w-4 h-4 text-primary" />
15+
<h2 className="text-sm font-semibold">Stashes</h2>
16+
</div>
17+
18+
<div className="flex-1 overflow-y-auto p-4 space-y-3">
19+
{stashes.length === 0 ? (
20+
<div className="flex flex-col items-center justify-center h-40 text-muted-foreground">
21+
<Archive className="w-8 h-8 mb-2 opacity-20" />
22+
<p className="text-xs">No saved stashes</p>
23+
</div>
24+
) : (
25+
stashes.map((stash) => (
26+
<Card key={stash.id} className="p-3 bg-secondary/30 border-border/40 hover:border-primary/30 transition-colors">
27+
<div className="flex items-start justify-between gap-3">
28+
<div className="flex-1 min-w-0">
29+
<p className="text-sm font-medium truncate" title={stash.message}>
30+
{stash.message || `Stash @{${stash.index}}`}
31+
</p>
32+
<p className="text-[10px] text-muted-foreground font-mono mt-1">
33+
{stash.id.slice(0, 7)}
34+
</p>
35+
</div>
36+
</div>
37+
38+
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-border/20">
39+
<Button
40+
variant="ghost"
41+
size="sm"
42+
className="h-7 px-2 text-xs hover:text-primary"
43+
onClick={() => stashApply(selectedRepoPath, stash.index)}
44+
>
45+
<Play className="w-3 h-3 mr-1" />
46+
Apply
47+
</Button>
48+
<Button
49+
variant="ghost"
50+
size="sm"
51+
className="h-7 px-2 text-xs hover:text-primary"
52+
onClick={() => stashPop(selectedRepoPath, stash.index)}
53+
>
54+
<CornerUpLeft className="w-3 h-3 mr-1" />
55+
Pop
56+
</Button>
57+
<div className="flex-1" />
58+
<Button
59+
variant="ghost"
60+
size="sm"
61+
className="h-7 px-2 text-xs hover:text-destructive text-destructive/70"
62+
onClick={() => stashDrop(selectedRepoPath, stash.index)}
63+
>
64+
<Trash2 className="w-3 h-3" />
65+
</Button>
66+
</div>
67+
</Card>
68+
))
69+
)}
70+
</div>
71+
</div>
72+
);
73+
};

0 commit comments

Comments
 (0)