1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
use std::fmt;
use std::path;
use std::process;
use std::str;

use chrono;
use chrono::TimeZone;

use dirs;
use files;
use print;
use sync;
use where_;

#[derive(Clone, Debug)]
pub struct P4 {
    custom_p4: Option<path::PathBuf>,
    port: Option<String>,
    user: Option<String>,
    password: Option<String>,
    client: Option<String>,
    retries: Option<usize>,
}

impl P4 {
    pub fn new() -> Self {
        Self {
            custom_p4: None,
            port: None,
            user: None,
            password: None,
            client: None,
            retries: None,
        }
    }

    /// Overrides the `p4` command used.
    ///
    /// This is useful for "portable" installs (not in system path) and performance (caching the
    /// `PATH` lookup via `where` crate).
    pub fn set_p4_cmd(mut self, custom_p4: Option<path::PathBuf>) -> Self {
        self.custom_p4 = custom_p4;
        self
    }

    /// Overrides any P4PORT setting with the specified protocol:host:port.
    pub fn set_port(mut self, port: Option<String>) -> Self {
        self.port = port;
        self
    }

    /// Overrides any P4USER, USER, or USERNAME setting with the specified user name.
    pub fn set_user(mut self, user: Option<String>) -> Self {
        self.user = user;
        self
    }

    /// Overrides any P4PASSWD setting with the specified passwo
    pub fn set_password(mut self, password: Option<String>) -> Self {
        self.password = password;
        self
    }

    /// Overrides any P4CLIENT setting with the specified client name.
    pub fn set_client(mut self, client: Option<String>) -> Self {
        self.client = client;
        self
    }

    /// Number of times a command should be retried if the network times out (takes longer than N
    /// seconds to respond to a single I/O operation) during command execution.
    pub fn set_retries(mut self, retries: Option<usize>) -> Self {
        self.retries = retries;
        self
    }

    /// Write a depot file to standard output
    ///
    /// Retrieve the contents of a depot file to the client's standard output.
    /// The file is not synced.  If file is specified using client syntax,
    /// Perforce uses the client view to determine the corresponding depot
    /// file.
    ///
    /// By default, the head revision is printed.  If the file argument
    /// includes a revision, the specified revision is printed.  If the
    /// file argument has a revision range,  then only files selected by
    /// that revision range are printed, and the highest revision in the
    /// range is printed. For details about revision specifiers, see 'p4
    /// help revisions'.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// let p4 = p4_cmd::P4::new();
    /// let files = p4.print("//depot/dir/file").run().unwrap();
    /// for file in files {
    ///     println!("{:?}", file);
    /// }
    /// ```
    pub fn print<'p, 'f>(&'p self, file: &'f str) -> print::PrintCommand<'p, 'f> {
        print::PrintCommand::new(self, file)
    }

    /// Synchronize the client with its view of the depot
    ///
    /// Sync updates the client workspace to reflect its current view (if
    /// it has changed) and the current contents of the depot (if it has
    /// changed). The client view maps client and depot file names and
    /// locations.
    ///
    /// Sync adds files that are in the client view and have not been
    /// retrieved before.  Sync deletes previously retrieved files that
    /// are no longer in the client view or have been deleted from the
    /// depot.  Sync updates files that are still in the client view and
    /// have been updated in the depot.
    ///
    /// By default, sync affects all files in the client workspace. If file
    /// arguments are given, sync limits its operation to those files.
    /// The file arguments can contain wildcards.
    ///
    /// If the file argument includes a revision specifier, then the given
    /// revision is retrieved.  Normally, the head revision is retrieved.
    ///
    /// If the file argument includes a revision range specification,
    /// only files selected by the revision range are updated, and the
    /// highest revision in the range is used.
    ///
    /// See 'p4 help revisions' for help specifying revisions or ranges.
    ///
    /// Normally, sync does not overwrite workspace files that the user has
    /// manually made writable.  Setting the 'clobber' option in the
    /// client specification disables this safety check.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// let p4 = p4_cmd::P4::new();
    /// let dirs = p4.sync("//depot/dir/*").run().unwrap();
    /// for dir in dirs {
    ///     println!("{:?}", dir);
    /// }
    /// ```
    pub fn sync<'p, 'f>(&'p self, file: &'f str) -> sync::SyncCommand<'p, 'f> {
        sync::SyncCommand::new(self, file)
    }

