/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Text; namespace JetBrains.Omea.SamplePlugins.SccPlugin { /// /// Describes a single Perforce changeset. /// internal class ChangeSetSummary { private int _number; private string _user; private string _client; private DateTime _date; public ChangeSetSummary( int number, string user, string client, DateTime date ) { _number = number; _user = user; _client = client; _date = date; } public int Number { get { return _number; } } public string User { get { return _user; } } public string Client { get { return _client; } } public DateTime Date { get { return _date; } } } internal class FileChangeData { private string _path; private int _revision; private string _changeType; private bool _binary = false; private StringBuilder _diffBuilder = new StringBuilder(); public FileChangeData( string path, int revision, string changeType ) { _path = path; _revision = revision; _changeType = changeType; _diffBuilder = new StringBuilder(); } public void AppendDiff( string line ) { _diffBuilder.Append( line ); } public string Path { get { return _path; } } public int Revision { get { return _revision; } } public string ChangeType { get { return _changeType; } } public bool Binary { get { return _binary; } set { _binary = value; } } public string Diff { get { return _diffBuilder.ToString(); } } } internal class ChangeSetDetails { public ChangeSetDetails( string description, FileChangeData[] fileChanges ) { Description = description; FileChanges = fileChanges; } public string Description { get; set; } public FileChangeData[] FileChanges { get; set; } } /// /// Base class for running executables and catching their console output. /// internal class RunnerBase { protected string ReadStdout( string exeName, string args ) { Process process = new Process(); process.StartInfo.FileName = exeName; process.StartInfo.Arguments = args; process.StartInfo.CreateNoWindow = true; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; process.Start(); string stdout = process.StandardOutput.ReadToEnd(); string stderr = process.StandardError.ReadToEnd(); process.WaitForExit(); if ( process.ExitCode != 0 ) { throw new RunnerException( stderr.Length > 0 ? stderr : stdout ); } return stdout; } } /// /// Supports running Perforce commands and parsing their output. /// internal class P4Runner: RunnerBase { private string _serverPort; private string _client; private string _userName; private string _password; public P4Runner( string serverPort, string client, string userName, string password ) { _serverPort = serverPort; _client = client; _userName = userName; _password = password; } /// /// Returns the specified number of most recent changesets from the repository. /// /// The number of changesets to return. /// The array of changeset descriptors. internal ChangeSetSummary[] GetLastChangeSets( int count ) { return ParseChangesList( "-m " + count ); } /// /// Returns the array of changesets which were made after the changeset with /// the specified number. /// /// The number of the changeset to start processing /// with (not included in the results). /// The repository paths for which changes should be loaded, /// or an empty array if changes should be loaded for the entire repository. /// The array of changeset descriptors. internal ChangeSetSummary[] GetChangeSetsAfter( int changeSetNo, string[] pathsToWatch ) { StringBuilder descriptorBuilder = new StringBuilder(); if (pathsToWatch.Length == 0) { descriptorBuilder.Append("@").Append(changeSetNo + 1).Append(",@now"); } else { foreach (string pathToWatch in pathsToWatch) { if (descriptorBuilder.Length > 0) { descriptorBuilder.Append(" "); } descriptorBuilder.Append(pathToWatch); if (!pathToWatch.EndsWith( "/" )) { descriptorBuilder.Append( "/..." ); } else { descriptorBuilder.Append( "..." ); } descriptorBuilder.Append( "@" ).Append( changeSetNo + 1 ).Append( ",@now" ); } } return ParseChangesList( descriptorBuilder.ToString() ); } private ChangeSetSummary[] ParseChangesList( string changeSelector ) { string changes = ReadPerforceStdout( "changes -s submitted -t " + changeSelector ); changes = changes.Replace( "\r\n", "\n" ); string[] modLines = changes.Split( '\n' ); ArrayList changeSets = new ArrayList(); ArrayList modLineList = new ArrayList( modLines ); while ( modLineList.Count > 0 ) { string modHeader = (string) modLineList [0]; modLineList.RemoveAt( 0 ); if ( modHeader == "" ) continue; int pos = modHeader.IndexOf( '\'' ); if ( pos != 0 ) { modHeader = modHeader.Substring( 0, pos ).Trim(); } string[] modHeaderFields = modHeader.Split( ' ' ); if ( modHeaderFields [0] != "Change" || modHeaderFields.Length != 7 ) { Trace.WriteLine( "Invalid changelog format: " + modHeader ); break; } changeSets.Add( ParseChangesLine( modHeaderFields ) ); } return (ChangeSetSummary[]) changeSets.ToArray( typeof(ChangeSetSummary) ); } /// /// Completes parsing of a single line of the changes list. /// /// The array of fields into which the line has been split. private ChangeSetSummary ParseChangesLine( string[] modHeaderFields ) { int changeSetNo = Int32.Parse( modHeaderFields [1] ); DateTime changeTime = DateTime.ParseExact(modHeaderFields [3] + " " + modHeaderFields [4], "yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture); string[] userClient = modHeaderFields [6].Split( new char[] { '@' }, 2 ); return new ChangeSetSummary( changeSetNo, userClient [0], userClient [1], changeTime ); } /// /// Returns the details of the changeset with the specified number. /// /// The changeset to describe. /// The details of the changeset. internal ChangeSetDetails DescribeChangeSet( int changeSetNumber ) { string desc = ReadPerforceStdout( "describe -du " + changeSetNumber ); desc = desc.Replace( "\r\n", "\n" ); var descLines = new List( desc.Split( '\n' ) ); // skip heading descLines.RemoveAt( 0 ); StringBuilder descBuilder = new StringBuilder(); FileChangeData lastChange = null; var fileChanges = new List(); var fileChangesMap = new Dictionary(); bool affectedStarted = false, differencesStarted = false; while ( descLines.Count > 0 ) { string descLine = descLines [0]; descLines.RemoveAt( 0 ); if ( descLine == "" ) continue; if ( !affectedStarted ) { if ( descLine.StartsWith("Affected files ..." ) ) { affectedStarted = true; } else { if ( descBuilder.Length > 0 ) { descBuilder.Append( ' ' ); } descBuilder.Append( descLine.Trim() ); } } else if ( !differencesStarted ) { if ( descLine.StartsWith( "Differences ..." ) ) { differencesStarted = true; } else { descLine = descLine.Substring( 4 ); int pos = descLine.IndexOf( '#' ); string path = descLine.Substring( 0, pos ); descLine = descLine.Substring( pos ); pos = descLine.IndexOf( ' ' ); int revision = Int32.Parse( descLine.Substring( 1, pos ) ); string changeType = descLine.Substring( pos + 1 ); var change = new FileChangeData( path, revision, changeType ); fileChanges.Add( change ); fileChangesMap.Add( path, change ); } } else { if ( descLine.StartsWith( "====" ) && descLine.EndsWith( "====" ) ) { int revisionPos = descLine.LastIndexOf( '#' ); string path = descLine.Substring( 5, revisionPos-5 ); // skip ===== lastChange = fileChangesMap [path]; if ( descLine.IndexOf( "(binary)", revisionPos ) >= 0 ) { lastChange.Binary = true; } } else { lastChange.AppendDiff( descLine + "\r\n" ); } } } return new ChangeSetDetails( descBuilder.ToString(), fileChanges.ToArray() ); } /// /// Runs a "p4 user" command to get the information about the Perforce user with /// the specified user name. /// /// The user name of the user. /// The e-mail address of the user. /// The full name of the user. internal void DescribeUser( string userName, out string email, out string fullName ) { fullName = userName; email = null; string desc = ReadPerforceStdout( "user -o " + userName ); desc = desc.Replace( "\r\n", "\n" ); foreach( string line in desc.Split( '\n' ) ) { if ( line.StartsWith( "Email:" ) ) { email = line.Substring( "Email:".Length ).Trim(); } else if ( line.StartsWith( "FullName:" ) ) { fullName = line.Substring( "FullName:".Length ).Trim(); } } } /// /// Starts the p4.exe executable with the specified arguments, reads and returns the /// text it prints to the stdout. /// /// The arguments to pass to p4.exe /// The standard output of the process. private string ReadPerforceStdout( string args ) { return ReadStdout("p4.exe", BuildLoginArgs() + " " + args); } private string BuildLoginArgs() { string args = ""; if ( _serverPort != null && _serverPort.Length > 0 ) { args += " -H " + _serverPort; } if ( _client != null && _client.Length > 0 ) { args += " -c " + _client; } if ( _userName != null && _userName.Length > 0 ) { args += " -u " + _userName; } if ( _password != null && _password.Length > 0 ) { args += " -P " + _password; } return args; } } /// /// The exception which is thrown when the p4.exe process returns a non-zero exit code. /// internal class RunnerException: Exception { internal RunnerException( string message ) : base( message ) { } } }