示例#1
0
 // Perforce Revert this file
 public void RevertCall(object sender)
 {
     Perforce.RevertFiles(new List <string>()
     {
         ItemPath
     });
     UpdateFileStatus();
 }
示例#2
0
 // Perforce Checkout this file
 public void CheckoutCall(object sender)
 {
     Perforce.CheckoutFiles(new List <string>()
     {
         ItemPath
     });
     UpdateFileStatus();
 }
示例#3
0
 // Perforce Delete this file
 public void DeleteCall(object sender)
 {
     Perforce.DeleteFiles(new List <string>()
     {
         ItemPath
     });
     UpdateFileStatus();
 }
示例#4
0
        //-------------------------------------------------------------------------

        private void LoadFromP4()
        {
            // Get users from P4.
            string output = Perforce.RunCommand("users");

            // Split the output into individual lines.
            string[] lines = output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);

            // The username is the first thing on each line followed by a space char.
            foreach (string line in lines)
            {
                if (line.IndexOf(' ') < 0)
                {
                    continue;
                }

                string username = line.Substring(0, line.IndexOf(' '));

                //Program.Log.AddEntry(
                //  Log.EntryType.INFO,
                //  "Found user '" + username + "' in P4." );

                try
                {
                    if (GetUser(username) != null)
                    {
                        continue;
                    }

                    User user =
                        new User(
                            false,
                            username,
                            "Unknown",
                            "Unknown",
                            "123454321",
                            "Unknown",
                            false,
                            false,
                            false);

                    Users.Add(user);

                    //Program.Log.AddEntry(
                    //  Log.EntryType.INFO,
                    //  "Added user '" + user.Username + "' to DB." );
                }
                catch (Exception ex)
                {
                    Program.Log.AddEntry(
                        Log.EntryType.ERROR,
                        "Error while creating a user: " + ex.Message);
                }
            }
        }
        void PollForUpdates()
        {
            while (!QuitEvent.WaitOne(5 * 60 * 1000))
            {
                StringWriter Log = new StringWriter();

                List <PerforceChangeSummary> Changes;
                if (Perforce.FindChanges(WatchPath, 1, out Changes, Log) && Changes.Count > 0)
                {
                    TriggerUpdate(UpdateType.Background, null);
                }
            }
        }
        // Updates the Perforce status of the file
        public static void UpdatePerforceStatus(List <UserFile> fileList)
        {
            string[] fileStatusList = fileList.Select(x => x.ItemPath).ToArray();

            if (fileStatusList.Any())
            {
                List <FileMetaData> p4Info = Perforce.FileStatus(fileStatusList);
                if (p4Info != null)
                {
                    foreach (FileMetaData metaData in p4Info)
                    {
                        UserFile updateFile = fileList.Where(x => x.ItemPath == metaData.LocalPath.Path).FirstOrDefault();
                        try
                        {
                            if (metaData == null)
                            {
                                updateFile.P4Status = P4Status.Unknown;
                            }
                            else if (metaData.OtherUsers != null)
                            {
                                updateFile.P4Status = P4Status.CheckedOutOther;
                            }
                            else if (metaData.Action == FileAction.None)
                            {
                                updateFile.P4Status = P4Status.CheckedIn;
                            }
                            else if (metaData.Action == FileAction.Edit)
                            {
                                updateFile.P4Status = P4Status.CheckedOut;
                            }
                            else if (metaData.Action == FileAction.Add)
                            {
                                updateFile.P4Status = P4Status.Add;
                            }
                            else if (metaData.Action == FileAction.Delete)
                            {
                                updateFile.P4Status = P4Status.Delete;
                            }
                            updateFile.P4Success = true;
                        }
                        catch (Exception e)
                        {
                            updateFile.P4Status  = P4Status.Unknown;
                            updateFile.P4Success = false;
                            updateFile.FileError = e;
                            System.Windows.Clipboard.SetText(updateFile.FileError.ToString());
                        }
                    }
                }
            }
        }
