// // Copyright 2014 Perforce Software Inc. // using log4net; using Perforce.Model; using Perforce.P4; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.Caching; using System.Runtime.Serialization; using System.Text; namespace Perforce.Helper { public class DepotMapping { Dictionary<string, string> depotMapping; Dictionary<string, string> clientMapping; public DepotMapping() { depotMapping = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase); clientMapping = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase); } public int Count { get { return depotMapping.Count; } } public void Add(string depotFile, string clientFile) { if (depotFile.EndsWith("/%25")) depotFile = depotFile.Substring(0, depotFile.Length - 4); depotFile = PerforceHelper.EscapePath(depotFile.TrimEnd('/', '%')); clientFile = PerforceHelper.EscapePath(clientFile.TrimEnd('\\', '%')); depotMapping.Add(depotFile, clientFile); clientMapping.Add(clientFile, depotFile); } private bool IsMapped(string path) { if (Utility.IsDepotPath(path)) { return depotMapping.Keys.Contains(path); } else { return clientMapping.Keys.Contains(path); } } public bool IsDirMapped(string path) { if (Utility.IsDepotPath(path)) { path = path.TrimEnd('/'); } else { path = path.TrimEnd('\\'); } return IsMapped(PerforceHelper.EscapePath(path)); } public string GetMappedDir(string path) { string result = null; path = PerforceHelper.EscapePath(path); if (Utility.IsDepotPath(path)) { path = path.TrimEnd('/'); result = depotMapping[path]; } else { path = path.TrimEnd('\\'); result = clientMapping[path]; } return result; } public bool IsFileMapped(string path) { return IsMapped(path); } } public class SyncMetaData { public DepotPath DepotPath { get; set; } public LocalPath LocalPath { get; set; } public ClientPath ClientPath { get; set; } public string Action { get; set; } //depotFile='//depot/jam/main/fred.txt' //clientFile='c:\temp\TEST_P4API.NET_CLIENT\depot\Jam\MAIN\fred.txt' //rev='2' //action='added' //fileSize='30' //totalFileSize='2350026' //totalFileCount='104' //change='12398' func='client-FstatInfo' public void FromCmdTaggedData(TaggedObject obj) { if (obj.ContainsKey("clientFile")) { string path = obj["clientFile"]; if (path.StartsWith("//")) { ClientPath = new ClientPath(obj["clientFile"]); } else { ClientPath = new ClientPath(obj["clientFile"]); LocalPath = new LocalPath(obj["clientFile"]); } } if (obj.ContainsKey("depotFile")) { string p = obj["depotFile"]; DepotPath = new DepotPath(p); } if (obj.ContainsKey("action")) { Action = obj["action"]; } } } public class PerforceHelper : IDisposable { enum PerforceErrors { NoFilesToSubmit = 806427694, NoFilesToSubmitWithError = 822418474 } private string _serverUri; private string _username; private string _password; private string _ticket; private Client _client; private P4Server _pserver = null; private static readonly ILog _log = LogManager.GetLogger(typeof(PerforceHelper)); // For testing purposes public WorkspaceWatcher TestWorkspaceWatcher = null; #region LOGGING public static void LoggingFunction(int log_level, String source, String message) { if (!_log.IsDebugEnabled) return; _log.DebugFormat("{0}:{1}:{2}", log_level, source, message); } private static void LogP4Exception(P4Exception p4e) { _log.WarnFormat("Exception: {0}", p4e.Message); } #endregion #region LOGIN AND CONNECTION TESTING /// <summary> /// Creates a connection to the Perforce server /// </summary> /// <returns>true if connected, false otherwise</returns> public bool Connect() { Log.TraceFunction(); var connected = false; var con = GetConnection(); if (con.Status == ConnectionStatus.Disconnected) { try { con.Connect(null); } catch (P4Exception p4e) { LogP4Exception(p4e); throw new P4HelperException(p4e.Message); } } if (con.Status == ConnectionStatus.Connected) { connected = true; } return connected; } /// <summary> /// Disconnects from the Perforce server /// </summary> public void Disconnect() { Log.TraceFunction(); var con = GetConnection(); if (con != null && con.Status == ConnectionStatus.Connected) { con.Disconnect(); } } /// <summary> /// Tests to see if the connection has a Connected status /// </summary> /// <returns>true if connected, false otherwise</returns> public bool IsConnected() { return GetRepository().Connection.Status == ConnectionStatus.Connected; } /// <summary> /// Logs into the Perforce server using the specified password /// </summary> /// <exception cref="P4HelperException">Thrown if a P4Exception is caught</exception> public Tuple<bool, string> Login(string password) { Log.TraceFunction(); var result = new Tuple<bool, string>(false, string.Empty); var con = GetConnection(useCache: false, testConnection: false); if (Connect()) { try { var options = new LoginCmdOptions(LoginCmdFlags.AllHosts, string.Empty); var cred = con.Login(password, options); if (cred != null) { if (string.IsNullOrEmpty(cred.Ticket)) { options = new LoginCmdOptions(LoginCmdFlags.AllHosts | LoginCmdFlags.DisplayTicket, string.Empty); cred = con.Login(password, options); } result = new Tuple<bool, string>(true, cred.Ticket); Ticket = cred.Ticket; } } catch (P4Exception p4e) { LogP4Exception(p4e); throw new P4HelperException(p4e.Message); } } return result; } /// <summary> /// Checks to see if the current session is logged in /// </summary> /// <returns>true if logged in, false otherwise</returns> public bool IsLoggedIn() { Log.TraceFunction(); var result = false; var con = GetConnection(useCache: true, testConnection: false); if (con != null) { try { string[] args = { "-s" }; P4Command cmd = new P4Command(con, "login", false, args); var r = cmd.Run(); result = true; } catch (P4Exception p4e) { // do nothing... an exception is thrown if login -s fails, indicating that // the user is not logged in LogP4Exception(p4e); } } return result; } /// <summary> /// Logs out the current session /// </summary> /// <returns>returns the result of the logout operation</returns> public bool Logout() { Log.TraceFunction(); var result = false; var con = GetConnection(useCache: false, testConnection: false); if (con != null) { var options = new LogoutCmdOptions(LogoutCmdFlags.AllHosts, string.Empty); result = con.Logout(options); } return result; } #endregion #region SERVER OPERATIONS /// <summary> /// Converts into depot syntax where appropriate - handling special chars /// </summary> /// <returns>converted path</returns> public static string EscapePath(string path) { if (!path.Contains("%") && !path.Contains("@") && !path.Contains("*") && !path.Contains("#")) { return path; } if (!path.Contains("%25") && !path.Contains("%23") && !path.Contains("%40") && !path.Contains("%2A")) { return PathSpec.EscapePath(path); } return path; } public static List<string> EscapePaths(List<string> paths) { var results = new List<string>(); foreach (var p in paths) { if (Utility.IsDepotPath(p)) results.Add(EscapePath(p)); else results.Add(p); } return results; } public static List<string> EscapePaths(params string[] paths) { return EscapePaths(new List<string>(paths)); } /// <summary> /// Converts into depot syntax where appropriate - handling special chars /// </summary> /// <returns>true if connected, false otherwise</returns> public static string UnescapePath(string path) { if (path.Contains("%25") || path.Contains("%23") || path.Contains("%40") || path.Contains("%2A")) return PathSpec.UnescapePath(path); return path; } private DepotPath MkEscapedDepotPath(string path) { return new DepotPath(EscapePath(path)); } private DepotPath MkUnescapedDepotPath(string path) { return new DepotPath(UnescapePath(path)); } private ClientPath MkClientPath(string path) { return new ClientPath(path); } public long GetServerTime() { return Utility.GetEpochTime(GetServer().Metadata.Date.ToUniversalTime()); } public bool IsServerCaseSensitive() { return GetServer().Metadata.CaseSensitive; } public string GetKey(string name) { Log.TraceFunction(); string value = null; if (string.IsNullOrEmpty(name)) return null; var args = new List<string>(); args.Add(name); var cmd = new P4Command(GetRepository(), "key", true, args.ToArray()); var results = cmd.Run(); if (results.TaggedOutput != null) { foreach (TaggedObject obj in results.TaggedOutput) { if (obj.ContainsKey("value")) { var x = obj["value"].ToString(); if (!x.Equals("0")) { value = x; } } } } return value; } #endregion #region USER OPERATIONS public User GetUserInfo(string userid) { Log.TraceFunction(); if (string.IsNullOrEmpty(userid)) return null; var repo = GetRepository(); return repo.GetUser(userid); } public bool SetPassword(string oldPassword, string newPassword) { Log.TraceFunction(); var repo = GetRepository(); return repo.Connection.SetPassword(oldPassword, newPassword); } #endregion #region LISTING OPERATIONS private string AppendWildcard(string path, string wildcard) { if (path.EndsWith("#0")) path = path.TrimEnd('#', '0'); if (path.EndsWith(wildcard)) return path; if (Utility.IsDepotPath(path)) return path + "/" + wildcard; else return path + "\\" + wildcard; } public bool IsDirectory(string path) { Log.TraceFunction(); if (path.Equals("//")) return true; if (path.EndsWith("/")) return true; if (path.EndsWith("/...")) return true; var repo = GetRepository(); string[] p = { EscapePath(path) }; var options = new GetDepotDirsCmdOptions(GetDepotDirsCmdFlags.None, null); try { var results = repo.GetDepotDirs(options, p); if (results.Count == 1) return true; } catch (P4Exception p4e) { LogP4Exception(p4e); } var clientPath = PredictClientPath(path); var exists = Directory.Exists(clientPath); return exists; } public bool IsFile(string path) { Log.TraceFunction(); var repo = GetRepository(); string[] p = { EscapePath(path) }; var specs = GetEscapedFileSpecs(p); var options = new GetDepotFilesCmdOptions(GetDepotFilesCmdFlags.None, 0); try { var results = repo.GetDepotFiles(specs, options); return results.Count == 1; } catch (P4Exception p4e) { LogP4Exception(p4e); return false; } } /// <summary> /// Gets a list of the depots on the Perforce server /// </summary> /// <param name="localOnly">optional boolean to limit depots to only those of type 'local'</param> /// <returns>A list of Depot specifications</returns> public IList<Depot> ListDepots(bool localOnly=false) { Log.TraceFunction(); IList<Depot> depots = null; try { var repo = GetRepository(); if (repo != null) { var allDepots = repo.GetDepots(); if (localOnly) { depots = allDepots.Where(d => d.Type == DepotType.Local).ToList(); } else { depots = allDepots; } } } catch (P4Exception p4e) { LogP4Exception(p4e); } return depots; } /// <summary> /// Gets a list of directories given the specified depot path /// </summary> /// <param name="path">The depot path to use as the base</param> /// <returns>A list of string objects - note these are unescaped (not depot syntax for special chars)</returns> public IList<string> ListDirectories(string path) { Log.TraceFunction(); var repo = GetRepository(); if (!path.EndsWith("/")) { path += "/"; } string[] p = { EscapePath(path) + "*" }; var options = new GetDepotDirsCmdOptions(GetDepotDirsCmdFlags.None, null); try { var dirs = repo.GetDepotDirs(options, p); IList<string> results = new List<string>(); foreach (var d in dirs) { results.Add(UnescapePath(d.ToString())); } return results; } catch (P4Exception p4e) { LogP4Exception(p4e); return new List<string>(); } } /// <summary> /// Gets a list of the files at the specified depot path /// </summary> /// <param name="depotPath">The depot path to use as the base</param> /// <returns>A list of FileMetaData (fstat) objects</returns> public IList<FileMetaData> ListFiles(string depotPath, bool showDeleted=false, long sinceTime = 0) { Log.TraceFunction(); var repo = GetRepository(); var filespec = new List<FileSpec>(); filespec.Add(new FileSpec(new DepotPath(UnescapePath(depotPath)), Revision.Head)); string filter = string.Empty; if (!showDeleted) { filter = "^headAction=delete ^headAction=move/delete"; } if (sinceTime > 0) { if (filter.Length > 0) { filter += " & "; } filter += string.Format("headTime > {0}", sinceTime); } var options = new GetFileMetaDataCmdOptions(GetFileMetadataCmdFlags.Attributes|GetFileMetadataCmdFlags.FileSize|GetFileMetadataCmdFlags.HexAttributes, filter, null, -1, null, null, "tags"); try { var results = repo.GetFileMetaData(filespec, options); return results; } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } #endregion #region CLIENT OPERATIONS /// <summary> /// Gets a list of the clients owned by the current user /// </summary> /// <returns>A list of Client objects</returns> public IList<Client> ListClients(bool validLocalOnly=false) { Log.TraceFunction(); var repo = GetRepository(); var options = new ClientsCmdOptions(ClientsCmdFlags.None, _username, null, -1, null); try { var clients = repo.GetClients(options); if (clients != null && validLocalOnly) { var subset = new List<Client>(); foreach (var c in clients) { var pathRoot = Path.GetPathRoot(c.Root); if (!pathRoot.StartsWith("\\") && Directory.Exists(pathRoot)) { subset.Add(c); } } clients = subset; } return clients; } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } /// <summary> /// Creates a basic client /// </summary> /// <param name="name">The name of the client</param> /// <param name="root">The root directory for the client</param> /// <param name="description">A description for the client</param> /// <returns>The client created</returns> public Client CreateClient(string name, string root, string description) { Log.TraceFunction(); Client client = null; try { if (!ClientExists(name)) { client = new Client(); client.Name = name; client.OwnerName = _username; client.Options = ClientOption.RmDir|ClientOption.Clobber; client.LineEnd = LineEnd.Local; client.Description = description; client.Root = root; client.SubmitOptions = new ClientSubmitOptions(false, SubmitType.RevertUnchanged); client.ViewMap = new ViewMap(); } else { client = GetRepository().GetClient(name); client.Root = root; client.Description = description; } SaveClient(client); } catch (P4Exception p4e) { LogP4Exception(p4e); } return client; } /// <summary> /// Creates a client based on the client specification /// </summary> /// <param name="client">The client specification to create</param> /// <returns>The client created</returns> public Client SaveClient(Client client) { Log.TraceFunction(); try { var result = GetRepository().CreateClient(client); CurrentClient = client; return CurrentClient; } catch (P4Exception p4e) { LogP4Exception(p4e); return client; } } public Client GetClient(string name) { Log.TraceFunction(); try { var result = GetRepository().GetClient(name); return result; } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } public Client UpdateClient(Client client) { Log.TraceFunction(); try { GetRepository().UpdateClient(client); CurrentClient = client; return CurrentClient; } catch (P4Exception p4e) { LogP4Exception(p4e); return client; } } /// <summary> /// Checks to see if a particular client already exists on the Perforce server /// </summary> /// <param name="name">The name of the client specification to look for</param> /// <returns>true if the client is found, false otherwise</returns> public bool ClientExists(string name) { Log.TraceFunction(); bool found = false; var repo = GetRepository(); var options = new ClientsCmdOptions(ClientsCmdFlags.IgnoreCase, null, name, -1, null); try { var matching = repo.GetClients(options); if (matching != null && matching.Count > 0) { found = true; } } catch (P4Exception p4e) { LogP4Exception(p4e); } return found; } // Returns true if the specified folder path is the LHS of a specific // client view mapping. Intended for use to decide if a folder can be // Synced or Unsynced. public bool ClientFolderMappingContains(string path) { Log.TraceFunction(); path = AppendWildcard(path, "..."); if (CurrentClient.ViewMap == null) return false; MapEntry line = CreateMapEntry(path, CurrentClient.Name, MapType.Include); return MappingContains(CurrentClient.ViewMap, line, path); } public string MapToDepot(string path, ViewMap vm=null) { if (Utility.IsDepotPath(path)) return path; if (_pserver == null) _pserver = new P4Server(_serverUri, _username, _password, _client.Name); P4MapApi map = new P4MapApi(_pserver); if (vm == null) vm = CurrentClient.ViewMap; foreach (var line in vm) { map.Insert(line.Left.Path, line.Right.Path, P4MapApi.Type.Include); } string relativePath = path.Replace(CurrentClient.Root, ""); relativePath = relativePath.TrimStart('\\').Replace("\\", "/"); string clientPath = string.Format(("//{0}/{1}"), CurrentClient.Name, relativePath); var depotPath = map.Translate(clientPath, P4MapApi.Direction.RightLeft); return depotPath; } private bool MappingContains(ViewMap vm, MapEntry me, string path) { bool caseSensitive = IsServerCaseSensitive(); // Convert from client path to depot path if necessary path = MapToDepot(path, vm); if (path == null) return false; path = EscapePath(path); foreach (var mapping in vm) { var left = mapping.Left.Path; if (left.EndsWith("/...")) { if (caseSensitive) { if (path.StartsWith(left.TrimEnd('.'))) return true; } else { if (path.StartsWith(left.TrimEnd('.'), StringComparison.CurrentCultureIgnoreCase)) return true; } } } return false; } public Client ClientFolderMappingAdd(string depotPath) { Log.TraceFunction(); Client c = GetClient(_client.Name); MapEntry line = CreateMapEntry(depotPath, _client.Name, MapType.Include); if (c.ViewMap == null) { c.ViewMap = new ViewMap(); } if (!MappingContains(c.ViewMap, line, depotPath)) { c.ViewMap.Add(line); } return UpdateClient(c); } public Client ClientFolderMappingRemove(string depotPath) { Log.TraceFunction(); Client updated = null; Client c = GetClient(_client.Name); depotPath = AppendWildcard(depotPath, "..."); Log.Debug(string.Format("Path: {0}", depotPath)); var entry = CreateMapEntry(depotPath, _client.Name, MapType.Include); var changed = false; if (c.ViewMap != null && c.ViewMap.Count > 0) { if (MappingContains(c.ViewMap, entry, depotPath)) { Log.Debug("Removed view line"); c.ViewMap.Remove(entry); changed = true; } else { // need to iterate over the viewmap to see if this is a subdirectory // of an existing item var found = false; foreach (var mapping in c.ViewMap) { var left = mapping.Left.Path; if (left.EndsWith("/...")) { if (depotPath.StartsWith(left.TrimEnd('.'))) { found = true; break; } } } if (found) { Log.Debug("Adding exlude mapping"); var excludeEntry = CreateMapEntry(depotPath, _client.Name, MapType.Exclude); c.ViewMap.Add(excludeEntry); changed = true; } } } if (changed) { updated = UpdateClient(c); } return updated; } public MapEntry CreateMapEntry(string depotPath, string clientName, MapType type) { Log.TraceFunction(); var left = MkEscapedDepotPath(depotPath); var clientPath = string.Format("//{0}/{1}", clientName, EscapePath(depotPath.Substring(2))); var right = new ClientPath(clientPath); return new MapEntry(type, left, right); } /// <summary> /// Removes the client specification from the server /// </summary> /// <param name="name">The name of the client to delete</param> public void DeleteClient(string name) { var repo = GetRepository(); try { var client = repo.GetClient(name); if (client != null) { var options = new DeleteFilesCmdOptions(DeleteFilesCmdFlags.None, -1); repo.DeleteClient(client, options); } } catch (P4Exception p4e) { LogP4Exception(p4e); } } /// <summary> /// CurrentClient property /// </summary> public Client CurrentClient { get { return _client; } set { _client = value; SetClient(_client.Name) ; } } public void SetClient(string name) { Log.TraceFunction(); GetConnection().SetClient(name); _client = GetClient(name); var needsUpdate = false; if (!_client.Options.HasFlag(ClientOption.RmDir)) { _client.Options = _client.Options | ClientOption.RmDir; needsUpdate = true; } if (!_client.Options.HasFlag(ClientOption.Clobber)) { _client.Options = _client.Options | ClientOption.Clobber; needsUpdate = true; } if(needsUpdate) { GetRepository().UpdateClient(_client); GetConnection().SetClient(name); _client = GetClient(name); } } #endregion #region CHANGELIST OPERATIONS /// <summary> /// Creates a new changelist /// </summary> /// <param name="description">The description for the changelist</param> /// <returns>The changelist created</returns> public Changelist CreateChangelist(string description) { Log.TraceFunction(); try { var change = new Changelist(); change.OwnerName = _username; change.ClientId = CurrentClient.Name; change.Description = description; var repo = GetRepository(); change = repo.CreateChangelist(change); EscapeChangelistFilePaths(change); return change; } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } /// <summary> /// Retrieves the numbered changelist /// </summary> /// <param name="id">The changelist Id number to retrieve</param> /// <returns>The changelist specified by the Id number</returns> public Changelist GetChangelist(int id, bool includeShelvedFiles = false) { Log.TraceFunction(); var repo = GetRepository(); Options opts = null; if (includeShelvedFiles) { var flags = DescribeChangelistCmdFlags.Shelved; opts = new DescribeCmdOptions(flags, 0, 0); } else { var flags = ChangeCmdFlags.None; opts = new ChangeCmdOptions(flags); } try { var result = repo.GetChangelist(id, opts); EscapeChangelistFilePaths(result); return result; } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } public IList<Changelist> GetAllPendingChangelists() { Log.TraceFunction(); IList<Changelist> result = new List<Changelist>(); var repo = GetRepository(); var options = new ChangesCmdOptions(ChangesCmdFlags.None, CurrentClient.Name, 0, ChangeListStatus.Pending, Username); try { result = repo.GetChangelists(options, null); foreach (var change in result) { EscapeChangelistFilePaths(change); } } catch (P4Exception p4e) { LogP4Exception(p4e); } return result; } public Changelist GetCurrentPendingChangelist(bool shelved=false) { Log.TraceFunction(); Changelist current = null; var repo = GetRepository(); var options = new ChangesCmdOptions(ChangesCmdFlags.None, CurrentClient.Name, 1, ChangeListStatus.Pending, Username); try { var changes = repo.GetChangelists(options, null); if (changes != null && changes.Count == 1) { //current = changes[0]; var id = changes[0].Id; Options opts = null; if (shelved) { var flags = DescribeChangelistCmdFlags.Shelved; opts = new DescribeCmdOptions(flags, 0, 0); } else { var flags = ChangeCmdFlags.None; opts = new ChangeCmdOptions(flags); } current = repo.GetChangelist(id, opts); EscapeChangelistFilePaths(current); } } catch (P4Exception p4e) { LogP4Exception(p4e); } return current; } public void DeletePendingChangeList() { Log.TraceFunction(); try { var change = GetCurrentPendingChangelist(); if (change != null) { var repo = GetRepository(); var options = new Options(); repo.DeleteChangelist(change, options); } } catch (P4Exception p4e) { LogP4Exception(p4e); } } // Cleans the current changelist, looking for any files that have been deleted locally since // the changelist was created. The basic idea of this method is to reconcile deletions on local // disk with the state of the changelist -- something that p4 reconcile doesn't find and // which causes issues when trying to submit a changelist public void CleanChangelist() { Log.TraceFunction(); var change = GetCurrentPendingChangelist(); // only look at things if the changelist exists and there are files in it if (change != null && change.Files != null && change.Files.Count > 0) { var helper = Utility.GetPerforceHelper(); var filesToRevert = new List<string>(); var filesToRemove = new List<string>(); foreach (var f in change.Files) { if (f.Action == FileAction.Add) { // if the added file no longer exists, then we just need to revert var md = helper.RunFstat(f.DepotPath.Path); if (!System.IO.File.Exists(md.LocalPath.Path)) { filesToRevert.Add(f.DepotPath.Path); } } else if (f.Action == FileAction.MoveAdd) { // if the renamed file does not exist, we will revert and then delete // the original var md = helper.RunFstat(f.DepotPath.Path); if (!System.IO.File.Exists(md.LocalPath.Path)) { filesToRevert.Add(f.ClientPath.Path); filesToRemove.Add(md.MovedFile.Path); } } else if (f.Action == FileAction.Edit) { // we were editing the file, and now it has been deleted so we need to // mark it for delete var md = helper.RunFstat(f.DepotPath.Path); if (!System.IO.File.Exists(md.LocalPath.Path)) { filesToRevert.Add(f.DepotPath.Path); filesToRemove.Add(f.DepotPath.Path); } } } // process any files that need to be reverted first if (filesToRevert.Count > 0) { var list = filesToRevert.ToArray(); helper.RevertFiles(serverOnly: false, paths: list); } // we only need to save the changelist if we actually cleaned stuff up if (filesToRemove.Count > 0) { var list = filesToRemove.ToArray(); helper.DeleteFiles(serverOnly: false, paths: list); } } } public IList<FileSpec> GatherOpenFilesInCurrentChangelist() { Log.TraceFunction(); IList<FileSpec> reopenedFiles = null; var repo = GetRepository(); var currentChange = GetOrCreatePendingChangelist(); var openedSpecs = new List<FileSpec>(); var reopenSpecs = new List<FileSpec>(); openedSpecs.Add(new FileSpec(MkEscapedDepotPath("//..."))); // GetOpenedFiles doesn't escape! try { var openedOptions = new GetOpenedFilesOptions(GetOpenedFilesCmdFlags.None, null, CurrentClient.Name, Username, 0); var list = GetRepository().GetOpenedFiles(openedSpecs, openedOptions); if (list != null) { foreach (var i in list) { if (i.ChangeId != currentChange.Id) { reopenSpecs.Add(new FileSpec(i.ClientPath)); } } } } catch (P4Exception) { } if (reopenSpecs.Count > 0) { var options = new ReopenCmdOptions(currentChange.Id, null); try { reopenedFiles = repo.Connection.Client.ReopenFiles(reopenSpecs, options); } catch (P4Exception) { } } return FixFileSpecs(reopenedFiles); } public IList<FileSpec> MoveFilesToNewChangelist(IList<string> files, int changeId=0) { Log.TraceFunction(); var repo = GetRepository(); if (!(changeId > 0)) { var change = CreateChangelist("Perforce-created changelist"); changeId = change.Id; } var options = new ReopenCmdOptions(changeId, null); var fileSpecs = new List<FileSpec>(); foreach (var f in files) { if (f.StartsWith("//")) { fileSpecs.Add(new FileSpec(new DepotPath(UnescapePath(f)))); } else { fileSpecs.Add(new FileSpec(new ClientPath(UnescapePath(f)))); } } try { return FixFileSpecs(repo.Connection.Client.ReopenFiles(fileSpecs, options)); } catch (P4Exception) {} return null; } /// <summary> /// Deletes the changelist /// </summary> /// <param name="id">The changelist Id number to delete</param> public void DeleteChangelist(int id) { Log.TraceFunction(); var change = GetChangelist(id); if (change != null && change.Pending) { DeleteChangelist(change); } } /// <summary> /// Deletes the changelist /// </summary> /// <param name="change">The changelist object to delete</param> public void DeleteChangelist(Changelist change) { Log.TraceFunction(); var repo = GetRepository(); var options = new ChangeCmdOptions(ChangeCmdFlags.Delete); repo.DeleteChangelist(change, options); } /// <summary> /// Lists 50 submitted changelists on the path /// </summary> /// <param name="path">The specified path</param> public IList<Changelist> ListChanges(string path) { Log.TraceFunction(); var repo = GetRepository(); if (IsDirectory(path) && !path.EndsWith("/...")) { path = path + "/..."; } // Unescaped local paths can cause problems - convert to depot paths first var depotPath = MapToDepot(path); if (depotPath == null) depotPath = path; var spec = GetEscapedFileSpec(depotPath); // GetChangelists doesn't escape var options = new ChangesCmdOptions(ChangesCmdFlags.FullDescription, null, 50, ChangeListStatus.Submitted, null); try { return repo.GetChangelists(options, spec); } catch (P4Exception p4e) { Log.Exception(p4e); return null; } } public ResultsWrapper SubmitSingleFile(string path, string description) { Log.TraceFunction(); var change = CreateChangelist(description); var reopenOptions = new ReopenCmdOptions(change.Id, null); var spec = GetEscapedFileSpec(path); // Reopen doesn't escape try { GetRepository().Connection.Client.ReopenFiles(reopenOptions, spec); return SubmitChangelist(change.Id, description, revertUnchanged: true); } catch (P4Exception) { } return null; } public ResultsWrapper SubmitChangelist(int id, string description=null, bool revertUnchanged=true) { Log.TraceFunction(); var change = GetChangelist(id); if (change != null && change.Pending) { if (description != null) { change.Description = description; } return SubmitChangelist(change, revertUnchanged); } else { return null; } } /// <summary> /// /// </summary> /// <param name="change"></param> /// <param name="revertUnchanged"></param> /// <returns></returns> public ResultsWrapper SubmitChangelist(Changelist change=null, bool revertUnchanged=true) { Log.TraceFunction(); var wrapper = new ResultsWrapper(); // get the current pending changelist if the changelist is not specified if (change == null) { change = GetCurrentPendingChangelist(); if (change == null) { return null; } } else { UnescapeChangelistFilePaths(change); GetRepository().UpdateChangelist(change); } var clientSubmitOptions = new ClientSubmitOptions(); clientSubmitOptions.Reopen = false; if (revertUnchanged) { clientSubmitOptions.SubmitType = SubmitType.RevertUnchanged; } else { clientSubmitOptions.SubmitType = SubmitType.SubmitUnchanged; } var options = new SubmitCmdOptions(SubmitFilesCmdFlags.None, change.Id, null, null, clientSubmitOptions); try { wrapper.Results = GetConnection().Client.SubmitFiles(options, null); wrapper.HasError = false; wrapper.Message = "Submit successful"; } catch (P4Exception p4e) { wrapper.HasError = true; switch (p4e.ErrorCode) { case (int)PerforceErrors.NoFilesToSubmit: wrapper.HasError = false; wrapper.Message = "There were no changed files to submit"; break; case (int)PerforceErrors.NoFilesToSubmitWithError: if (p4e.Details.Count > 0) { var errorText = new StringBuilder(); foreach (var d in p4e.Details) { errorText.AppendLine(d.Message); } wrapper.Message = errorText.ToString(); } break; default: if (p4e.Details != null && p4e.Details.Count > 0) { var errorText = new StringBuilder(); foreach (var d in p4e.Details) { errorText.AppendLine(d.Message); } wrapper.Message = errorText.ToString(); } else if(p4e.Message != null) { wrapper.Message = p4e.Message; } break; } } return wrapper; } /// <summary> /// /// </summary> /// <param name="change"></param> /// <returns></returns> public IList<FileMetaData> GetChangelistFiles(Changelist change = null) { Log.TraceFunction(); // get the current pending changelist if the changelist is not specified if (change == null) { change = GetCurrentPendingChangelist(); } if (change == null) { return null; } return change.Files; } public IList<ShelvedFile> GetShelvedChangelistFiles(Changelist change = null) { Log.TraceFunction(); if (change == null) { change = GetCurrentPendingChangelist(shelved: true); } if (change == null) { return null; } return change.ShelvedFiles; } /// <summary> /// /// </summary> /// <param name="change"></param> public IList<FileSpec> RevertChangelist(Changelist change=null) { Log.TraceFunction(); // get the current pending changelist if the changelist is not specified if (change == null) { change = GetCurrentPendingChangelist(); } if (change == null) { return null; } var fileSpec = new FileSpec(new DepotPath("//...")); var options = new RevertCmdOptions(RevertFilesCmdFlags.None, change.Id); try { return FixFileSpecs(GetRepository().Connection.Client.RevertFiles(options, fileSpec)); } catch (P4Exception) { } return null; } public Changelist GetOrCreatePendingChangelist() { Log.TraceFunction(); var change = GetCurrentPendingChangelist(); if (change == null) { change = CreateChangelist(Constants.GENERATED_CHANGELIST_DESCRIPTION); } return change; } #endregion #region FILE OPERATIONS public bool PathExists(string depotPath) { Log.TraceFunction(); // first check to see if the path is an existing file var fileResults = RunCmd("files", depotPath, new List<string>() {"-e"}); if (fileResults != null && fileResults.Count == 1) return true; // if the file does not exist, check to see if it is a directory var dirResults = RunCmd("dirs", depotPath); if (dirResults != null && dirResults.Count == 1) return true; return false; } public bool IsDirectoryMapped(string dir) { Log.TraceFunction(); // normalize directory dir = dir.TrimEnd('/'); var list = new List<string>(); list.Add(dir); var mappings = GetClientMappings(list, true); return mappings.IsDirMapped(dir); } public bool IsPathInClientView(string depotPath) { Log.TraceFunction(); var inView = false; if (depotPath != null && GetConnection().Client != null && GetConnection().Client.ViewMap != null) { var map = GetConnection().Client.ViewMap; foreach (var e in map) { if (e.Left.Path.StartsWith(depotPath)) { inView = true; break; } } } return inView; } public DepotMapping GetClientMappings(IList<string> paths, bool pathsAreDirectories=false) { Log.TraceFunction(); var mappings = new DepotMapping(); if (paths != null && paths.Count > 0) { var fileArr = new List<string>(); foreach (var path in paths) { var p = path; if (pathsAreDirectories) { if (Utility.IsDepotPath(p)) { p = string.Format("{0}/%", p); } else { p = string.Format("{0}\\%", p); } } fileArr.Add(p); } try { var cmdResults = WhereResults(fileArr.ToArray()); if (cmdResults.TaggedOutput != null) { foreach (TaggedObject obj in cmdResults.TaggedOutput) { if (!obj.ContainsKey("unmap")) { if (obj.ContainsKey("depotFile") && obj.ContainsKey("path")) { var depotFile = Utility.NormalizeDepotPath(obj["depotFile"].ToString()); var clientFile = Utility.NormalizeClientPath(obj["path"].ToString()); mappings.Add(depotFile, clientFile); } } } } } catch (Exception) { // caught exception, probably because client mappings do not exist. } } return mappings; } public P4CommandResult WhereResults(params string[] paths) { Log.TraceFunction(); if (paths == null) return null; var cmd = new P4Command(GetRepository(), "where", true, EscapePaths(paths).ToArray()); return cmd.Run(); } public DepotMapping GetDepotMappings(IList<string> paths, bool pathsAreDirectories = false) { Log.TraceFunction(); var mappings = new DepotMapping(); if (paths.Count > 0) { var fromFiles = new List<FileSpec>(); foreach (var path in paths) { var p = path; if (pathsAreDirectories) { p = string.Format("{0}\\%", p); } fromFiles.Add(new FileSpec(new ClientPath(p), null)); } IList<FileSpec> results = null; try { results = GetConnection().Client.GetClientFileMappings(fromFiles); } catch (Exception) { // caught exception, probably because client mappings do not exist } if (results != null) { foreach (var r in results) { mappings.Add(r.DepotPath.Path, Utility.NormalizeClientPath(r.LocalPath.Path)); } } } return mappings; } public string PredictDepotPath(string clientPath) { Log.TraceFunction(); var depotPath = string.Empty; var root = GetConnection().Client.Root; if (root != null) { depotPath = clientPath.Replace(root, "/").Replace("\\", "/"); } return depotPath; } public string PredictClientPath(string depotPath) { Log.TraceFunction(); var clientPath = string.Empty; var root = GetConnection().Client.Root; if (root != null) { clientPath = Path.Combine(root, depotPath.TrimStart('/').Replace('/', '\\')); } return clientPath; } public FileSpec GetEscapedFileSpec(string path) { Log.TraceFunction(); if (Utility.IsDepotPath(path)) { return new FileSpec(MkEscapedDepotPath(path)); } else { return new FileSpec(MkClientPath(path)); } } public FileSpec GetUnescapedFileSpec(string path) { Log.TraceFunction(); if (Utility.IsDepotPath(path)) { return new FileSpec(MkUnescapedDepotPath(path)); } else { return new FileSpec(MkClientPath(path)); } } public FileSpec[] GetEscapedFileSpecs(string[] paths) { Log.TraceFunction(); var specs = new FileSpec[paths.Count()]; for (var i = 0; i < paths.Count(); i++) { specs[i] = GetEscapedFileSpec(paths[i]); } return specs; } public FileSpec[] GetUnescapedFileSpecs(string[] paths) { Log.TraceFunction(); var specs = new FileSpec[paths.Count()]; for (var i = 0; i < paths.Count(); i++) { specs[i] = GetUnescapedFileSpec(paths[i]); } return specs; } /// <summary> /// Synchronizes the specified pathwith repository /// </summary> /// <param name="syncArgs">Various sync flags possible, e.g. -f</param> /// <param name="notifyOnError">Whether to report sync errors via UI</param> /// <returns></returns> public IList<SyncMetaData> RunSync(string path, string revSpec = null, List<string> syncArgs = null, bool notifyOnError = false) { List<SyncMetaData> results = new List<SyncMetaData>(); path = EscapePath(path); if (revSpec != null) path += revSpec; var paths = new List<string>() {path}; var ws = GetWorkspaceWatcher(); bool syncErrorOccurred = false; string syncError = ""; P4Command cmd = null; P4CommandResult r = null; var sr = new SyncReporter(ws, path); try { if (ws != null) ws.SyncStart(path); cmd = new P4Command(GetRepository(), "sync", true, paths.ToArray()); cmd.Connection.TaggedOutputReceived += sr.TaggedOutputCallbackFn; if (syncArgs == null) r = cmd.Run(); else r = cmd.Run(syncArgs.ToArray()); } catch (P4Exception ex) { syncErrorOccurred = true; syncError = ex.Message; } finally { cmd.Connection.TaggedOutputReceived -= sr.TaggedOutputCallbackFn; } // This is probably not necessary any more as have reported to WorkspaceWatcher as we went, // but we return them to the caller. if (!((r == null) || (r.TaggedOutput == null) || (r.TaggedOutput.Count <= 0))) { foreach (P4.TaggedObject obj in r.TaggedOutput) { if ((obj.Count <= 2) && (obj.ContainsKey("desc"))) { // hack, but this not really a file, it's just a // the description of the change if -e option is // specified, so skip it continue; } SyncMetaData smd = new SyncMetaData(); smd.FromCmdTaggedData(obj); results.Add(smd); } } if (ws != null) { ws.SyncComplete(path); } if (syncErrorOccurred && notifyOnError) UIHelper.ShowMessage(string.Format("Error synchronizing from server:\n\n{0}", syncError)); return results; } private class SyncReporter { private WorkspaceWatcher _ws = null; private string _originalPath = ""; public SyncReporter(WorkspaceWatcher ws, string originalPath) { _ws = ws; _originalPath = originalPath; } // This will immediately report sync output to WorkspaceWatcher public void TaggedOutputCallbackFn(uint cmdId, int objId, P4.TaggedObject obj) { if (obj == null || _ws == null) { return; } if ((obj.Count <= 2) && (obj.ContainsKey("desc"))) { // hack, but this not really a file, it's just a // the description of the change if -e option is // specified, so skip it return; } var smd = new SyncMetaData(); smd.FromCmdTaggedData(obj); _ws.SyncRecordProgress(_originalPath, new List<SyncMetaData>() {smd}); } } /// <summary> /// Performs a p4 add on the specified file /// </summary> /// <param name="clientPath">The client path for the file</param> /// <returns></returns> public IList<FileSpec> MarkFileForAdd(params string[] paths) { Log.TraceFunction(); var change = GetOrCreatePendingChangelist(); var specs = GetEscapedFileSpecs(paths); try { var options = new AddFilesCmdOptions(AddFilesCmdFlags.KeepWildcards, change.Id, null); var result = GetConnection().Client.AddFiles(options, specs); return FixFileSpecs(result); } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } /// <summary> /// Performs a p4 edit on the specified file /// </summary> /// <param name="paths">The path(s) (either depot or client path) of the file to be edited</param> public IList<FileSpec> CheckoutFiles(bool serverOnly=false, params string[] paths) { Log.TraceFunction(); var change = GetOrCreatePendingChangelist(); var specs = GetEscapedFileSpecs(paths); var flags = EditFilesCmdFlags.None; if (serverOnly) { flags = EditFilesCmdFlags.ServerOnly; } try { var options = new EditCmdOptions(flags, change.Id, null); var client = GetConnection().Client; return FixFileSpecs(client.EditFiles(options, specs)); } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } // Fix the unescaping of depot paths private IList<FileSpec> FixFileSpecs(IList<FileSpec> specList) { if (specList != null) { foreach (var f in specList) { f.DepotPath = new DepotPath(EscapePath(f.DepotPath.Path)); } } return specList; } private IList<FileMetaData> FixFileMetaData(IList<FileMetaData> metaList) { if (metaList != null) { foreach (var f in metaList) { f.DepotPath = new DepotPath(EscapePath(f.DepotPath.Path)); } } return metaList; } // Escape paths so that Depot paths are always escaped in application. private void EscapeChangelistFilePaths(Changelist change) { if (change != null && change.ShelvedFiles != null) { foreach (var f in change.ShelvedFiles) { f.Path = new DepotPath(EscapePath(f.Path.Path)); } } if (change != null && change.Files != null) { foreach (var f in change.Files) { f.DepotPath = new DepotPath(EscapePath(f.DepotPath.Path)); } } } private void UnescapeChangelistFilePaths(Changelist change) { if (change != null && change.ShelvedFiles != null) { foreach (var f in change.ShelvedFiles) { f.Path = new DepotPath(UnescapePath(f.Path.Path)); } } if (change != null && change.Files != null) { foreach (var f in change.Files) { f.DepotPath = new DepotPath(UnescapePath(f.DepotPath.Path)); } } } /// <summary> /// /// </summary> /// <param name="sourcePath"></param> /// <param name="destPath"></param> /// <returns></returns> public IList<FileSpec> RenameFile(string sourcePath, string destPath, bool serverOnly=false) { Log.TraceFunction(); var md = RunFstat(sourcePath); // some error handling here // if there is no metadata, then the file hasn't even been added to the server yet, so just exit the method if (md == null) return null; // if the file is locked by someone else, then prevent the rename -- we can't do much if the rename occurs in the filesystem // but we can at least prevent the rename in Perforce if (!MetadataHelper.CanEdit(md)) return null; // if the file is deleted in Perforce, then prevent the rename if (md.Action == FileAction.Delete) return null; // now check the destination name var destMd = RunFstat(destPath); if (destMd != null) { // if someone else has the destination locked, bail out if (!MetadataHelper.CanEdit(destMd)) return null; // if the destination has been deleted previously, then continue otherwise bail out if (destMd.HeadAction != FileAction.Delete) return null; } // mark the file for edit if (md.Action != FileAction.Add) { CheckoutFiles(true, sourcePath); } // now do the rename var change = GetOrCreatePendingChangelist(); var sourceFileSpec = GetEscapedFileSpec(sourcePath); var destFileSpec = GetEscapedFileSpec(destPath); var flags = MoveFileCmdFlags.None; if (serverOnly) { flags = MoveFileCmdFlags.ServerOnly; } try { var options = new MoveCmdOptions(flags, change.Id, null); return FixFileSpecs(GetConnection().Client.MoveFiles(sourceFileSpec, destFileSpec, options)); } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } public IList<FileSpec> RenameFolder(string sourceDir, string destDir, bool serverOnly = false) { Log.TraceFunction(); // check to see if the source directory exists if (!IsDirectory(sourceDir)) { // directory does not exist, but maybe it is a path with adds... var md = GetFileMetaData(sourceDir + @"/...", maxItems: 1); // if we have no metadata, then there's nothing to rename if (md == null) return null; // if we aren't seeing ADD actions, then we should bail out too if (md.Action != FileAction.Add) return null; } // check to see if the destination directory exists and bail out if it does // if we are doing a 'serverOnly' rename, then we want the destDir to exist var destExists = IsDirectory(destDir); if (serverOnly && !destExists) return null; if (!serverOnly && destExists) return null; // if we got to this point, things look good... now look for any locks var paths = new List<string>(); paths.Add(sourceDir + @"/..."); var allMD = GetFileMetaData(paths); var lockFound = false; foreach (var md in allMD) { if (!MetadataHelper.CanEdit(md)) { lockFound = true; break; } } // bail out if a lock is found anywhere in the renamed directory if (lockFound) return null; // if we made it here, let's go ahead and do the rename var change = GetOrCreatePendingChangelist(); var sourceDirWC = string.Empty; if (sourceDir.StartsWith("//")) { sourceDirWC = sourceDir + @"/..."; } else { sourceDirWC = sourceDir + @"\..."; } var destDirWC = string.Empty; if (destDir.StartsWith("//")) { destDirWC = destDir + @"/..."; } else { destDirWC = destDir + @"\..."; } var sourceFileSpec = GetEscapedFileSpec(sourceDirWC); var destFileSpec = GetEscapedFileSpec(destDirWC); CheckoutFiles(serverOnly: true, paths: sourceDirWC); var flags = MoveFileCmdFlags.None; if (serverOnly) { flags = MoveFileCmdFlags.ServerOnly; } try { var options = new MoveCmdOptions(flags, change.Id, null); return FixFileSpecs(GetConnection().Client.MoveFiles(sourceFileSpec, destFileSpec, options)); } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } public IList<FileSpec> CopyFile(string sourcePath, string destPath, bool serverOnly = false) { Log.TraceFunction(); var md = RunFstat(sourcePath); // some error handling here // if there is no metadata, then the file hasn't even been added to the server yet, so just exit the method if (md == null) return null; // if the file is locked by someone else, then prevent the rename -- we can't do much if the rename occurs in the filesystem // but we can at least prevent the rename in Perforce if (!MetadataHelper.CanEdit(md)) return null; // if the file is deleted in Perforce, then prevent the rename if (md.Action == FileAction.Delete) return null; // now check the destination name var destMd = RunFstat(destPath); if (destMd != null) { // if someone else has the destination locked, bail out if (!MetadataHelper.CanEdit(destMd)) return null; // if the destination has been deleted previously, then continue otherwise bail out if (destMd.HeadAction != FileAction.Delete) return null; } var change = GetOrCreatePendingChangelist(); var sourceFileSpec = GetEscapedFileSpec(sourcePath); var destFileSpecList = new List<FileSpec>(); destFileSpecList.Add(GetEscapedFileSpec(destPath)); var flags = CopyFilesCmdFlags.None; if(serverOnly) { flags = CopyFilesCmdFlags.Virtual; } try { var options = new CopyFilesCmdOptions(flags, null, null, null, change.Id, 0); return FixFileSpecs(GetConnection().Client.CopyFiles(sourceFileSpec, destFileSpecList, options)); } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } /// <summary> /// /// </summary> /// <param name="depotPath"></param> /// <returns></returns> public IList<FileSpec> DeleteFiles(bool serverOnly = false, params string[] paths) { Log.TraceFunction(); var change = GetOrCreatePendingChangelist(); var specs = GetEscapedFileSpecs(paths); var flags = DeleteFilesCmdFlags.None; if (serverOnly) { flags = DeleteFilesCmdFlags.ServerOnly; } try { var options = new DeleteFilesCmdOptions(flags, change.Id); return FixFileSpecs(GetConnection().Client.DeleteFiles(options, specs)); } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } public IList<FileSpec> RevertFiles(bool serverOnly = false, params string[] paths) { Log.TraceFunction(); var change = GetOrCreatePendingChangelist(); var specs = GetEscapedFileSpecs(paths); var flags = RevertFilesCmdFlags.None; if (serverOnly) { flags = RevertFilesCmdFlags.ServerOnly; } var options = new RevertCmdOptions(flags, change.Id); try { var results = GetConnection().Client.RevertFiles(options, specs); return FixFileSpecs(results); } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } public P4CommandResult ReconcileFiles(string path, List<string> recArgs = null) { var paths = new List<string>() {path}; return ReconcileFiles(paths.ToArray(), recArgs); } public P4CommandResult ReconcileFiles(string[] paths, List<string> recArgs = null) { Log.TraceFunction(); if (paths == null) return null; var change = GetOrCreatePendingChangelist(); var args = new List<string>(); if (recArgs == null) args.Add("-eadf"); else { args.AddRange(recArgs); } args.Add("-c"); args.Add(change.Id.ToString()); args.AddRange(EscapePaths(paths)); var cmd = new P4Command(GetRepository(), "reconcile", true, args.ToArray()); try { return cmd.Run(); } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } public IList<FileSpec> ShelveFiles(params string[] paths) { Log.TraceFunction(); var change = GetOrCreatePendingChangelist(); var specs = GetEscapedFileSpecs(paths); var options = new ShelveFilesCmdOptions(ShelveFilesCmdFlags.Force, null, change.Id); try { var results = GetConnection().Client.ShelveFiles(options, specs); return FixFileSpecs(results); } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } public IList<FileSpec> DeleteShelvedFiles(int changeId = 0, params string[] paths) { Log.TraceFunction(); if (changeId == 0) { var change = GetOrCreatePendingChangelist(); changeId = change.Id; } var specs = GetEscapedFileSpecs(paths); try { var options = new ShelveFilesCmdOptions(ShelveFilesCmdFlags.Delete, null, changeId); return FixFileSpecs(GetConnection().Client.ShelveFiles(options, specs)); } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } public List<int> GetShelvedLocations(string depotPath) { Log.TraceFunction(); var changeIds = new List<int>(); var changes = GetAllPendingChangelists(); foreach (var c in changes) { if (c.Shelved) { var change = GetChangelist(c.Id, includeShelvedFiles: true); if (change.ShelvedFiles != null) { foreach (var sf in change.ShelvedFiles) { if (sf.Path.Path.Equals(depotPath, StringComparison.CurrentCultureIgnoreCase)) { changeIds.Add(c.Id); break; } } } } } return changeIds; } // this method should look at all pending changelists for this workspace to see if the file is shelved anywhere public bool IsFileShelved(string depotPath) { Log.TraceFunction(); var shelved = false; var shelveIds = GetShelvedLocations(depotPath); if (shelveIds.Count > 0) { shelved = true; } return shelved; } public IList<FileSpec> UnshelveFiles(int changeId = 0, params string[] paths) { Log.TraceFunction(); var change = GetCurrentPendingChangelist(); if (changeId == 0) { changeId = change.Id; } var specs = GetEscapedFileSpecs(paths); var options = new UnshelveFilesCmdOptions(UnshelveFilesCmdFlags.Force, changeId, change.Id); try { var results = GetConnection().Client.UnshelveFiles(options, specs); return FixFileSpecs(results); } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } public bool IsFileOpened(string depotPath) { Log.TraceFunction(); var opened = false; if (!string.IsNullOrEmpty(depotPath)) { var spec = GetEscapedFileSpec(depotPath); var specs = new List<FileSpec>(); specs.Add(spec); try { var options = new GetOpenedFilesOptions(GetOpenedFilesCmdFlags.None, null, CurrentClient.Name, Username, 0); var list = GetRepository().GetOpenedFiles(specs, options); if (list != null && list.Count > 0) { foreach (var f in list) { if(f.DepotPath.Path.Equals(depotPath, StringComparison.CurrentCultureIgnoreCase)) { opened = true; break; } } } } catch (P4Exception p4e) { LogP4Exception(p4e); } } return opened; } public bool PathHasAnyOpened(string path) { Log.TraceFunction(); var anyOpened = false; if (!string.IsNullOrEmpty(path)) { var results = GetAllOpened(path); if (results != null && results.Count > 0) { foreach (var file in results) { if (file.Action == FileAction.Add) continue; anyOpened = true; } } } return anyOpened; } public IList<P4.File> GetAllOpened(string path, int max=0) { Log.TraceFunction(); var fileSpecs = new List<FileSpec>(); path = AppendWildcard(path, "..."); fileSpecs.Add(GetEscapedFileSpec(EscapePath(path))); var options = new GetOpenedFilesOptions(GetOpenedFilesCmdFlags.AllClients, null, null, null, max); try { var result = GetRepository().GetOpenedFiles(fileSpecs, options); return result; } catch (P4Exception p4e) { LogP4Exception(p4e); return null; } } private List<string> RunCmd(string cmd, string path, List<string> args = null) { return RunCmd(cmd, new List<string>() {path}, args); } private List<string> RunCmd(string cmd, List<string> paths, List<string> args = null) { var results = new List<string>(); P4CommandResult r = null; try { P4Command p4Cmd = new P4Command(GetRepository(), cmd, false, EscapePaths(paths).ToArray()); if (args == null) r = p4Cmd.Run(); else r = p4Cmd.Run(args.ToArray()); } catch (P4Exception) { } if (r != null) { foreach (P4.P4ClientInfoMessage l in r.InfoOutput) { results.Add(l.Message); } } return results; } /// <summary> /// Runs fstat on paths /// </summary> /// <param name="fstatArgs">Various sync flags possible, e.g. -f</param> /// <param name="escapePaths">Various sync flags possible, e.g. -f</param> /// <param name="notifyOnError">Whether to report sync errors via UI</param> /// <returns></returns> public List<FileMetaData> RunFstat(List<string> paths, bool escapePaths = true, List<string> fstatArgs = null) { var results = new List<FileMetaData>(); if (fstatArgs == null) fstatArgs = new List<string>() { "-Oa", "-Oe", "-Ol" }; P4CommandResult r = null; try { List<string> fpaths = paths; if (escapePaths) fpaths = EscapePaths(fpaths); P4Command cmd = new P4Command(GetRepository(), "fstat", true, fpaths.ToArray()); r = cmd.Run(fstatArgs.ToArray()); } catch (P4Exception) {} if (!((r == null) || (r.TaggedOutput == null) || (r.TaggedOutput.Count <= 0))) { foreach (P4.TaggedObject obj in r.TaggedOutput) { if ((obj.Count <= 2) && (obj.ContainsKey("desc"))) { // hack, but this not really a file, it's just a // the description of the change if -e option is // specified, so skip it continue; } var md = new FileMetaData(); md.FromFstatCmdTaggedData(obj); // Fix the stupidity of Fstat which changes depot paths!!!! md.DepotPath = new DepotPath(EscapePath(md.DepotPath.Path)); results.Add(md); } } return results; } public FileMetaData RunFstat(string path, int revision = -1, List<string> fstatArgs = null) { var depotPath = MapToDepot(path); if (depotPath == null) depotPath = path; if (revision != -1) { path = string.Format("{0}#{1}", EscapePath(depotPath), revision); } else { path = EscapePath(depotPath); } var results = RunFstat(new List<string>() {path}, escapePaths: false, fstatArgs: fstatArgs); if (results.Count > 0) return results[0]; return null; } public FileMetaData GetFileMetaData(string path, string attrFilter = null, bool allVersions = false, int maxItems = 0) { Log.TraceFunction(); FileMetaData md = null; var paths = new List<string>(); paths.Add(path); var results = GetFileMetaData(paths, attrFilter, allVersions, maxItems); if (results != null && results.Count == 1) { md = results[0]; } return md; } /// <summary> /// /// </summary> /// <param name="depotPath"></param> /// <param name="allAttributes"></param> /// <param name="allVersions"></param> /// <returns></returns> public IList<FileMetaData> GetFileMetaData(List<string> paths, string attrFilter = null, bool allVersions = false, int maxItems = 0) { Log.TraceFunction(); IList<FileMetaData> results = new List<FileMetaData>(); if (paths == null || paths.Count == 0 || paths[0] == null) { return results; } IList<FileSpec> specs = new List<FileSpec>(); foreach (var path in paths) { if (path.Equals(Constants.DUMMY_DEPOT_PATH)) { continue; } if (Utility.IsDepotPath(path)) { specs.Add(new FileSpec(MkUnescapedDepotPath(path), null)); } else { specs.Add(new FileSpec(MkClientPath(path), null)); } } if (specs.Count == 0) return null; GetFileMetadataCmdFlags flags = GetFileMetadataCmdFlags.None; if (allVersions) { flags = GetFileMetadataCmdFlags.Attributes | GetFileMetadataCmdFlags.FileSize | GetFileMetadataCmdFlags.HexAttributes | GetFileMetadataCmdFlags.AllRevisions; } else { flags = GetFileMetadataCmdFlags.Attributes | GetFileMetadataCmdFlags.FileSize | GetFileMetadataCmdFlags.HexAttributes; } var options = new GetFileMetaDataCmdOptions(flags, null, null, maxItems, null, null, attrFilter); try { results = GetRepository().GetFileMetaData(specs, options); } catch (P4Exception) { // unable to get metadata -- can happen when bad specs are passed. } return FixFileMetaData(results); } public IList<FileMetaData> GetSearchFileMetaData(List<string> paths, bool mappedOnly = false) { Log.TraceFunction(); GetFileMetadataCmdFlags flags = GetFileMetadataCmdFlags.Attributes | GetFileMetadataCmdFlags.HexAttributes; IList<FileSpec> specs = new List<FileSpec>(); foreach(var p in paths) { var fs = new FileSpec(MkUnescapedDepotPath(p), null); specs.Add(fs); } var filter = new StringBuilder(); filter.Append("headRev & ^headAction=delete & ^headAction=move/delete"); if (mappedOnly) { filter.Append(" & isMapped"); } var options = new GetFileMetaDataCmdOptions(flags, filter.ToString(), null, -1, null, null, "tags"); return GetRepository().GetFileMetaData(specs, options); } public SizeData GetPathSizes(string path) { Log.TraceFunction(); SizeData data = null; if (IsDirectory(path) && !path.EndsWith("/...")) { path = path.TrimEnd('/') + "/..."; } var args = new List<string>(); args.Add("-s"); args.Add(path); var cmd = new P4Command(GetRepository(), "sizes", true, args.ToArray()); var results = cmd.Run(); if (results.TaggedOutput != null) { data = new SizeData(); foreach (TaggedObject obj in results.TaggedOutput) { if (obj.ContainsKey("path")) { data.Path = obj["path"].ToString(); } if (obj.ContainsKey("fileCount")) { data.FileCount = int.Parse(obj["fileCount"].ToString()); } if (obj.ContainsKey("fileSize")) { data.FileSize = long.Parse(obj["fileSize"].ToString()); } } } return data; } public string GetFileFromServer(string depotPath, int revision = -1) { Log.TraceFunction(); string filePath = null; if (depotPath != null) { var extIndex = depotPath.LastIndexOf('.'); if (extIndex < depotPath.Length) { var extension = depotPath.Substring(extIndex); var path = Path.GetTempPath(); var fileName = Guid.NewGuid().ToString() + extension; filePath = Path.Combine(path, fileName); var options = new GetFileContentsCmdOptions(GetFileContentsCmdFlags.None, filePath); FileSpec spec = null; if (revision > 0) { spec = FileSpec.DepotSpec(depotPath, revision); } else if (revision == 0) { spec = new FileSpec(MkUnescapedDepotPath(depotPath), VersionSpec.None); } else { spec = new FileSpec(MkUnescapedDepotPath(depotPath), VersionSpec.Head); } try { GetRepository().GetFileContents(options, spec); } catch (P4Exception) { } } } return filePath; } public string GetDirectoryFromServer(string depotPath, string targetDir = null) { Log.TraceFunction(); Log.Debug(string.Format("GetDirectoryFromServer {0} -> {1}", depotPath, targetDir)); if (depotPath != null) { depotPath = depotPath.TrimEnd('.').TrimEnd('/'); var dirname = depotPath.Substring(depotPath.LastIndexOf('/') + 1); var parentDepotPath = depotPath.Substring(0, depotPath.LastIndexOf('/') + 1); if (string.IsNullOrEmpty(targetDir)) { targetDir = Path.GetTempPath(); } var dirSpec = GetEscapedFileSpec(depotPath + "/..."); var filesOptions = new FilesCmdOptions(FilesCmdFlags.None, 0); try { var files = GetRepository().GetFiles(filesOptions, dirSpec); foreach (var file in files) { var fileDepotPath = file.DepotPath.Path; var subPath = fileDepotPath.Replace(parentDepotPath, "").Replace('/', '\\'); var outputFilePath = Path.Combine(targetDir, subPath); var getFileContentsOptions = new GetFileContentsCmdOptions(GetFileContentsCmdFlags.None, outputFilePath); var fileSpec = GetEscapedFileSpec(fileDepotPath); GetRepository().GetFileContents(getFileContentsOptions, fileSpec); } } catch (P4Exception) { } } return targetDir; } public string GetFileFromShelf(string depotPath, int changeId = -1) { Log.TraceFunction(); string filePath = null; if (depotPath != null) { if (changeId < 1) { changeId = GetCurrentPendingChangelist().Id; } var extIndex = depotPath.LastIndexOf('.'); if (extIndex < depotPath.Length) { var extension = depotPath.Substring(extIndex); var path = Path.GetTempPath(); var fileName = Guid.NewGuid().ToString() + extension; filePath = Path.Combine(path, fileName); var shelf = new ShelvedInChangelistIdVersion(changeId); var options = new GetFileContentsCmdOptions(GetFileContentsCmdFlags.None, filePath); var spec = new FileSpec(MkUnescapedDepotPath(depotPath), shelf); var results = GetRepository().GetFileContents(options, spec); } } return filePath; } public IList<FileHistory> GetFileHistory(string path) { Log.TraceFunction(); var flags = GetFileHistoryCmdFlags.IncludeInherited | GetFileHistoryCmdFlags.FullDescription; var fileSpec = GetUnescapedFileSpec(path); // GetFileHistory escapes var options = new GetFileHistoryCmdOptions(flags, 0, 0); try { return GetRepository().GetFileHistory(options, fileSpec); } catch (P4Exception) { } return null; } public SubmitResults RollbackFileToRevision(string path, int revision) { Log.TraceFunction(); var origFileSpec = FileSpec.DepotSpec(UnescapePath(path), revision); var fileSpec = FileSpec.DepotSpec(UnescapePath(path)); var desc = string.Format("Rolling file {0} back to revision {1}", path, revision); var change = CreateChangelist(desc); var mdOptions = new GetFileMetaDataCmdOptions(GetFileMetadataCmdFlags.None, null, null, -1, null, null, null); var mdList = GetRepository().GetFileMetaData(mdOptions, fileSpec); if (mdList == null || mdList.Count != 1) return null; var md = mdList[0]; if (md.HeadRev <= revision) return null; // sync the desired revision of the file var syncOptions = new SyncFilesCmdOptions(SyncFilesCmdFlags.Force, 0); try { GetConnection().Client.SyncFiles(syncOptions, origFileSpec); } catch (P4Exception) { } // edit the file try { var editOptions = new EditCmdOptions(EditFilesCmdFlags.None, change.Id, null); GetConnection().Client.EditFiles(editOptions, fileSpec); } catch (P4Exception) { } // sync the head revision of the file try { GetConnection().Client.SyncFiles(syncOptions, fileSpec); } catch (P4Exception) { } // resolve the file (ay) try { var resolveOptions = new ResolveCmdOptions(ResolveFilesCmdFlags.AutomaticYoursMode, change.Id); GetConnection().Client.ResolveFiles(resolveOptions, fileSpec); } catch (P4Exception) { } // submit the changes var clientSubmitOptions = new ClientSubmitOptions(); clientSubmitOptions.SubmitType = SubmitType.RevertUnchanged; var submitOptions = new SubmitCmdOptions(SubmitFilesCmdFlags.None, change.Id, null, null, clientSubmitOptions); try { return GetConnection().Client.SubmitFiles(submitOptions, null); } catch (Exception) { var deleteChangelistOptions = new ChangeCmdOptions(ChangeCmdFlags.None); GetRepository().DeleteChangelist(change, deleteChangelistOptions); return null; } } public SubmitResults RollbackFolderToChangelist(string path, int changeId) { Log.TraceFunction(); // add wildcard to the path if it isn't there if (!path.EndsWith("/...")) { path = path + "/..."; } // create a changelist var desc = string.Format("Rolling folder {0} back to change @{1}", path, changeId); var change = CreateChangelist(desc); // create a filespec with the changeID var fileSpec = new FileSpec(MkEscapedDepotPath(path), new ChangelistIdVersion(changeId)); var deletedList = new List<FileSpec>(); var addedList = new List<FileSpec>(); var updatedList = new List<FileSpec>(); // preview the sync string[] previewArgs = { "-f", "-n", fileSpec.ToString() }; var p4cmd = GetConnection().CreateCommand("sync", true, previewArgs); try { var p4cmdResults = p4cmd.Run(); if (p4cmdResults.TaggedOutput != null) { foreach (TaggedObject obj in p4cmdResults.TaggedOutput) { string depotFile = null; string clientFile = null; var res = obj.TryGetValue("depotFile", out depotFile); res = obj.TryGetValue("clientFile", out clientFile); if (depotFile != null) { var depotFileSpec = GetEscapedFileSpec(depotFile); var clientFileSpec = GetEscapedFileSpec(clientFile); if (obj.ContainsKey("action")) { var action = obj["action"].ToString(); if (action.Equals("deleted")) { deletedList.Add(depotFileSpec); } else if (action.Equals("added")) { addedList.Add(clientFileSpec); } else if (action.Equals("updated")) { updatedList.Add(depotFileSpec); } } } } } } catch (P4Exception) { } // sync to the desired changelist string[] args = { "-f", fileSpec.ToString() }; p4cmd = GetConnection().CreateCommand("sync", true, args); try { var p4cmdResults = p4cmd.Run(); } catch (P4Exception) { } if (updatedList.Count > 0) { // edit the files in the path try { var editOptions = new EditCmdOptions(EditFilesCmdFlags.None, change.Id, null); var editResults = GetConnection().Client.EditFiles(editOptions, updatedList.ToArray()); } catch (P4Exception) { } // sync the head revision of the file try { var syncOptions1 = new SyncFilesCmdOptions(SyncFilesCmdFlags.Force, 0); var syncResults2 = GetConnection().Client.SyncFiles(syncOptions1, updatedList.ToArray()); } catch (P4Exception) { } // resolve the file (ay) try { var resolveOptions = new ResolveCmdOptions(ResolveFilesCmdFlags.AutomaticYoursMode, change.Id); var resolveResults = GetConnection().Client.ResolveFiles(resolveOptions, updatedList.ToArray()); } catch (P4Exception) { } } if (addedList.Count > 0) { // find any deleted items that need to be re-added with the -d (downgrade) flag try { // AddFilesCmdFlags.Downgrade | var addOptions = new AddFilesCmdOptions(AddFilesCmdFlags.KeepWildcards, change.Id, null); var results = GetConnection().Client.AddFiles(addOptions, addedList.ToArray()); } catch (P4Exception) { } } if (deletedList.Count > 0) { // delete any items that aren't supposed to be there try { var deleteOptions = new DeleteFilesCmdOptions(DeleteFilesCmdFlags.DeleteUnsynced, change.Id); var results = GetConnection().Client.DeleteFiles(deleteOptions, deletedList.ToArray()); } catch (P4Exception) { } } // submit the changes var clientSubmitOptions = new ClientSubmitOptions(); clientSubmitOptions.SubmitType = SubmitType.SubmitUnchanged; try { var submitOptions = new SubmitCmdOptions(SubmitFilesCmdFlags.None, change.Id, null, null, clientSubmitOptions); var submitResult = GetConnection().Client.SubmitFiles(submitOptions, null); return submitResult; } catch (Exception ex) { var deleteChangelistOptions = new ChangeCmdOptions(ChangeCmdFlags.None); GetRepository().DeleteChangelist(change, deleteChangelistOptions); throw new ApplicationException(ex.Message); } } public P4CommandResult RunCmd(string cmd, bool tagged, string[] args) { var p4Cmd = GetConnection(true).CreateCommand(cmd, tagged, args); return p4Cmd.Run(); } #endregion #region CONSTRUCTORS public PerforceHelper(string serverUri, string username) { Log.TraceFunction(); _serverUri = serverUri; _username = username; _server = null; var cache = MemoryCache.Default; cache.Remove(REPO_CACHE_KEY); var repo = GetRepository(useCache: false, testConnection: false); var addr = repo.Server.Address; if(repo.Connection == null) { throw new ApplicationException("ERROR: unable to connect to server"); } } #endregion #region ACCESSOR FUNCTIONS public string Username { get { return _username; } set { _username = value; } } public string ServerURI { get { return _serverUri; } set { _serverUri = value; } } public string Password { set { _password = value; } } public string Ticket { get { return _ticket; } set { _ticket = value; } } public bool ClientEnabled { get { var enabled = false; if (CurrentClient != null) { enabled = true; } return enabled; } } #endregion #region PRIVATE COMMUNICATION METHODS private Server _server; private Server GetServer() { try { if (_server == null) { _server = new Server(new ServerAddress(_serverUri)); } return _server; } catch (Exception e) { throw new P4HelperException(e.Message); } } private static string REPO_CACHE_KEY = "repository"; private Repository GetRepository(bool useCache = true, bool testConnection = false) { ObjectCache cache = null; Repository repo = null; if (testConnection) { // test the connection with a 5-second timeout. Why 5 seconds? no real // reason. it had to be some timeout value, and 5 seconds seemed a reasonably // short time to wait without having false positives. if (!Utility.CheckTCPConnection(_serverUri, timeout: 5)) { var exception = new P4HelperException("Cannot reach the server"); UIHelper.CriticalError(exception); } } if (useCache) { cache = MemoryCache.Default; repo = (Repository)cache.Get(REPO_CACHE_KEY); } if (repo == null) { repo = new Repository(GetServer()); repo.Connection.UserName = _username; if (_client != null) { repo.Connection.SetClient(_client.Name); } var options = new Options(); repo.Connection.Connect(options); repo.Connection.CommandTimeout = TimeSpan.FromMinutes(20); if (useCache) { var policy = new CacheItemPolicy(); policy.SlidingExpiration = TimeSpan.FromSeconds(30.0); cache.Add(REPO_CACHE_KEY, repo, policy); } repo.Connection.InfoResultsReceived += CommandLine_InfoResultsCallbackFn; repo.Connection.ErrorReceived += CommandLine_ErrorCallbackFn; repo.Connection.TextResultsReceived += CommandLine_TextResultsCallbackFn; repo.Connection.TaggedOutputReceived += CommandLine_TaggedOutputCallbackFn; repo.Connection.CommandEcho += CommandLine_CommandEchoCallbackFn; } return repo; } private Connection GetConnection(bool useCache = true, bool testConnection = false) { Connection con = null; var repo = GetRepository(useCache, testConnection); if (repo != null) { con = repo.Connection; } return con; } private WorkspaceWatcher GetWorkspaceWatcher() { if (TestWorkspaceWatcher != null) return TestWorkspaceWatcher; return Utility.GetWorkspaceWatcher(); } #endregion #region Logging stuff static string[] ErrorSeverity = new string[] { "Empty", "Info", "Warning", "Failed", "Fatal" }; static int[] MessageSeverity = new int[] { 4, //"E_EMPTY", 3, //"E_INFO", 2, //"E_WARN", 1, //"E_FAILED", 0 //"E_FATAL", }; private void CommandLine_ErrorCallbackFn(uint cmdId, int severity, int errorNumber, string data) { try { string severityStr = string.Empty; if ((severity < 0) || (severity >= ErrorSeverity.Length)) { severityStr = string.Format("E_{0}", severity); } else { severityStr = ErrorSeverity[severity]; } string msg = string.Format("{0}: {1}", severityStr, data); if (_log.IsDebugEnabled) { } if ((severity < 0) || (severity >= ErrorSeverity.Length)) { LoggingFunction(0, "P4API.NET", msg); } else { LoggingFunction(MessageSeverity[severity], "P4API.NET", msg); } } catch (Exception ex) { string msg = string.Format("Error trying to log message: [{0}]: {1}", severity, data); LoggingFunction(0, "P4API.NET", msg); Log.Exception(ex); } } private void CommandLine_InfoResultsCallbackFn(uint cmdId, int msgId, int level, string data) { if (_log.IsDebugEnabled) { // level is generally from 0-9, though for some reason trying to add an // ignored file sends a message with a level of 34, so ignor level of 34 if (level == 34) { level = 0; } try { string msg = "--->"; for (int idx = 0; idx < level; idx++) { msg += ". "; } msg += (data != null) ? data : string.Empty; LoggingFunction(3, "P4API.NET", msg); } catch (Exception ex) { Log.Exception(ex); } } } private void CommandLine_TaggedOutputCallbackFn(uint cmdId, int ObjId, P4.TaggedObject Obj) { if (_log.IsDebugEnabled) { try { if (Obj == null) { return; } string msg = "--->Tagged Data: { "; foreach (string key in Obj.Keys) { msg += string.Format("{{{0}:{1}}} ", key, Obj[key]); } msg += "}"; LoggingFunction(3, "P4API.NET", msg); } catch (Exception ex) { Log.Exception(ex); } } } private void CommandLine_TextResultsCallbackFn(uint cmdId, string data) { if (_log.IsDebugEnabled) { string msg = string.Format("->{0}", data); LoggingFunction(3, "P4API.NET", msg); } } private void CommandLine_CommandEchoCallbackFn(string data) { if (data.StartsWith("DataSet set to:")) { // echoing the commands data set, record this only to the log file. LoggingFunction(3, "P4API.NET", data); return; } string[] cmds = data.Split(new char[] { ' ' }, 2, StringSplitOptions.RemoveEmptyEntries); string cmd = cmds[0]; bool log = false; switch (cmd) { case "depot": case "client": case "workspace": case "user": case "group": case "changelist": case "change": case "job": case "branch": case "label": case "remote": case "stream": case "server": case "protect": case "triggers": if ((data.IndexOf(" -i ") > 0) || (data.EndsWith(" -i")) || (data.IndexOf(" -d ") > 0) || (data.EndsWith(" -d"))) { log = true; } break; default: log = true; break; } if (log && _log.IsDebugEnabled) { string msg = string.Format("->{0}", data); LoggingFunction(3, "P4API.NET", msg); return; } } #endregion [Conditional("DEBUG")] public static void Debug(String logMessage) { _log.Debug(logMessage); } #region CLEANUP protected virtual void Dispose(bool disposing) { if (disposing) { // dispose managed resources GetRepository().Dispose(); } // free native resources } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion } #region EXCEPTION CLASS [Serializable] public class P4HelperException : Exception, ISerializable { public P4HelperException(string msg) : base(msg) { } } #endregion }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 16507 | perforce_software | Move to main branch. | ||
//guest/perforce_software/piper/windows/R1.1/Perforce/Helper/PerforceHelper.cs | |||||
#3 | 16474 | Robert Cowham | Bring up-to-date with //guest/perforce_software/piper/windows/main/...@16473 | ||
#2 | 13572 | alan_petersen | updating R1.1 | ||
#1 | 11256 | alan_petersen |
Populate //guest/perforce_software/piper/windows/R1.1/... from //guest/perforce_software/piper/windows/main/.... |
||
//guest/perforce_software/piper/windows/main/Perforce/Helper/PerforceHelper.cs | |||||
#1 | 11255 | alan_petersen | Rename/move file(s) | ||
//guest/perforce_software/piper/windows/Perforce/Helper/PerforceHelper.cs | |||||
#5 | 11037 | alan_petersen |
UPDATE - reworked initial calls to GetPerforceHelper to avoid the 'alive' check - fixed error in the connection failed logic to launch the dialog using a dispatcher (without this, a threading error occurs if this code was called from a background thread) |
||
#4 | 11032 | alan_petersen |
MEGA UPDATE: - fixed bug in folder rollback was failing due to deleted files - files needed to be re-added with the downgrade (-d) flag - put some checking in to stop/start the background workers when - folder versioning - can select 'show versions' on mapped folders - currently limites the number of changelists displayed to the last 50 - rollback button is disabled/hidden unless there are no files in the path (//path/to/folder/...) that are opened (uses p4 opened -a path) - various fixes for some strange null pointer exceptions - fixed folder items (edit all/revert all) - needs some testing to ensure that it is functioning correctly (e.g. when there are locked files in the path being checked out) - general code clean-up -- primarily using the Utility method to obtain the PerforceHelper, rather than having to cast all the time. - found some stability issues (at least when communicating with a local p4d running in the same virtual machine) so some additional error checking/handling was added - reconcile files action now provides feedback via a wait dialog: while reconcile is running, the dialog is displayed along with a wait indicator, when complete a message is displayed that the reconcile is finished - submit changes - submit now perfomed within a wait dialog: dialog displays message along with a wait indicator, results of the submit are displayed when complete - currently, the 'ok' button is disabled until the submit completes. - looking into providing a progress bar (using the feedback mechanism in the API) to provide some more interesting information (and to give users the sense that something is actually happening when a large changelist is begin submitted) - added copy function to PerforceHelper along with test case - implemented copy (ctrl-c) and paste (ctrl-v) in DESI - limitations: - currently only implemented for FILES (not folders) - to paste, one must select the destination FOLDER - next steps: - get working for folders - implement cut (ctrl-x) - permit paste into a column (more intuitive) - rebuilt using 'Any CPU' target -- not sure if this will fix the error seen by the Win7-64 user -- we may need to switch to separate 32- and 64-bit builds - fixed defect #78: delete and move/delete files are no longer displayed in the server view - fixed defect #76: tags now displayed in version history - added MemoryCache for repository reference -- the repository reference was getting stale and creating strange errors, so a reference is now stored in the MemoryCache with a SlidingExpiration of 2 minutes. - fixes #80: copy checks out file -- remnant of code from the rename logic was checking out the code - fixes #14: mail now opens outlook using office API - fixes #72: submit dialog now waits for results from submit, and if there is an error the error message is displayed, otherwise the changelist number is displayed - fixes #75: folder versioning changes -- restore version button now always displayed, but not connected to anything if the folder has opened items; -- opened items now ignored items marked for Add -- this could result in the folder being re-created if/when the user submits the added files - fixed bug in submit dialog in which changelist number was not displayed correctly - fixed bug in submission of partial changelists -- files moved to new changelist but original not updated, resulting in error - #79 - add file now works correctly, relying on the client path rather than the //dummy_depot_path #41: various changes and refactoring to support 'Recent' view - background sync causes immediate refresh of the recent list - ability to mark items as 'read' -- removed from the recent list and the view is refreshed #43 - udpates to logic to deal with transient threading issues (causing the NoSuchObject exceptions) and sync/refresh of workspace files to added files. #48: submit single file now working -- when submit file is selected, the file is moved to a shiny new changelist, and that changelist is presented in the UI - refactoring of submit pane logic to allow passing in a changelist ID incremental build with partially working toolbar buttons -- still working on the forward/back buttons (they aren't quite working right yet) - toolbar items are now functional: left/right nav, add files, create folder addresses #12: favorite folders -- added favorite folder functionality to the application - favorites are now stored in a workspace-specific property (client_name.favoriteFolders) in the favorites.json file in the user's configuration directory (this file is created if it does not exist) - favorites can be edited by double-clicking on the label, right-clicking the label and selecting 'edit', or clicking on the edit icon - favorites can be deleted by right-clicking on the label and selecting 'delete' update involved various refactoring so enable the hidden 'Favorites' selector and select the appropriate sidebar selector (Workspace or Server) when the favorite is selected - favorite tags implemented, satisfying ticket #18 - favorite tags are stored in a workspace-specific property (client_name.favoriteTags) in the favorites.json file in the user's configuration directory (this file is created if it does not exist) - favorite tags can be added by clicking on the '+' that appears when hovering over the Favorite Tags header in the sidebar - a popup appears with a textfield - ESC cancels the add and closes the popup - Enter or clicking on the add button adds the tag to the property (immediately saved) and updates the Favorite Tags sidebar list and closes the popup - changing focus (ie clicking somewhere else in the application) cancels the add and closes the popup - favorite tags can be selected/deselected with a single click - selected tags have a checkmark in the rightmost column - favorite tags can be deleted by right-clicking on the label and selecting 'delete' - list items in ColumnDisplay.xaml modified - FileItem model class modified with boolean method to determine if the file has a selected tag (matching is case insensitive) - FavoritesHelper caches selected tags (to avoid unnecessary IO and parsing) - FavoriteTagItem modified to - fixing copy/paste functionality: - copy/paste now uses filesystem copy and paste (rather than p4 copy) - CopyDirectory method added to Utility class to assist in copying of entire directory structures - copy enabled from either the server tab or the workspace tab... paste only allowed on the workspace tab - wait dialog implemented for the paste process -- should happen quickly if copy is on local filesystem, but if copy is from the server then copy may take a while if the file(s) being copied are large (they have to be downloaded first) - confirmation dialog when folders selected for the copy (giving user the option of skipping large directories if they were selected accidentally) - implementation of p4 sizes code to determine size/number of files in a given path - implementation of p4 print for a depot path -- by default saves to a temp directory, but in the case of copy/paste the print is performed directly to the target directory Addresses DEFECT #91 -- Desi crashing on viewing old versions - previous refactoring had introduced the VersionItem model class, but there were still some remnants of references to FileHistory around, causing issues (ie. crashing) - changelist cleanup had used the -f (force) flag, which hadn't appeared as an issue because the test cases currently run as an admin user, but that masked the fact that only admins can do 'p4 change -d -f'. Doh #94 DEFECT: Selecting tags causes error in Server view - issue was due to tag attributes not retrieved in HEX format, so hex decoding then failed (because they weren't hex!) - this is now fixed in the PerforceHelper - general code cleanup and optimization - helper methods in the utility class to retrieve common objects from the App context - start sync now first checks the preferences to ensure that sync is actually turned on - background processes now wait for initial workspace selection before starting up #36: DEFECT: Refresh color coding - modifications for #88 fix this too #52: DEFECT: Cannot move files from "My Pending Changes" to "Trash" - modified CommandHelper logic to revert files that are in any state other than 'None' when trying to delete #88: DEFECT: icon should be different for items not on server - logic to determine file status updated - icon sizes bumped up slightly (from 18px to 24px) to make differences more visible.. #96: DEFECT: Adding files double window - this was due to a bug in the AddFilesButton_Click method of ToolBar.xaml.cs... basically ShowDialog() was called twice! DOH!! #99: DEFECT: Refresh color coding - modified logic around refreshing after a paste, seems to have fixed this issue #106: DEFECT: Paste a file in a folder where the name exists and the application crashes - added CopyFileToDirectory method to utility class to check to see if the destination file already exists... if it does, then (Copy) is appended to the filename - if there is already a file named xxx (Copy), then a counter is appended: xxx (Copy 2) - this is repeated until it comes up with a filename that doesn't conflict #104: DEFECT: right click option to open file as read only does not work - Code in ContextMenuHelper's ViewWorkspaceFile method was not wired to the code that opens a file. Now it is! - no specific bugs fixed, just trying to get refresh to be a little more responsive and context menus working a little better - reworked DelegateCommand to permit parameter - reworked various menuitem commands to include item(s) selected as parameters - reworked command methods to get parameter, cast it appropriately to the item(s), perform the operation(s), and then refresh the items as well as associated UI components - reworked some methods in MainWindow to take item(s) as arguments (rather than just trying to get the currently selected item -- sometimes when you right-click on something it isn't fully selected) #107: Defect: Unsync from computer not working - modified code that syncs/unsyncs to refresh items after change -- should make for a more responsive interface - modified PerforceHelper code that sets the client to ensure that rmdir is set as an option - fixed Exit menu item to actually shut down the application (YAY) #49: Defect: Desi throws exception when internet connection is lost - modified the GetPerforceHelper utility method to check for a live server. If the server is unreachable, the user is presented with the option to relaunch the application or shutdown #97: DEFECT: Object error when using navigation buttons - code was checking Perforce server to determine if a path was a directory or file, but when the file was newly created it doesn't exist on the server so it returned false, which later caused a null pointer exception. The PerforceHelper IsDirectory code was modified to check, as a last restort, the client path to see if the file is a directory using System.IO.Directory.exists() - modification to MainWindow.cs to re-enable the filesystem watcher #73: Defect: Checkout and delete file .. appropriate change in status not done - code in filesystem watcher was not handing deletes of opened files correctly... opened files need to be reverted first, then they can be deleted using a p4 delete. Since the file no longer exists on the filesystem, these p4 commands are performed as server-only (-k option). When the file is an Add or MoveAdd, then the server-side revert is just performed. Otherwise, if the file is in any other state (other than none), a server-side revert is performed, then the server-side delete. #92: Defect: Option for unshelving missing - added a button to the file info screen for unshelving. Since unshelving may overwrite changes the user has made, a dialog is presented and the user must confirm the unshelve operation. - application now opens with window displaying the file/folder in question when given a command-line argument in the form p4://... |
||
#3 | 10804 | alan_petersen |
UPDATE: - some code cleanup - submit now catches errors more intelligently and displays them - connection failures in background processes now display dialog indicating connection failure - this stops the background process (this will avoid the endless dialog boxes seen in the Mac version!) - currently, dismissing the dialog exits the application -- this may change once I get the 'change connection' feature working |
||
#2 | 10800 | alan_petersen |
UPDATE: - Various fixes - fixed bug in which double-click would not work on files locked by the user - fixed bug in helper application dialog - added 'reconcile files' to context menu in Workspace view - updated submit dialog to display error message if submit fails |
||
#1 | 10761 | alan_petersen |
initial drop of Piper for Windows.... this version still has _many_ bugs (er... i mean "unintended features") but I will be updating it over the next week as more stability is added. |