Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,14 +394,14 @@

**ゴール**: コミット位置の操作と誤操作からの復元ができる

- [ ] Reset
- [ ] Soft Reset(コミットのみ取り消し、変更はステージに残る)
- [ ] Mixed Reset(コミットとステージを取り消し、変更はワーキングツリーに残る)
- [ ] Hard Reset(すべて取り消し)※確認ダイアログ必須
- [ ] ファイル単位の Reset(特定ファイルを特定コミットの状態に戻す)
- [ ] Reflog
- [ ] HEAD / ブランチの移動履歴表示
- [ ] Reflog エントリからの復元操作(失われたコミットの救出)
- [x] Reset
- [x] Soft Reset(コミットのみ取り消し、変更はステージに残る)
- [x] Mixed Reset(コミットとステージを取り消し、変更はワーキングツリーに残る)
- [x] Hard Reset(すべて取り消し)※確認ダイアログ必須
- [x] ファイル単位の Reset(特定ファイルを特定コミットの状態に戻す)
- [x] Reflog
- [x] HEAD / ブランチの移動履歴表示
- [x] Reflog エントリからの復元操作(失われたコミットの救出)

**完動品としての価値**: Reset で柔軟にコミット位置を操作しつつ、Reflog で誤操作からも復帰できる

Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod history;
pub mod hosting;
pub mod rebase;
pub mod remote;
pub mod reset;
pub mod revert;
pub mod stash;
pub mod tag;
44 changes: 44 additions & 0 deletions src-tauri/src/commands/reset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use tauri::State;

use crate::git::types::{ReflogEntry, ResetMode, ResetResult};
use crate::state::AppState;

#[tauri::command]
pub fn reset(
oid: String,
mode: ResetMode,
state: State<'_, AppState>,
) -> Result<ResetResult, String> {
let repo_lock = state
.repo
.lock()
.map_err(|e| format!("Lock poisoned: {e}"))?;
let backend = repo_lock.as_ref().ok_or("No repository opened")?;
backend.reset(&oid, mode).map_err(|e| e.to_string())
}

#[tauri::command]
pub fn reset_file(path: String, oid: String, state: State<'_, AppState>) -> Result<(), String> {
let repo_lock = state
.repo
.lock()
.map_err(|e| format!("Lock poisoned: {e}"))?;
let backend = repo_lock.as_ref().ok_or("No repository opened")?;
backend.reset_file(&path, &oid).map_err(|e| e.to_string())
}

#[tauri::command]
pub fn get_reflog(
ref_name: String,
limit: usize,
state: State<'_, AppState>,
) -> Result<Vec<ReflogEntry>, String> {
let repo_lock = state
.repo
.lock()
.map_err(|e| format!("Lock poisoned: {e}"))?;
let backend = repo_lock.as_ref().ok_or("No repository opened")?;
backend
.get_reflog(&ref_name, limit)
.map_err(|e| e.to_string())
}
11 changes: 9 additions & 2 deletions src-tauri/src/git/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use crate::git::types::{
BlameResult, BranchInfo, CherryPickMode, CherryPickResult, CommitDetail, CommitInfo,
CommitLogResult, CommitResult, ConflictFile, ConflictResolution, DiffOptions, FetchResult,
FileDiff, HunkIdentifier, LineRange, LogFilter, MergeBaseContent, MergeOption, MergeResult,
PullOption, PushResult, RebaseResult, RebaseState, RebaseTodoEntry, RemoteInfo, RepoStatus,
RevertMode, RevertResult, StashEntry, TagInfo,
PullOption, PushResult, RebaseResult, RebaseState, RebaseTodoEntry, ReflogEntry, RemoteInfo,
RepoStatus, ResetMode, ResetResult, RevertMode, RevertResult, StashEntry, TagInfo,
};

