p4_cmd/
p4.rs

1use std::fmt;
2use std::path;
3use std::process;
4use std::str;
5
6use chrono;
7use chrono::TimeZone;
8
9use dirs;
10use files;
11use print;
12use sync;
13use where_;
14
15#[derive(Clone, Debug)]
16pub struct P4 {
17    custom_p4: Option<path::PathBuf>,
18    port: Option<String>,
19    user: Option<String>,
20    password: Option<String>,
21    client: Option<String>,
22    retries: Option<usize>,
23}
24
25impl P4 {
26    pub fn new() -> Self {
27        Self {
28            custom_p4: None,
29            port: None,
30            user: None,
31            password: None,
32            client: None,
33            retries: None,
34        }
35    }
36
37    /// Overrides the `p4` command used.
38    ///
39    /// This is useful for "portable" installs (not in system path) and performance (caching the
40    /// `PATH` lookup via `where` crate).
41    pub fn set_p4_cmd(mut self, custom_p4: Option<path::PathBuf>) -> Self {
42        self.custom_p4 = custom_p4;
43        self
44    }
45
46    /// Overrides any P4PORT setting with the specified protocol:host:port.
47    pub fn set_port(mut self, port: Option<String>) -> Self {
48        self.port = port;
49        self
50    }
51
52    /// Overrides any P4USER, USER, or USERNAME setting with the specified user name.
53    pub fn set_user(mut self, user: Option<String>) -> Self {
54        self.user = user;
55        self
56    }
57
58    /// Overrides any P4PASSWD setting with the specified passwo
59    pub fn set_password(mut self, password: Option<String>) -> Self {
60        self.password = password;
61        self
62    }
63
64    /// Overrides any P4CLIENT setting with the specified client name.
65    pub fn set_client(mut self, client: Option<String>) -> Self {
66        self.client = client;
67        self
68    }
69
70    /// Number of times a command should be retried if the network times out (takes longer than N
71    /// seconds to respond to a single I/O operation) during command execution.
72    pub fn set_retries(mut self, retries: Option<usize>) -> Self {
73        self.retries = retries;
74        self
75    }
76
77    /// Write a depot file to standard output
78    ///
79    /// Retrieve the contents of a depot file to the client's standard output.
80    /// The file is not synced.  If file is specified using client syntax,
81    /// Perforce uses the client view to determine the corresponding depot
82    /// file.
83    ///
84    /// By default, the head revision is printed.  If the file argument
85    /// includes a revision, the specified revision is printed.  If the
86    /// file argument has a revision range,  then only files selected by
87    /// that revision range are printed, and the highest revision in the
88    /// range is printed. For details about revision specifiers, see 'p4
89    /// help revisions'.
90    ///
91    /// # Examples
92    ///
93    /// ```rust,no_run
94    /// let p4 = p4_cmd::P4::new();
95    /// let files = p4.print("//depot/dir/file").run().unwrap();
96    /// for file in files {
97    ///     println!("{:?}", file);
98    /// }
99    /// ```
100    pub fn print<'p, 'f>(&'p self, file: &'f str) -> print::PrintCommand<'p, 'f> {
101        print::PrintCommand::new(self, file)
102    }
103
104    /// Synchronize the client with its view of the depot
105    ///
106    /// Sync updates the client workspace to reflect its current view (if
107    /// it has changed) and the current contents of the depot (if it has
108    /// changed). The client view maps client and depot file names and
109    /// locations.
110    ///
111    /// Sync adds files that are in the client view and have not been
112    /// retrieved before.  Sync deletes previously retrieved files that
113    /// are no longer in the client view or have been deleted from the
114    /// depot.  Sync updates files that are still in the client view and
115    /// have been updated in the depot.
116    ///
117    /// By default, sync affects all files in the client workspace. If file
118    /// arguments are given, sync limits its operation to those files.
119    /// The file arguments can contain wildcards.
120    ///
121    /// If the file argument includes a revision specifier, then the given
122    /// revision is retrieved.  Normally, the head revision is retrieved.
123    ///
124    /// If the file argument includes a revision range specification,
125    /// only files selected by the revision range are updated, and the
126    /// highest revision in the range is used.
127    ///
128    /// See 'p4 help revisions' for help specifying revisions or ranges.
129    ///
130    /// Normally, sync does not overwrite workspace files that the user has
131    /// manually made writable.  Setting the 'clobber' option in the
132    /// client specification disables this safety check.
133    ///
134    /// # Examples
135    ///
136    /// ```rust,no_run
137    /// let p4 = p4_cmd::P4::new();
138    /// let dirs = p4.sync("//depot/dir/*").run().unwrap();
139    /// for dir in dirs {
140    ///     println!("{:?}", dir);
141    /// }
142    /// ```
143    pub fn sync<'p, 'f>(&'p self, file: &'f str) -> sync::SyncCommand<'p, 'f> {
144        sync::SyncCommand::new(self, file)
145    }
146
147    /// List files in the depot.
148    ///
149    /// List details about specified files: depot file name, revision,
150    /// file, type, change action and changelist number of the current
151    /// head revision. If client syntax is used to specify the file
152    /// argument, the client view mapping is used to determine the
153    /// corresponding depot files.
154    ///
155    /// By default, the head revision is listed.  If the file argument
156    /// specifies a revision, then all files at that revision are listed.
157    /// If the file argument specifies a revision range, the highest revision
158    /// in the range is used for each file. For details about specifying
159    /// revisions, see 'p4 help revisions'.
160    ///
161    /// # Examples
162    ///
163    /// ```rust,no_run
164    /// let p4 = p4_cmd::P4::new();
165    /// let files = p4.files("//depot/dir/*").run().unwrap();
166    /// for file in files {
167    ///     println!("{:?}", file);
168    /// }
169    /// ```
170    pub fn files<'p, 'f>(&'p self, file: &'f str) -> files::FilesCommand<'p, 'f> {
171        files::FilesCommand::new(self, file)
172    }
173
174    /// List depot subdirectories
175    ///
176    /// List directories that match the specified file pattern (dir).
177    /// This command does not support the recursive wildcard (...).
178    /// Use the * wildcard instead.
179    ///
180    /// Perforce does not track directories individually. A path is treated
181    /// as a directory if there are any undeleted files with that path as a
182    /// prefix.
183    ///
184    /// By default, all directories containing files are listed. If the dir
185    /// argument includes a revision range, only directories containing files
186    /// in the range are listed. For details about specifying file revisions,
187    /// see 'p4 help revisions'.
188    ///
189    /// # Examples
190    ///
191    /// ```rust,no_run
192    /// let p4 = p4_cmd::P4::new();
193    /// let dirs = p4.dirs("//depot/dir/*").run().unwrap();
194    /// for dir in dirs {
195    ///     println!("{:?}", dir);
196    /// }
197    /// ```
198    pub fn dirs<'p, 'f, 's>(&'p self, dir: &'f str) -> dirs::DirsCommand<'p, 'f, 's> {
199        dirs::DirsCommand::new(self, dir)
200    }
201
202    /// Show how file names are mapped by the client view
203    ///
204    /// Where shows how the specified files are mapped by the client view.
205    /// For each argument, three names are produced: the name in the depot,
206    /// the name on the client in Perforce syntax, and the name on the client
207    /// in local syntax.
208    ///
209    /// If the file parameter is omitted, the mapping for all files in the
210    /// current directory and below) is returned.
211    ///
212    /// Note that 'p4 where' does not determine where any real files reside.
213    /// It only displays the locations that are mapped by the client view.
214    ///
215    /// # Examples
216    ///
217    /// ```rust,no_run
218    /// let p4 = p4_cmd::P4::new();
219    /// let files = p4.where_().file("//depot/dir/*").run().unwrap();
220    /// for file in files {
221    ///     println!("{:?}", file);
222    /// }
223    /// ```
224    pub fn where_<'p, 'f>(&'p self) -> where_::WhereCommand<'p, 'f> {
225        where_::WhereCommand::new(self)
226    }
227
228    pub(crate) fn connect(&self) -> process::Command {
229        let p4_cmd = self
230            .custom_p4
231            .as_ref()
232            .map(path::PathBuf::as_path)
233            .unwrap_or_else(|| path::Path::new("p4"));
234        let mut cmd = process::Command::new(p4_cmd);
235        cmd.args(&["-Gs", "-C utf8"]);
236        if let Some(ref port) = self.port {
237            cmd.args(&["-p", port.as_str()]);
238        }
239        if let Some(ref user) = self.user {
240            cmd.args(&["-u", user.as_str()]);
241        }
242        if let Some(ref password) = self.password {
243            cmd.args(&["-P", password.as_str()]);
244        }
245        if let Some(ref client) = self.client {
246            cmd.args(&["-c", client.as_str()]);
247        }
248        cmd
249    }
250
251    pub(crate) fn connect_with_retries(&self, retries: Option<usize>) -> process::Command {
252        let mut cmd = self.connect();
253        if let Some(retries) = retries.or(self.retries) {
254            let retries = format!("{}", retries);
255            cmd.args(&["-r", &retries]);
256        }
257        cmd
258    }
259}
260
261pub type Time = chrono::DateTime<chrono::Utc>;
262
263// Keeping around for future use.
264#[allow(dead_code)]
265pub(crate) fn to_timestamp(time: &Time) -> i64 {
266    time.timestamp()
267}
268
269pub(crate) fn from_timestamp(timestamp: i64) -> Time {
270    chrono::Utc.timestamp(timestamp, 0)
271}
272
273/// Action performed on a file at a given revision.
274///
275/// # Example
276///
277/// ```rust
278/// assert_eq!(p4_cmd::Action::MoveDelete.to_string(), "move/delete");
279/// assert_eq!("move/delete".parse::<p4_cmd::Action>().unwrap(), p4_cmd::Action::MoveDelete);
280/// ```
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub enum Action {
283    #[doc(hidden)]
284    __Nonexhaustive,
285
286    Add,
287    Edit,
288    Delete,
289    Branch,
290    MoveAdd,
291    MoveDelete,
292    Integrate,
293    Import,
294    Purge,
295    Archive,
296
297    Unknown(String),
298}
299
300impl str::FromStr for Action {
301    type Err = fmt::Error;
302
303    fn from_str(s: &str) -> Result<Self, Self::Err> {
304        let ft = match s {
305            "add" => Action::Add,
306            "edit" => Action::Edit,
307            "delete" => Action::Delete,
308            "branch" => Action::Branch,
309            "move/add" => Action::MoveAdd,
310            "move/delete" => Action::MoveDelete,
311            "integrate" => Action::Integrate,
312            "import" => Action::Import,
313            "purge" => Action::Purge,
314            "archive" => Action::Archive,
315            s => Action::Unknown(s.to_owned()),
316        };
317        Ok(ft)
318    }
319}
320
321impl fmt::Display for Action {
322    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
323        let value = match self {
324            Action::Add => "add",
325            Action::Edit => "edit",
326            Action::Delete => "delete",
327            Action::Branch => "branch",
328            Action::MoveAdd => "move/add",
329            Action::MoveDelete => "move/delete",
330            Action::Integrate => "integrate",
331            Action::Import => "import",
332            Action::Purge => "purge",
333            Action::Archive => "archive",
334            Action::Unknown(ref s) => s.as_str(),
335            Action::__Nonexhaustive => unreachable!("This is a private variant"),
336        };
337        write!(f, "{}", value)
338    }
339}
340
341/// Perforce base file type.
342///
343/// # Example
344///
345/// ```rust
346/// assert_eq!(p4_cmd::BaseFileType::Utf8.to_string(), "utf8");
347/// assert_eq!("utf8".parse::<p4_cmd::BaseFileType>().unwrap(), p4_cmd::BaseFileType::Utf8);
348/// ```
349#[derive(Debug, Clone, PartialEq, Eq)]
350pub enum BaseFileType {
351    #[doc(hidden)]
352    __Nonexhaustive,
353
354    /// Text file
355    ///
356    /// Synced as text in the workspace. Line-ending translations are performed automatically.
357    ///
358    /// Stored as: deltas in RCS format
359    Text,
360    /// Non-text file
361    ///
362    /// Synced as binary files in the workspace. Stored compressed within the depot.
363    ///
364    /// Stored as: full file, compressed
365    Binary,
366    /// Symbolic link
367    ///
368    /// Helix Server applications on UNIX, OS X, recent versions of Windows treat these files as
369    /// symbolic links. On other platforms, these files appear as (small) text files.
370    ///
371    /// On Windows, you require admin privileges or an appropriate group policy must be set, otherwise, you get text files.
372    ///
373    /// Stored as: deltas in RCS format
374    Symlink,
375    /// Unicode file
376    ///
377    /// Services operating in unicode mode support the unicode file type. These files are
378    /// translated into the local character set specified by P4CHARSET.
379    ///
380    /// Line-ending translations are performed automatically.
381    ///
382    /// Services not in unicode mode do not support the unicode file type.
383    ///
384    ///Stored as: RCS deltas in UTF-8 format
385    Unicode,
386    /// Unicode file
387    ///
388    /// Synced in the client workspace with the UTF-8 BOM (byte order mark).
389    ///
390    /// Whether the service is in unicode mode or not, files are transferred as UTF-8 in the client workspace.
391    ///
392    /// Line-ending translations are performed automatically.
393    ///
394    /// Stored as: RCS deltas in UTF-8 format without the UTF-8 BOM (byte order mark).
395    Utf8,
396    /// Unicode file
397    ///
398    /// Whether the service is in unicode mode or not, files are transferred as UTF-8, and
399    /// translated to UTF-16 (with byte order mark, in the byte order appropriate for the user's
400    /// machine) in the client workspace.
401    ///
402    /// Line-ending translations are performed automatically.
403    ///
404    /// Stored as: RCS deltas in UTF-8 format
405    Utf16,
406
407    Unknown(String),
408}
409
410impl Default for BaseFileType {
411    fn default() -> Self {
412        BaseFileType::Text
413    }
414}
415
416impl str::FromStr for BaseFileType {
417    type Err = fmt::Error;
418
419    fn from_str(s: &str) -> Result<Self, Self::Err> {
420        let ft = match s {
421            "text" => BaseFileType::Text,
422            "binary" => BaseFileType::Binary,
423            "symlink" => BaseFileType::Symlink,
424            "unicode" => BaseFileType::Unicode,
425            "utf8" => BaseFileType::Utf8,
426            "utf16" => BaseFileType::Utf16,
427            s => BaseFileType::Unknown(s.to_owned()),
428        };
429        Ok(ft)
430    }
431}
432
433impl fmt::Display for BaseFileType {
434    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
435        let value = match self {
436            BaseFileType::Text => "text",
437            BaseFileType::Binary => "binary",
438            BaseFileType::Symlink => "symlink",
439            BaseFileType::Unicode => "unicode",
440            BaseFileType::Utf8 => "utf8",
441            BaseFileType::Utf16 => "utf16",
442            BaseFileType::Unknown(ref s) => s.as_str(),
443            BaseFileType::__Nonexhaustive => unreachable!("This is a private variant"),
444        };
445        write!(f, "{}", value)
446    }
447}
448
449/// Perforce file type modifiers.
450///
451/// # Example
452///
453/// ```rust
454/// let mut modifiers = p4_cmd::FileTypeModifiers::new();
455/// modifiers.exclusive = true;
456/// assert_eq!(modifiers.to_string(), "l");
457/// assert_eq!("l".parse::<p4_cmd::FileTypeModifiers>().unwrap(), modifiers);
458/// ```
459#[derive(Debug, Clone, PartialEq, Eq, Default)]
460pub struct FileTypeModifiers {
461    /// File is always writable on client
462    pub always_writeable: bool,
463    /// Execute bit set on client
464    pub executable: bool,
465    /// RCS keyword expansion
466    pub rcs_expansion: bool,
467    /// Exclusive open (locking)
468    pub exclusive: bool,
469    /// Perforce stores the full compressed version of each file revision
470    pub full: bool,
471    /// Perforce stores deltas in RCS format
472    pub deltas: bool,
473    /// Perforce stores full file per revision, uncompressed
474    pub full_uncompressed: bool,
475    /// Only the head revision is stored
476    pub head: bool,
477    /// Only the most recent n revisions are stored
478    pub revisions: Option<usize>,
479    /// Preserve original modtime
480    pub modtime: bool,
481    /// Archive trigger required
482    pub archive: bool,
483    non_exhaustive: (),
484}
485
486impl FileTypeModifiers {
487    pub fn new() -> Self {
488        Default::default()
489    }
490}
491
492impl str::FromStr for FileTypeModifiers {
493    type Err = fmt::Error;
494
495    fn from_str(s: &str) -> Result<Self, Self::Err> {
496        let mut modifiers = FileTypeModifiers::default();
497
498        for flag in s.chars() {
499            match flag {
500                'w' => modifiers.always_writeable = true,
501                'x' => modifiers.executable = true,
502                'k' => modifiers.rcs_expansion = true,
503                'l' => modifiers.exclusive = true,
504                'C' => modifiers.full = true,
505                'D' => modifiers.deltas = true,
506                'F' => modifiers.full_uncompressed = true,
507                'S' => modifiers.head = true,
508                // TODO: handle `revisions`.
509                'm' => modifiers.modtime = true,
510                'X' => modifiers.archive = true,
511                _ => return Err(fmt::Error),
512            }
513        }
514
515        Ok(modifiers)
516    }
517}
518
519impl fmt::Display for FileTypeModifiers {
520    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
521        if self.always_writeable {
522            write!(f, "w")?;
523        }
524        if self.executable {
525            write!(f, "x")?;
526        }
527        if self.rcs_expansion {
528            write!(f, "k")?;
529        }
530        if self.exclusive {
531            write!(f, "l")?;
532        }
533        if self.full {
534            write!(f, "C")?;
535        }
536        if self.deltas {
537            write!(f, "D")?;
538        }
539        if self.full_uncompressed {
540            write!(f, "S")?;
541        }
542        if self.head {
543            write!(f, "S")?;
544        }
545        if let Some(revisions) = self.revisions {
546            write!(f, "S{}", revisions)?;
547        }
548        if self.modtime {
549            write!(f, "m")?;
550        }
551        if self.archive {
552            write!(f, "X")?;
553        }
554
555        Ok(())
556    }
557}
558
559/// Perforce file type.
560///
561/// # Example
562///
563/// ```rust
564/// let mut modifiers = p4_cmd::FileTypeModifiers::default();
565/// modifiers.exclusive = true;
566/// let ft = p4_cmd::FileType::new()
567///     .base(p4_cmd::BaseFileType::Binary)
568///     .modifiers(Some(modifiers));
569/// assert_eq!(ft.to_string(), "binary+l");
570/// assert_eq!("binary+l".parse::<p4_cmd::FileType>().unwrap(), ft);
571/// ```
572#[derive(Debug, Clone, PartialEq, Eq, Default)]
573pub struct FileType {
574    /// The base Perforce file type
575    pub base: BaseFileType,
576    pub modifiers: Option<FileTypeModifiers>,
577    non_exhaustive: (),
578}
579
580impl FileType {
581    pub fn new() -> Self {
582        Default::default()
583    }
584
585    pub fn base(mut self, base: BaseFileType) -> Self {
586        self.base = base;
587        self
588    }
589
590    pub fn modifiers(mut self, modifiers: Option<FileTypeModifiers>) -> Self {
591        self.modifiers = modifiers;
592        self
593    }
594}
595
596impl str::FromStr for FileType {
597    type Err = fmt::Error;
598
599    fn from_str(s: &str) -> Result<Self, Self::Err> {
600        let mut itr = s.splitn(2, '+');
601        let base = itr.next().ok_or(fmt::Error)?;
602        let base = base.parse().map_err(|_| fmt::Error)?;
603
604        let modifiers = itr
605            .next()
606            .map(|f| {
607                let modifiers: FileTypeModifiers = f.parse()?;
608                Ok(modifiers)
609            }).map_or(Ok(None), |r| r.map(Some))?;
610
611        let ft = FileType {
612            base,
613            modifiers,
614            non_exhaustive: (),
615        };
616
617        Ok(ft)
618    }
619}
620
621impl fmt::Display for FileType {
622    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
623        write!(f, "{}", self.base)?;
624        if let Some(ref modifiers) = self.modifiers {
625            write!(f, "+{}", modifiers)?;
626        }
627        Ok(())
628    }
629}