Skip to content

Commit 054d4ba

Browse files
authored
Merge pull request #10457 from gitbutlerapp/kv-branch-55
Implement but oplog
2 parents fa1b7c9 + f1b836a commit 054d4ba

File tree

3 files changed

+131
-0
lines changed

3 files changed

+131
-0
lines changed

crates/but/src/args.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ For examples see `but rub --help`."
8484
/// Commit ID to edit the message for
8585
commit: String,
8686
},
87+
/// Show operation history (last 20 entries).
88+
Oplog {
89+
/// Start from this oplog SHA instead of the head
90+
#[clap(long)]
91+
since: Option<String>,
92+
},
8793
/// Starts up the MCP server.
8894
Mcp {
8995
/// Starts the internal MCP server which has more granular tools.
@@ -123,6 +129,8 @@ pub enum CommandName {
123129
Commit,
124130
#[clap(alias = "describe")]
125131
Describe,
132+
#[clap(alias = "oplog")]
133+
Oplog,
126134
BaseCheck,
127135
BaseUpdate,
128136
BranchNew,

crates/but/src/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mod mark;
1818
mod mcp;
1919
mod mcp_internal;
2020
mod metrics;
21+
mod oplog;
2122
mod rub;
2223
mod status;
2324

@@ -172,6 +173,11 @@ async fn main() -> Result<()> {
172173
metrics_if_configured(app_settings, CommandName::Describe, props(start, &result)).ok();
173174
result
174175
}
176+
Subcommands::Oplog { since } => {
177+
let result = oplog::show_oplog(&args.current_dir, args.json, since.as_deref());
178+
metrics_if_configured(app_settings, CommandName::Oplog, props(start, &result)).ok();
179+
result
180+
}
175181
Subcommands::Init { repo } => init::repo(&args.current_dir, args.json, *repo)
176182
.context("Failed to initialize GitButler project."),
177183
}

crates/but/src/oplog/mod.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use colored::Colorize;
2+
use gitbutler_oplog::entry::OperationKind;
3+
use gitbutler_project::Project;
4+
use std::path::Path;
5+
6+
pub(crate) fn show_oplog(repo_path: &Path, json: bool, since: Option<&str>) -> anyhow::Result<()> {
7+
let project = Project::find_by_path(repo_path)?;
8+
9+
let snapshots = if let Some(since_sha) = since {
10+
// Get all snapshots first to find the starting point
11+
let all_snapshots = but_api::undo::list_snapshots(project.id, 1000, None, None)?; // Get a large number to find the SHA
12+
let mut found_index = None;
13+
14+
// Find the snapshot that matches the since SHA (partial match supported)
15+
for (index, snapshot) in all_snapshots.iter().enumerate() {
16+
let snapshot_sha = snapshot.commit_id.to_string();
17+
if snapshot_sha.starts_with(since_sha) {
18+
found_index = Some(index);
19+
break;
20+
}
21+
}
22+
23+
match found_index {
24+
Some(index) => {
25+
// Take 20 entries starting from the found index
26+
all_snapshots.into_iter().skip(index).take(20).collect()
27+
}
28+
None => {
29+
return Err(anyhow::anyhow!(
30+
"No oplog entry found matching SHA: {}",
31+
since_sha
32+
));
33+
}
34+
}
35+
} else {
36+
but_api::undo::list_snapshots(project.id, 20, None, None)?
37+
};
38+
39+
if snapshots.is_empty() {
40+
if json {
41+
println!("[]");
42+
} else {
43+
println!("No operations found in history.");
44+
}
45+
return Ok(());
46+
}
47+
48+
if json {
49+
// Output JSON format
50+
let json_output = serde_json::to_string_pretty(&snapshots)?;
51+
println!("{json_output}");
52+
} else {
53+
// Output human-readable format
54+
println!("{}", "Operations History".blue().bold());
55+
println!("{}", "─".repeat(50).dimmed());
56+
57+
for snapshot in snapshots {
58+
let time_string = chrono::DateTime::from_timestamp(snapshot.created_at.seconds(), 0)
59+
.ok_or(anyhow::anyhow!("Could not parse timestamp"))?
60+
.format("%Y-%m-%d %H:%M:%S")
61+
.to_string();
62+
63+
let commit_id = format!(
64+
"{}{}",
65+
&snapshot.commit_id.to_string()[..7].blue().underline(),
66+
&snapshot.commit_id.to_string()[7..12].blue().dimmed()
67+
);
68+
69+
let (operation_type, title) = if let Some(details) = &snapshot.details {
70+
let op_type = match details.operation {
71+
OperationKind::CreateCommit => "CREATE",
72+
OperationKind::CreateBranch => "BRANCH",
73+
OperationKind::AmendCommit => "AMEND",
74+
OperationKind::UndoCommit => "UNDO",
75+
OperationKind::SquashCommit => "SQUASH",
76+
OperationKind::UpdateCommitMessage => "REWORD",
77+
OperationKind::MoveCommit => "MOVE",
78+
OperationKind::RestoreFromSnapshot => "RESTORE",
79+
OperationKind::ReorderCommit => "REORDER",
80+
OperationKind::InsertBlankCommit => "INSERT",
81+
OperationKind::MoveHunk => "MOVE_HUNK",
82+
OperationKind::ReorderBranches => "REORDER_BRANCH",
83+
OperationKind::UpdateWorkspaceBase => "UPDATE_BASE",
84+
OperationKind::UpdateBranchName => "RENAME",
85+
OperationKind::GenericBranchUpdate => "BRANCH_UPDATE",
86+
OperationKind::ApplyBranch => "APPLY",
87+
OperationKind::UnapplyBranch => "UNAPPLY",
88+
OperationKind::DeleteBranch => "DELETE",
89+
OperationKind::DiscardChanges => "DISCARD",
90+
_ => "OTHER",
91+
};
92+
(op_type, details.title.clone())
93+
} else {
94+
("UNKNOWN", "Unknown operation".to_string())
95+
};
96+
97+
let operation_colored = match operation_type {
98+
"CREATE" => operation_type.green(),
99+
"AMEND" | "REWORD" => operation_type.yellow(),
100+
"UNDO" | "RESTORE" => operation_type.red(),
101+
"BRANCH" | "CHECKOUT" => operation_type.purple(),
102+
"MOVE" | "REORDER" | "MOVE_HUNK" => operation_type.cyan(),
103+
_ => operation_type.normal(),
104+
};
105+
106+
println!(
107+
"{} {} [{}] {}",
108+
commit_id,
109+
time_string.dimmed(),
110+
operation_colored,
111+
title
112+
);
113+
}
114+
}
115+
116+
Ok(())
117+
}

0 commit comments

Comments
 (0)