示例#7
0
        //-------------------------------------------------------------------------

        private void uiChangelists_SelectedIndexChanged(object sender, EventArgs e)
        {
            // Reset form section.
            uiChangelistFiles.Items.Clear();

            uiChangelistNumber.Text = "";
            uiChangelistUser.Text   = "";
            uiChangelistDate.Text   = "";

            // Get the selected changelist.
            Changelist changelist = uiChangelists.SelectedItem as Changelist;

            if (changelist == null)
            {
                return;
            }

            uiChangelistNumber.Text = changelist.Id.ToString();
            uiChangelistUser.Text   = changelist.Submitter.Username;
            uiChangelistDate.Text   = changelist.SubmittedDate.ToString("yyyy/MM/dd");

            //-- Get changelist's files from P4.
            string output = Perforce.RunCommand("describe -s " + changelist.Id);

            // Exract files from output.
            int index = output.IndexOf("Affected files ...");

            while ((index = output.IndexOf("... ", index + 1)) > -1)
            {
                // Skip the "... " prefixing the path.
                index += 4;

                // Grab the path from between the ellipses and the revision.
                int revisionIndex = output.IndexOf(' ', index);

                if (revisionIndex < 0)
                {
                    continue;
                }

                string path = output.Substring(index, revisionIndex - index);

                // Add file path to the ui list.
                uiChangelistFiles.Items.Add(path);
            }
        }
