Change.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • library/
  • P4/
  • Change.php
  • View
  • Commits
  • Open Download .zip Download (35 KB)
<?php
/**
 * Abstracts operations against Perforce changes.
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 * @todo        support shelved changes.
 */
class P4_Change extends P4_Spec_PluralAbstract implements P4_ResolvableInterface
{
    const DEFAULT_CHANGE         = 'default';
    const PENDING_CHANGE         = 'pending';
    const SUBMITTED_CHANGE       = 'submitted';

    const FETCH_BY_FILESPEC      = 'filespec';
    const FETCH_BY_STATUS        = 'status';
    const FETCH_INTEGRATED       = 'integrated';
    const FETCH_BY_CLIENT        = 'client';
    const FETCH_BY_USER          = 'user';

    const RESOLVE_FILE           = 'file';

    const MAX_SUBMIT_ATTEMPTS    = 3;

    protected static $_specType  = 'change';
    protected static $_idField   = 'Change';
    protected static $_accessors = array(
        'Date'          => 'getDateTime',
        'User'          => 'getUser',
        'Client'        => 'getClient',
        'Status'        => 'getStatus',
        'Description'   => 'getDescription',
        'JobStatus'     => 'getJobStatus',
        'Jobs'          => 'getJobs',
        'Files'         => 'getFiles'
    );
    protected static $_mutators  = array(
        'Description'   => 'setDescription',
        'JobStatus'     => 'setJobStatus',
        'Jobs'          => 'setJobs',
        'Files'         => 'setFiles'
    );

    protected $_cache   = array();

    /**
     * Get the number of this change.
     * Extends parent to return an integer value for numbered changes.
     *
     * @return  null|string|id  the integer number of the change, the literal string 'default'
     *                          or null if no id has been set.
     */
    public function getId()
    {
        $id = parent::getId();
        if ($id !== null && $id !== static::DEFAULT_CHANGE) {
            $id = intval($id);
        }
        return $id;
    }

    /**
     * Set the id of this spec entry. Id must be in a valid format or null.
     * Extended from parent to clear cache.
     *
     * @param   null|string     $id     the id of this entry - pass null to clear.
     * @return  P4_Spec_PluralAbstract  provides a fluent interface
     * @throws  InvalidArgumentException    if id does not pass validation.
     */
    public function setId($id)
    {
        if ($this->getId() !== $id) {
            $this->_cache = array();
        }

        return parent::setId($id);
    }

    /**
     * Determine if the given change id exists.
     *
     * @param   string                   $id          the id to check for.
     * @param   P4_Connection_Interface  $connection  optional - a specific connection to use.
     * @return  bool  true if the given id matches an existing change.
     */
    public static function exists($id, P4_Connection_Interface $connection = null)
    {
        // check id for valid format
        if (!static::_isValidId($id)) {
            return false;
        }

        // if no connection given, use default.
        $connection = $connection ?: static::getDefaultConnection();

        // default change always exists.
        if ($id === static::DEFAULT_CHANGE) {
            return true;
        }

        // attempt to fetch change - assume id does not exist on failure.
        try {
            $connection->run('change', array('-o', $id));
            return true;
        } catch (P4_Exception $e) {
            return false;
        }
    }

    /**
     * Get all changes from Perforce. Adds filtering options.
     *
     * @param   array   $options    optional - array of options to augment fetch behavior.
     *                              supported options are:
     *
     *                                   FETCH_MAXIMUM - set to integer value to limit to the
     *                                                   first 'max' number of entries.
     *                               FETCH_BY_FILESPEC - set to a filespec to limit changes to those
     *                                                   affecting the file(s) matching the filespec.
     *                                 FETCH_BY_STATUS - set to a valid change status to limit result
     *                                                   to changes with that status (e.g. 'pending').
     *                                FETCH_INTEGRATED - set to true to include changes integrated
     *                                                   into the specified files.
     *                                 FETCH_BY_CLIENT - set to a client to limit changes to those
     *                                                   on the named client.
     *                                   FETCH_BY_USER - set to a user to limit changes to those
     *                                                   owned by the named user.
     *
     * @param   P4_Connection_Interface $connection  optional - a specific connection to use.
     * @return  P4_Model_Iterator   all records of this type.
     */
    public static function fetchAll($options = array(), P4_Connection_Interface $connection = null)
    {
        // simply return parent - method exists to document options.
        return parent::fetchAll($options, $connection);
    }

