Skip to content

Commit 062fc4f

Browse files
committed
feat: add cherry-pick command and status display
1 parent 76ce478 commit 062fc4f

19 files changed

+419
-4
lines changed

src/default_config.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,15 @@ revert_menu.revert_continue = ["c"]
227227
revert_menu.revert_commit = ["V"]
228228
revert_menu.quit = ["q", "esc"]
229229

230+
root.cherry_pick_menu = ["A"]
231+
cherry_pick_menu.--no-commit = ["-n"]
232+
cherry_pick_menu.--signoff = ["-s"]
233+
cherry_pick_menu.--edit = ["-e"]
234+
cherry_pick_menu.cherry_pick_abort = ["a"]
235+
cherry_pick_menu.cherry_pick_continue = ["c"]
236+
cherry_pick_menu.cherry_pick = ["A"]
237+
cherry_pick_menu.quit = ["q", "esc"]
238+
230239
root.stash_menu = ["z"]
231240
stash_menu.--all = ["-a"]
232241
stash_menu.--include-untracked = ["-u"]

src/git/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,34 @@ pub(crate) fn revert_status(repo: &Repository) -> Res<Option<RevertStatus>> {
111111
}
112112
}
113113

114+
#[derive(Debug, Clone)]
115+
pub(crate) struct CherryPickStatus {
116+
pub head: String,
117+
}
118+
119+
pub(crate) fn cherry_pick_status(repo: &Repository) -> Res<Option<CherryPickStatus>> {
120+
let dir = repo.workdir().expect("No workdir");
121+
let mut cherry_pick_head_file = dir.to_path_buf();
122+
cherry_pick_head_file.push(".git/CHERRY_PICK_HEAD");
123+
124+
match fs::read_to_string(&cherry_pick_head_file) {
125+
Ok(content) => {
126+
let head = content.trim().to_string();
127+
Ok(Some(CherryPickStatus {
128+
head: branch_name_lossy(dir, &head)?.unwrap_or(head[..7].to_string()),
129+
}))
130+
}
131+
Err(err) => {
132+
log::warn!(
133+
"Couldn't read {}, due to {}",
134+
cherry_pick_head_file.to_string_lossy(),
135+
err
136+
);
137+
Ok(None)
138+
}
139+
}
140+
}
141+
114142
fn branch_name_lossy(dir: &Path, hash: &str) -> Res<Option<String>> {
115143
let out = Command::new("git")
116144
.args(["for-each-ref", "--format", "%(objectname) %(refname:short)"])

src/item_data.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ pub(crate) enum SectionHeader {
149149
Rebase(String, String),
150150
Merge(String),
151151
Revert(String),
152+
CherryPick(String),
152153
Stashes,
153154
RecentCommits,
154155
Commit(String),

src/items.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ impl Item {
145145
SectionHeader::Rebase(head, onto) => format!("Rebasing {head} onto {onto}"),
146146
SectionHeader::Merge(head) => format!("Merging {head}"),
147147
SectionHeader::Revert(head) => format!("Reverting {head}"),
148+
SectionHeader::CherryPick(head) => format!("Cherry-picking {head}"),
148149
SectionHeader::Stashes => "Stashes".to_string(),
149150
SectionHeader::RecentCommits => "Recent commits".to_string(),
150151
SectionHeader::Commit(oid) => format!("commit {oid}"),

src/menu.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ pub(crate) enum Menu {
5151
#[serde(rename = "revert_menu")]
5252
#[strum(serialize = "revert_menu")]
5353
Revert,
54+
#[serde(rename = "cherry_pick_menu")]
55+
#[strum(serialize = "cherry_pick_menu")]
56+
CherryPick,
5457
#[serde(rename = "stash_menu")]
5558
#[strum(serialize = "stash_menu")]
5659
Stash,
@@ -81,6 +84,7 @@ impl PendingMenu {
8184
Menu::Remote => vec![],
8285
Menu::Reset => ops::reset::init_args(),
8386
Menu::Revert => ops::revert::init_args(),
87+
Menu::CherryPick => ops::cherry_pick::init_args(),
8488
Menu::Stash => ops::stash::init_args(),
8589
}
8690
.into_iter()

src/ops/cherry_pick.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use std::{process::Command, rc::Rc};
2+
3+
use crate::{
4+
Res,
5+
app::{App, PromptParams, State},
6+
item_data::{ItemData, Rev},
7+
menu::arg::Arg,
8+
term::Term,
9+
};
10+
11+
use super::{Action, OpTrait, selected_rev};
12+
13+
pub(crate) fn init_args() -> Vec<Arg> {
14+
vec![
15+
Arg::new_flag("--no-commit", "Don't commit", false),
16+
Arg::new_flag("--signoff", "Add Signed-off-by lines", false),
17+
Arg::new_flag("--edit", "Edit commit message", false),
18+
]
19+
}
20+
21+
pub(crate) struct CherryPickAbort;
22+
impl OpTrait for CherryPickAbort {
23+
fn get_action(&self, _target: &ItemData) -> Option<Action> {
24+
Some(Rc::new(|app: &mut App, term: &mut Term| {
25+
let mut cmd = Command::new("git");
26+
cmd.args(["cherry-pick", "--abort"]);
27+
app.run_cmd_interactive(term, cmd)?;
28+
Ok(())
29+
}))
30+
}
31+
32+
fn display(&self, _state: &State) -> String {
33+
"Abort".into()
34+
}
35+
}
36+
37+
pub(crate) struct CherryPickContinue;
38+
impl OpTrait for CherryPickContinue {
39+
fn get_action(&self, _target: &ItemData) -> Option<Action> {
40+
Some(Rc::new(|app: &mut App, term: &mut Term| {
41+
let mut cmd = Command::new("git");
42+
cmd.args(["cherry-pick", "--continue"]);
43+
app.run_cmd_interactive(term, cmd)?;
44+
Ok(())
45+
}))
46+
}
47+
48+
fn display(&self, _state: &State) -> String {
49+
"Continue".into()
50+
}
51+
}
52+
53+
pub(crate) struct CherryPick;
54+
impl OpTrait for CherryPick {
55+
fn get_action(&self, _target: &ItemData) -> Option<Action> {
56+
Some(Rc::new(move |app: &mut App, term: &mut Term| {
57+
let commit = app.prompt(
58+
term,
59+
&PromptParams {
60+
prompt: "Cherry-pick commit",
61+
create_default_value: Box::new(|app| {
62+
selected_rev(app)
63+
.as_ref()
64+
.map(Rev::shorthand)
65+
.map(String::from)
66+
}),
67+
..Default::default()
68+
},
69+
)?;
70+
71+
cherry_pick(app, term, &commit)?;
72+
Ok(())
73+
}))
74+
}
75+
76+
fn display(&self, _state: &State) -> String {
77+
"Cherry-pick commit(s)".into()
78+
}
79+
}
80+
81+
fn cherry_pick(app: &mut App, term: &mut Term, input: &str) -> Res<()> {
82+
let mut cmd = Command::new("git");
83+
cmd.arg("cherry-pick");
84+
cmd.args(app.state.pending_menu.as_ref().unwrap().args());
85+
cmd.arg(input);
86+
app.run_cmd_interactive(term, cmd)
87+
}

src/ops/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
use std::{fmt::Display, rc::Rc};
1111

1212
pub(crate) mod branch;
13+
pub(crate) mod cherry_pick;
1314
pub(crate) mod commit;
1415
pub(crate) mod copy_hash;
1516
pub(crate) mod discard;
@@ -93,6 +94,9 @@ pub(crate) enum Op {
9394
RevertAbort,
9495
RevertContinue,
9596
RevertCommit,
97+
CherryPickAbort,
98+
CherryPickContinue,
99+
CherryPick,
96100
Merge,
97101
MergeAbort,
98102
MergeContinue,
@@ -186,6 +190,9 @@ impl Op {
186190
Op::RevertAbort => Box::new(revert::RevertAbort),
187191
Op::RevertContinue => Box::new(revert::RevertContinue),
188192
Op::RevertCommit => Box::new(revert::RevertCommit),
193+
Op::CherryPickAbort => Box::new(cherry_pick::CherryPickAbort),
194+
Op::CherryPickContinue => Box::new(cherry_pick::CherryPickContinue),
195+
Op::CherryPick => Box::new(cherry_pick::CherryPick),
189196
Op::Show => Box::new(show::Show),
190197
Op::Stage => Box::new(stage::Stage),
191198
Op::Unstage => Box::new(unstage::Unstage),
@@ -218,6 +225,7 @@ impl Display for Menu {
218225
Menu::Rebase => "Rebase",
219226
Menu::Reset => "Reset",
220227
Menu::Revert => "Revert",
228+
Menu::CherryPick => "Cherry-pick",
221229
Menu::Stash => "Stash",
222230
})
223231
}

src/screen/status.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ enum SectionID {
1515
RebaseStatus,
1616
MergeStatus,
1717
RevertStatus,
18+
CherryPickStatus,
1819
Untracked,
1920
Stashes,
2021
RecentCommits,
@@ -29,6 +30,7 @@ impl Hash for SectionID {
2930
SectionID::RebaseStatus => "rebase_status",
3031
SectionID::MergeStatus => "merge_status",
3132
SectionID::RevertStatus => "revert_status",
33+
SectionID::CherryPickStatus => "cherry_pick_status",
3234
SectionID::Untracked => "untracked",
3335
SectionID::Stashes => "stashes",
3436
SectionID::RecentCommits => "recent_commits",
@@ -77,6 +79,13 @@ pub(crate) fn create(config: Arc<Config>, repo: Rc<Repository>, size: Size) -> R
7779
..Default::default()
7880
}]
7981
.into_iter()
82+
} else if let Some(cherry_pick) = git::cherry_pick_status(&repo)? {
83+
vec![Item {
84+
id: hash(SectionID::CherryPickStatus),
85+
data: ItemData::Header(SectionHeader::CherryPick(cherry_pick.head)),
86+
..Default::default()
87+
}]
88+
.into_iter()
8089
} else {
8190
branch_status_items(&status.branch_status)?.into_iter()
8291
}

src/tests/cherry_pick.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use super::*;
2+
3+
fn setup_cherry_pickable(ctx: TestContext) -> TestContext {
4+
run(&ctx.dir, &["git", "checkout", "-b", "other-branch"]);
5+
commit(&ctx.dir, "cherry-file", "cherry content");
6+
run(&ctx.dir, &["git", "checkout", "main"]);
7+
ctx
8+
}
9+
10+
fn setup_conflict(ctx: TestContext) -> TestContext {
11+
commit(&ctx.dir, "conflict-file", "hello");
12+
run(&ctx.dir, &["git", "checkout", "-b", "other-branch"]);
13+
commit(&ctx.dir, "conflict-file", "hey");
14+
run(&ctx.dir, &["git", "checkout", "main"]);
15+
commit(&ctx.dir, "conflict-file", "hi");
16+
run_ignore_status(&ctx.dir, &["git", "cherry-pick", "other-branch"]);
17+
ctx
18+
}
19+
20+
#[test]
21+
fn cherry_pick_menu() {
22+
snapshot!(setup_clone!(), "A");
23+
}
24+
25+
#[test]
26+
fn cherry_pick_prompt() {
27+
// Navigate to the log, select the initial commit, then open cherry-pick prompt
28+
snapshot!(setup_cherry_pickable(setup_clone!()), "llAA");
29+
}
30+
31+
#[test]
32+
fn cherry_pick_prompt_cancel() {
33+
snapshot!(setup_cherry_pickable(setup_clone!()), "llAA<esc>");
34+
}
35+
36+
#[test]
37+
fn cherry_pick() {
38+
snapshot!(
39+
setup_cherry_pickable(setup_clone!()),
40+
"llAAother-branch<enter>"
41+
);
42+
}
43+
44+
#[test]
45+
fn cherry_pick_no_commit() {
46+
snapshot!(
47+
setup_cherry_pickable(setup_clone!()),
48+
"llA-nAAother-branch<enter>"
49+
);
50+
}
51+
52+
#[test]
53+
fn cherry_pick_conflict_status() {
54+
let mut ctx = setup_conflict(setup_clone!());
55+
ctx.init_app();
56+
insta::assert_snapshot!(ctx.redact_buffer());
57+
}
58+
59+
#[test]
60+
fn cherry_pick_abort() {
61+
snapshot!(setup_conflict(setup_clone!()), "Aa");
62+
}
63+
64+
#[test]
65+
fn cherry_pick_continue() {
66+
snapshot!(setup_conflict(setup_clone!()), "Ac");
67+
}

src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use std::fs;
1515
mod helpers;
1616
mod arg;
1717
mod branch;
18+
mod cherry_pick;
1819
mod commit;
1920
mod discard;
2021
mod editor;

0 commit comments

Comments
 (0)