示例#8
0
 // Update the Perforce status of the file, and whether or not Perforce successfully checked the file
 public virtual void UpdatePerforceStatus()
 {
     // If Perforce is Down or errors on getting file status fail gracefully and set P4 status to unknown
     try
     {
         List <FileMetaData> p4Info = Perforce.FileStatus(ItemPath);
         if (p4Info == null)
         {
             P4Status = P4Status.Unknown;
         }
         else if (p4Info.FirstOrDefault().OtherUsers != null)
         {
             P4Status = P4Status.CheckedOutOther;
         }
         else if (p4Info.FirstOrDefault().Action == FileAction.None)
         {
             P4Status = P4Status.CheckedIn;
         }
         else if (p4Info.FirstOrDefault().Action == FileAction.Edit)
         {
             P4Status = P4Status.CheckedOut;
         }
         else if (p4Info.FirstOrDefault().Action == FileAction.Add)
         {
             P4Status = P4Status.Add;
         }
         else if (p4Info.FirstOrDefault().Action == FileAction.Delete)
         {
             P4Status = P4Status.Delete;
         }
         P4Success = true;
     }
     catch (Exception e)
     {
         P4Status  = P4Status.Unknown;
         P4Success = false;
         FileError = e;
     }
 }
        public bool Run(out string ErrorMessage)
        {
            PerforceConnection Perforce = new PerforceConnection(null, null, null);

            // Get the P4PORT setting in this folder, so we can respect the contents of any P4CONFIG file
            string PrevDirectory = Directory.GetCurrentDirectory();

            try
            {
                Directory.SetCurrentDirectory(Path.GetDirectoryName(NewSelectedFileName));

                string ServerAndPort;
                if (Perforce.GetSetting("P4PORT", out ServerAndPort, Log))
                {
                    Perforce = new PerforceConnection(null, null, ServerAndPort);
                }
            }
            finally
            {
                Directory.SetCurrentDirectory(PrevDirectory);
            }

            // Get the Perforce server info
            PerforceInfoRecord PerforceInfo;

            if (!Perforce.Info(out PerforceInfo, Log))
            {
                ErrorMessage = String.Format("Couldn't get Perforce server info");
                return(false);
            }
            if (String.IsNullOrEmpty(PerforceInfo.UserName))
            {
                ErrorMessage = "Missing user name in call to p4 info";
                return(false);
            }
            if (String.IsNullOrEmpty(PerforceInfo.HostName))
            {
                ErrorMessage = "Missing host name in call to p4 info";
                return(false);
            }
            ServerTimeZone = PerforceInfo.ServerTimeZone;

            // Find all the clients on this machine
            Log.WriteLine("Enumerating clients on local machine...");
            List <PerforceClientRecord> Clients;

            if (!Perforce.FindClients(out Clients, Log))
            {
                ErrorMessage = String.Format("Couldn't find any clients for this host.");
                return(false);
            }

            // Find any clients which are valid. If this is not exactly one, we should fail.
            List <PerforceConnection> CandidateClients = new List <PerforceConnection>();

            foreach (PerforceClientRecord Client in Clients)
            {
                // Make sure the client is well formed
                if (!String.IsNullOrEmpty(Client.Name) && (!String.IsNullOrEmpty(Client.Host) || !String.IsNullOrEmpty(Client.Owner)) && !String.IsNullOrEmpty(Client.Root))
                {
                    // Require either a username or host name match
                    if ((String.IsNullOrEmpty(Client.Host) || String.Compare(Client.Host, PerforceInfo.HostName, StringComparison.InvariantCultureIgnoreCase) == 0) && (String.IsNullOrEmpty(Client.Owner) || String.Compare(Client.Owner, PerforceInfo.UserName, StringComparison.InvariantCultureIgnoreCase) == 0))
                    {
                        if (!Utility.SafeIsFileUnderDirectory(NewSelectedFileName, Client.Root))
                        {
                            Log.WriteLine("Rejecting {0} due to root mismatch ({1})", Client.Name, Client.Root);
                            continue;
                        }

                        PerforceConnection CandidateClient = new PerforceConnection(PerforceInfo.UserName, Client.Name, Perforce.ServerAndPort);

                        bool bFileExists;
                        if (!CandidateClient.FileExists(NewSelectedFileName, out bFileExists, Log) || !bFileExists)
                        {
                            Log.WriteLine("Rejecting {0} due to file not existing in workspace", Client.Name);
                            continue;
                        }

                        List <PerforceFileRecord> Records;
                        if (!CandidateClient.Stat(NewSelectedFileName, out Records, Log))
                        {
                            Log.WriteLine("Rejecting {0} due to {1} not in depot", Client.Name, NewSelectedFileName);
                            continue;
                        }

                        Records.RemoveAll(x => !x.IsMapped);
                        if (Records.Count == 0)
                        {
                            Log.WriteLine("Rejecting {0} due to {1} matching records", Client.Name, Records.Count);
                            continue;
                        }

                        Log.WriteLine("Found valid client {0}", Client.Name);
                        CandidateClients.Add(CandidateClient);
                    }
                }
            }

            // Check there's only one client
            if (CandidateClients.Count == 0)
            {
                ErrorMessage = String.Format("Couldn't find any Perforce workspace containing {0}.", NewSelectedFileName);
                return(false);
            }
            else if (CandidateClients.Count > 1)
            {
                ErrorMessage = String.Format("Found multiple workspaces containing {0}:\n\n{1}\n\nCannot determine which to use.", Path.GetFileName(NewSelectedFileName), String.Join("\n", CandidateClients.Select(x => x.ClientName)));
                return(false);
            }

            // Take the client we've chosen
            PerforceClient = CandidateClients[0];

            // Get the client path for the project file
            if (!PerforceClient.ConvertToClientPath(NewSelectedFileName, out NewSelectedClientFileName, Log))
            {
                ErrorMessage = String.Format("Couldn't get client path for {0}", NewSelectedFileName);
                return(false);
            }

            // Figure out where the engine is in relation to it
            for (int EndIdx = NewSelectedClientFileName.Length - 1;; EndIdx--)
            {
                if (EndIdx < 2)
                {
                    ErrorMessage = String.Format("Could not find engine in Perforce relative to project path ({0})", NewSelectedClientFileName);
                    return(false);
                }
                if (NewSelectedClientFileName[EndIdx] == '/')
                {
                    bool bFileExists;
                    if (PerforceClient.FileExists(NewSelectedClientFileName.Substring(0, EndIdx) + "/Engine/Source/UE4Editor.target.cs", out bFileExists, Log) && bFileExists)
                    {
                        BranchClientPath = NewSelectedClientFileName.Substring(0, EndIdx);
                        break;
                    }
                }
            }
            Log.WriteLine("Found branch root at {0}", BranchClientPath);

            // Get the local branch root
            if (!PerforceClient.ConvertToLocalPath(BranchClientPath + "/Engine/Source/UE4Editor.target.cs", out BaseEditorTargetPath, Log))
            {
                ErrorMessage = String.Format("Couldn't get local path for editor target file");
                return(false);
            }

            // Find the editor target for this project
            if (NewSelectedFileName.EndsWith(".uproject", StringComparison.InvariantCultureIgnoreCase))
            {
                List <PerforceFileRecord> Files;
                if (PerforceClient.FindFiles(PerforceUtils.GetClientOrDepotDirectoryName(NewSelectedClientFileName) + "/Source/*Editor.Target.cs", out Files, Log) && Files.Count >= 1)
                {
                    PerforceFileRecord File = Files.FirstOrDefault(x => x.Action == null || !x.Action.Contains("delete"));
                    if (File == null)
                    {
                        Log.WriteLine("Couldn't find any non-deleted editor targets for this project.");
                    }
                    else
                    {
                        string DepotPath = File.DepotPath;
                        NewProjectEditorTarget = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(DepotPath.Substring(DepotPath.LastIndexOf('/') + 1)));
                        Log.WriteLine("Using {0} as editor target name (from {1})", NewProjectEditorTarget, Files[0]);
                    }
                }
                else
                {
                    Log.WriteLine("Couldn't find any editor targets for this project.");
                }
            }

            // Get a unique name for the project that's selected. For regular branches, this can be the depot path. For streams, we want to include the stream name to encode imports.
            if (PerforceClient.GetActiveStream(out StreamName, Log))
            {
                string ExpectedPrefix = String.Format("//{0}/", PerforceClient.ClientName);
                if (!NewSelectedClientFileName.StartsWith(ExpectedPrefix, StringComparison.InvariantCultureIgnoreCase))
                {
                    ErrorMessage = String.Format("Unexpected client path; expected '{0}' to begin with '{1}'", NewSelectedClientFileName, ExpectedPrefix);
                    return(false);
                }
                string StreamPrefix;
                if (!TryGetStreamPrefix(PerforceClient, StreamName, Log, out StreamPrefix))
                {
                    ErrorMessage = String.Format("Failed to get stream info for {0}", StreamName);
                    return(false);
                }
                NewSelectedProjectIdentifier = String.Format("{0}/{1}", StreamPrefix, NewSelectedClientFileName.Substring(ExpectedPrefix.Length));
            }
            else
            {
                if (!PerforceClient.ConvertToDepotPath(NewSelectedClientFileName, out NewSelectedProjectIdentifier, Log))
                {
                    ErrorMessage = String.Format("Couldn't get depot path for {0}", NewSelectedFileName);
                    return(false);
                }
            }

            // Read the project logo
            if (NewSelectedFileName.EndsWith(".uproject", StringComparison.InvariantCultureIgnoreCase))
            {
                string LogoFileName = Path.Combine(Path.GetDirectoryName(NewSelectedFileName), "Build", "UnrealGameSync.png");
                if (File.Exists(LogoFileName))
                {
                    try
                    {
                        // Duplicate the image, otherwise we'll leave the file locked
                        using (Image Image = Image.FromFile(LogoFileName))
                        {
                            ProjectLogo = new Bitmap(Image);
                        }
                    }
                    catch
                    {
                        ProjectLogo = null;
                    }
                }
            }

            // Succeeed!
            ErrorMessage = null;
            return(true);
        }