    /**
     * Extend parent to set id to 'new' if unset and to reopen files that
     * are open in other pending changelists where necessary.
     *
     * @param   bool        $force      optional - default false - true to save submitted change.
     * @return  P4_Change   provides a fluent interface
     * @throws  P4_UnopenedException                if change contains unopened files.
     * @throws  P4_Connection_CommandException      if save command fails for some reason.
     * @todo    support the -u flag to force update of submitted change description.
     */
    public function save($force = false)
    {
        $values = $this->_getValues();
        if (!isset($values[static::_getIdField()]) || $this->isDefault()) {
            $values[static::_getIdField()] = "new";
        }

        // ensure all required fields have values.
        $this->_validateRequiredFields($values);

        // can't update a submitted change without the force option.
        if (!$force && $this->isSubmitted()) {
            throw new P4_Spec_Exception(
                "Cannot update a submitted change without the force option."
            );
        }

        // perform save.
        $connection = $this->getConnection();
        try {

            $flags = array("-i");
            if ($force) {
                $flags[] = "-u";
            }
            $result = $connection->run(static::_getSpecType(), $flags, $values);

            // extract change number from command result.
            $data = $result->getData(0);

            if (!preg_match('/^Change ([^ ]+) (created|updated)/', $data, $matches)) {
                throw new P4_Spec_Exception('Cannot determine number of saved change.');
            }

            $id = $matches[1];

        } catch (P4_Connection_CommandException $e) {

            // if the exception was caused by non-existant jobs, the change should
            // have been created.
            if (preg_match(
                "/Change ([^ ]+) (created|updated).*Job '([^']+)' doesn't exist\./s",
                $e->getMessage(), $matches
            )) {
                $this->setId($matches[1]);
                throw $e;
            }

            // if exception not caused by un-opened files, re-throw.
            if (strpos($e->getMessage(), "Can't include file(s) not already opened.") === false) {
                throw $e;
            }

            // if any files are truly un-opened throw a special un-opened files exception.
            // (save will complain of un-opened files if files are not in default change)
            $flags  = array("-Ro", "-T", "depotFile");
            $flags  = array_merge($flags, $values['Files']);
            $result = $connection->run("fstat", $flags);
            if ($result->hasWarnings()) {
                throw new P4_UnopenedException(
                    "Cannot save change. One or more files are not open."
                );
            }

            // all files are actually open, save w.out files first, then reopen.
            $change = clone $this;
            $id     = $change->setFiles(null)->save()->getId();
            $flags  = $values['Files'];
            array_unshift($flags, "-c", $id);
            $connection->run("reopen", $flags);

        }

        // Store the retrieved id.
        $this->setId($id);

        // should re-populate (server may change values).
        $this->_deferPopulate(true);

        return $this;
    }

    /**
     * Save and submit this changelist.
     *
     * @param   string              $description    optional - a description of this change.
     * @param   null|string|array   $options        optional resolve flags, to be used if conflict
     *                                              occurs. See resolve() for details.
     * @return  P4_Change           provides fluent interface.
     * @throws  P4_Spec_Exception   if the change is not a pending change.
     * @throws  P4_Change_ResolveConflictException  if change contains files requiring resolve.
     * @throws  P4_Change_DeleteConflictException   if change contains files that have been deleted.
     */
    public function submit($description = null, $options = null)
    {
        // ensure change is a pending change.
        if (!$this->isPending()) {
            throw new P4_Spec_Exception("Can only submit pending changes.");
        }

        // if description is given, use it.
        if (strlen($description)) {
            $this->setDescription($description);
        }

        // save the change before submit.
        $this->save();

        // try repeatedly to submit (with resolves in-between attempts)
        // note: no need to explicitly sync as submit schedules resolve
        for ($i = static::MAX_SUBMIT_ATTEMPTS; $i > 0; $i--) {
            try {
                $result = $this->getConnection()->run("submit", array("-c", $this->getId()));

                // everything went ok no need to retry.
                break;
            } catch (P4_Connection_ConflictException $e) {
                // if there are no resolve options or we have exceeded
                // max resolve attempts; re-throw the resolve exception
                if ($i <= 1 || empty($options)) {
                    throw $e;
                }

                // our changelist id has possibly been updated
                // update our id field to match.
                $this->setId($e->getChange()->getId());

                $this->resolve($options);
            }
        }

        // extract change number from last data block of command result
        // because the change number may have changed during submit.
        $last = end($result->expandSequences()->getData());
        $this->setId($last['submittedChange']);

        return $this;
    }