    /// List files in the depot.
    ///
    /// List details about specified files: depot file name, revision,
    /// file, type, change action and changelist number of the current
    /// head revision. If client syntax is used to specify the file
    /// argument, the client view mapping is used to determine the
    /// corresponding depot files.
    ///
    /// By default, the head revision is listed.  If the file argument
    /// specifies a revision, then all files at that revision are listed.
    /// If the file argument specifies a revision range, the highest revision
    /// in the range is used for each file. For details about specifying
    /// revisions, see 'p4 help revisions'.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// let p4 = p4_cmd::P4::new();
    /// let files = p4.files("//depot/dir/*").run().unwrap();
    /// for file in files {
    ///     println!("{:?}", file);
    /// }
    /// ```
    pub fn files<'p, 'f>(&'p self, file: &'f str) -> files::FilesCommand<'p, 'f> {
        files::FilesCommand::new(self, file)
    }

    /// List depot subdirectories
    ///
    /// List directories that match the specified file pattern (dir).
    /// This command does not support the recursive wildcard (...).
    /// Use the * wildcard instead.
    ///
    /// Perforce does not track directories individually. A path is treated
    /// as a directory if there are any undeleted files with that path as a
    /// prefix.
    ///
    /// By default, all directories containing files are listed. If the dir
    /// argument includes a revision range, only directories containing files
    /// in the range are listed. For details about specifying file revisions,
    /// see 'p4 help revisions'.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// let p4 = p4_cmd::P4::new();
    /// let dirs = p4.dirs("//depot/dir/*").run().unwrap();
    /// for dir in dirs {
    ///     println!("{:?}", dir);
    /// }
    /// ```
    pub fn dirs<'p, 'f, 's>(&'p self, dir: &'f str) -> dirs::DirsCommand<'p, 'f, 's> {
        dirs::DirsCommand::new(self, dir)
    }

    /// Show how file names are mapped by the client view
    ///
    /// Where shows how the specified files are mapped by the client view.
    /// For each argument, three names are produced: the name in the depot,
    /// the name on the client in Perforce syntax, and the name on the client
    /// in local syntax.
    ///
    /// If the file parameter is omitted, the mapping for all files in the
    /// current directory and below) is returned.
    ///
    /// Note that 'p4 where' does not determine where any real files reside.
    /// It only displays the locations that are mapped by the client view.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// let p4 = p4_cmd::P4::new();
    /// let files = p4.where_().file("//depot/dir/*").run().unwrap();
    /// for file in files {
    ///     println!("{:?}", file);
    /// }
    /// ```
    pub fn where_<'p, 'f>(&'p self) -> where_::WhereCommand<'p, 'f> {
        where_::WhereCommand::new(self)
    }

    pub(crate) fn connect(&self) -> process::Command {
        let p4_cmd = self
            .custom_p4
            .as_ref()
            .map(path::PathBuf::as_path)
            .unwrap_or_else(|| path::Path::new("p4"));
        let mut cmd = process::Command::new(p4_cmd);
        cmd.args(&["-Gs", "-C utf8"]);
        if let Some(ref port) = self.port {
            cmd.args(&["-p", port.as_str()]);
        }
        if let Some(ref user) = self.user {
            cmd.args(&["-u", user.as_str()]);
        }
        if let Some(ref password) = self.password {
            cmd.args(&["-P", password.as_str()]);
        }
        if let Some(ref client) = self.client {
            cmd.args(&["-c", client.as_str()]);
        }
        cmd
    }

    pub(crate) fn connect_with_retries(&self, retries: Option<usize>) -> process::Command {
        let mut cmd = self.connect();
        if let Some(retries) = retries.or(self.retries) {
            let retries = format!("{}", retries);
            cmd.args(&["-r", &retries]);
        }
        cmd
    }
}

pub type Time = chrono::DateTime<chrono::Utc>;

// Keeping around for future use.
#[allow(dead_code)]
pub(crate) fn to_timestamp(time: &Time) -> i64 {
    time.timestamp()
}

pub(crate) fn from_timestamp(timestamp: i64) -> Time {
    chrono::Utc.timestamp(timestamp, 0)
}

/// Action performed on a file at a given revision.
///
/// # Example
///
/// ```rust
/// assert_eq!(p4_cmd::Action::MoveDelete.to_string(), "move/delete");
/// assert_eq!("move/delete".parse::<p4_cmd::Action>().unwrap(), p4_cmd::Action::MoveDelete);
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
    #[doc(hidden)]
    __Nonexhaustive,

    Add,
    Edit,
    Delete,
    Branch,
    MoveAdd,
    MoveDelete,
    Integrate,
    Import,
    Purge,
    Archive,

    Unknown(String),
}