示例#10
0
        //-------------------------------------------------------------------------

        // Processes diff content produced using "p4 diff2 -du file1 file2".

        public static string GetHtmlDiffContent(
            string p4filePath,
            int revision)
        {
            //-- Get the complete prev revision.
            string prevContent =
                Perforce.RunCommand(
                    "print -q " + p4filePath + '#' + ((revision > 0) ? revision - 1 : revision));

            // Make it html safe (replace the angle brackets).
            prevContent = prevContent.Replace("<", "&lt;");
            prevContent = prevContent.Replace(">", "&gt;");

            // Split into individual lines.
            string[] tmpPrevContentLines =
                prevContent.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);

            List <string> prevContentLines = new List <string>();

            foreach (string line in tmpPrevContentLines)
            {
                prevContentLines.Add(line);
            }

            //-- Perform the diff.
            string diffContent =
                Perforce.RunCommand(
                    "diff2 -du " +
                    p4filePath + '#' + (revision - 1) + ' ' +
                    p4filePath + '#' + revision);

            // Make HTML safe (replace the angle brackets).
            diffContent = diffContent.Replace("<", "&lt;");
            diffContent = diffContent.Replace(">", "&gt;");

            string[] lines =
                diffContent.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);

            // Iterate through the diff's lines, applying changes to the previous revision as we go.
            int insertPoint = 0;
            int linesAdded  = 0;

            foreach (string line in lines)
            {
                // Is this line the beginning of a diff section?
                if (line.IndexOf("@@") == 0)
                {
                    // Get the line number that the diff applies to.
                    string tmp           = line.Remove(0, 4);
                    string lineNumberStr = tmp.Substring(0, tmp.IndexOf(','));

                    if (int.TryParse(lineNumberStr, out insertPoint) == false)
                    {
                        throw new Exception("Failed to extract line number from diff.");
                    }

                    // Translate insert-point from range start at 1 to range starting at 0.
                    insertPoint--;
                    insertPoint += linesAdded;

                    continue;
                }

                if (line.Length > 0)
                {
                    // Content was added.
                    if (line[0] == '+')
                    {
                        if (insertPoint > prevContentLines.Count)
                        {
                            throw new Exception("Insert-point is out-of-bounds.");
                        }

                        string tmp = line.Remove(0, 1);

                        prevContentLines.Insert(
                            insertPoint, "<font bgcolor='" + c_changeColour_addition + "'>" + tmp + "</font>");

                        linesAdded++;
                    }
                    // Content was removed.
                    else if (line[0] == '-')
                    {
                        if (insertPoint > prevContentLines.Count)
                        {
                            throw new Exception("Insert-point is out-of-bounds.");
                        }

                        string tmp = line.Remove(0, 1);
                        while (tmp.Length < 80)
                        {
                            tmp += ' ';
                        }

                        prevContentLines.RemoveAt(insertPoint);
                        prevContentLines.Insert(insertPoint, "<font bgcolor='" + c_changeColour_subtraction + "'>" + tmp + "</font>");
                    }

                    insertPoint++;
                }
            }

            // Create a line-number format string - we want to pad the line number
            // with enough zeroes so all line numbers in this file have the same
            // number of digits.
            int    lineNumber       = 1;
            string lineNumberFormat = "";

            for (int i = 0; i < prevContentLines.Count.ToString().Length; i++)
            {
                lineNumberFormat += '0';
            }

            // Create html.
            string html =
                "<html>" + Environment.NewLine +
                "<head></head>" + Environment.NewLine +
                "<body>" + Environment.NewLine;

            foreach (string line in prevContentLines)
            {
                // TODO: Line numbering is wrong.
                html +=
                    /*lineNumber.ToString( lineNumberFormat ) +*/ "&nbsp;|&nbsp;" + line + "<br />" + Environment.NewLine;

                lineNumber++;
            }

            html += Environment.NewLine + "</body>" + Environment.NewLine + "</html>";

            // Highlight tab chars.
            // TODO: This doesn't seem to work.
            html = html.Replace("" + System.Windows.Forms.Keys.Tab, "<font bgcolor='red'>_</font>");

            // Replace spaces with '@nbsp;' (exclude tags).
            string htmlCopy         = html;
            bool   isAngleBrackOpen = false;
            int    htmlOffset       = 0;

            for (int i = 0; i < htmlCopy.Length; i++)
            {
                if (htmlCopy[i] == '<')
                {
                    isAngleBrackOpen = true;
                }
                else if (htmlCopy[i] == '>')
                {
                    isAngleBrackOpen = false;
                }
                else if (htmlCopy[i] == ' ' &&
                         isAngleBrackOpen == false)
                {
                    html        = html.Remove(htmlOffset, 1);
                    html        = html.Insert(htmlOffset, "&nbsp;");
                    htmlOffset += "&nbsp;".Length;
                    continue;
                }

                htmlOffset++;
            }

            html =
                html.Insert(
                    html.IndexOf("</head>"),
                    "<style>body { white-space: pre; font-family: Courier New; font-size: 100%; }</style>");

            return(html);
        }
