Skip to content

Commit 8131964

Browse files
committed
feat(git): add per-file revert button in Source Control
- Add git:revert socket handler to restore files from HEAD or delete untracked files - Add revert button (↩) to each file in ChangesPanel, visible on hover - Button turns red on hover to indicate destructive action
1 parent a5e68e1 commit 8131964

5 files changed

Lines changed: 107 additions & 1 deletion

File tree

anycode-backend/src/handlers/git_handler.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ pub struct GitCommitRequest {
3030
pub message: String,
3131
}
3232

33+
#[derive(Debug, Serialize, Deserialize, Clone)]
34+
pub struct GitRevertRequest {
35+
pub path: String,
36+
}
37+
3338
fn git_status_impl() -> Result<Value> {
3439
let workdir = crate::utils::current_dir();
3540

@@ -332,3 +337,56 @@ pub async fn handle_git_pull(ack: AckSender, _state: State<AppState>) {
332337
info!("Received git:pull");
333338
send_response(ack, git_pull_impl());
334339
}
340+
341+
fn git_revert_impl(request: &GitRevertRequest) -> Result<Value> {
342+
let workdir = crate::utils::current_dir();
343+
let repo = Repository::discover(&workdir)?;
344+
let repo_root = repo.workdir().unwrap_or(Path::new("."));
345+
346+
// Convert to relative path
347+
let file_path = Path::new(&request.path);
348+
let relative_path = if file_path.is_absolute() {
349+
file_path.strip_prefix(repo_root).unwrap_or(file_path)
350+
} else {
351+
file_path
352+
};
353+
354+
// Check file status to know if it's tracked or untracked
355+
let mut opts = StatusOptions::new();
356+
opts.include_untracked(true)
357+
.pathspec(&request.path);
358+
359+
let statuses = repo.statuses(Some(&mut opts))?;
360+
let is_new_file = statuses.iter().any(|entry| {
361+
entry.status().contains(Status::WT_NEW) ||
362+
entry.status().contains(Status::INDEX_NEW)
363+
});
364+
365+
if is_new_file {
366+
// For new/untracked files, just delete
367+
let full_path = repo_root.join(relative_path);
368+
if full_path.exists() {
369+
std::fs::remove_file(&full_path)
370+
.context("Failed to delete untracked file")?;
371+
}
372+
info!("Git revert: deleted untracked file {}", request.path);
373+
} else {
374+
// For tracked files, restore from HEAD
375+
let mut checkout_opts = git2::build::CheckoutBuilder::new();
376+
checkout_opts.path(relative_path).force();
377+
repo.checkout_head(Some(&mut checkout_opts))
378+
.context("Failed to restore file from HEAD")?;
379+
info!("Git revert: restored {} from HEAD", request.path);
380+
}
381+
382+
Ok(json!({}))
383+
}
384+
385+
pub async fn handle_git_revert(
386+
Data(request): Data<GitRevertRequest>,
387+
ack: AckSender,
388+
_state: State<AppState>,
389+
) {
390+
info!("Received git:revert: {:?}", request.path);
391+
send_response(ack, git_revert_impl(&request));
392+
}

anycode-backend/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ async fn on_connect(socket: SocketRef, state: State<AppState>) {
8787
socket.on("git:commit", handle_git_commit);
8888
socket.on("git:push", handle_git_push);
8989
socket.on("git:pull", handle_git_pull);
90+
socket.on("git:revert", handle_git_revert);
9091

9192
socket.on_disconnect(on_disconnect)
9293
}

anycode/App.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,21 @@ const App: React.FC = () => {
881881
}
882882
}, [isConnected, fetchGitStatus]);
883883

884+
const handleGitRevert = useCallback((path: string) => {
885+
console.log('handleGitRevert', path);
886+
if (wsRef.current && isConnected) {
887+
wsRef.current.emit('git:revert', { path }, (response: any) => {
888+
if (response.success) {
889+
console.log('Revert successful');
890+
fetchGitStatus();
891+
} else {
892+
alert('Revert failed: ' + response.error);
893+
console.error('Revert failed:', response.error);
894+
}
895+
});
896+
}
897+
}, [isConnected, fetchGitStatus]);
898+
884899

885900
const handleOpenFileResponse = (path: string, content: string, history: { changes: Change[], index: number }) => {
886901
const fileName = getFileName(path);
@@ -1809,6 +1824,7 @@ const App: React.FC = () => {
18091824
onCommit={handleGitCommit}
18101825
onPush={handleGitPush}
18111826
onPull={handleGitPull}
1827+
onRevert={handleGitRevert}
18121828
/>
18131829
);
18141830
case 'files':

anycode/components/ChangesPanel.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,28 @@
168168
background-color: #2a2a2a;
169169
}
170170

171+
.changes-revert-btn {
172+
background: none;
173+
border: none;
174+
color: #888;
175+
font-size: 14px;
176+
cursor: pointer;
177+
padding: 2px 6px;
178+
border-radius: 3px;
179+
opacity: 0;
180+
transition: opacity 0.1s ease, color 0.1s ease, background-color 0.1s ease;
181+
flex-shrink: 0;
182+
}
183+
184+
.changes-item:hover .changes-revert-btn {
185+
opacity: 1;
186+
}
187+
188+
.changes-revert-btn:hover {
189+
color: #ea4335;
190+
background-color: rgba(234, 67, 53, 0.15);
191+
}
192+
171193

172194
.changes-checkbox {
173195
width: 16px;

anycode/components/ChangesPanel.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface ChangesPanelProps {
1414
onCommit: (files: string[], message: string) => void;
1515
onPush: () => void;
1616
onPull: () => void;
17+
onRevert: (path: string) => void;
1718
}
1819

1920
const statusIcons: Record<string, string> = {
@@ -50,7 +51,8 @@ export const ChangesPanel: React.FC<ChangesPanelProps> = ({
5051
onRefresh,
5152
onCommit,
5253
onPush,
53-
onPull
54+
onPull,
55+
onRevert
5456
}) => {
5557
const [message, setMessage] = useState('');
5658
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
@@ -198,6 +200,13 @@ export const ChangesPanel: React.FC<ChangesPanelProps> = ({
198200
{getDirectory(file.path)}
199201
</span>
200202
</div>
203+
<button
204+
className="changes-revert-btn"
205+
onClick={(e) => { e.stopPropagation(); onRevert(file.path); }}
206+
title="Discard Changes"
207+
>
208+
209+
</button>
201210
<div
202211
className={`changes-checkbox ${selectedFiles.has(file.path) ? 'checked' : ''}`}
203212
onClick={(e) => toggleFile(file.path, e)}

0 commit comments

Comments
 (0)