// // 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; using System.Threading.Tasks; using System.Windows; namespace Perforce.Helper { 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 static readonly ILog _log = LogManager.GetLogger(typeof(PerforceHelper)); #region LOGIN AND CONNECTION TESTING /// <summary> /// Creates a connection to the Perforce server /// </summary> /// <returns>true if connected, false otherwise</returns> public bool Connect() { var connected = false; var con = GetConnection(); if (con.Status == ConnectionStatus.Disconnected) { try { con.Connect(null); } catch (P4Exception pe) { throw new P4HelperException(pe.Message); } } if (con.Status == ConnectionStatus.Connected) { connected = true; } return connected; } /// <summary> /// Disconnects from the Perforce server /// </summary> public void Disconnect() { 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) { 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 pe) { throw new P4HelperException(pe.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() { var result = false; var con = GetConnection(useCache: false, 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) { // do nothing... an exception is thrown if login -s fails, indicating that // the user is not logged in } } return result; } /// <summary> /// Logs out the current session /// </summary> /// <returns>returns the result of the logout operation</returns> public bool Logout() { 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 public long GetServerTime() { return Utility.GetEpochTime(GetServer().Metadata.Date.ToUniversalTime()); } public string GetKey(string name) { 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) { if (string.IsNullOrEmpty(userid)) return null; var repo = GetRepository(); return repo.GetUser(userid); } #endregion #region LISTING OPERATIONS public bool IsDirectory(string path) { if (path.Equals("//")) return true; if (path.EndsWith("/")) return true; if (path.EndsWith("/...")) return true; var repo = GetRepository(); string[] p = { path }; var options = new GetDepotDirsCmdOptions(GetDepotDirsCmdFlags.None, null); var results = repo.GetDepotDirs(options, p); if (results.Count == 1) { return true; } else { var clientPath = PredictClientPath(path); var exists = Directory.Exists(clientPath); return exists; } } public bool IsFile(string path) { var repo = GetRepository(); string[] p = { path }; var specs = GetFileSpecs(p); var options = new GetDepotFilesCmdOptions(GetDepotFilesCmdFlags.None, 0); var results = repo.GetDepotFiles(specs, options); return results.Count == 1; } /// <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) { IList<Depot> depots = null; var repo = GetRepository(); if (repo != null) { var allDepots = repo.GetDepots(); if (localOnly) { depots = allDepots.Where(d => d.Type == DepotType.Local).ToList(); } else { depots = allDepots; } } 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</returns> public IList<string> ListDirectories(string path) { var repo = GetRepository(); if (!path.EndsWith("/")) { path += "/"; } string[] p = { path + "*" }; var options = new GetDepotDirsCmdOptions(GetDepotDirsCmdFlags.None, null); return repo.GetDepotDirs(options, p); } /// <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) { var repo = GetRepository(); var filespec = new List<FileSpec>(); filespec.Add(new FileSpec(new DepotPath(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"); return repo.GetFileMetaData(filespec, options); } #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) { var repo = GetRepository(); var options = new ClientsCmdOptions(ClientsCmdFlags.None, _username, null, -1, null); 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; } /// <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) { Client client = null; 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); 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) { var result = GetRepository().CreateClient(client); CurrentClient = client; return CurrentClient; } public Client GetClient(string name) { return GetRepository().GetClient(name); } public Client UpdateClient(Client client) { GetRepository().UpdateClient(client); CurrentClient = client; return CurrentClient; } /// <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) { bool found = false; var repo = GetRepository(); var options = new ClientsCmdOptions(ClientsCmdFlags.IgnoreCase, null, name, -1, null); var matching = repo.GetClients(options); if (matching != null && matching.Count > 0) { found = true; } return found; } public Client IncludeInClient(string depotPath) { Client c = GetClient(_client.Name); MapEntry line = CreateMapEntry(depotPath, _client.Name, MapType.Include); if (c.ViewMap == null) { c.ViewMap = new ViewMap(); } if (!c.ViewMap.Contains(line)) { // check to see if c.ViewMap.Add(line); } return UpdateClient(c); } public Client RemoveFromClient(string depotPath) { Client updated = null; Client c = GetClient(_client.Name); var entry = CreateMapEntry(depotPath, _client.Name, MapType.Include); var changed = false; if (c.ViewMap != null && c.ViewMap.Count > 0) { if (c.ViewMap.Contains(entry)) { 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) { 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) { var left = new DepotPath(depotPath); var clientPath = string.Format("//{0}/{1}", clientName, 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(); var client = repo.GetClient(name); if (client != null) { var options = new DeleteFilesCmdOptions(DeleteFilesCmdFlags.None, -1); repo.DeleteClient(client, options); } } /// <summary> /// CurrentClient property /// </summary> public Client CurrentClient { get { return _client; } set { _client = value; SetClient(_client.Name) ; } } public void SetClient(string name) { 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) { var change = new Changelist(); change.OwnerName = _username; change.ClientId = CurrentClient.Name; change.Description = description; var repo = GetRepository(); change = repo.CreateChangelist(change); return change; } /// <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) { 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); } return repo.GetChangelist(id, opts); } public IList<Changelist> GetAllPendingChangelists() { var repo = GetRepository(); var options = new ChangesCmdOptions(ChangesCmdFlags.None, CurrentClient.Name, 0, ChangeListStatus.Pending, Username); return repo.GetChangelists(options, null); } public Changelist GetCurrentPendingChangelist(bool shelved=false) { Changelist current = null; var repo = GetRepository(); var options = new ChangesCmdOptions(ChangesCmdFlags.None, CurrentClient.Name, 1, ChangeListStatus.Pending, Username); 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); } return current; } public void DeletePendingChangeList() { var change = GetCurrentPendingChangelist(); if (change != null) { var repo = GetRepository(); var options = new Options(); repo.DeleteChangelist(change, options); } } // 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() { 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.GetFileMetaData(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.GetFileMetaData(f.DepotPath.Path); if (!System.IO.File.Exists(md.LocalPath.Path)) { filesToRevert.Add(f.DepotPath.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.GetFileMetaData(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() { IList<FileSpec> reopenedFiles = null; var repo = GetRepository(); var currentChange = GetOrCreatePendingChangelist(); var openedSpecs = new List<FileSpec>(); openedSpecs.Add(GetFileSpec("//...")); var openedOptions = new GetOpenedFilesOptions(GetOpenedFilesCmdFlags.None, null, CurrentClient.Name, Username, 0); var list = GetRepository().GetOpenedFiles(openedSpecs, openedOptions); var reopenSpecs = new List<FileSpec>(); foreach (var i in list) { if (i.ChangeId != currentChange.Id) { reopenSpecs.Add(new FileSpec(i.DepotPath)); } } if (reopenSpecs.Count > 0) { var options = new ReopenCmdOptions(currentChange.Id, null); reopenedFiles = repo.Connection.Client.ReopenFiles(reopenSpecs, options); } return reopenedFiles; } public IList<FileSpec> MoveFilesToNewChangelist(IList<string> files, int changeId=0) { 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(f))); } else { fileSpecs.Add(new FileSpec(new ClientPath(f))); } } return repo.Connection.Client.ReopenFiles(fileSpecs, options); } /// <summary> /// Deletes the changelist /// </summary> /// <param name="id">The changelist Id number to delete</param> public void DeleteChangelist(int id) { 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) { var repo = GetRepository(); var options = new ChangeCmdOptions(ChangeCmdFlags.Delete); repo.DeleteChangelist(change, options); } public IList<Changelist> ListChanges(string depotPath) { var repo = GetRepository(); if (IsDirectory(depotPath) && !depotPath.EndsWith("/...")) { depotPath = depotPath + "/..."; } var spec = GetFileSpec(depotPath); var options = new ChangesCmdOptions(ChangesCmdFlags.FullDescription, null, 50, ChangeListStatus.Submitted, null); return repo.GetChangelists(options, spec); } public ResultsWrapper SubmitSingleFile(string depotPath, string description) { var change = CreateChangelist(description); var reopenOptions = new ReopenCmdOptions(change.Id, null); var spec = GetFileSpec(depotPath); GetRepository().Connection.Client.ReopenFiles(reopenOptions, spec); return SubmitChangelist(change.Id, description, revertUnchanged: true); } public ResultsWrapper SubmitChangelist(int id, string description=null, bool revertUnchanged=true) { 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) { 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 { 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) { // 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) { 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) { // 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); return GetRepository().Connection.Client.RevertFiles(options, fileSpec); } public Changelist GetOrCreatePendingChangelist() { var change = GetCurrentPendingChangelist(); if (change == null) { change = CreateChangelist(Constants.GENERATED_CHANGELIST_DESCRIPTION); } return change; } #endregion #region FILE OPERATIONS public bool PathExists(string depotPath) { var exists = false; // first check to see if the path is an existing file var specs = new List<FileSpec>(); specs.Add(GetFileSpec(depotPath)); var fileOptions = new GetDepotFilesCmdOptions(GetDepotFilesCmdFlags.NotDeleted, 0); try { var fileResults = GetRepository().GetDepotFiles(specs, fileOptions); if (fileResults != null && fileResults.Count == 1) { exists = true; } } catch (P4Exception p4e) { // expected exception if the file does not exist } // if the file does not exist, check to see if it is a directory if (!exists) { var dirOptions = new GetDepotDirsCmdOptions(GetDepotDirsCmdFlags.None, null); try { var dirList = new List<string>(); dirList.Add(depotPath); var dirResults = GetRepository().GetDepotDirs(dirList, dirOptions); if (dirResults != null && dirResults.Count == 1) { exists = true; } } catch (P4Exception p4e) { // expected exception if dir does not exist } } return exists; } public bool IsDirectoryMapped(string depotDir) { // normalize directory depotDir = depotDir.TrimEnd('/'); var list = new List<string>(); list.Add(depotDir); var response = GetClientMappings(list, true); return response.Keys.Contains(depotDir); } public bool IsPathInClientView(string depotPath) { 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; } //** TODO: need to figure out why this is being called with invalid paths **// public Dictionary<string, string> GetClientMappings(IList<string> paths, bool pathsAreDirectories=false) { var mappings = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase); if (paths != null && paths.Count > 0) { var fileArr = new List<string>(); foreach (var path in paths) { var p = path; if (pathsAreDirectories) { 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 left = Utility.NormalizeDepotPath(obj["depotFile"].ToString()); var right = Utility.NormalizeClientPath(obj["path"].ToString()); mappings.Add(left, right); } } } } } catch (Exception ex) { // caught exception, probably because client mappings do not exist. } } return mappings; } public P4CommandResult WhereResults(params string[] paths) { if (paths == null) return null; var args = new List<string>(); foreach (var p in paths) { args.Add(p); } var cmd = new P4Command(GetRepository(), "where", true, args.ToArray()); return cmd.Run(); } public Dictionary<string, string> GetDepotMappings(IList<string> paths, bool pathsAreDirectories = false) { var mappings = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase); 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 ex) { // caught exception, probably because client mappings do not exist } if (results != null) { foreach (var r in results) { mappings.Add(Utility.NormalizeClientPath(r.LocalPath.Path), r.DepotPath.Path); } } } return mappings; } public string PredictDepotPath(string clientPath) { var depotPath = string.Empty; var root = GetConnection().Client.Root; if (root != null) { depotPath = clientPath.Replace(root, "/").Replace("\\", "/"); } return depotPath; } public string PredictClientPath(string depotPath) { var clientPath = string.Empty; var root = GetConnection().Client.Root; if (root != null) { clientPath = Path.Combine(root, depotPath.TrimStart('/').Replace('/', '\\')); } return clientPath; } public FileSpec GetFileSpec(string path) { if(path.StartsWith("//")) { return new FileSpec(new DepotPath(path)); } else { return new FileSpec(new ClientPath(path)); } } public FileSpec[] GetFileSpecs(string[] paths) { var specs = new FileSpec[paths.Count()]; for (var i = 0; i < paths.Count(); i++) { specs[i] = GetFileSpec(paths[i]); } return specs; } /// <summary> /// Synchronizes the workspace with the depot /// </summary> /// <param name="serverOnly">Performs server-side (db.have) sync</param> /// <param name="force">Performs a forced synchronization</param> /// <param name="preview">Preview but do not sync</param> /// <returns></returns> public IList<FileSpec> SyncWorkspaceFiles(VersionSpec versionSpec = null, bool force = false, bool serverOnly = false, bool preview = false, bool notifyOnError = false) { return SyncWorkspacePath("//...", versionSpec: versionSpec, force: force, serverOnly: serverOnly, preview: preview, notifyOnError: notifyOnError); } public IList<FileSpec> SyncWorkspacePath(string path, VersionSpec versionSpec = null, bool force = false, bool serverOnly = false, bool preview = false, bool notifyOnError = false) { SyncFilesCmdOptions options = null; if (preview && force && serverOnly) { options = new SyncFilesCmdOptions((SyncFilesCmdFlags.Preview | SyncFilesCmdFlags.Force | SyncFilesCmdFlags.ServerOnly), -1); } else if (preview && serverOnly && !force) { options = new SyncFilesCmdOptions(SyncFilesCmdFlags.Preview | SyncFilesCmdFlags.ServerOnly, -1); } else if (force && serverOnly && !preview) { options = new SyncFilesCmdOptions(SyncFilesCmdFlags.Force | SyncFilesCmdFlags.ServerOnly, -1); } else if (force && preview && !serverOnly) { options = new SyncFilesCmdOptions(SyncFilesCmdFlags.Force | SyncFilesCmdFlags.Preview, -1); } else if (force && !serverOnly && !preview) { options = new SyncFilesCmdOptions(SyncFilesCmdFlags.Force, -1); } else if (preview && !serverOnly && !force) { options = new SyncFilesCmdOptions(SyncFilesCmdFlags.Preview, -1); } else if (serverOnly && !force && !preview) { options = new SyncFilesCmdOptions(SyncFilesCmdFlags.ServerOnly, -1); } else { options = new SyncFilesCmdOptions(SyncFilesCmdFlags.None, -1); } FileSpec pathSpec = null; if (path != null) { pathSpec = new FileSpec(new DepotPath(path), versionSpec); } IList<FileSpec> results = null; try { results = GetConnection().Client.SyncFiles(options, pathSpec); } catch (P4Exception ex) { // sync errors could be displayed to user, but for now they are just silently ignored // unless 'notifyOnError' is set if (notifyOnError) { UIHelper.ShowMessage(string.Format("Error synchronizing from server:\n\n{0}", ex.Message)); } } return results; } /// <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) { var change = GetOrCreatePendingChangelist(); var specs = GetFileSpecs(paths); var options = new AddFilesCmdOptions(AddFilesCmdFlags.KeepWildcards, change.Id, null); return GetConnection().Client.AddFiles(options, specs); } /// <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) { var change = GetOrCreatePendingChangelist(); var specs = GetFileSpecs(paths); var flags = EditFilesCmdFlags.None; if (serverOnly) { flags = EditFilesCmdFlags.ServerOnly; } var options = new EditCmdOptions(flags, change.Id, null); var client = GetConnection().Client; return client.EditFiles(options, specs); } /// <summary> /// /// </summary> /// <param name="sourcePath"></param> /// <param name="destPath"></param> /// <returns></returns> public IList<FileSpec> RenameFile(string sourcePath, string destPath, bool serverOnly=false) { IList<FileSpec> moved = null; var md = GetFileMetaData(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 = GetFileMetaData(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 = GetFileSpec(sourcePath); var destFileSpec = GetFileSpec(destPath); var flags = MoveFileCmdFlags.None; if (serverOnly) { flags = MoveFileCmdFlags.ServerOnly; } var options = new MoveCmdOptions(flags, change.Id, null); moved = GetConnection().Client.MoveFiles(sourceFileSpec, destFileSpec, options); return moved; } public IList<FileSpec> RenameFolder(string sourceDir, string destDir, bool serverOnly = false) { IList<FileSpec> moved = null; // 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 = GetFileSpec(sourceDirWC); var destFileSpec = GetFileSpec(destDirWC); CheckoutFiles(serverOnly: true, paths: sourceDirWC); var flags = MoveFileCmdFlags.None; if (serverOnly) { flags = MoveFileCmdFlags.ServerOnly; } var options = new MoveCmdOptions(flags, change.Id, null); moved = GetConnection().Client.MoveFiles(sourceFileSpec, destFileSpec, options); return moved; } public IList<FileSpec> CopyFile(string sourcePath, string destPath, bool serverOnly = false) { IList<FileSpec> copied = null; var md = GetFileMetaData(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 = GetFileMetaData(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 = GetFileSpec(sourcePath); var destFileSpecList = new List<FileSpec>(); destFileSpecList.Add(GetFileSpec(destPath)); var flags = CopyFilesCmdFlags.None; if(serverOnly) { flags = CopyFilesCmdFlags.Virtual; } var options = new CopyFilesCmdOptions(flags, null, null, null, change.Id, 0); copied = GetConnection().Client.CopyFiles(sourceFileSpec, destFileSpecList, options); return copied; } /// <summary> /// /// </summary> /// <param name="depotPath"></param> /// <returns></returns> public IList<FileSpec> DeleteFiles(bool serverOnly = false, params string[] paths) { var change = GetOrCreatePendingChangelist(); var specs = GetFileSpecs(paths); var flags = DeleteFilesCmdFlags.None; if (serverOnly) { flags = DeleteFilesCmdFlags.ServerOnly; } var options = new DeleteFilesCmdOptions(flags, change.Id); var results = GetConnection().Client.DeleteFiles(options, specs); return results; } public IList<FileSpec> RevertFiles(bool serverOnly = false, params string[] paths) { var change = GetOrCreatePendingChangelist(); var specs = GetFileSpecs(paths); var flags = RevertFilesCmdFlags.None; if (serverOnly) { flags = RevertFilesCmdFlags.ServerOnly; } var options = new RevertCmdOptions(flags, change.Id); return GetConnection().Client.RevertFiles(options, specs); } public P4CommandResult ReconcileFiles(params string[] paths) { if (paths == null) return null; var change = GetOrCreatePendingChangelist(); var args = new List<string>(); args.Add("-ead"); args.Add("-c"); args.Add(change.Id.ToString()); foreach (var p in paths) { args.Add(p); } var cmd = new P4Command(GetRepository(), "reconcile", true, args.ToArray()); return cmd.Run(); } public IList<FileSpec> ShelveFiles(params string[] paths) { var change = GetOrCreatePendingChangelist(); var specs = GetFileSpecs(paths); var options = new ShelveFilesCmdOptions(ShelveFilesCmdFlags.Force, null, change.Id); var results = GetConnection().Client.ShelveFiles(options, specs); return results; } public IList<FileSpec> DeleteShelvedFiles(int changeId = 0, params string[] paths) { if (changeId == 0) { var change = GetOrCreatePendingChangelist(); changeId = change.Id; } var specs = GetFileSpecs(paths); var options = new ShelveFilesCmdOptions(ShelveFilesCmdFlags.Delete, null, changeId); return GetConnection().Client.ShelveFiles(options, specs); } public List<int> GetShelvedLocations(string depotPath) { 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) { 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) { var change = GetCurrentPendingChangelist(); if (changeId == 0) { changeId = change.Id; } var specs = GetFileSpecs(paths); var options = new UnshelveFilesCmdOptions(UnshelveFilesCmdFlags.Force, changeId, change.Id); var results = GetConnection().Client.UnshelveFiles(options, specs); return results; } public bool IsFileOpened(string depotPath) { var opened = false; if (!string.IsNullOrEmpty(depotPath)) { var spec = GetFileSpec(depotPath); var specs = new List<FileSpec>(); specs.Add(spec); 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; } } } } return opened; } public bool PathHasAnyOpened(string depotPath) { var anyOpened = false; if (!string.IsNullOrEmpty(depotPath)) { var results = GetAllOpened(depotPath); 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 depotPath, int max=0) { var fileSpecs = new List<FileSpec>(); fileSpecs.Add(GetFileSpec(depotPath)); var options = new GetOpenedFilesOptions(GetOpenedFilesCmdFlags.AllClients, null, null, null, max); return GetRepository().GetOpenedFiles(fileSpecs, options); } public FileMetaData GetFileMetaData(PathSpec path, int revision, int maxItems = 0) { FileMetaData md = null; var filespec = new FileSpec(path, new Revision(revision)); var flags = GetFileMetadataCmdFlags.Attributes | GetFileMetadataCmdFlags.FileSize | GetFileMetadataCmdFlags.HexAttributes; var options = new GetFileMetaDataCmdOptions(flags, null, null, maxItems, null, null, null); var metaDataList = GetRepository().GetFileMetaData(options, filespec); if (metaDataList != null && metaDataList.Count > 0) { md = metaDataList[0]; } return md; } public FileMetaData GetFileMetaData(string path, string attrFilter = null, bool allVersions = false, int maxItems = 0) { 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) { 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; } // figure out if this is a depot path or client path // if it starts with //, assume a depot path; client path otherwise if(path.StartsWith("//")) { specs.Add(new FileSpec(new DepotPath(path), null)); } else { specs.Add(new FileSpec(new ClientPath(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 (Exception ex) { // unable to get metadata -- can happen when bad specs are passed. need to handle gracefully (rather than crashing) } return results; } public IList<FileMetaData> GetSearchFileMetaData(List<string> paths, bool mappedOnly = false) { GetFileMetadataCmdFlags flags = GetFileMetadataCmdFlags.Attributes | GetFileMetadataCmdFlags.HexAttributes; IList<FileSpec> specs = new List<FileSpec>(); foreach(var p in paths) { var fs = new FileSpec(new DepotPath(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 depotPath) { SizeData data = null; if (IsDirectory(depotPath) && !depotPath.EndsWith("/...")) { depotPath = depotPath.TrimEnd('/') + "/..."; } var args = new List<string>(); args.Add("-s"); args.Add(depotPath); 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) { 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(new DepotPath(depotPath), VersionSpec.None); } else { spec = new FileSpec(new DepotPath(depotPath), VersionSpec.Head); } var results = GetRepository().GetFileContents(options, spec); } } return filePath; } public string GetDirectoryFromServer(string depotPath, string targetDir = null) { 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 = GetFileSpec(depotPath + "/..."); var filesOptions = new FilesCmdOptions(FilesCmdFlags.None, 0); 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 = GetFileSpec(fileDepotPath); var results = GetRepository().GetFileContents(getFileContentsOptions, fileSpec); } } return targetDir; } public string GetFileFromShelf(string depotPath, int changeId = -1) { 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(new DepotPath(depotPath), shelf); var results = GetRepository().GetFileContents(options, spec); } } return filePath; } public IList<FileHistory> GetFileHistory(string path) { var flags = GetFileHistoryCmdFlags.IncludeInherited|GetFileHistoryCmdFlags.FullDescription; var fileSpec = GetFileSpec(path); var options = new GetFileHistoryCmdOptions(flags, 0, 0); return GetRepository().GetFileHistory(options, fileSpec); } public SubmitResults RollbackFileToRevision(string path, int revision) { var fileSpec = FileSpec.DepotSpec(path, revision); 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.DepotSpec(path)); if (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); GetConnection().Client.SyncFiles(syncOptions, fileSpec); // edit the file var editOptions = new EditCmdOptions(EditFilesCmdFlags.None, change.Id, null); GetConnection().Client.EditFiles(editOptions, FileSpec.DepotSpec(path)); // sync the head revision of the file GetConnection().Client.SyncFiles(syncOptions, FileSpec.DepotSpec(path)); // resolve the file (ay) var resolveOptions = new ResolveCmdOptions(ResolveFilesCmdFlags.AutomaticYoursMode, change.Id); GetConnection().Client.ResolveFiles(resolveOptions, fileSpec); // 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 ex) { var deleteChangelistOptions = new ChangeCmdOptions(ChangeCmdFlags.None); GetRepository().DeleteChangelist(change, deleteChangelistOptions); return null; } } public SubmitResults RollbackFolderToChangelist(string path, int changeId) { // 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(new DepotPath(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); var p4cmdResults = p4cmd.Run(); if (p4cmdResults.TaggedOutput != null) { foreach (TaggedObject obj in p4cmdResults.TaggedOutput) { string depotFile = null; var res = obj.TryGetValue("depotFile", out depotFile); if (depotFile != null) { var depotFileSpec = GetFileSpec(depotFile); if (obj.ContainsKey("action")) { var action = obj["action"].ToString(); if (action.Equals("deleted")) { deletedList.Add(depotFileSpec); } else if (action.Equals("added")) { addedList.Add(depotFileSpec); } else if (action.Equals("updated")) { updatedList.Add(depotFileSpec); } } } } } // sync to the desired changelist var syncOptions1 = new SyncFilesCmdOptions(SyncFilesCmdFlags.Force, 0); var syncResults1 = GetConnection().Client.SyncFiles(syncOptions1, fileSpec); if (updatedList.Count > 0) { // edit the files in the path var editOptions = new EditCmdOptions(EditFilesCmdFlags.None, change.Id, null); var editResults = GetConnection().Client.EditFiles(editOptions, updatedList.ToArray()); // sync the head revision of the file var syncResults2 = GetConnection().Client.SyncFiles(syncOptions1, updatedList.ToArray()); // resolve the file (ay) var resolveOptions = new ResolveCmdOptions(ResolveFilesCmdFlags.AutomaticYoursMode, change.Id); var resolveResults = GetConnection().Client.ResolveFiles(resolveOptions, updatedList.ToArray()); } if (addedList.Count > 0) { // find any deleted items that need to be re-added with the -d (downgrade) flag var addOptions = new AddFilesCmdOptions(AddFilesCmdFlags.Downgrade | AddFilesCmdFlags.KeepWildcards, change.Id, null); var results = GetConnection().Client.AddFiles(addOptions, addedList.ToArray()); } if (deletedList.Count > 0) { // delete any items that aren't supposed to be there var deleteOptions = new DeleteFilesCmdOptions(DeleteFilesCmdFlags.DeleteUnsynced, change.Id); var results = GetConnection().Client.DeleteFiles(deleteOptions, deletedList.ToArray()); } // submit the changes var clientSubmitOptions = new ClientSubmitOptions(); clientSubmitOptions.SubmitType = SubmitType.SubmitUnchanged; var submitOptions = new SubmitCmdOptions(SubmitFilesCmdFlags.None, change.Id, null, null, clientSubmitOptions); try { 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); } } #endregion #region CONSTRUCTORS public PerforceHelper(string serverUri, string username) { _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); } } 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; } #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 | 15071 | alan_petersen |
Populate -o //guest/perforce_software/piper/... //guest/alan_petersen/piper/.... |
||
//guest/perforce_software/piper/windows/main/Perforce/Helper/PerforceHelper.cs | |||||
#3 | 13571 | alan_petersen | Lots 'o bug fixes | ||
#2 | 12025 | alan_petersen |
**** MEGA UPDATE **** 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) 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) reg script to add URL handler UPDATE: - updated P4.NET api to latest release ### Bug fixes: ### #84: DEFECT: submit single item should just have item checked in the current changelist - reworked the submit logic to take selected item(s) and move them to a new changelist. - failed submission restores files to original changelist and the new changelist is deleted #110: DEFECT: submit fails if changelist has shelved items - now that items are moved to a new changelist and then that is submitted, shelved items do not cause an issue. - shelved items are not automatically deleted -- they will need to be removed from the shelf manually #111: DEFECT: submit changelist should sort with checked items on top - added CompareTo to the ChangelistFile class to allow comparison for sorting #112: DEFECT: application will not open login dialog if server check fails - reworked helper creation logic to create most helpers when the application first starts - fixed bug in which cached Repository object was retrieved (rather than the new Repo) ### Bug fixes: ### #85: DEFECT: submit error not useful - submit errors now list files that could not be submitted (and caused submit failure) #113: DEFECT: watcher does not restart if submit cancelled - cancel button was not associated with any operations (it just made the dialog go away). Now cancel calls Utility.StartBackgroundProcesses(). UPDATE: - combined syncworker and refreshworker into 1 background thread, rather than 2 separate threads - UI refresh now occurs immediately after sync - removed refreshworker class, settings, etc. ### Bug fixes: ### #114: Defect: Deleted items are not unsynced from workspace - fixed two issues: - sync worker was not starting correctly - sync / refresh being on different schedules was causing some strange behavior UPDATE: - modified CheckInButtonEnabled logic in ToolBarViewModel to enable the button if a valid client is configured. - previous version had more complex logic and cached data, but refresh was slow and this is now simpler. ### Bug fixes: ### #25: Defect: Ability to check in part of the move operation - modified checkbox click logic to see if file action is move/add or move/delete and if it is, an fstat is executed to get the source (MovedFile) and then the associated line in the table is checked/unchecked. This will prevent a user from deselecting part of a move operation #107: Defect: Unsync from computer not working - modified 'unsync from workspace' logic to use exclude mapping if directory has a parent directory that is mapped - modified code that gets client mappings for depot paths to use the raw 'p4 where' command rather than the API -- the API results don't let you distinguish between directories that are mapped and those that are excluded. The p4 where command tagged output includes an extra field 'unmap' for paths that are excluded. this is probably a bug in the API #116: DEFECT: submit reports changelist -1 if no files submitted - modified logic in command helper to only display the changelist ID if it is greater than 0 (i.e. files were submitted) #117: Defect: Desi application crashes on delete folder - modified logic in the CommandHelper delete method to check to see if any files are openeed in the directory path, and if so the delete is prevented and a message dialog is displayed indicating that the folder could not be deleted. #49: Defect: Desi throws exception when internet connection is lost - modified the unhandled exception method in App.xaml.cs to give the user the option to quit or reopen the application - modified the GetRepository logic in PerforceHelper to avoid the try/catch and keep the cached repo (for performance) UPDATE: - modified CommandHelper.RevertFiles to stop / start the background processes (sync, watchdog) to hopefully prevent watchdog from seeing files appear in the workspace - modified watchdog with slight delay and then check to see if file exists when issuing a create (p4 add) ### Bug fixes: ### #82: DEFECT: strange add/remove behavior with Microsoft Excel - was able to replicate, added delay to logic in watchdog and a check to see if the created file actually exists -- these temporary excel files appear and disappear quickly -- the delay can be increased if files still appear in DESI #85: DEFECT: submit error not useful - modified submit logic in CommandHelper to display the error message if the details are empty (this happens sometimes, apparently) #115: Defect: File watchdog does not register deletes and moves from the explorer - modified watchdog code a little to try to catch deletes and moves better - files with .tmp extension were completely ignored, which could cause issues if there are actual files with .tmp extensions #118: Defect: search does not return result if keywords are from two different source (filename + content) - modified search query building logic to match that used by mac DESI #119: Defect : Checkout folders should be deleted - modified logic used when sending a folder to the trash to ignore files marked for add when running 'p4 opened' #83: DEFECT: folder history should have button to show files in changelist - added new window to display changelist information, specifically the description, modification date, and files (with actions) in the changelist. - currently, this opens up as a modal window (dialog) that can be resized, etc. I thought that it was better than a fixed window attached to the main DESI window. #60: Defect: Search should be restricted to current folder - added new toggle to the 'more options' section that gets populated if the user has a folder selected in the Workspace or Server tabs when s/he performs a search. #49: Defect: Desi throws exception when internet connection is lost - Modified logic in App.xaml.cs to do the following when an unhandled exception is caught: - stops background processes (to prevent double dialog seen in latest reported error) - uses the DialogManager to display dialog aligned at the top of the window, giving the user the option of restarting or quitting the application #118: Defect: search does not return result if keywords are from two different source (filename + content) - Modified logic in SearchHelper.cs to format the search terms with wildcards (*) before and after each term #60: Defect: Search should be restricted to current folder - modified logic to keep selected folder if the current view is on the search screen #120: Defect : Adding Same File with add button - Changed add logic in ToolBar.xaml.cs to use the Utility.CopyFileToDirectory method to copy files to the destination directory, adding "(Copy n)" to the filenames if a file with that name already exists in the destination #83: DEFECT: folder history should have button to show files in changelist - Modified logic executed when clicking the rollback button in the FolderHistory.xaml.cs file to display a wait dialog when rolling back, which will help prevent the perception of the application "locking up" #101: DEFECT: Add button not working for favorite - modified AddFile and AddFolder toolbar button logic to enable the buttons if the favorite points to a workspace folder (as opposed to a server folder) #102: DEFECT: Favorite pointing to folder on server tab should have server background - modified FavoritesView.xaml to display background contingent on favorite item's source (workspace or server) - re-worked FavoritesViewModel as datacontext for FavoritesView - added server check logic to FavoritesViewModel #95: DEFECT: Positioning of add tag popup - Due to differences in popup positioning between Win7 and Win8, the only positioning that seemed to be consistent was "center" on the parent position. I modified the Sidebar.xaml to position the Add Tag popup relative to the + button. #86: DEFECT: workspace thumbnails - updated ListingItemInfoViewModel.cs to try to generate png thumbnail of file if the file's action is Edit, Add or MoveAdd, which might indicate that the file has changed. #93: Defect: Bread crumbs cuts off if the path is too long - modified ControlTemplate in BreadcrumbBar.xaml to include a thin (5px) horizontal scrollbar on the bottom UPDATE: Added new logging facility (available via the View menu). Currently, logging is set to "ALL" (which means "everything") but for release this will be set to INFO or WARN. The user can change the log level by opening the log window and selecting the log level from the combo box at the top. The Clear button clears the log, and the Save button allows the user to save the log to a file ### Bug fixes: ### #100: DEFECT: Copy/paste results in Windows "Cannot create directory" error - unable to replicate, added a new logging facility (Log window under the View menu) to provide more information #105: Defect: Object reference error pops up intermittently - unclear when this is happening... cleaned up the one place #49: Defect: Desi throws exception when internet connection is lost - Updated exception handler in App.xaml.cs to stop background processes and display integrated dialog (rather than standalone dialog) UPDATE: - update to logic regarding switching workspaces - update to breadcrumb logic to display breadcrumbs more accurately when the selection is changed ### Bug fixes: ### #121: DESI Screen not get updated - changing the workspace now clears the workspace grid and deselects the workspace selector, if it is already selected #121: DESI Screen not get updated - two null references were not being caught, resulting in application crashes under specific circumstances. these have now been fixed! - updated workspace selection logic to clear out old values (that was resulting in old data still present when the workspace is switched) #122: Drag/Drop Folder not working - modified logic for drag&drop to more intelligently handle a dropped item UPDATE: - modified sync button functionality - reports number of changes synchronized from server - waits for 20 seconds (or until the user clicks the OK button) ### Bug fixes: ### #123: Defect :Files Deleted is also displaying in Recently added - modified count of files during sync so this should be fine now #124: DEFECT: add button does not work when creating favorite tag - removed the add button (since it is superfluous anyway) and so users can just hit enter after typing in the tag name UPDATE: - modified "LOCKED BY" text in button to be left aligned ### Bug fixes: ### #125 DEFECT: submitting single moved item should also select corresponding deleted item - modified the SubmitFile method in CommandHelper to check to see if the file's action is MoveAdd, and if so, then to get the MovedFile attribute from the metadata (the corresponding delete) and select that too, so the submit dialog will have both items selected. #126 DEFECT: Licensing/version info - created 'About' menu under the 'Help' menu - added licenses to Resources/Licenses - added version information (derived from the project version data) #127 DEFECT: Toolbar items need tooltips - tooltips now available on buttons. note that tooltips are only visible for enabled buttons #128 DEFECT: Unversioned files show strange & useless modification timestamp + version number - modified ListingItemInfoViewModel.cs ModifiedDate method to return the string ---- if the timestamp is zero - modified ListingItemInfoViewModel.cs, adding a Revision property and having the method return {none} if the revision is not greater than zero. #129 DEFECT: Application window should have minimum size - MainWindow now has attributes MinHeight="400" MinWidth="550" which restrict how small the window can be made #130 DEFECT: Date format should be DD.MM.YYYY or YYYY-MM-DD, but not MM.DD.YYYY - modified ListingItemInfoViewModel.cs ModifiedDate method to return a formatted string, rather than having WPF format the date - modified VersionInfo.xaml to format the "CHECKED IN" field using the string formatter "yyyy/MM/dd HH:mm:ss" - modified VersionItem.cs to ensure that the date returned is expressed as local time (time is now retrieved from the metadata instead of the file history to ensure that it matches TZ-wise) #131 DEFECT: When entering Submit message, pressing <ENTER> starts submission instead of creating a new line - added AcceptsReturn="True" AcceptsTab="True" to the Changelist description field in the SubmitForm.xaml dialog #132 DEFECT: In menu bar there is no "Help" item to link to any help page or show support contact information - added help menu - TODO: need URL for help system #133 DEFECT: Tags names do not display underscore - modified tag display in ListingItemInfo.xaml to use TextBlock instead of Label UPDATE: added copyright and cleaned up usings removed unused method UPDATE: - modified favorites, tags, and breadcrumbs to use TextBox instead of Label (dealing with the deleted underscore issue) - removed unused BreadcrumbLabel user control ### Bug fixes: ### #124 DEFECT: add button does not work when creating favorite tag - modified tag display to be TextBox instead of label - modified positioning of add popup (again!) #127 DEFECT: Toolbar items need tooltips - added ToolTipService.ShowOnDisabled="true" to each toolbar button to get around strange issue where enabling/disabling button resulted in blank tooltips. Probably a good idea to have the tooltips always visible anyway (rather than only when buttons are enabled) #134 Defect : Breadcrumbs should also show the underscore sign. - updated to use TextBox instead of Label #130 DEFECT: Date format should be DD.MM.YYYY or YYYY-MM-DD, but not MM.DD.YYYY - modified FolderHistory.xaml to use ContentStringFormat="yyyy/MM/dd HH:mm:ss" for the "CHECKED IN" date format, which should provide consistency across the application now. UPDATE: - fixed error in which changelist could get orphaned if all files submitted were reverted ### Bug fixes: ### #136 Defect: Submit all folder option missing - added menu item "Submit all files" for mapped folders - added handler code for menu item - added CommandHelper SubmitFolder method to submit all files contained in a folder. ** NOTE: this needs to iterate over all files in the changelist, so for a changelist with many files, there could be a delay. #135 Defect: Keyboard navigation refinement - updated keyboard navigation code to provide more "smooth" navigation through the application #135 Defect: File watchdog does not monitor folder deletions - fixed copy/paste typo that existed in the folder deletion code that would cause the system to miss folders that are deleted. UPDATE: Adding code for msi installer generation Fixing dist script to deal with Debug or Release distros Changes to installer definition to create Desktop shortcut - fixes log window reopening that caused an error ### Bug fixes: ### #141 DEFECT: Adding files with strange characters causes crash - modified Add command in PerforceHelper to include the AddFilesCmdFlags.KeepWildcards option UPDATE: - fixed issue in which application would crash if URL supplied and the user was not logged in - added CleanChangelist method to PerforceHelper to cycle through list of changelist files to ensure that all files marked for add still exist on the local filesystem (they may have been deleted since they were added) - workspace watcher logic revamped to remove delays - removed shutdown of watchdog to prevent situations in which file modifications were missed - context menu text updated to "selected files" instead of "all files" ### Bug fixes: ### #151 Defect: On Submission fail, Files are becoming read only - logic now calls the CleanChangelist method before bringing up the submit dialog #145 Defect: Refresh on long folder paths leads to auto selection of random folders - refresh logic revisited -- refresh of a panel has been reworked to proceed one column at a time, synchronously. #149 Defect: Option to submit selected items from pending list and trash list is not available - context menu added for submission of files from Pending and Trash UPDATE: - modified MainWindow.cs and Utility.cs to call CleanChangelist when workspace is first selected - modified CleanChangelist in PerforceHelper.cs to do the following: - for each file in the changelist: - if action is Add, revert if local file no longer exists - if action is Move/Add, revert file and delete original file - if action is Edit, revert the edit and delete ### Bug fixes: ### #149 Defect: Option to submit selected items from pending list and trash list is not available - fixed typo (the font was really small!!) - fixed casting bug that caused Null exception #108 DEFECT? - reconcile on submit - added CleanChangelist method to perform "reconcile" options -- reconcile itself cannot detect when files are added or edited and then deleted locally #139 Defect: Rollback on folder does not delete files - updated PerforceHelper.cs to delete files correctly when a folder is rolled back #153: Defect : Move Action should get default selected if one get selected - fixed typo in context menu - updated SubmitFiles method in ContextMenuHelper.cs to do an fstat on files being submitted and select associated files if the action is MoveAdd or MoveDelete #49 Defect: Desi throws exception when internet connection is lost - updated PerforceHelper to test connection (with 5 second timeout) every time it is requested. #154 Defect: Weird behavior with folders with different upper case are mapped - updated various aspects of the code to make string comparisons case insensitive. #155 Defect : Cannot rename the Favorite Folder name - updated xaml to rearrange elements to make edit box visible again. UPDATE: - added Shanghai and Tokyo to drop down list - replaced IP addresses with hostnames - modified dist.cmd to create installer and copy to dist\Release directory if Release is the specified distribution ### Bug fixes: ### #147 Defect: Installer should add shortcut to start menu - updated Wix configuration to include start menu shortcut #129 DEFECT: Application window should have minimum size - sidebar now has auto-appearing 5-pixel wide scrollbar if the window height is not enough to accommodate the number of things in the sidebar UPDATE: - enlarged submit dialog (cannot make it resizable) - made submit datagrid columns resizable and sortable - added help URL to help menu (opens in default web browser) ### Bug fixes: ### #157 DEFECT: orphaned changelists - application now gathers changelist files when it starts - menu item in File menu to gather changelist files on demand UPDATE: - removed automatic "Gathering changes" functionality ### Bug fixes: ### #158 DEFECT: p4 urls open up new DESI instances - modified App.xml.cs to look for existing DESI processes and to set focus to the existing process and exit if there is already one running - added the P4Listener.cs helper class to create a named pipe to listen for p4:// URLs - modified App.xml.cs to send the URL to the named pipe if it is passed on the command line |
||
#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. |