    /**
     * Revert all of the files in this changelist.
     *
     * @return  P4_Change           provides fluent interface.
     * @throws  P4_Spec_Exception   if the change is not a pending change.
     */
    public function revert()
    {
        // ensure change is a pending change.
        if (!$this->isPending()) {
            throw new P4_Spec_Exception("Can only revert pending changes.");
        }

        // save the change before revert (updates files in change).
        $this->save();

        // perform revert.
        $result = $this->getConnection()->run(
            "revert",
            array("-c", $this->getId(), '//...')
        );

        // should re-populate.
        $this->_deferPopulate(true);

        return $this;
    }


    /**
     * Delete this changelist.
     *
     * @param   bool        $force  optional - defaults to false - set to true to force delete of
     *                              another user/client's changelist or a submitted (empty) change.
     * @return  P4_Change   provides fluent interface.
     */
    public function delete($force = false)
    {
        $id = $this->getId();
        if ($id === null) {
            throw new P4_Spec_Exception("Cannot delete change. No id has been set.");
        }

        // default change cannot be deleted.
        if ($id === P4_Change::DEFAULT_CHANGE) {
            throw new P4_Spec_Exception("Cannot delete the default change.");
        }

        // ensure id exists.
        $connection = $this->getConnection();
        if (!static::exists($id, $connection)) {
            throw new P4_Spec_NotFoundException(
                "Cannot delete change $id. Record does not exist."
            );
        }

        // unknown or unhandled change status (e.g. 'shelved').
        if (!$this->isPending() && !$this->isSubmitted()) {
            throw new P4_Spec_Exception(
                "Unable to delete change with status '" . $this->getStatus() . "'."
            );
        }

        // handle submitted changes.
        $connection = $this->getConnection();
        if ($this->isSubmitted()) {

            // requires force option.
            if (!$force) {
                throw new P4_Spec_Exception(
                    "Cannot delete a submitted change without the force option."
                );
            }

            // check for files.
            if (count($this->getFiles())) {
                throw new P4_Spec_Exception(
                    "Cannot delete a submitted change that contains files."
                );
            }

            $result = $connection->run("change", array("-d", "-f", $id));
        }

        // handle pending changes (must remove files and fixes first).
        if ($this->isPending()) {
            if (!$force &&
                ($this->getUser()   !== $connection->getUser() ||
                 $this->getClient() !== $connection->getClient())) {
                throw new P4_Spec_Exception(
                    "Cannot delete a change from another user/client without the force option."
                );
            }

            // remove any associated files or jobs.
            $change = clone $this;
            $change->setFiles(null)->setJobs(null)->save();

            // delete the change.
            $flags = array("-d", $id);
            if ($force) {
                array_unshift($flags, "-f");
            }
            $connection->run("change", $flags);
        }

        // confirm delete successful (change -d does not surface errors).
        if (static::exists($id)) {
            throw new P4_Spec_Exception("Failed to delete change $id.");
        }

        // should re-populate.
        $this->_deferPopulate(true);

        return $this;
    }

