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}