示例#11
0
        //-------------------------------------------------------------------------

        public static void GetChangelistsFromP4(
            DateTime fromDate,
            DateTime toDate,
            ref List <Changelist> changelists)
        {
            string output =
                Perforce.RunCommand(
                    "changes -s submitted @" + fromDate.ToString("yyyy/MM/dd:00:00") + ",@" +
                    toDate.ToString("yyyy/MM/dd:23:59"));

            string[] lines =
                output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);

            foreach (string line in lines)
            {
                // Extract the changelist number.
                if (line.Length < "Change ".Length)
                {
                    continue;
                }

                string tmp = line.Remove(0, "Change ".Length);

                int index = tmp.IndexOf(' ');
                if (index < 0)
                {
                    continue;
                }

                string changelistNumberStr = tmp.Substring(0, index);
                int    changelistNumber;
                if (int.TryParse(changelistNumberStr, out changelistNumber) == false)
                {
                    continue;
                }

                // Exract the date.
                tmp = tmp.Remove(0, (changelistNumberStr + " on ").Length);
                string   dateStr = tmp.Substring(0, 10);
                DateTime date    = new DateTime();

                if (DateTime.TryParse(dateStr, out date) == false)
                {
                    continue;
                }

                // Extract the user.
                tmp = tmp.Remove(0, (dateStr + " by ").Length);
                string username = tmp.Substring(0, tmp.IndexOf('@'));
                User   user     = Program.UserCollection.GetUser(username);

                if (user == null ||
                    user.IsReviewCandidate == false)
                {
                    continue;
                }

                // Extract the description.
                int descriptionStartIndex = tmp.IndexOf("'") + 1;
                int descriptionLength     = tmp.LastIndexOf("'") - descriptionStartIndex;

                string description = "";

                if (descriptionStartIndex > 0 &&
                    descriptionLength > 0)
                {
                    description = tmp.Substring(descriptionStartIndex, descriptionLength);
                }

                if (description.Length == 0)
                {
                    description = "(No description)";
                }

                //if( description.Length > 30 )
                //{
                //  description = description.TrimEnd() + "...";
                //}

                // Create the changelist object if we don't already have one.
                if (Changelists.ContainsKey(changelistNumber) == false)
                {
                    changelists.Add(
                        new Changelist(
                            changelistNumber,
                            description,
                            user,
                            date));
                }
            }
        }