    /**
     * Resolves the change based on the passed option(s).
     *
     * You must specify one of the below:
     *  RESOLVE_ACCEPT_MERGED
     *   Automatically accept the Perforce-recom mended file revision:
     *   if theirs is identical to base, accept yours; if yours is identical
     *   to base, accept theirs; if yours and theirs are different from base,
     *   and there are no conflicts between yours and theirs; accept merge;
     *   other wise, there are conflicts between yours and theirs, so skip this file.
     *  RESOLVE_ACCEPT_YOURS
     *   Accept Yours, ignore theirs.
     *  RESOLVE_ACCEPT_THEIRS
     *   Accept Theirs. Use this flag with caution!
     *  RESOLVE_ACCEPT_SAFE
     *   Safe Accept. If either yours or theirs is different from base,
     *   (and the changes are in common) accept that revision. If both
     *   are different from base, skip this file.
     *  RESOLVE_ACCEPT_FORCE
     *   Force Accept. Accept the merge file no matter what. If the merge file
     *   has conflict markers, they will be left in, and you'll need to remove
     *   them by editing the file.
     *
     * Additionally, one of the following whitespace options can, optionally, be passed:
     *  IGNORE_WHITESPACE_CHANGES
     *   Ignore whitespace-only changes (for instance, a tab replaced by eight spaces)
     *  IGNORE_WHITESPACE
     *   Ignore whitespace altogether (for instance, deletion of tabs or other whitespace)
     *  IGNORE_LINE_ENDINGS
     *   Ignore differences in line-ending convention
     *
     * Lastly, the resolve can be limited to a particular file in the change by passing:
     *  RESOLVE_FILE => filespec with no wildcards
     *
     * @param   array|string    $options    Resolve option(s); must include a RESOLVE_* preference.
     * @return  P4_Change       provide fluent interface.
     * @todo implement a way to accept edit
     */
    public function resolve($options)
    {
        if (is_string($options)) {
            $options = array($options);
        }

        if (!is_array($options)) {
            throw new InvalidArgumentException('Expected a string or array of options.');
        }

        $mode       = '';
        $whitespace = '';
        $arguments  = array();

        // loop options so we accept the last mode
        // and whitespace setting we encounter.
        foreach ($options as $option) {
            switch ($option)
            {
                case static::RESOLVE_ACCEPT_MERGED:
                    $mode = '-am';
                    break;
                case static::RESOLVE_ACCEPT_YOURS:
                    $mode = '-ay';
                    break;
                case static::RESOLVE_ACCEPT_THEIRS:
                    $mode = '-at';
                    break;
                case static::RESOLVE_ACCEPT_SAFE:
                    $mode = '-as';
                    break;
                case static::RESOLVE_ACCEPT_FORCE:
                    $mode = '-af';
                    break;
            }

            switch ($option)
            {
                case static::IGNORE_WHITESPACE_CHANGES:
                    $whitespace = '-db';
                    break;
                case static::IGNORE_WHITESPACE:
                    $whitespace = '-dw';
                    break;
                case static::IGNORE_LINE_ENDINGS:
                    $whitespace = '-dl';
                    break;
            }
        }

        // we can't do anything without a mode; throw
        if (empty($mode)) {
            throw new InvalidArgumentException(
                'No action specified. Expected Resolve Accept Merged|Yours|Theirs|Safe|Force'
            );
        }

        // compile our various flags into our arguments array
        $arguments[] = $mode;
        if ($whitespace) {
            $arguments[] = $whitespace;
        }

        $files = $this->getFiles();
        if (isset($options[static::RESOLVE_FILE])) {
            $file = $options[static::RESOLVE_FILE];
            if (!in_array($file, $files)) {
                throw new InvalidArgumentException(
                    "The RESOLVE_FILE specified is not in this change."
                );
            }

            $files = array($file);
        }

        // resolve files in several (as few as possible) runs as
        // there is a potential to exceed the arg-max on this command
        $connection = $this->getConnection();
        $batches    = $connection->batchArgs($files, $arguments);
        foreach ($batches as $batch) {
            $connection->run('resolve', $batch);
        }

        return $this;
    }

    /**
     * Get the date this change was last modified on the server.
     *
     * @return  null|string     the date this change was last modified on the server,
     *                          or null if the change does not exist on the server.
     * @todo    modify to use DateTime object.
     */
    public function getDateTime()
    {
        return $this->_getValue('Date');
    }

    /**
     * Get the user that created this change.
     *
     * @return  string  the user that created this change.
     */
    public function getUser()
    {
        $user = $this->_getValue('User');
        if (!$user) {
            $user = $this->getConnection()->getUser();
        }
        return $user;
    }

    /**
     * Get the client on which this change was created.
     *
     * @return  string  the client on which this change was created.
     */
    public function getClient()
    {
        $client = $this->_getValue('Client');
        if (!$client) {
            $client = $this->getConnection()->getClient();
        }
        return $client;
    }

    /**
     * Get the status of this change (either 'pending' or 'submitted').
     *
     * @return  string  the status of this change: 'pending', 'submitted'.
     */
    public function getStatus()
    {
        $status = $this->_getValue('Status');
        if (!$status) {
            $status = static::PENDING_CHANGE;
        }
        return $status;
    }