impl str::FromStr for Action {
    type Err = fmt::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let ft = match s {
            "add" => Action::Add,
            "edit" => Action::Edit,
            "delete" => Action::Delete,
            "branch" => Action::Branch,
            "move/add" => Action::MoveAdd,
            "move/delete" => Action::MoveDelete,
            "integrate" => Action::Integrate,
            "import" => Action::Import,
            "purge" => Action::Purge,
            "archive" => Action::Archive,
            s => Action::Unknown(s.to_owned()),
        };
        Ok(ft)
    }
}

impl fmt::Display for Action {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let value = match self {
            Action::Add => "add",
            Action::Edit => "edit",
            Action::Delete => "delete",
            Action::Branch => "branch",
            Action::MoveAdd => "move/add",
            Action::MoveDelete => "move/delete",
            Action::Integrate => "integrate",
            Action::Import => "import",
            Action::Purge => "purge",
            Action::Archive => "archive",
            Action::Unknown(ref s) => s.as_str(),
            Action::__Nonexhaustive => unreachable!("This is a private variant"),
        };
        write!(f, "{}", value)
    }
}

/// Perforce base file type.
///
/// # Example
///
/// ```rust
/// assert_eq!(p4_cmd::BaseFileType::Utf8.to_string(), "utf8");
/// assert_eq!("utf8".parse::<p4_cmd::BaseFileType>().unwrap(), p4_cmd::BaseFileType::Utf8);
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BaseFileType {
    #[doc(hidden)]
    __Nonexhaustive,

    /// Text file
    ///
    /// Synced as text in the workspace. Line-ending translations are performed automatically.
    ///
    /// Stored as: deltas in RCS format
    Text,
    /// Non-text file
    ///
    /// Synced as binary files in the workspace. Stored compressed within the depot.
    ///
    /// Stored as: full file, compressed
    Binary,
    /// Symbolic link
    ///
    /// Helix Server applications on UNIX, OS X, recent versions of Windows treat these files as
    /// symbolic links. On other platforms, these files appear as (small) text files.
    ///
    /// On Windows, you require admin privileges or an appropriate group policy must be set, otherwise, you get text files.
    ///
    /// Stored as: deltas in RCS format
    Symlink,
    /// Unicode file
    ///
    /// Services operating in unicode mode support the unicode file type. These files are
    /// translated into the local character set specified by P4CHARSET.
    ///
    /// Line-ending translations are performed automatically.
    ///
    /// Services not in unicode mode do not support the unicode file type.
    ///
    ///Stored as: RCS deltas in UTF-8 format
    Unicode,
    /// Unicode file
    ///
    /// Synced in the client workspace with the UTF-8 BOM (byte order mark).
    ///
    /// Whether the service is in unicode mode or not, files are transferred as UTF-8 in the client workspace.
    ///
    /// Line-ending translations are performed automatically.
    ///
    /// Stored as: RCS deltas in UTF-8 format without the UTF-8 BOM (byte order mark).
    Utf8,
    /// Unicode file
    ///
    /// Whether the service is in unicode mode or not, files are transferred as UTF-8, and
    /// translated to UTF-16 (with byte order mark, in the byte order appropriate for the user's
    /// machine) in the client workspace.
    ///
    /// Line-ending translations are performed automatically.
    ///
    /// Stored as: RCS deltas in UTF-8 format
    Utf16,

    Unknown(String),
}

impl Default for BaseFileType {
    fn default() -> Self {
        BaseFileType::Text
    }
}

impl str::FromStr for BaseFileType {
    type Err = fmt::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let ft = match s {
            "text" => BaseFileType::Text,
            "binary" => BaseFileType::Binary,
            "symlink" => BaseFileType::Symlink,
            "unicode" => BaseFileType::Unicode,
            "utf8" => BaseFileType::Utf8,
            "utf16" => BaseFileType::Utf16,
            s => BaseFileType::Unknown(s.to_owned()),
        };
        Ok(ft)
    }
}

impl fmt::Display for BaseFileType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let value = match self {
            BaseFileType::Text => "text",
            BaseFileType::Binary => "binary",
            BaseFileType::Symlink => "symlink",
            BaseFileType::Unicode => "unicode",
            BaseFileType::Utf8 => "utf8",
            BaseFileType::Utf16 => "utf16",
            BaseFileType::Unknown(ref s) => s.as_str(),
            BaseFileType::__Nonexhaustive => unreachable!("This is a private variant"),
        };
        write!(f, "{}", value)
    }
}