示例#12
0
        //-------------------------------------------------------------------------

        public static void GetChangelistsFromP4(
            DateTime fromDate,
            DateTime toDate,
            ref List <Changelist> changelists)
        {
            string output =
                Perforce.RunCommand(
                    "changes -s submitted -t -l @" + fromDate.ToString("yyyy/MM/dd:HH:mm:ss") + ",@" +
                    toDate.ToString("yyyy/MM/dd:HH:mm:ss"));

            string[] lines =
                output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);

            for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++)
            {
                string line = lines[lineIndex];

                // Extract the changelist number.
                if (line.Length < "Change ".Length)
                {
                    continue;
                }

                string tmp = line.Remove(0, "Change ".Length);

                int index = tmp.IndexOf(' ');
                if (index < 0)
                {
                    continue;
                }

                string changelistNumberStr = tmp.Substring(0, index);
                int    changelistNumber;
                if (int.TryParse(changelistNumberStr, out changelistNumber) == false)
                {
                    continue;
                }

                // Exract the date & time.
                tmp = tmp.Remove(0, (changelistNumberStr + " on ").Length);
                string   dateStr = tmp.Substring(0, 19);
                DateTime date    = new DateTime();

                if (DateTime.TryParse(dateStr, out date) == false)
                {
                    continue;
                }

                // Extract the user.
                tmp = tmp.Remove(0, (dateStr + " by ").Length);
                string username = tmp.Substring(0, tmp.IndexOf('@'));

                // Extract the description.
                string description = "";

                if (lineIndex + 2 < lines.Length)
                {
                    lineIndex += 2;

                    while (lineIndex < lines.Length)
                    {
                        if (lines[lineIndex].IndexOf("Change") == 0)
                        {
                            lineIndex--;
                            break;
                        }

                        description += lines[lineIndex++];
                    }
                }

                // Create the changelist object if we don't already have one.
                if (Changelists.ContainsKey(changelistNumber) == false)
                {
                    changelists.Add(
                        new Changelist(
                            changelistNumber,
                            description,
                            username,
                            date));
                }
            }
        }