    /**
     * Get the description for this change.
     *
     * @return  string  the description for this change.
     */
    public function getDescription()
    {
        return $this->_getValue('Description');
    }

    /**
     * Set the description for this change.
     *
     * @param   string|null     $description    description for this change.
     * @return  P4_Change       provides a fluent interface.
     * @throws  InvalidArgumentException  if description is incorrect type.
     */
    public function setDescription($description)
    {
        if ($description !== null && !is_string($description)) {
            throw new InvalidArgumentException("Cannot set description. Invalid type given.");
        }

        return $this->_setValue('Description', $description);
    }

    /**
     * Get the job status of this change (the status that associated jobs will
     * have when the change is submitted).
     *
     * The value of the job status field is not preserved. You cannot get the
     * job status of a saved or submitted change. Once a changelist is saved or
     * submitted, the job status field is cleared. It can only be read after it
     * has been explicitly set, and before the change is saved or submitted.
     *
     * @return  null|string     the job status of this change if not yet saved or submitted.
     *                          null otherwise.
     */
    public function getJobStatus()
    {
        return $this->_getValue('JobStatus');
    }

    /**
     * Get the jobs attached to this change.
     *
     * @return  array   the list of jobs attached to this change.
     * @todo    return P4_Job objects in an iterator.
     */
    public function getJobs()
    {
        $jobs = $this->_getValue('Jobs');
        return is_array($jobs) ? $jobs : array();
    }

    /**
     * Set the list of jobs attached to this change.
     *
     * @param   null|array      $jobs   the jobs to attach to this change.
     * @return  P4_Change       provides a fluent interface.
     * @throws  InvalidArgumentException    if jobs is incorrect type.
     * @throws  P4_Spec_Exception           if change is submitted.
     */
    public function setJobs($jobs)
    {
        if ($jobs === null) {
            $jobs = array();
        }

        // if jobs is an iterator, extract the job numbers.
        if ($jobs instanceof P4_Model_Iterator) {
            $newJobs = array();
            foreach ($jobs as $job) {
                if ($job instanceof P4_Job) {
                    $newJobs[] = $job->getId();
                } else {
                    throw new InvalidArgumentException('Each iterator job must be a P4_Job object.');
                }
            }
            $jobs = $newJobs;
        }

        // ensure jobs is an array.
        if (!is_array($jobs)) {
            throw new InvalidArgumentException('Cannot set jobs. Invalid type given.');
        }

        // ensure job elements are strings.
        foreach ($jobs as $job) {
            if (!is_string($job)) {
                throw new InvalidArgumentException('Each job must be a string.');
            }
        }

        // don't permit set jobs on submitted changes.
        if ($this->isSubmitted()) {
            throw new P4_Spec_Exception('Cannot set jobs on a submitted change.');
        }

        return $this->_setValue('Jobs', $jobs);
    }

    /**
     * Add a job to the list of jobs attached to this change.
     *
     * @param   string      $job    the id of the job to attach to this change.
     * @return  P4_Change   provides fluent interface.
     */
    public function addJob($job)
    {
        $jobs = $this->getJobs();
        if (!in_array($job, $jobs)) {
            $jobs[] = $job;
        }
        $this->setJobs($jobs);

        return $this;
    }

    /**
     * Get the files attached to this change. Revspecs are included for submitted
     * changes but are not present on pending changes.
     *
     * @return  array   list of files associated with this change.
     */
    public function getFiles()
    {
        $files = $this->_getValue('Files');
        return is_array($files) ? $files : array();
    }

    /**
     * Get the files attached to this change in P4_File format.
     *
     * @return  P4_Model_Iterator   list of P4_File's associated with this change.
     */
    public function getFileObjects()
    {
        if (!isset($this->_cache['fileObjects'])
            || !$this->_cache['fileObjects'] instanceof P4_Model_Iterator
        ) {
            $this->_cache['fileObjects'] = P4_File::fetchAll(
                P4_File_Query::create()->addFilespecs(
                    $this->getFiles()
                )
            );
        }

        return clone $this->_cache['fileObjects'];
    }

