use std::{
ffi::OsStr,
path::PathBuf,
process::{Child, Stdio},
};
use super::{ExclusiveOption, SubCommand, Unselected};
use crate::global::GlobalOpts;
use crate::spawn::ParameterizedSpawn;
#[derive(Debug, Clone, Copy, Default)]
pub struct FullDescription;
impl ExclusiveOption for FullDescription {
fn inject_args(&self, command: &mut std::process::Command) {
command.arg("-l");
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TruncatedDescription;
impl ExclusiveOption for TruncatedDescription {
fn inject_args(&self, command: &mut std::process::Command) {
command.arg("-L");
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum User {
User(String),
#[cfg(not(feature = "lt2016_1"))]
Me,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
Pending,
Submitted,
Shelved,
}
impl Status {
pub fn as_str(&self) -> &'static str {
match self {
Status::Pending => "pending",
Status::Submitted => "submitted",
Status::Shelved => "shelved",
}
}
}
#[cfg_attr(
feature = "lt2015_2",
doc = "`p4 [g-opts] changes [-i -t -l -L -f] [-c client] [-m max] [-s status] [-u user] [file[RevRange] ...]`"
)]
#[cfg_attr(
all(feature = "lt2016_1", not(feature = "lt2015_2")),
doc = "`p4 [g-opts] changes [-i -t -l -L -f] [-c client] [ -e changelist#][-m max] [-s status] [-u user][file[RevRange] ...]`"
)]
#[cfg_attr(
all(feature = "lt2017_2", not(feature = "lt2016_1")),
doc = "`p4 [g-opts] changes [-i -t -l -L -f] [-c client] [ -e changelist#][-m max] [-s status] [-u user | --me][file[RevRange] ...]`"
)]
#[cfg_attr(
all(feature = "lt2022_2", not(feature = "lt2017_2")),
doc = "`p4 [g-opts] changes [-i -t -l -L -f] [-c client] [ -e changelist#][-m max] [-r] [-s status] [-u user | --me] [file[RevRange] ...]`"
)]
#[cfg_attr(
not(feature = "lt2022_2"),
doc = "`p4 [g-opts] changes [-i -t -l -L -f] [-c client] [ -e changelist#][-m max] [-r] [-s status] [-u user | --me] [file[RevRange] ...] [--stream | --nostream]`"
)]
#[derive(Debug, Clone, Default)]
pub struct Changes<L = Unselected> {
bin: PathBuf,
global_opts: GlobalOpts,
#[cfg(not(feature = "lt2015_2"))]
min_change_list: Option<String>,
include_restricted: bool,
include_integrated: bool,
include_time: bool,
long_output: L,
limit: Option<u64>,
#[cfg(not(feature = "lt2017_2"))]
reverse_order: bool,
filter_status: Option<Status>,
filter_users: Option<Vec<User>>,
filter_clients: Option<Vec<String>>,
#[cfg(not(feature = "lt2025_1"))]
client_case_insensitive: bool,
#[cfg(not(feature = "lt2022_2"))]
stream: Option<bool>,
}
impl Changes<Unselected> {
pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
Self {
bin: bin.into(),
global_opts,
..Self::default()
}
}
pub fn long_output_full(self) -> Changes<FullDescription> {
Changes {
bin: self.bin,
global_opts: self.global_opts,
#[cfg(not(feature = "lt2015_2"))]
min_change_list: self.min_change_list,
include_restricted: self.include_restricted,
include_integrated: self.include_integrated,
include_time: self.include_time,
long_output: FullDescription,
limit: self.limit,
#[cfg(not(feature = "lt2017_2"))]
reverse_order: self.reverse_order,
filter_status: self.filter_status,
filter_users: self.filter_users,
filter_clients: self.filter_clients,
#[cfg(not(feature = "lt2025_1"))]
client_case_insensitive: self.client_case_insensitive,
#[cfg(not(feature = "lt2022_2"))]
stream: self.stream,
}
}
pub fn long_output_truncated(self) -> Changes<TruncatedDescription> {
Changes {
bin: self.bin,
global_opts: self.global_opts,
#[cfg(not(feature = "lt2015_2"))]
min_change_list: self.min_change_list,
include_restricted: self.include_restricted,
include_integrated: self.include_integrated,
include_time: self.include_time,
long_output: TruncatedDescription,
limit: self.limit,
#[cfg(not(feature = "lt2017_2"))]
reverse_order: self.reverse_order,
filter_status: self.filter_status,
filter_users: self.filter_users,
filter_clients: self.filter_clients,
#[cfg(not(feature = "lt2025_1"))]
client_case_insensitive: self.client_case_insensitive,
#[cfg(not(feature = "lt2022_2"))]
stream: self.stream,
}
}
}
impl<L: ExclusiveOption> ParameterizedSpawn for Changes<L> {
type Input<'a> = &'a [&'a OsStr];
type Output<'a> = Child;
type Error = std::io::Error;
fn spawn_with<'a>(&mut self, files: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
self.setup_command(&self.bin)
.args(files)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
}
}
impl<L: ExclusiveOption> Changes<L> {
#[cfg_attr(
feature = "lt2014_2",
doc = "See the [Global Options](GlobalOpts) section."
)]
#[cfg_attr(
all(feature = "lt2015_1", not(feature = "lt2014_2")),
doc = "See the [“Global Options”](GlobalOpts) section."
)]
#[cfg_attr(
all(feature = "lt2017_1", not(feature = "lt2015_1")),
doc = "See [“Global Options”](GlobalOpts)."
)]
#[cfg_attr(
all(feature = "lt2018_2", not(feature = "lt2017_1")),
doc = "See [Global Options](GlobalOpts)."
)]
#[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
pub fn get_global_opts(&self) -> &GlobalOpts {
&self.global_opts
}
#[cfg_attr(
feature = "lt2014_2",
doc = "See the [Global Options](GlobalOpts) section."
)]
#[cfg_attr(
all(feature = "lt2015_1", not(feature = "lt2014_2")),
doc = "See the [“Global Options”](GlobalOpts) section."
)]
#[cfg_attr(
all(feature = "lt2017_1", not(feature = "lt2015_1")),
doc = "See [“Global Options”](GlobalOpts)."
)]
#[cfg_attr(
all(feature = "lt2018_2", not(feature = "lt2017_1")),
doc = "See [Global Options](GlobalOpts)."
)]
#[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
self.global_opts = v;
self
}
#[cfg_attr(
feature = "lt2014_2",
doc = "See the [Global Options](GlobalOpts) section."
)]
#[cfg_attr(
all(feature = "lt2015_1", not(feature = "lt2014_2")),
doc = "See the [“Global Options”](GlobalOpts) section."
)]
#[cfg_attr(
all(feature = "lt2017_1", not(feature = "lt2015_1")),
doc = "See [“Global Options”](GlobalOpts)."
)]
#[cfg_attr(
all(feature = "lt2018_2", not(feature = "lt2017_1")),
doc = "See [Global Options](GlobalOpts)."
)]
#[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
pub fn global_opts(mut self, v: GlobalOpts) -> Self {
self.global_opts = v;
self
}
pub fn get_filter_clients(&self) -> Option<&[String]> {
self.filter_clients.as_deref()
}
pub fn set_filter_client(&mut self, v: impl Into<String>) -> &mut Self {
self.filter_clients
.get_or_insert_with(Vec::new)
.push(v.into());
self
}
pub fn filter_client(mut self, v: impl Into<String>) -> Self {
self.filter_clients
.get_or_insert_with(Vec::new)
.push(v.into());
self
}
#[cfg(not(feature = "lt2015_2"))]
pub fn get_min_change_list(&self) -> Option<&String> {
self.min_change_list.as_ref()
}
#[cfg(not(feature = "lt2015_2"))]
pub fn set_min_change_list(&mut self, v: impl Into<String>) -> &mut Self {
self.min_change_list = Some(v.into());
self
}
#[cfg(not(feature = "lt2015_2"))]
pub fn min_change_list(mut self, v: impl Into<String>) -> Self {
self.min_change_list = Some(v.into());
self
}
pub fn get_include_restricted(&self) -> bool {
self.include_restricted
}
pub fn set_include_restricted(&mut self, v: bool) -> &mut Self {
self.include_restricted = v;
self
}
pub fn include_restricted(mut self, v: bool) -> Self {
self.include_restricted = v;
self
}
pub fn get_include_integrated(&self) -> bool {
self.include_integrated
}
pub fn set_include_integrated(&mut self, v: bool) -> &mut Self {
self.include_integrated = v;
self
}
pub fn include_integrated(mut self, v: bool) -> Self {
self.include_integrated = v;
self
}
pub fn get_limit(&self) -> Option<u64> {
self.limit
}
pub fn set_limit(&mut self, v: u64) -> &mut Self {
self.limit = Some(v);
self
}
pub fn limit(mut self, v: u64) -> Self {
self.limit = Some(v);
self
}
#[cfg(not(feature = "lt2017_2"))]
pub fn get_reverse_order(&self) -> bool {
self.reverse_order
}
#[cfg(not(feature = "lt2017_2"))]
pub fn set_reverse_order(&mut self, v: bool) -> &mut Self {
self.reverse_order = v;
self
}
#[cfg(not(feature = "lt2017_2"))]
pub fn reverse_order(mut self, v: bool) -> Self {
self.reverse_order = v;
self
}
pub fn get_status(&self) -> Option<Status> {
self.filter_status
}
pub fn set_status(&mut self, v: Status) -> &mut Self {
self.filter_status = Some(v);
self
}
pub fn status(mut self, v: Status) -> Self {
self.filter_status = Some(v);
self
}
pub fn get_include_time(&self) -> bool {
self.include_time
}
pub fn set_include_time(&mut self, v: bool) -> &mut Self {
self.include_time = v;
self
}
pub fn include_time(mut self, v: bool) -> Self {
self.include_time = v;
self
}
pub fn get_filter_users(&self) -> Option<&[User]> {
self.filter_users.as_deref()
}
pub fn set_filter_user(&mut self, v: impl Into<String>) -> &mut Self {
self.filter_users
.get_or_insert_with(Vec::new)
.push(User::User(v.into()));
self
}
pub fn filter_user(mut self, v: impl Into<String>) -> Self {
self.filter_users
.get_or_insert_with(Vec::new)
.push(User::User(v.into()));
self
}
#[cfg(not(feature = "lt2016_1"))]
pub fn set_filter_me(&mut self) -> &mut Self {
self.filter_users
.get_or_insert_with(Vec::new)
.push(User::Me);
self
}
#[cfg(not(feature = "lt2016_1"))]
pub fn filter_me(mut self) -> Self {
self.filter_users
.get_or_insert_with(Vec::new)
.push(User::Me);
self
}
#[cfg(not(feature = "lt2025_1"))]
pub fn get_client_case_insensitive(&self) -> bool {
self.client_case_insensitive
}
#[cfg(not(feature = "lt2025_1"))]
pub fn set_client_case_insensitive(&mut self, v: bool) -> &mut Self {
self.client_case_insensitive = v;
self
}
#[cfg(not(feature = "lt2025_1"))]
pub fn client_case_insensitive(mut self, v: bool) -> Self {
self.client_case_insensitive = v;
self
}
#[cfg(not(feature = "lt2022_2"))]
pub fn get_stream(&self) -> Option<bool> {
self.stream
}
#[cfg(not(feature = "lt2022_2"))]
pub fn set_stream(&mut self, v: bool) -> &mut Self {
self.stream = Some(v);
self
}
#[cfg(not(feature = "lt2022_2"))]
pub fn stream(mut self, v: bool) -> Self {
self.stream = Some(v);
self
}
}
impl<L: ExclusiveOption> SubCommand for Changes<L> {
fn name(&self) -> &str {
"changes"
}
fn inject_local_args(&self, command: &mut std::process::Command) {
if let Some(ref clients) = self.filter_clients {
for client in clients {
command.arg("-c").arg(client);
}
}
#[cfg(not(feature = "lt2025_1"))]
if self.client_case_insensitive {
command.arg("--client-case-insensitive");
}
#[cfg(not(feature = "lt2015_2"))]
if let Some(ref min_change) = self.min_change_list {
command.arg("-e").arg(min_change);
}
if self.include_restricted {
command.arg("-f");
}
if self.include_integrated {
command.arg("-i");
}
self.long_output.inject_args(command);
if let Some(max) = self.limit {
command.arg("-m").arg(max.to_string());
}
#[cfg(not(feature = "lt2017_2"))]
if self.reverse_order {
command.arg("-r");
}
if let Some(status) = self.filter_status {
command.arg("-s").arg(status.as_str());
}
if self.include_time {
command.arg("-t");
}
if let Some(ref users) = self.filter_users {
for user in users {
match user {
User::User(name) => {
command.arg("-u").arg(name);
}
#[cfg(not(feature = "lt2016_1"))]
User::Me => {
command.arg("--me");
}
}
}
}
#[cfg(not(feature = "lt2022_2"))]
if let Some(stream) = self.stream {
if stream {
command.arg("--stream");
} else {
command.arg("--nostream");
}
}
}
fn global_opts(&self) -> Option<&GlobalOpts> {
Some(&self.global_opts)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cmd::args_of;
#[test]
fn without_options() {
let changes = Changes::new("p4", GlobalOpts::default());
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes"]);
}
#[test]
fn with_files() {
let changes = Changes::new("p4", GlobalOpts::default());
let mut cmd = changes.setup_command("p4");
cmd.arg("//depot/project/...");
assert_eq!(args_of(&cmd), vec!["changes", "//depot/project/..."]);
}
#[test]
fn client() {
let changes = Changes::new("p4", GlobalOpts::default()).filter_client("eds_elm");
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-c", "eds_elm"]);
}
#[test]
fn multiple_clients() {
let changes = Changes::new("p4", GlobalOpts::default())
.filter_client("eds_elm")
.filter_client("build_ws");
let cmd = changes.setup_command("p4");
assert_eq!(
args_of(&cmd),
vec!["changes", "-c", "eds_elm", "-c", "build_ws"]
);
}
#[cfg(not(feature = "lt2015_2"))]
#[test]
fn min_change_list() {
let changes = Changes::new("p4", GlobalOpts::default()).min_change_list("800");
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-e", "800"]);
}
#[test]
fn include_restricted() {
let changes = Changes::new("p4", GlobalOpts::default()).include_restricted(true);
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-f"]);
}
#[test]
fn include_integrated() {
let changes = Changes::new("p4", GlobalOpts::default()).include_integrated(true);
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-i"]);
}
#[test]
fn long_output_full() {
let changes = Changes::new("p4", GlobalOpts::default()).long_output_full();
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-l"]);
}
#[test]
fn long_output_truncated() {
let changes = Changes::new("p4", GlobalOpts::default()).long_output_truncated();
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-L"]);
}
#[test]
fn limit() {
let changes = Changes::new("p4", GlobalOpts::default()).limit(5);
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-m", "5"]);
}
#[cfg(not(feature = "lt2017_2"))]
#[test]
fn reverse_order() {
let changes = Changes::new("p4", GlobalOpts::default()).reverse_order(true);
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-r"]);
}
#[test]
fn status_pending() {
let changes = Changes::new("p4", GlobalOpts::default()).status(Status::Pending);
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-s", "pending"]);
}
#[test]
fn status_submitted() {
let changes = Changes::new("p4", GlobalOpts::default()).status(Status::Submitted);
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-s", "submitted"]);
}
#[test]
fn status_shelved() {
let changes = Changes::new("p4", GlobalOpts::default()).status(Status::Shelved);
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-s", "shelved"]);
}
#[test]
fn include_time() {
let changes = Changes::new("p4", GlobalOpts::default()).include_time(true);
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-t"]);
}
#[test]
fn user_name() {
let changes = Changes::new("p4", GlobalOpts::default()).filter_user("edk");
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-u", "edk"]);
}
#[test]
fn multiple_users() {
let changes = Changes::new("p4", GlobalOpts::default())
.filter_user("maria")
.filter_user("edk");
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-u", "maria", "-u", "edk"]);
}
#[cfg(not(feature = "lt2016_1"))]
#[test]
fn user_me() {
let changes = Changes::new("p4", GlobalOpts::default()).filter_me();
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "--me"]);
}
#[cfg(not(feature = "lt2016_1"))]
#[test]
fn multiple_users_with_me() {
let changes = Changes::new("p4", GlobalOpts::default())
.filter_user("maria")
.filter_me();
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-u", "maria", "--me"]);
}
#[cfg(not(feature = "lt2025_1"))]
#[test]
fn client_case_insensitive() {
let changes = Changes::new("p4", GlobalOpts::default())
.filter_client("eds_elm")
.client_case_insensitive(true);
let cmd = changes.setup_command("p4");
assert_eq!(
args_of(&cmd),
vec!["changes", "-c", "eds_elm", "--client-case-insensitive"]
);
}
#[cfg(not(feature = "lt2022_2"))]
#[test]
fn stream_spec() {
let changes = Changes::new("p4", GlobalOpts::default()).stream(true);
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "--stream"]);
}
#[cfg(not(feature = "lt2022_2"))]
#[test]
fn no_stream_spec() {
let changes = Changes::new("p4", GlobalOpts::default()).stream(false);
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "--nostream"]);
}
#[test]
fn all_options_order() {
let changes = Changes::new("p4", GlobalOpts::default())
.filter_client("eds_elm")
.include_restricted(true)
.include_integrated(true)
.long_output_full()
.limit(5)
.status(Status::Submitted)
.include_time(true)
.filter_user("edk");
#[cfg(not(feature = "lt2015_2"))]
let changes = changes.min_change_list("800");
#[cfg(not(feature = "lt2017_2"))]
let changes = changes.reverse_order(true);
#[cfg(not(feature = "lt2022_2"))]
let changes = changes.stream(true);
let cmd = changes.setup_command("p4");
let mut expected = vec!["changes", "-c", "eds_elm"];
#[cfg(not(feature = "lt2015_2"))]
expected.extend(["-e", "800"]);
expected.extend(["-f", "-i", "-l", "-m", "5"]);
#[cfg(not(feature = "lt2017_2"))]
expected.push("-r");
expected.extend(["-s", "submitted", "-t", "-u", "edk"]);
#[cfg(not(feature = "lt2022_2"))]
expected.push("--stream");
assert_eq!(args_of(&cmd), expected);
}
#[test]
fn long_output_full_preserves_other_options() {
let changes = Changes::new("p4", GlobalOpts::default())
.include_restricted(true)
.include_time(true)
.long_output_full();
let cmd = changes.setup_command("p4");
assert_eq!(args_of(&cmd), vec!["changes", "-f", "-l", "-t"]);
}
}