示例#13
0
        //-------------------------------------------------------------------------

        public static void GetChangelistFilesFromP4(
            int changelistId,
            out List <ChangelistFile> files)
        {
            files = new List <ChangelistFile>();

            //-- Get changelist's files from P4.
            string output = Perforce.RunCommand("describe -ds " + changelistId);

            // Exract files from output.
            int index = output.IndexOf("Affected files ...");

            if (index > -1)
            {
                while ((index = output.IndexOf("... ", index + 1)) > -1)
                {
                    // Skip the "... " prefixing the path.
                    index += 4;

                    // Grab the path.
                    int revisionIndex = output.IndexOf('#', index);

                    if (revisionIndex < 0)
                    {
                        Program.Log.AddEntry(
                            Log.EntryType.ERROR,
                            "Failed to find revision index in file path.",
                            true);
                        continue;
                    }

                    int revisionEndIndex = output.IndexOf(' ', revisionIndex);

                    if (revisionIndex < 0)
                    {
                        Program.Log.AddEntry(
                            Log.EntryType.ERROR,
                            "Failed to find revision END index in file path.",
                            true);
                        continue;
                    }

                    string path = output.Substring(index, revisionEndIndex - index);

                    // Add file path to the ui list.
                    files.Add(new ChangelistFile(path));
                }
            }

            // Go through the differences section and extract the various counts.
            index = output.IndexOf("Differences ...");

            if (index > -1)
            {
                string filename     = "";
                int    endIndex     = -1;
                int    addCount     = 0;
                int    deletedCount = 0;
                int    changedCount = 0;

                while ((index = output.IndexOf("==== ", index)) > -1)
                {
                    // Extract the filename.
                    index   += "==== ".Length;
                    endIndex = output.IndexOf(" ", index);

                    filename = output.Substring(index, endIndex - index);

                    // 'Add' count.
                    index = output.IndexOf("add ", index);

                    if (index < 0)
                    {
                        break;
                    }

                    index   += "add ".Length;
                    endIndex = output.IndexOf("chunks", index);

                    if (int.TryParse(
                            output.Substring(
                                index,
                                endIndex - index),
                            out addCount) == false)
                    {
                        continue;
                    }

                    // 'Deleted' count.
                    index    = output.IndexOf("deleted ", index);
                    index   += "deleted ".Length;
                    endIndex = output.IndexOf("chunks", index);

                    if (int.TryParse(
                            output.Substring(
                                index,
                                endIndex - index),
                            out deletedCount) == false)
                    {
                        continue;
                    }

                    // 'Changed' count.
                    index    = output.IndexOf("changed ", index);
                    index   += "changed ".Length;
                    endIndex = output.IndexOf("chunks", index);

                    if (int.TryParse(
                            output.Substring(
                                index,
                                endIndex - index),
                            out changedCount) == false)
                    {
                        continue;
                    }

                    // Update the file with its stats.
                    foreach (ChangelistFile file in files)
                    {
                        if (file.Filename.Equals(filename, StringComparison.OrdinalIgnoreCase))
                        {
                            file.AdditionsCount = addCount;
                            file.DeletionsCount = deletedCount;
                            file.ChangesCount   = changedCount;
                        }
                    }
                }
            }
        }