    /**
     * Get the requested file attached to this change in P4_File format.
     *
     * @param   P4_File|string  $file       Filespec in string or P4_File format; rev is ignored
     * @return  P4_File         The requested file at the revision associated with this change
     * @throws  InvalidArgumentException    If the specified file doesn't exist.
     */
    public function getFileObject($file)
    {
        // normalize to string
        if ($file instanceof P4_File) {
            $file = $file->getDepotFilename();
        }

        // validate input
        if (!is_string($file)) {
            throw new InvalidArgumentException('File must be a string or P4_File object.');
        }

        // ensure no rev-spec is present on our comparison entry
        $file = P4_File::stripRevspec($file);
        foreach ($this->getFileObjects() as $changeFile) {
            if (P4_File::stripRevspec($changeFile->getDepotfilename()) == $file) {
                return $changeFile;
            }
        }

        throw new InvalidArgumentException('The requested file was not found in this change');
    }

    /**
     * Set the list of opened files attached to this change.
     *
     * @param   null|array|P4_Model_Iterator  $files  the files to attach to this change.
     * @return  P4_Change                     provides a fluent interface.
     * @throws  InvalidArgumentException      if files is incorrect type.
     * @throws  P4_Spec_Exception             if change is submitted.
     * @todo    accept model iterator of p4 file objects as input.
     */
    public function setFiles($files)
    {
        if ($files === null) {
            $files = array();
        }

        if ($files instanceof P4_Model_Iterator) {
            $files = iterator_to_array($files);
        }

        // ensure files is an array.
        if (!is_array($files)) {
            throw new InvalidArgumentException('Cannot set files. Invalid type given.');
        }

        // normalize the array entries to a string, stripping revspecs
        foreach ($files as &$file) {
            if ($file instanceof P4_File) {
                $file = $file->getFilespec();
            }

            if (!is_string($file)) {
                throw new InvalidArgumentException('All files must be a string or P4_File');
            }

            $file = P4_File::stripRevspec($file);
        }

        // don't permit set files on submitted changes.
        if ($this->isSubmitted()) {
            throw new P4_Spec_Exception('Cannot set files on a submitted change.');
        }

        // we cache file objects; clear that out
        $this->_cache = array();

        $this->_setValue('Files', $files);

        return $this;
    }

    /**
     * Add a file to the list of files in this changelist.
     *
     * @param   string|P4_File  $file   the file to attach to this change.
     * @return  P4_Change       provides fluent interface.
     */
    public function addFile($file)
    {
        // if file is a P4_File object, extract the filespecs.
        if ($file instanceof P4_File) {
            $file = $file->getFilespec();
        }

        $files = $this->getFiles();
        if (!in_array($file, $files)) {
            $files[] = $file;
        }
        $this->setFiles($files);

        return $this;
    }

    /**
     * Check if this is a pending change.
     *
     * @return  bool  true if this is a pending change - false otherwise.
     */
    public function isPending()
    {
        return ($this->isDefault() || $this->getStatus() === static::PENDING_CHANGE);
    }

    /**
     * Check if this is a submitted change.
     *
     * @return  bool  true if this is a submitted change - false otherwise.
     */
    public function isSubmitted()
    {
        return ($this->getStatus() === static::SUBMITTED_CHANGE);
    }

    /**
     * Test if this is the default change.
     *
     * @return  bool  true if this is the default change - false otherwise.
     */
    public function isDefault()
    {
        return ($this->getId() === static::DEFAULT_CHANGE);
    }

    /**
     * Get files that need to be resolved.
     *
     * @return  P4_Model_Iterator   files that need to be resolved.
     */
    public function getFilesToResolve()
    {
        $query = P4_File_Query::create()
                 ->addFilespec(P4_File::ALL_FILES)
                 ->setLimitToChangelist($this->getId())
                 ->setLimitToNeedsResolve(true)
                 ->setLimitToOpened(true);
        return P4_File::fetchAll($query, $this->getConnection());
    }

    /**
     * Get files that must be reverted.
     *
     * @return  P4_Model_Iterator   files that must be reverted.
     */
    public function getFilesToRevert()
    {
        // setup fstat filter to match files that must be reverted.
        // several conditions to catch:
        //  - files open for add, but already existing and not deleted, or
        //  - files that are not open for add, but are deleted in the depot.
        $filter = "(action=add & headAction=* & ^headAction=...deleted) | "
                . "(headAction=...delete & ^action=add)";

        $query = P4_File_Query::create()
                 ->addFilespec(P4_File::ALL_FILES)
                 ->setLimitToChangelist($this->getId())
                 ->setLimitToOpened(true)
                 ->setFilter($filter);
        return P4_File::fetchAll($query, $this->getConnection());
    }

