Skip to content

Commit 4aeb8fb

Browse files
authored
Merge pull request #10461 from gitbutlerapp/kv-branch-55
Implement but oplog restore
2 parents 054d4ba + 4ef9b45 commit 4aeb8fb

File tree

3 files changed

+93
-0
lines changed

3 files changed

+93
-0
lines changed

crates/but/src/args.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ For examples see `but rub --help`."
9090
#[clap(long)]
9191
since: Option<String>,
9292
},
93+
/// Restore to a specific oplog snapshot.
94+
Restore {
95+
/// Oplog SHA to restore to
96+
oplog_sha: String,
97+
},
9398
/// Starts up the MCP server.
9499
Mcp {
95100
/// Starts the internal MCP server which has more granular tools.
@@ -131,6 +136,8 @@ pub enum CommandName {
131136
Describe,
132137
#[clap(alias = "oplog")]
133138
Oplog,
139+
#[clap(alias = "restore")]
140+
Restore,
134141
BaseCheck,
135142
BaseUpdate,
136143
BranchNew,

crates/but/src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,11 @@ async fn main() -> Result<()> {
178178
metrics_if_configured(app_settings, CommandName::Oplog, props(start, &result)).ok();
179179
result
180180
}
181+
Subcommands::Restore { oplog_sha } => {
182+
let result = oplog::restore_to_oplog(&args.current_dir, args.json, oplog_sha);
183+
metrics_if_configured(app_settings, CommandName::Restore, props(start, &result)).ok();
184+
result
185+
}
181186
Subcommands::Init { repo } => init::repo(&args.current_dir, args.json, *repo)
182187
.context("Failed to initialize GitButler project."),
183188
}

crates/but/src/oplog/mod.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,84 @@ pub(crate) fn show_oplog(repo_path: &Path, json: bool, since: Option<&str>) -> a
115115

116116
Ok(())
117117
}
118+
119+
pub(crate) fn restore_to_oplog(
120+
repo_path: &Path,
121+
_json: bool,
122+
oplog_sha: &str,
123+
) -> anyhow::Result<()> {
124+
let project = Project::find_by_path(repo_path)?;
125+
126+
// Parse the oplog SHA (support partial SHAs)
127+
let commit_sha_string = if oplog_sha.len() >= 7 {
128+
// Try to find a snapshot that starts with this SHA
129+
let snapshots = but_api::undo::list_snapshots(project.id, 100, None, None)?;
130+
131+
let matching_snapshot = snapshots
132+
.iter()
133+
.find(|snapshot| snapshot.commit_id.to_string().starts_with(oplog_sha))
134+
.ok_or_else(|| anyhow::anyhow!("No oplog snapshot found matching '{}'", oplog_sha))?;
135+
136+
matching_snapshot.commit_id.to_string()
137+
} else {
138+
anyhow::bail!("Oplog SHA must be at least 7 characters long");
139+
};
140+
141+
// Get information about the target snapshot
142+
let snapshots = but_api::undo::list_snapshots(project.id, 100, None, None)?;
143+
let target_snapshot = snapshots
144+
.iter()
145+
.find(|snapshot| snapshot.commit_id.to_string() == commit_sha_string)
146+
.ok_or_else(|| anyhow::anyhow!("Snapshot {} not found in oplog", commit_sha_string))?;
147+
148+
let target_operation = target_snapshot
149+
.details
150+
.as_ref()
151+
.map(|d| d.title.as_str())
152+
.unwrap_or("Unknown operation");
153+
154+
let target_time = chrono::DateTime::from_timestamp(target_snapshot.created_at.seconds(), 0)
155+
.ok_or(anyhow::anyhow!("Could not parse timestamp"))?
156+
.format("%Y-%m-%d %H:%M:%S")
157+
.to_string();
158+
159+
println!("{}", "Restoring to oplog snapshot...".blue().bold());
160+
println!(
161+
" Target: {} ({})",
162+
target_operation.green(),
163+
target_time.dimmed()
164+
);
165+
println!(" Snapshot: {}", commit_sha_string[..7].cyan().underline());
166+
167+
// Confirm the restoration (safety check)
168+
println!(
169+
"\n{}",
170+
"⚠️ This will overwrite your current workspace state."
171+
.yellow()
172+
.bold()
173+
);
174+
print!("Continue with restore? [y/N]: ");
175+
use std::io::{self, Write};
176+
io::stdout().flush()?;
177+
178+
let mut input = String::new();
179+
io::stdin().read_line(&mut input)?;
180+
181+
let input = input.trim().to_lowercase();
182+
if input != "y" && input != "yes" {
183+
println!("{}", "Restore cancelled.".yellow());
184+
return Ok(());
185+
}
186+
187+
// Restore to the target snapshot using the but-api crate
188+
but_api::undo::restore_snapshot(project.id, commit_sha_string)?;
189+
190+
println!("\n{} Restore completed successfully!", "✓".green().bold(),);
191+
192+
println!(
193+
"{}",
194+
"\nWorkspace has been restored to the selected snapshot.".green()
195+
);
196+
197+
Ok(())
198+
}

0 commit comments

Comments
 (0)