pub trait GitBackend: Send + Sync {
Expand Down Expand Up @@ -101,4 +101,11 @@ pub trait GitBackend: Send + Sync {
fn is_reverting(&self) -> GitResult<bool>;
fn abort_revert(&self) -> GitResult<()>;
fn continue_revert(&self) -> GitResult<RevertResult>;

// Reset operations
fn reset(&self, oid: &str, mode: ResetMode) -> GitResult<ResetResult>;
fn reset_file(&self, path: &str, oid: &str) -> GitResult<()>;

// Reflog operations
fn get_reflog(&self, ref_name: &str, limit: usize) -> GitResult<Vec<ReflogEntry>>;
}
6 changes: 6 additions & 0 deletions src-tauri/src/git/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ pub enum GitError {

#[error("failed to revert: {0}")]
RevertFailed(#[source] Box<dyn std::error::Error + Send + Sync>),

#[error("failed to reset: {0}")]
ResetFailed(#[source] Box<dyn std::error::Error + Send + Sync>),

#[error("failed to read reflog: {0}")]
ReflogFailed(#[source] Box<dyn std::error::Error + Send + Sync>),
}

pub type GitResult<T> = Result<T, GitError>;
91 changes: 89 additions & 2 deletions src-tauri/src/git/git2_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use crate::git::types::{
DiffHunk, DiffLine, DiffLineKind, DiffOptions, FetchResult, FileDiff, FileStatus,
FileStatusKind, GraphEdge, GraphNodeType, HunkIdentifier, LineRange, LogFilter,
MergeBaseContent, MergeKind, MergeOption, MergeResult, PullOption, PushResult, RebaseAction,
RebaseResult, RebaseState, RebaseTodoEntry, RemoteInfo, RepoStatus, RevertMode, RevertResult,
StagingState, StashEntry, TagInfo, WordSegment,
RebaseResult, RebaseState, RebaseTodoEntry, ReflogEntry, RemoteInfo, RepoStatus, ResetMode,
ResetResult, RevertMode, RevertResult, StagingState, StashEntry, TagInfo, WordSegment,
};

pub struct Git2Backend {
Expand Down Expand Up @@ -1950,6 +1950,84 @@ impl GitBackend for Git2Backend {
oid: Some(oid.to_string()),
})
}

fn reset(&self, oid_str: &str, mode: ResetMode) -> GitResult<ResetResult> {
let repo = self.repo.lock().unwrap();

let oid = Oid::from_str(oid_str).map_err(|e| GitError::ResetFailed(Box::new(e)))?;
let commit = repo
.find_commit(oid)
.map_err(|e| GitError::ResetFailed(Box::new(e)))?;

let reset_type = match mode {
ResetMode::Soft => git2::ResetType::Soft,
ResetMode::Mixed => git2::ResetType::Mixed,
ResetMode::Hard => git2::ResetType::Hard,
};

repo.reset(commit.as_object(), reset_type, None)
.map_err(|e| GitError::ResetFailed(Box::new(e)))?;

Ok(ResetResult {
oid: oid_str.to_string(),
})
}

fn reset_file(&self, path: &str, oid_str: &str) -> GitResult<()> {
let repo = self.repo.lock().unwrap();

let oid = Oid::from_str(oid_str).map_err(|e| GitError::ResetFailed(Box::new(e)))?;
let commit = repo
.find_commit(oid)
.map_err(|e| GitError::ResetFailed(Box::new(e)))?;

repo.reset_default(Some(commit.as_object()), [path])
.map_err(|e| GitError::ResetFailed(Box::new(e)))?;

Ok(())
}

fn get_reflog(&self, ref_name: &str, limit: usize) -> GitResult<Vec<ReflogEntry>> {
let repo = self.repo.lock().unwrap();

let reflog = repo
.reflog(ref_name)
.map_err(|e| GitError::ReflogFailed(Box::new(e)))?;

let mut entries = Vec::new();
for i in 0..reflog.len() {
if entries.len() >= limit {
break;
}
let entry = reflog.get(i).ok_or_else(|| {
GitError::ReflogFailed(format!("reflog entry {i} not found").into())
})?;

let old_oid = entry.id_old().to_string();
let new_oid = entry.id_new().to_string();
let new_short_oid = new_oid[..7.min(new_oid.len())].to_string();

let raw_message = entry.message().unwrap_or("").to_string();
let (action, message) = parse_reflog_message(&raw_message);

let committer = entry.committer();
let committer_name = committer.name().unwrap_or("").to_string();
let committer_date = committer.when().seconds();

entries.push(ReflogEntry {
index: i,
old_oid,
new_oid,
new_short_oid,
action,
message,
committer_name,
committer_date,
});
}

Ok(entries)
}
}

impl Git2Backend {
Expand Down Expand Up @@ -2667,6 +2745,15 @@ fn compute_word_diff_pair(
(del_segments, add_segments)
}

/// Parse reflog message into action and description.
/// Format: "action: description" (e.g. "commit: initial commit", "checkout: moving from main to feature")
fn parse_reflog_message(message: &str) -> (String, String) {
match message.find(": ") {
Some(pos) => (message[..pos].to_string(), message[pos + 2..].to_string()),
None => (message.to_string(), String::new()),
}
}

/// Parse branch name from git stash message.
/// Format: "WIP on <branch>: ..." or "On <branch>: <msg>"
fn parse_stash_branch_name(message: &str) -> String {
Expand Down
29 changes: 29 additions & 0 deletions src-tauri/src/git/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,35 @@ pub struct CherryPickResult {
pub oid: Option<String>,
}

// === Reset types ===

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResetMode {
Soft,
Mixed,
Hard,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResetResult {
pub oid: String,
}

// === Reflog types ===

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReflogEntry {
pub index: usize,
pub old_oid: String,
pub new_oid: String,
pub new_short_oid: String,
pub action: String,
pub message: String,
pub committer_name: String,
pub committer_date: i64,
}

// === Revert types ===

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ pub fn run() {
commands::revert::is_reverting,
commands::revert::abort_revert,
commands::revert::continue_revert,
commands::reset::reset,
commands::reset::reset_file,
commands::reset::get_reflog,
commands::ai::detect_cli_adapters,
commands::ai::generate_commit_message,
commands::ai::review_diff,
Expand Down
Loading