    /**
     * Check if the given id is in a valid format for a change number.
     *
     * @param   string      $id     the id to check
     * @return  bool        true if id is valid, false otherwise
     */
    protected static function _isValidId($id)
    {
        $validator = new P4_Validate_ChangeNumber;
        return $validator->isValid($id);
    }

    /**
     * Get raw spec data direct from Perforce. No caching involved.
     * Overrides parent to suppress id when id is 'default' and to
     * fetch files for submitted changes.
     *
     * @return  array   $data   the raw spec output from Perforce.
     * @todo    get jobs for submitted changes.
     */
    protected function _getSpecData()
    {
        $flags = array('-o');
        if ($this->getId() !== static::DEFAULT_CHANGE) {
            $flags[] = $this->getId();
        }
        $data = $this->getConnection()
                     ->run(static::_getSpecType(), $flags)
                     ->expandSequences()
                     ->getData(0);

        // get files/jobs if this is a submitted change
        // note: can't use isSubmitted here - populate not complete yet.
        if ($data['Status'] == P4_Change::SUBMITTED_CHANGE) {
            $query = P4_File_Query::create()
                     ->addFilespec(P4_File::ALL_FILES)
                     ->setLimitToChangelist($this->getId());
            $files = P4_File::fetchAll($query, $this->getConnection());

            $this->_cache['fileObjects'] = $files;
            $data['Files'] = array();
            foreach ($files as $file) {
                $data['Files'][] = $file->getFilespec() . '#' . $file->getStatus('headRev');
            }
        }

        return $data;
    }

    /**
     * Produce set of flags for the spec list command, given fetch all options array.
     * Extends parent to add support for additional options.
     *
     * @param   array   $options    array of options to augment fetch behavior.
     *                              see fetchAll for documented options.
     * @return  array   set of flags suitable for passing to spec list command.
     */
    protected static function _getFetchAllFlags($options)
    {
        $flags = parent::_getFetchAllFlags($options);

        // always use -l (for full descriptions).
        $flags[] = "-l";

        if (isset($options[static::FETCH_INTEGRATED]) &&
            $options[static::FETCH_INTEGRATED] === true) {
            $flags[] = "-i";
        }

        if (isset($options[static::FETCH_BY_STATUS])) {
            $flags[] = "-s";
            $flags[] = $options[static::FETCH_BY_STATUS];
        }

        if (isset($options[static::FETCH_BY_CLIENT])) {
            $flags[] = "-c";
            $flags[] = $options[static::FETCH_BY_CLIENT];
        }

        if (isset($options[static::FETCH_BY_USER])) {
            $flags[] = "-u";
            $flags[] = $options[static::FETCH_BY_USER];
        }

        // filespec must come last.
        if (isset($options[static::FETCH_BY_FILESPEC])) {
            $flags[] = $options[static::FETCH_BY_FILESPEC];
        }

        return $flags;
    }

    /**
     * Given a spec entry from spec list output (p4 changes), produce
     * an instance of this spec with field values set where possible.
     *
     * @param   array                       $listEntry      a single spec entry from spec list output.
     * @param   array                       $flags          the flags that were used for this 'fetchAll' run.
     * @param   P4_Connection_Interface     $connection     a specific connection to use.
     * @return  P4_User                     a (partially) populated instance of this spec class.
     * @todo    properly convert unixtime to DateTime object.
     */
    protected static function _fromSpecListEntry($listEntry, $flags, P4_Connection_Interface $connection)
    {
        // time is in unixtime - unset to avoid figuring out timezone
        // for a proper conversion.
        unset($listEntry['time']);

        // rename 'desc' field to 'Description'.
        $listEntry['Description'] = $listEntry['desc'];
        unset($listEntry['desc']);

        return parent::_fromSpecListEntry($listEntry, $flags, $connection);
    }
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/library/P4/Change.php
#1 8972 Matt Attaway Initial add of the Chronicle source code