/// Perforce file type modifiers.
///
/// # Example
///
/// ```rust
/// let mut modifiers = p4_cmd::FileTypeModifiers::new();
/// modifiers.exclusive = true;
/// assert_eq!(modifiers.to_string(), "l");
/// assert_eq!("l".parse::<p4_cmd::FileTypeModifiers>().unwrap(), modifiers);
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FileTypeModifiers {
    /// File is always writable on client
    pub always_writeable: bool,
    /// Execute bit set on client
    pub executable: bool,
    /// RCS keyword expansion
    pub rcs_expansion: bool,
    /// Exclusive open (locking)
    pub exclusive: bool,
    /// Perforce stores the full compressed version of each file revision
    pub full: bool,
    /// Perforce stores deltas in RCS format
    pub deltas: bool,
    /// Perforce stores full file per revision, uncompressed
    pub full_uncompressed: bool,
    /// Only the head revision is stored
    pub head: bool,
    /// Only the most recent n revisions are stored
    pub revisions: Option<usize>,
    /// Preserve original modtime
    pub modtime: bool,
    /// Archive trigger required
    pub archive: bool,
    non_exhaustive: (),
}

impl FileTypeModifiers {
    pub fn new() -> Self {
        Default::default()
    }
}

impl str::FromStr for FileTypeModifiers {
    type Err = fmt::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut modifiers = FileTypeModifiers::default();

        for flag in s.chars() {
            match flag {
                'w' => modifiers.always_writeable = true,
                'x' => modifiers.executable = true,
                'k' => modifiers.rcs_expansion = true,
                'l' => modifiers.exclusive = true,
                'C' => modifiers.full = true,
                'D' => modifiers.deltas = true,
                'F' => modifiers.full_uncompressed = true,
                'S' => modifiers.head = true,
                // TODO: handle `revisions`.
                'm' => modifiers.modtime = true,
                'X' => modifiers.archive = true,
                _ => return Err(fmt::Error),
            }
        }

        Ok(modifiers)
    }
}

impl fmt::Display for FileTypeModifiers {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.always_writeable {
            write!(f, "w")?;
        }
        if self.executable {
            write!(f, "x")?;
        }
        if self.rcs_expansion {
            write!(f, "k")?;
        }
        if self.exclusive {
            write!(f, "l")?;
        }
        if self.full {
            write!(f, "C")?;
        }
        if self.deltas {
            write!(f, "D")?;
        }
        if self.full_uncompressed {
            write!(f, "S")?;
        }
        if self.head {
            write!(f, "S")?;
        }
        if let Some(revisions) = self.revisions {
            write!(f, "S{}", revisions)?;
        }
        if self.modtime {
            write!(f, "m")?;
        }
        if self.archive {
            write!(f, "X")?;
        }

        Ok(())
    }
}

/// Perforce file type.
///
/// # Example
///
/// ```rust
/// let mut modifiers = p4_cmd::FileTypeModifiers::default();
/// modifiers.exclusive = true;
/// let ft = p4_cmd::FileType::new()
///     .base(p4_cmd::BaseFileType::Binary)
///     .modifiers(Some(modifiers));
/// assert_eq!(ft.to_string(), "binary+l");
/// assert_eq!("binary+l".parse::<p4_cmd::FileType>().unwrap(), ft);
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FileType {
    /// The base Perforce file type
    pub base: BaseFileType,
    pub modifiers: Option<FileTypeModifiers>,
    non_exhaustive: (),
}

impl FileType {
    pub fn new() -> Self {
        Default::default()
    }

    pub fn base(mut self, base: BaseFileType) -> Self {
        self.base = base;
        self
    }

    pub fn modifiers(mut self, modifiers: Option<FileTypeModifiers>) -> Self {
        self.modifiers = modifiers;
        self
    }
}

impl str::FromStr for FileType {
    type Err = fmt::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut itr = s.splitn(2, '+');
        let base = itr.next().ok_or(fmt::Error)?;
        let base = base.parse().map_err(|_| fmt::Error)?;

        let modifiers = itr
            .next()
            .map(|f| {
                let modifiers: FileTypeModifiers = f.parse()?;
                Ok(modifiers)
            }).map_or(Ok(None), |r| r.map(Some))?;

        let ft = FileType {
            base,
            modifiers,
            non_exhaustive: (),
        };

        Ok(ft)
    }
}

impl fmt::Display for FileType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.base)?;
        if let Some(ref modifiers) = self.modifiers {
            write!(f, "+{}", modifiers)?;
        }
        Ok(())
    }
}