Skip to content

Commit 2032d6c

Browse files
authored
Add refs argument do diff command (#160)
It is often useful to only update the PR further up the stack. Eg if you have a stack of 4 but made changes to the second one only you could run: `spr diff -r HEAD~3`. Alternatively if you made changes to all but this one (for example because you haven't made a PR from it): `spr diff -r HEAD~4..HEAD~1`
1 parent 5b2e03a commit 2032d6c

File tree

1 file changed

+48
-5
lines changed

1 file changed

+48
-5
lines changed

spr/src/commands/diff.rs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
use std::iter::zip;
9+
use std::collections::HashSet;
910

1011
use crate::{
1112
error::{add_error, Error, Result, ResultExt},
@@ -41,12 +42,34 @@ pub struct DiffOptions {
4142
#[clap(long, short = 'm')]
4243
message: Option<String>,
4344

45+
/// Which commits in the branch should be created/updated. This can be a
46+
/// revspec such as HEAD~4..HEAD~1 or just one commit like HEAD~7.
47+
#[clap(long, short = 'r')]
48+
refs: Option<String>,
49+
4450
/// Submit this commit as if it was cherry-picked on master. Do not base it
4551
/// on any intermediate changes between the master branch and this commit.
4652
#[clap(long)]
4753
cherry_pick: bool,
4854
}
4955

56+
fn get_oids(refs: &str, repo: &git2::Repository) -> Result<HashSet<Oid>> {
57+
// refs might be a single (eg 012345abc or HEAD) or a range (HEAD~4..HEAD~2)
58+
let revspec = repo.revparse(&refs)?;
59+
60+
let from = revspec.from().ok_or(Error::new("Unexpectedly no from id in range"))?.id();
61+
if revspec.mode().contains(git2::RevparseMode::SINGLE) {
62+
// simple case, just return the id
63+
return Ok(HashSet::from([from]));
64+
}
65+
let to = revspec.to().ok_or(Error::new("Unexpectedly no to id in range"))?.id();
66+
67+
let mut walk = repo.revwalk()?;
68+
walk.push(to)?;
69+
walk.hide(from)?;
70+
walk.map(|r| Ok(r?)).collect()
71+
}
72+
5073
pub async fn diff(
5174
opts: DiffOptions,
5275
git: &crate::git::Git,
@@ -70,11 +93,25 @@ pub async fn diff(
7093
return result;
7194
};
7295

73-
if !opts.all {
74-
// Remove all prepared commits from the vector but the last. So, if
75-
// `--all` is not given, we only operate on the HEAD commit.
76-
prepared_commits.drain(0..prepared_commits.len() - 1);
77-
}
96+
// If refs is set, we want to track which commits to run `diff`
97+
// against. The simple approach would be to adjust the prepared_commits
98+
// Vec (as with opts.all above). This does not work however, as we need
99+
// to know the entire list (or more specifically the list after the first update)
100+
// for the rewrite_commit_messages step.
101+
// This is not a problem for opts.all as it only ever has a single commit to update,
102+
// and so nothing after it.
103+
let revs_to_pr = match(&opts.refs, opts.all) {
104+
(Some(refs), false) => get_oids(refs, &git.repo()).map(|i| Some(i)),
105+
(Some(_), true) => Err(Error::new("Do not use --refs with --all")),
106+
(None, all) => {
107+
// Remove all prepared commits from the vector but the last. So, if
108+
// `--all` is not given, we only operate on the HEAD commit.
109+
if !all {
110+
prepared_commits.drain(0..prepared_commits.len() - 1);
111+
}
112+
Ok(None)
113+
}
114+
}?;
78115

79116
#[allow(clippy::needless_collect)]
80117
let pull_request_tasks: Vec<_> = prepared_commits
@@ -94,6 +131,12 @@ pub async fn diff(
94131
break;
95132
}
96133

134+
if let Some(revs) = &revs_to_pr {
135+
if !revs.contains(&prepared_commit.oid) {
136+
continue;
137+
}
138+
}
139+
97140
let pull_request = if let Some(task) = pull_request_task {
98141
Some(task.await??)
99142
} else {

0 commit comments

Comments
 (0)