From 8b0984d6c19dad819e02d351772c9a39257ebd4c Mon Sep 17 00:00:00 2001 From: Groboclown Date: Tue, 11 Aug 2015 08:37:21 -0500 Subject: [PATCH] Many fixes across the system, mostly to account for bug fixes in IntelliJ 15, but also to fix some lingering issues that those IntelliJ bugs were masking. --- CHANGES.md | 9 + .../mapbased/rpc/sys/RpcPerforceFile.java | 580 +++++++++--------- plugin/META-INF/plugin.xml | 23 +- .../groboclown/idea/p4ic/P4Bundle.properties | 1 + .../p4ic/actions/P4RollbackHistoryAction.java | 87 +++ .../idea/p4ic/changes/ChangeListSync.java | 542 +++++++--------- .../p4ic/changes/P4ChangeListMapping.java | 2 +- .../p4ic/config/UserProjectPreferences.java | 12 + .../idea/p4ic/server/tasks/AddCopyRunner.java | 84 ++- .../idea/p4ic/server/tasks/MoveRunner.java | 12 +- 10 files changed, 690 insertions(+), 662 deletions(-) create mode 100644 plugin/src/net/groboclown/idea/p4ic/actions/P4RollbackHistoryAction.java diff --git a/CHANGES.md b/CHANGES.md index afd73961..a5c2225d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,12 +5,21 @@ ### Overview +* Added user preference (not available through UI yet) that allows selecting whether a copy + command means an integrate. +* Started work on adding in the rollback on history view. * Bug fixes. ### Details +* Added user preference (not available through UI yet) that allows selecting whether a copy + command means an integrate. + * UI will be available by v0.7. * Bug fixes. * Move operations could not find the Perforce version of the locally moved file (#62). + This also affected the copy operation. + * Rewrote how the IDEA changelists sync up with the Perforce changelists. It should now + be more accurate and eliminate some of the null mappings that occurred before. ## ::v0.6.5:: diff --git a/p4java/src/com/perforce/p4java/impl/mapbased/rpc/sys/RpcPerforceFile.java b/p4java/src/com/perforce/p4java/impl/mapbased/rpc/sys/RpcPerforceFile.java index 8c8ac1c7..119d1c2a 100644 --- a/p4java/src/com/perforce/p4java/impl/mapbased/rpc/sys/RpcPerforceFile.java +++ b/p4java/src/com/perforce/p4java/impl/mapbased/rpc/sys/RpcPerforceFile.java @@ -1,290 +1,290 @@ -/* - * Copyright 2009 Perforce Software Inc., All Rights Reserved. - */ -package com.perforce.p4java.impl.mapbased.rpc.sys; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.channels.FileChannel; - -import com.perforce.p4java.Log; -import com.perforce.p4java.exception.NullPointerError; -import com.perforce.p4java.impl.generic.client.ClientLineEnding; -import com.perforce.p4java.impl.generic.sys.ISystemFileCommandsHelper; -import com.perforce.p4java.impl.mapbased.rpc.sys.helper.SysFileHelperBridge; - -/** - * Encapsulates and implements a lot of Perforce-specific information - * and operations on Perforce client-side files by extending the basic - * java.io.File class with Perforce-specific fields and methods. - * - * - */ - -public class RpcPerforceFile extends File { - - public static String TRACE_PREFIX = "RpcPerforceFile"; - - public static String systemTmpDirName = null; - - public static final String TMP_FILE_PFX = "p4j"; - public static final String TMP_FILE_SFX = ".tmp"; - - private static final long serialVersionUID = 1L; - - private RpcPerforceFileType fileType = null; - private ClientLineEnding lineEnding = null; - - - public static String createTempFileName(String tmpDirName) { - - // Kinda cheating, really... - - File tmpDir = null; - - try { - if (tmpDirName != null) { - tmpDir = new File(tmpDirName); - } - - File tmpFile = File.createTempFile(TMP_FILE_PFX, TMP_FILE_SFX, - tmpDir); - - return tmpFile.getPath(); - } catch (IOException ioexc) { - Log.error( - "Unable to create temporary file: " + ioexc.getLocalizedMessage()); - } - - return null; - } - - public RpcPerforceFile(String fileName, String fileTypeStr) { - super(fileName); - if (fileName == null) { - // We don't need or want (much less expect) null paths in the API: - throw new NullPointerError( - "Null file name passed to RpcPerforceFile constructor"); - } - - this.fileType = RpcPerforceFileType.decodeFromServerString(fileTypeStr); - this.lineEnding = ClientLineEnding.decodeFromServerString( - fileTypeStr, this.fileType); - } - - public RpcPerforceFile(String fileName, RpcPerforceFileType fileType) { - super(fileName); - if (fileName == null) { - // We don't need or want (much less expect) null paths in the API: - throw new NullPointerError( - "Null file name passed to RpcPerforceFile constructor"); - } - - this.fileType = fileType; - this.lineEnding = ClientLineEnding.FST_L_LOCAL; - } - - /** - * Our "special" version of rename, intended to cope with the - * cases when the normal rename won't work (typically cross-device - * renames) or when we need to do some under-the-covers stitching - * up (for example, GKZIP stream decoding). - */ - - public boolean renameTo(File targetFile) { - return renameTo(targetFile, false); - } - - /** - * Another special version of renameTo to support RPC implementation- - * specific needs. This one allows callers to specify whether to - * always copy as-is (no munging). - */ - public boolean renameTo(File targetFile, boolean alwaysCopyUnMunged) { - if (targetFile == null) { - throw new NullPointerError( - "Null target file in RpcPerforceFile.renameTo"); - } - try { - if ((this.fileType == null) || alwaysCopyUnMunged || canCopyAsIs()) { - if (super.renameTo(targetFile)) { - return true; - } else { - return copyTo(targetFile); - } - } else { - // !canCopyAsIs... - - return decodeTo(targetFile); - } - } catch (IOException ioexc) { - Log.error("Unexpected problem with renaming / copying file '" - + targetFile.getName() + "': " + ioexc.getLocalizedMessage()); - Log.exception(ioexc); - return false; - } - } - - private boolean setWritable(String filePath) { - boolean writable = false; - ISystemFileCommandsHelper helper = SysFileHelperBridge.getSysFileCommands(); - if( helper != null) { - writable = helper.setWritable(filePath, true); - } - return writable; - } - - /** - * Copy this file to another (target file). Assumes no - * decoding necessary. If the target file exists, - * it's removed before copying. - */ - - public boolean copyTo(File targetFile) throws IOException { - - if (targetFile == null) { - throw new NullPointerError( - "Null target file in RpcPerforceFile.copyTo"); - } - - if (targetFile.exists()) { - if (!targetFile.delete()) { - // Attempt to make the file writable if it isn't deleted, - // continue even if it fails as an exception will be thrown by - // the output stream - if (!targetFile.canWrite()) { - if(setWritable(targetFile.getAbsolutePath())) { - targetFile.delete(); - } - } - - //Warn if file still exists - if (targetFile.exists()) { - // FIXME: cope better with delete fail -- HR. - Log - .warn("Unable to delete target file for copy in RpcPerforceFile.copyTo; target: '" - + targetFile.getPath()); - } - } - } - FileInputStream inStream = null; - FileOutputStream outStream = null; - FileChannel sourceChannel = null; - FileChannel targetChannel = null; - try { - long bytesTransferred = 0; - - inStream = new FileInputStream(this); - outStream = new FileOutputStream(targetFile); - sourceChannel = inStream.getChannel(); - targetChannel = outStream.getChannel(); - - if ((sourceChannel != null) && (targetChannel != null)) { - // Light fuse, stand back... - - bytesTransferred = sourceChannel.transferTo( - 0, sourceChannel.size(), targetChannel); - - if (bytesTransferred != sourceChannel.size()) { - Log.error("channel copy for copyTo operation failed with fewer bytes" - + " transferred than expected; expected: " + sourceChannel.size() - + "; saw: " + bytesTransferred); - return false; // FIXME: clean up... - } - - return true; - } - } finally { - try { - if (sourceChannel != null) sourceChannel.close(); - } catch (Exception exc) { - Log.warn("source channel file close error in RpcPerforceFile.copyTo(): " - + exc.getLocalizedMessage()); - Log.exception(exc); - } - try{ - if (targetChannel != null) targetChannel.close(); - } catch (Exception exc) { - Log.warn("target channel file close error in RpcPerforceFile.copyTo(): " - + exc.getLocalizedMessage()); - Log.exception(exc); - } - try { - if (inStream != null) inStream.close(); - } catch (Exception exc) { - Log.warn("instream file close error in RpcPerforceFile.copyTo(): " - + exc.getLocalizedMessage()); - Log.exception(exc); - } try { - if (outStream != null) outStream.close(); - } catch (Exception exc) { - Log.warn("outstream file close error in RpcPerforceFile.copyTo(): " - + exc.getLocalizedMessage()); - Log.exception(exc); - } - } - - return false; - } - - public boolean decodeTo(File targetFile) throws IOException { - - if (targetFile == null) { - throw new NullPointerError( - "Null target file in RpcPerforceFile.decodeTo"); - } - - return copyTo(targetFile); - } - - public RpcPerforceFileType getFileType() { - return this.fileType; - } - - public void setFileType(RpcPerforceFileType fileType) { - this.fileType = fileType; - } - - public ClientLineEnding getLineEnding() { - return lineEnding; - } - - public void setLineEnding(ClientLineEnding lineEnding) { - this.lineEnding = lineEnding; - } - - /** - * True IFF we should be able to copy this file as-is, i.e. without - * GKZIP decoding or munging, etc. Currently all file types can - * be copied as-is, but this wasn't always true and may not always - * be true... - */ - public boolean canCopyAsIs() { - return true; - } - - /** - * @see File#equals() - */ - @Override - public boolean equals(Object obj) { - if ((obj != null) && (obj instanceof RpcPerforceFile)) { - if ((super.equals((File)obj)) && - (((RpcPerforceFile)obj).getFileType() == this.fileType) && - (((RpcPerforceFile)obj).getLineEnding() == this.lineEnding)) { - return true; - } - } - return false; - } - - /** - * @see File#hashCode() - */ - @Override - public int hashCode() { - return super.hashCode(); - } -} +/* + * Copyright 2009 Perforce Software Inc., All Rights Reserved. + */ +package com.perforce.p4java.impl.mapbased.rpc.sys; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; + +import com.perforce.p4java.Log; +import com.perforce.p4java.exception.NullPointerError; +import com.perforce.p4java.impl.generic.client.ClientLineEnding; +import com.perforce.p4java.impl.generic.sys.ISystemFileCommandsHelper; +import com.perforce.p4java.impl.mapbased.rpc.sys.helper.SysFileHelperBridge; + +/** + * Encapsulates and implements a lot of Perforce-specific information + * and operations on Perforce client-side files by extending the basic + * java.io.File class with Perforce-specific fields and methods. + * + * + */ + +public class RpcPerforceFile extends File { + + public static String TRACE_PREFIX = "RpcPerforceFile"; + + public static String systemTmpDirName = null; + + public static final String TMP_FILE_PFX = "p4j"; + public static final String TMP_FILE_SFX = ".tmp"; + + private static final long serialVersionUID = 1L; + + private RpcPerforceFileType fileType = null; + private ClientLineEnding lineEnding = null; + + + public static String createTempFileName(String tmpDirName) { + + // Kinda cheating, really... + + File tmpDir = null; + + try { + if (tmpDirName != null) { + tmpDir = new File(tmpDirName); + } + + File tmpFile = File.createTempFile(TMP_FILE_PFX, TMP_FILE_SFX, + tmpDir); + + return tmpFile.getPath(); + } catch (IOException ioexc) { + Log.error( + "Unable to create temporary file: " + ioexc.getLocalizedMessage()); + } + + return null; + } + + public RpcPerforceFile(String fileName, String fileTypeStr) { + super(fileName); + if (fileName == null) { + // We don't need or want (much less expect) null paths in the API: + throw new NullPointerError( + "Null file name passed to RpcPerforceFile constructor"); + } + + this.fileType = RpcPerforceFileType.decodeFromServerString(fileTypeStr); + this.lineEnding = ClientLineEnding.decodeFromServerString( + fileTypeStr, this.fileType); + } + + public RpcPerforceFile(String fileName, RpcPerforceFileType fileType) { + super(fileName); + if (fileName == null) { + // We don't need or want (much less expect) null paths in the API: + throw new NullPointerError( + "Null file name passed to RpcPerforceFile constructor"); + } + + this.fileType = fileType; + this.lineEnding = ClientLineEnding.FST_L_LOCAL; + } + + /** + * Our "special" version of rename, intended to cope with the + * cases when the normal rename won't work (typically cross-device + * renames) or when we need to do some under-the-covers stitching + * up (for example, GKZIP stream decoding). + */ + + public boolean renameTo(File targetFile) { + return renameTo(targetFile, false); + } + + /** + * Another special version of renameTo to support RPC implementation- + * specific needs. This one allows callers to specify whether to + * always copy as-is (no munging). + */ + public boolean renameTo(File targetFile, boolean alwaysCopyUnMunged) { + if (targetFile == null) { + throw new NullPointerError( + "Null target file in RpcPerforceFile.renameTo"); + } + try { + if ((this.fileType == null) || alwaysCopyUnMunged || canCopyAsIs()) { + if (super.renameTo(targetFile)) { + return true; + } else { + return copyTo(targetFile); + } + } else { + // !canCopyAsIs... + + return decodeTo(targetFile); + } + } catch (IOException ioexc) { + Log.error("Unexpected problem with renaming / copying file '" + + targetFile.getName() + "': " + ioexc.getLocalizedMessage()); + Log.exception(ioexc); + return false; + } + } + + private boolean setWritable(String filePath) { + boolean writable = false; + ISystemFileCommandsHelper helper = SysFileHelperBridge.getSysFileCommands(); + if( helper != null) { + writable = helper.setWritable(filePath, true); + } + return writable; + } + + /** + * Copy this file to another (target file). Assumes no + * decoding necessary. If the target file exists, + * it's removed before copying. + */ + + public boolean copyTo(File targetFile) throws IOException { + + if (targetFile == null) { + throw new NullPointerError( + "Null target file in RpcPerforceFile.copyTo"); + } + + if (targetFile.exists()) { + if (!targetFile.delete()) { + // Attempt to make the file writable if it isn't deleted, + // continue even if it fails as an exception will be thrown by + // the output stream + if (!targetFile.canWrite()) { + if(setWritable(targetFile.getAbsolutePath())) { + targetFile.delete(); + } + } + + //Warn if file still exists + if (targetFile.exists()) { + // FIXME: cope better with delete fail -- HR. + Log + .warn("Unable to delete target file for copy in RpcPerforceFile.copyTo; target: '" + + targetFile.getPath()); + } + } + } + FileInputStream inStream = null; + FileOutputStream outStream = null; + FileChannel sourceChannel = null; + FileChannel targetChannel = null; + try { + long bytesTransferred = 0; + + inStream = new FileInputStream(this); + outStream = new FileOutputStream(targetFile); + sourceChannel = inStream.getChannel(); + targetChannel = outStream.getChannel(); + + if ((sourceChannel != null) && (targetChannel != null)) { + // Light fuse, stand back... + + bytesTransferred = sourceChannel.transferTo( + 0, sourceChannel.size(), targetChannel); + + if (bytesTransferred != sourceChannel.size()) { + Log.error("channel copy for copyTo operation failed with fewer bytes" + + " transferred than expected; expected: " + sourceChannel.size() + + "; saw: " + bytesTransferred); + return false; // FIXME: clean up... + } + + return true; + } + } finally { + try { + if (sourceChannel != null) sourceChannel.close(); + } catch (Exception exc) { + Log.warn("source channel file close error in RpcPerforceFile.copyTo(): " + + exc.getLocalizedMessage()); + Log.exception(exc); + } + try{ + if (targetChannel != null) targetChannel.close(); + } catch (Exception exc) { + Log.warn("target channel file close error in RpcPerforceFile.copyTo(): " + + exc.getLocalizedMessage()); + Log.exception(exc); + } + try { + if (inStream != null) inStream.close(); + } catch (Exception exc) { + Log.warn("instream file close error in RpcPerforceFile.copyTo(): " + + exc.getLocalizedMessage()); + Log.exception(exc); + } try { + if (outStream != null) outStream.close(); + } catch (Exception exc) { + Log.warn("outstream file close error in RpcPerforceFile.copyTo(): " + + exc.getLocalizedMessage()); + Log.exception(exc); + } + } + + return false; + } + + public boolean decodeTo(File targetFile) throws IOException { + + if (targetFile == null) { + throw new NullPointerError( + "Null target file in RpcPerforceFile.decodeTo"); + } + + return copyTo(targetFile); + } + + public RpcPerforceFileType getFileType() { + return this.fileType; + } + + public void setFileType(RpcPerforceFileType fileType) { + this.fileType = fileType; + } + + public ClientLineEnding getLineEnding() { + return lineEnding; + } + + public void setLineEnding(ClientLineEnding lineEnding) { + this.lineEnding = lineEnding; + } + + /** + * True IFF we should be able to copy this file as-is, i.e. without + * GKZIP decoding or munging, etc. Currently all file types can + * be copied as-is, but this wasn't always true and may not always + * be true... + */ + public boolean canCopyAsIs() { + return true; + } + + /** + * @see File#equals() + */ + @Override + public boolean equals(Object obj) { + if ((obj != null) && (obj instanceof RpcPerforceFile)) { + if ((super.equals((File)obj)) && + (((RpcPerforceFile)obj).getFileType() == this.fileType) && + (((RpcPerforceFile)obj).getLineEnding() == this.lineEnding)) { + return true; + } + } + return false; + } + + /** + * @see File#hashCode() + */ + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/plugin/META-INF/plugin.xml b/plugin/META-INF/plugin.xml index 6b6bddf1..c52b390e 100644 --- a/plugin/META-INF/plugin.xml +++ b/plugin/META-INF/plugin.xml @@ -5,10 +5,11 @@ VCS Integration +
  1. 0.6.6
      -
    1. Fixed issues around moving files not matching to the Perforce files.
    2. +
    3. Fixed issues around moving or copying files not matching to the Perforce files.
    4. +
    5. Rewrote how the IDEA changelists sync up with the Perforce changelists.
  2. 0.6.5 @@ -31,7 +32,7 @@

    Limitations:

    -
      +
      • Does not indicate to the user whether a file is synchronized to the head revision or not.
      • No repository browsing.
      • @@ -91,6 +92,22 @@ + + + + + Configuration file {0} for user {1} failed to connect\: {2} configuration.error.problem-list=Encountered {0} problems\: {1} +exception.rollback.history=Incorrect setup for rollback diff --git a/plugin/src/net/groboclown/idea/p4ic/actions/P4RollbackHistoryAction.java b/plugin/src/net/groboclown/idea/p4ic/actions/P4RollbackHistoryAction.java new file mode 100644 index 00000000..8aaf191a --- /dev/null +++ b/plugin/src/net/groboclown/idea/p4ic/actions/P4RollbackHistoryAction.java @@ -0,0 +1,87 @@ +/* ************************************************************************* + * (c) Copyright 2015 Zilliant Inc. All rights reserved. * + * ************************************************************************* + * * + * THIS MATERIAL IS PROVIDED "AS IS." ZILLIANT INC. DISCLAIMS ALL * + * WARRANTIES OF ANY KIND WITH REGARD TO THIS MATERIAL, INCLUDING, * + * BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF NONINFRINGEMENT, * + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. * + * * + * Zilliant Inc. shall not be liable for errors contained herein * + * or for incidental or consequential damages in connection with the * + * furnishing, performance, or use of this material. * + * * + * Zilliant Inc. assumes no responsibility for the use or reliability * + * of interconnected equipment that is not furnished by Zilliant Inc, * + * or the use of Zilliant software with such equipment. * + * * + * This document or software contains trade secrets of Zilliant Inc. as * + * well as proprietary information which is protected by copyright. * + * All rights are reserved. No part of this document or software may be * + * photocopied, reproduced, modified or translated to another language * + * prior written consent of Zilliant Inc. * + * * + * ANY USE OF THIS SOFTWARE IS SUBJECT TO THE TERMS AND CONDITIONS * + * OF A SEPARATE LICENSE AGREEMENT. * + * * + * The information contained herein has been prepared by Zilliant Inc. * + * solely for use by Zilliant Inc., its employees, agents and customers. * + * Dissemination of the information and/or concepts contained herein to * + * other parties is prohibited without the prior written consent of * + * Zilliant Inc.. * + * * + * (c) Copyright 2015 Zilliant Inc. All rights reserved. * + * * + * *************************************************************************/ + +package net.groboclown.idea.p4ic.actions; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vfs.VirtualFile; +import net.groboclown.idea.p4ic.P4Bundle; +import net.groboclown.idea.p4ic.extension.P4Vcs; +import net.groboclown.idea.p4ic.server.exceptions.P4ApiException; +import net.groboclown.idea.p4ic.server.exceptions.P4InvalidConfigException; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; + +public class P4RollbackHistoryAction extends BasicAction { + private static final Logger LOG = Logger.getInstance(P4RollbackHistoryAction.class); + public static final String ACTION_NAME = "Rollback"; + + @Override + protected void perform(@NotNull final Project project, @NotNull final P4Vcs vcs, + @NotNull final List exceptions, + @NotNull final List affectedFiles) { + if (affectedFiles.isEmpty()) { + return; + } + if (affectedFiles.size() > 1) { + exceptions.add(new P4ApiException(P4Bundle.message("exception.rollback.history"))); + return; + } + LOG.info("perform rollback for " + affectedFiles); + + // TODO get the head revision and the rollback revision. + exceptions.add(new P4InvalidConfigException("not implemented yet")); + } + + @NotNull + @Override + protected String getActionName() { + return ACTION_NAME; + } + + @Override + protected boolean isEnabled(@NotNull final Project project, @NotNull final P4Vcs vcs, + @NotNull final VirtualFile... vFiles) { + // TODO return false if this is the latest version. + LOG.info("is enabled for " + Arrays.asList(vFiles)); + + return true; + } +} diff --git a/plugin/src/net/groboclown/idea/p4ic/changes/ChangeListSync.java b/plugin/src/net/groboclown/idea/p4ic/changes/ChangeListSync.java index c3bbe84a..2b33c449 100644 --- a/plugin/src/net/groboclown/idea/p4ic/changes/ChangeListSync.java +++ b/plugin/src/net/groboclown/idea/p4ic/changes/ChangeListSync.java @@ -21,6 +21,7 @@ import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.*; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.vcsUtil.VcsUtil; import net.groboclown.idea.p4ic.config.Client; import net.groboclown.idea.p4ic.extension.P4Vcs; import net.groboclown.idea.p4ic.history.P4ContentRevision; @@ -62,7 +63,7 @@ public ChangeListSync(@NotNull final P4Vcs vcs) { /** * Refreshes the changes * - * @param builder data access and storage + * @param builder data access and storage * @param progress progress bar * @throws VcsException */ @@ -70,24 +71,28 @@ public void syncChanges(@NotNull VcsDirtyScope dirtyScope, @NotNull ChangeListBu @NotNull final ChangeListManagerGate addGate, @NotNull ProgressIndicator progress) throws VcsException { LOG.info("start changelist refresh"); - // Pull in all the changes from Perforce that are within the dirty scope, into - // the builder. - progress.setIndeterminate(false); progress.setFraction(0.0); - Set filePaths = dirtyScope.getDirtyFiles(); + // 1. Find all the perforce files check-out state and changelist mappings. + // 2. Create or remove IDEA changelists as needed. + // 3. Put the perforce files into idea changes, while comparing them to the + // dirty list (to see if something should be marked as dirty that isn't). + // 4. All remaining dirty files need to be correctly filed. - // Strip out non-files from the dirty files - Iterator iter = filePaths.iterator(); - while (iter.hasNext()) { - FilePath next = iter.next(); - if (next.isDirectory()) { - iter.remove(); - } - } - progress.setFraction(0.1); + // Step 1: find the perforce files and changelists + + // - the reload cache reloads the changelists and the files. + final Map> pendingChangelists = + P4ChangeListCache.getInstance().reloadCachesFor(vcs.getClients()); + LOG.debug("pending changelists: " + pendingChangelists); + progress.setFraction(0.2); + final Map> known = vcs.getChangeListMapping().cleanMappings(); + LOG.debug("known changelist mappings: " + known); + + progress.setFraction(0.5); + /* reports data to ChangelistBuilder using the following methods: @@ -102,45 +107,19 @@ public void syncChanges(@NotNull VcsDirtyScope dirtyScope, @NotNull ChangeListBu */ - final Map> pendingChangelists = - P4ChangeListCache.getInstance().reloadCachesFor(vcs.getClients()); - - final Map> known = vcs.getChangeListMapping().cleanMappings(); - LOG.debug("pending changelists: " + pendingChangelists); - LOG.debug("known changelists: " + known); - - progress.setFraction(0.2); - - SubProgressIndicator sub = new SubProgressIndicator(progress, 0.2, 0.5); - loadUnmappedP4ChangeListsToExistingIdea(addGate, pendingChangelists, known, sub); - - progress.setFraction(0.5); + // Step 2 & 3: + progress.setFraction(0.55); + SubProgressIndicator sub = new SubProgressIndicator(progress, 0.55, 0.9); + Set remainingFiles = loadP4IntoIdeaChangelists(addGate, pendingChangelists, known, sub, dirtyScope, + builder); - // Update the files - List clients = vcs.getClients(); - double clientIndex = 0.0; - for (Client client : vcs.getClients()) { - sub = new SubProgressIndicator(progress, - 0.5 + 0.5 * (clientIndex / (double) clients.size()), - 0.5 + 0.5 * ((clientIndex + 1.0) / (double) clients.size())); - clientIndex += 1.0; - if (client.isWorkingOnline()) { - List files = moveDirtyFilesIntoIdeaChangeLists(client, builder, - sub, getFilesUnderClient(client, filePaths)); - sub.setFraction(0.9); - moveP4FilesIntoIdeaChangeLists(client, builder, files); - for (P4FileInfo f : files) { - filePaths.remove(f.getPath()); - ensureOnlyIn(builder, client, f); - } - } else { - LOG.info("not refreshing changelists for " + client + ": working offline"); - } - sub.setFraction(1.0); - } + // Step 4: + // Pull in all the changes from Perforce that are within the dirty scope, into + // the builder. + progress.setFraction(0.9); // Process the remaining dirty files - for (FilePath fp : filePaths) { + for (FilePath fp : remainingFiles) { VirtualFile vf = fp.getVirtualFile(); if (vf != null) { if (!vf.isDirectory()) { @@ -154,53 +133,18 @@ public void syncChanges(@NotNull VcsDirtyScope dirtyScope, @NotNull ChangeListBu } } - /** - * Ensure every file in an idea changelist is in the correct - * associated Perforce changelist. (bug #22 comment 3) - * There's a situation that can occur where a file has 2 different - * changes, each split across multiple change lists. IDEA supports - * this, but Perforce doesn't. - * - * @param client client - * @param f file - */ - private void ensureOnlyIn(@NotNull ChangeListBuilderCache data, @NotNull final Client client, - @NotNull final P4FileInfo f) { - File file = f.getPath().getIOFile(); - LocalChangeList actualLocalChange = - vcs.getChangeListMapping().getLocalChangelist(client, f.getChangelist()); - if (actualLocalChange != null) { - List allIdeaChangeLists = ChangeListManager.getInstance(vcs.getProject()).getChangeLists(); - for (LocalChangeList lcl : allIdeaChangeLists) { - if (!lcl.equals(actualLocalChange)) { - for (Change change : lcl.getChanges()) { - if (change.affectsFile(file)) { - LOG.info("moving to correct changelist (was double-filed): " + f); - data.processChange(change, actualLocalChange); - } - } - } - } - } - } - + private Set getDirtyFiles(final VcsDirtyScope dirtyScope) { + Set filePaths = dirtyScope.getDirtyFiles(); - private List getFilesUnderClient(Client client, Collection files) { - List ret = new ArrayList(files.size()); - for (FilePath file : files) { - try { - for (FilePath root : client.getFilePathRoots()) { - if (file.isUnder(root, false)) { - ret.add(file); - } - } - } catch (P4InvalidConfigException e) { - // the thrower properly makes the call to the - // config error listener - LOG.info(e); + // Strip out non-files from the dirty files + Iterator iter = filePaths.iterator(); + while (iter.hasNext()) { + FilePath next = iter.next(); + if (next.isDirectory()) { + iter.remove(); } } - return ret; + return filePaths; } /** @@ -208,128 +152,210 @@ private List getFilesUnderClient(Client client, Collection f * changelist, map it to an existing IDEA changelist that isn't * mapped to a Perforce changelist. There are a number of criteria * to pass here. + *

        + * When returned, the "known" mapping will be filled with all known p4 changelists. * * @param addGate gate * @param pendingChangelists all pending changelists on all server configs. * @param known the current mappings of IDEA changes to p4 changes (must be kept up to date) * @param prog progress bar + * @param dirtyScope dirty files + * @param builder changelist builder * @throws VcsException */ - private void loadUnmappedP4ChangeListsToExistingIdea( + @NotNull + private Set loadP4IntoIdeaChangelists( @NotNull final ChangeListManagerGate addGate, @NotNull Map> pendingChangelists, @NotNull Map> known, - @NotNull SubProgressIndicator prog) + @NotNull SubProgressIndicator prog, final VcsDirtyScope dirtyScope, final ChangeListBuilderCache builder) throws VcsException { - List allIdeaChangeLists = ChangeListManager.getInstance(vcs.getProject()).getChangeLists(); - + Set dirtyFiles = getDirtyFiles(dirtyScope); prog.setFraction(0.1); + Set allIdeaChangeLists = new HashSet( + ChangeListManager.getInstance(vcs.getProject()).getChangeLists()); + prog.setFraction(0.2); + - Set associatedP4clIds = new HashSet(); - for (Map map : known.values()) { - for (Map.Entry en : map.entrySet()) { - // It's possible to have a client map to a null changelist. - P4ChangeList cl = en.getValue(); - if (cl != null) { - associatedP4clIds.add(cl.getId()); - LOG.debug("mapped client " + en.getKey() + " to changelist " + cl.getId()); - } else { - LOG.debug("mapped client " + en.getKey() + " to null changelist"); + // FIXME set the progress. + + Map fileInfoMap = new HashMap(); + Map filesToChanges = new HashMap(); + Map> changesToFiles = new HashMap>(); + for (Map.Entry> en : pendingChangelists.entrySet()) { + final Client client = en.getKey(); + for (P4ChangeList p4Change : en.getValue()) { + LocalChangeList local = findLocalChangeList(addGate, client, p4Change, known, allIdeaChangeLists); + for (P4FileInfo fileInfo : p4Change.getFiles()) { + final FilePath path = fileInfo.getPath(); + fileInfoMap.put(path, fileInfo); + if (dirtyFiles.contains(path)) { + dirtyFiles.remove(path); + } else { + VcsUtil.markFileAsDirty(project, path); + } + filesToChanges.put(path.getIOFile(), local); + if (!changesToFiles.containsKey(local)) { + changesToFiles.put(local, new HashSet()); + } + changesToFiles.get(local).add(path); } } } - prog.setFraction(0.2); - - // For each p4 pending changelist that isn't already associated with an - // IDEA changelist, find a match to any IDEA changelist that doesn't - // already have a p4 changelist for that specific client. - - // However, the outer loop is the matchers. This prevents aggressive - // consumption of changes - allow the more exact matchers to match the - // changelists before a looser one tries to match. - - double matcherIndex = 0.0; - for (ClMatcher matcher : CL_MATCHERS) { - prog.setFraction(0.2 + 0.7 * (matcherIndex / (double) CL_MATCHERS.length)); - matcherIndex += 1.0; - - for (Map.Entry> pending : pendingChangelists.entrySet()) { - final Client client = pending.getKey(); - final List unusedIdeaChangeLists = - filterOutUsedChangelists(allIdeaChangeLists, known, client); - for (P4ChangeList p4cl : pending.getValue()) { - if (associatedP4clIds.contains(p4cl.getId())) { - continue; + for (Entry> changeListSetEntry : changesToFiles.entrySet()) { + final LocalChangeList local = changeListSetEntry.getKey(); + final Set fileSet = changeListSetEntry.getValue(); + + // Ensure all the existing changes belong in the changelist. + changeLoop: + for (Change change : local.getChanges()) { + final ContentRevision[] revs = + new ContentRevision[]{change.getBeforeRevision(), change.getAfterRevision()}; + final List revFileList = new ArrayList(); + for (ContentRevision rev : revs) { + if (rev != null) { + revFileList.add(rev.getFile()); + } + } + for (ContentRevision revision : revs) { + if (revision != null) { + final FilePath revFile = revision.getFile(); + final LocalChangeList revChangeList = filesToChanges.get(revFile.getIOFile()); + if (revChangeList == null) { + // don't know what to do with this file. It's not a Perforce managed file, but it's + // in an IDEA changelist. + final Client client = vcs.getClientFor(revFile); + if (client != null) { + // The file is under Perforce control, is in an IDEA changelist, but is + // not marked by Perforce as being open. + //openFile(change, revision, client); + //builder.processChange(change, local); + if (revFile.getVirtualFile() == null) { + builder.processLocallyDeletedFile(revFile); + } else { + builder.processModifiedWithoutCheckout(revFile.getVirtualFile()); + } + fileSet.removeAll(revFileList); + continue changeLoop; + } else { + // Leave the file in the changelist + LOG.info("Non-perforce file " + revFile + " in perforce controlled IDEA change"); + } + } else if (!local.equals(revChangeList)) { + // wrongly filed change. + builder.processChange(change, revChangeList); + final Set revChangeFiles = changesToFiles.get(revChangeList); + if (revChangeFiles != null) { + revChangeFiles.removeAll(revFileList); + } // else error; should always be non-null + continue changeLoop; + } else { + // mark the change as being in the right place. + builder.processChange(change, local); + fileSet.removeAll(revFileList); + continue changeLoop; + } } + } + } + } - if (p4cl.getId().isDefaultChangelist()) { - // special default changelist handling. The IDEA default - // named changelist should always exist to correspond to the - // Perforce default changelist. Note that this is - // independent of the matchers. + // All the remaining files in the lists are ones that don't have existing changes. + // We can't have this as part of the above loop, because that must process all existing + // changes first. + for (Entry> changeListSetEntry : changesToFiles.entrySet()) { + final LocalChangeList local = changeListSetEntry.getKey(); + final Set fileSet = changeListSetEntry.getValue(); - LOG.debug("Associating " + client.getClientName() + " default changelist to IDEA changelist " + - P4ChangeListMapping.DEFAULT_CHANGE_NAME); - addGate.findOrCreateList(P4ChangeListMapping.DEFAULT_CHANGE_NAME, ""); - associatedP4clIds.add(p4cl.getId()); + for (FilePath path : fileSet) { + builder.processChange(createChange(fileInfoMap.get(path)), local); + } + } - // The mapping from default to default is implicit in the - // P4ChangeListMappingNew class. - } else { - // it's not associated with any IDEA changelist. - LocalChangeList match = matcher.match(p4cl, splitNameComment(p4cl), unusedIdeaChangeLists); + return dirtyFiles; + } - if (match != null) { - LOG.debug("Associating " + p4cl.getId() + " to IDEA changelist " + match); + @NotNull + private LocalChangeList findLocalChangeList(@NotNull final ChangeListManagerGate addGate, + @NotNull final Client client, @NotNull final P4ChangeList p4Change, + @NotNull final Map> knownChanges, + @NotNull final Set allChangeLists) { + + // allChangeLists is a set, so that we don't accidentally add duplicates, which would just + // end up wasting time. + + final P4ChangeListId id = p4Change.getId(); + // Must reuse the existing all changelists, because one changelist can have multiple mappings + List unmatched = new ArrayList(allChangeLists); + for (Map.Entry> entry : knownChanges.entrySet()) { + if (entry.getKey() == null) { + LOG.error("Internal error: null IDEA changelist mapped to perforce changes"); + continue; + } - // Ensure the name matches the changelist, with a unique name - setUniqueName(match, p4cl, allIdeaChangeLists); + final Map clientChangeMapping = entry.getValue(); + final P4ChangeList p4cl = clientChangeMapping.get(client); + if (p4cl != null) { + if (id.getChangeListId() == p4cl.getId().getChangeListId()) { + return entry.getKey(); + } - // Record this new association - associatedP4clIds.add(p4cl.getId()); - if (known.containsKey(match)) { - known.get(match).put(client, p4cl); - } else { - Map km = new HashMap(); - km.put(client, p4cl); - known.put(match, km); - } - vcs.getChangeListMapping().bindChangelists(match, p4cl.getId()); - } - } + // only one client can be associated with this changelist, and we just found our client's mapping + unmatched.remove(entry.getKey()); + break; + } + } + + if (id.isDefaultChangelist()) { + // default changelist for this client has not yet been mapped + final LocalChangeList ret = addGate.findOrCreateList(P4ChangeListMapping.DEFAULT_CHANGE_NAME, ""); + if (ret == null) { + LOG.error("Internal API error: findOrCreateList returned null"); + } else { + if (!allChangeLists.contains(ret)) { + allChangeLists.add(ret); + } + if (!knownChanges.containsKey(ret)) { + knownChanges.put(ret, new HashMap()); } + knownChanges.get(ret).put(client, p4Change); + return ret; } } - prog.setFraction(0.9); + // attempt to match the change against existing IDEA changelists + for (ClMatcher matcher : CL_MATCHERS) { + final LocalChangeList match = matcher.match(p4Change, splitNameComment(p4Change), unmatched); + if (match != null) { + LOG.debug("Associating " + id + " to IDEA changelist " + match); - // All the remaining changelists need to be mapped to a new changelist. - for (Map.Entry> en : pendingChangelists.entrySet()) { - for (List pendingChanges : pendingChangelists.values()) { - for (P4ChangeList p4cl : pendingChanges) { - if (!associatedP4clIds.contains(p4cl.getId())) { - LocalChangeList lcl = createUniqueChangeList(addGate, p4cl, allIdeaChangeLists); - vcs.getChangeListMapping().bindChangelists(lcl, p4cl.getId()); - if (known.containsKey(lcl)) { - // shouldn't happen, but just for completion - known.get(lcl).put(en.getKey(), p4cl); - } else { - Map km = new HashMap(); - km.put(en.getKey(), p4cl); - known.put(lcl, km); - } - } + // Ensure the name matches the changelist, with a unique name + setUniqueName(match, p4Change, allChangeLists); + + // Record this new association + if (!knownChanges.containsKey(match)) { + knownChanges.put(match, new HashMap()); } + knownChanges.get(match).put(client, p4Change); + vcs.getChangeListMapping().bindChangelists(match, id); + return match; } } - prog.setFraction(1.0); + // Create a new changelist + LocalChangeList lcl = createUniqueChangeList(addGate, p4Change, allChangeLists); + if (!knownChanges.containsKey(lcl)) { + knownChanges.put(lcl, new HashMap()); + } + knownChanges.get(lcl).put(client, p4Change); + vcs.getChangeListMapping().bindChangelists(lcl, id); + return lcl; } // This matches with createUniqueChangeList private void setUniqueName(LocalChangeList changeList, P4ChangeList cls, - List existingIdeaChangeLists) { + Collection existingIdeaChangeLists) { String[] desc = splitNameComment(cls); LOG.debug("Mapped @" + cls.getId() + " to " + changeList.getName()); String name = desc[0]; @@ -353,8 +379,9 @@ private void setUniqueName(LocalChangeList changeList, P4ChangeList cls, } // This matches with setUniqueName + @NotNull private LocalChangeList createUniqueChangeList(@NotNull final ChangeListManagerGate addGate, - @NotNull P4ChangeList cls, @NotNull List allIdeaChangeLists) { + @NotNull P4ChangeList cls, @NotNull Collection allIdeaChangeLists) { String[] desc = splitNameComment(cls); String baseDesc = desc[0]; if (baseDesc == null || baseDesc.length() <= 0) { @@ -384,161 +411,6 @@ private LocalChangeList createUniqueChangeList(@NotNull final ChangeListManagerG } } - private List filterOutUsedChangelists( - List allIdeaChangeLists, - Map> known, - Client client) { - List ret = new ArrayList(allIdeaChangeLists); - for (Map.Entry> e : known.entrySet()) { - if (e.getValue().containsKey(client)) { - ret.remove(e.getKey()); - } - } - Iterator iter = ret.iterator(); - while (iter.hasNext()) { - if (P4ChangeListMapping.isDefaultChangelist(iter.next())) { - iter.remove(); - } - } - return ret; - } - - - /** - * Put dirty IDEA files into correct IDEA changelists, as per the dirty file's associated Perforce changelist. - * - * @param client server client owning the files - * @param data changelist sync data - * @param progress progress bar - * @param filePaths files to move - * @return files processed - * @throws VcsException - */ - private List moveDirtyFilesIntoIdeaChangeLists( - @NotNull Client client, @NotNull ChangeListBuilderCache data, - @NotNull ProgressIndicator progress, @NotNull Collection filePaths) throws VcsException { - LOG.debug("processing incoming files " + new ArrayList(filePaths)); - final List files = client.getServer().getFilePathInfo(filePaths); - progress.setFraction(0.1); - - - // This code looks to be too aggressive. It is probably the - // root cause behind #22 (2nd comment). - - - for (P4FileInfo file : files) { - VirtualFile vf = file.getPath().getVirtualFile(); - LOG.debug("processing " + file); - - // VirtualFile can be null. That means the file is deleted. - - if (file.isOpenInClient()) { - LOG.debug("already open in a changelist on the server"); - - LocalChangeList changeList = - vcs.getChangeListMapping().getLocalChangelist(client, file.getChangelist()); - if (changeList == null) { - // This can happen if the changelist was submitted, - // and the file status hasn't been updated to be - // marked as not open. - - LOG.warn("Did not map an IntelliJ changelist for Perforce changelist " + file.getChangelist() + - " (file " + file + ")"); - continue; - } - - // Create a new change for the file, to replace any - // default changes created by the CLM. - // (that is, don't call clm.getChange(file.getPath()) - LOG.debug("added to local changelist " + changeList + ": " + file); - Change change = createChange(file); - data.processChange(change, changeList); - } else if (file.isInDepot()) { - if (vf == null || ! vf.exists()) { - LOG.info("marked as locally deleted: " + file); - data.processLocallyDeletedFile(file.getPath()); - } else { - // See bug #49 - // Changelists show files that are unchanged as locally - // modified without checkout. - - if (isServerAndLocalEqual(client, file, vf)) { - LOG.info("marked as locally modified without edit: " + vf); - data.processModifiedWithoutCheckout(vf); - } else { - LOG.info("idea thinks is locally modified without checkout, but it's the same: " + vf); - file.getPath().hardRefresh(); - } - } - } else if (file.isInClientView() && vf != null) { - LOG.info("marked as locally added: " + vf); - data.processUnversionedFile(vf); - } else if (vf != null) { - LOG.debug("marked as ignored: " + vf); - data.processIgnoredFile(vf); - } else { - LOG.debug("not in depot but deleted: " + vf); - } - } - return files; - } - - private boolean isServerAndLocalEqual(@NotNull final Client client, @NotNull final P4FileInfo file, - @NotNull VirtualFile vf) - throws VcsException { - byte[] src = client.getServer().loadFileAsBytes(file.getPath(), file.getHaveRev()); - if (src == null) { - return false; - } - try { - byte[] tgt = vf.contentsToByteArray(); - if (src.length != tgt.length) { - return false; - } - for (int pos = 0; pos < src.length; ++pos) { - if (src[pos] != tgt[pos]) { - return false; - } - } - return true; - } catch (IOException e) { - throw new P4FileException(e); - } - } - - private void moveP4FilesIntoIdeaChangeLists( - @NotNull Client client, @NotNull ChangeListBuilderCache data, - @NotNull List files) throws VcsException { - // go through the changelist cache, because it should be fresh. - Collection opened = P4ChangeListCache.getInstance().getOpenedFiles(client); - LOG.debug("opened files: " + opened); - // remove files not already handled - opened.removeAll(files); - LOG.debug("opened but not passed into this method: " + opened); - - for (P4FileInfo file : opened) { - LOG.debug("looks like " + file + " is in changelist " + file.getChangelist()); - Change change = createChange(file); - - LocalChangeList changeList = vcs.getChangeListMapping().getLocalChangelist(client, file.getChangelist()); - if (changeList != null) { - LOG.debug("Putting " + file + " into local change " + changeList); - data.processChange(change, changeList); - } else if (client.isWorkingOffline()) { - // Probably was disconnected during the call. - LOG.warn("Disconnected while processing changes."); - if (file.getPath().getVirtualFile() != null) { - data.processModifiedWithoutCheckout(file.getPath().getVirtualFile()); - } - } else { - LOG.error("Could not put " + file + " into local change (null changelist for " + - file.getChangelist() + ")."); - } - - // Any way to use this call? - //builder.reportChangesOutsideProject() ? - } - } private static String[] splitNameComment(P4ChangeList p4cl) { String[] ret = new String[2]; diff --git a/plugin/src/net/groboclown/idea/p4ic/changes/P4ChangeListMapping.java b/plugin/src/net/groboclown/idea/p4ic/changes/P4ChangeListMapping.java index 979f0935..874e7238 100644 --- a/plugin/src/net/groboclown/idea/p4ic/changes/P4ChangeListMapping.java +++ b/plugin/src/net/groboclown/idea/p4ic/changes/P4ChangeListMapping.java @@ -411,7 +411,7 @@ Map> cleanMappings() throws VcsExcept ideaIdToChange.put(lcl.getId(), lcl); // Step 3: find all the IDEA changelists that we have as a mapping, but don't exist - // any more. That will be what remains in the localIdea variable after this loop. + // any more. That will be what remains in the localIdea variable after this loop. if (localIdea.remove(lcl.getId())) { // This is a changelist that still exists and that we have in a mapping. diff --git a/plugin/src/net/groboclown/idea/p4ic/config/UserProjectPreferences.java b/plugin/src/net/groboclown/idea/p4ic/config/UserProjectPreferences.java index badeec34..fef87481 100644 --- a/plugin/src/net/groboclown/idea/p4ic/config/UserProjectPreferences.java +++ b/plugin/src/net/groboclown/idea/p4ic/config/UserProjectPreferences.java @@ -35,6 +35,7 @@ public class UserProjectPreferences implements PersistentStateComponent run(@NotNull P4Exec exec) throws VcsException, Canc Map clientCopySource = sortMap(allMappings, copiedFiles.values()); Map clientCopyTarget = sortMap(allMappings, copiedFiles.keySet()); - boolean useIntegrate = isCopyAnIntegrate(); List ret = new ArrayList(); @@ -120,7 +121,6 @@ public List run(@NotNull P4Exec exec) throws VcsException, Canc reverted.add(target); } - if (source.isInDepot()) { if (useIntegrate) { LOG.debug("Copy: integrate " + source + " to " + target); @@ -149,13 +149,14 @@ public List run(@NotNull P4Exec exec) throws VcsException, Canc ret.addAll(exec.revertFiles(project, P4FileInfo.toClientList(reverted))); } + // TODO debug for testing out #62; switch these back to "debug" if (! added.isEmpty()) { - LOG.debug("Adding " + added); + LOG.info("Adding " + added); ret.addAll(exec.addFiles(project, P4FileInfo.toClientList(added), changelistId)); } if (!edited.isEmpty()) { - LOG.debug("Editing " + added); + LOG.info("Editing " + added); ret.addAll(exec.editFiles(project, P4FileInfo.toClientList(edited), changelistId)); } @@ -172,9 +173,11 @@ public List run(@NotNull P4Exec exec) throws VcsException, Canc private boolean isCopyAnIntegrate() { - // TODO make this a user preference at the project level - - return false; + final UserProjectPreferences preferences = UserProjectPreferences.getInstance(project); + if (preferences == null) { + return UserProjectPreferences.DEFAULT_INTEGRATE_ON_COPY; + } + return preferences.getIntegrateOnCopy(); } private static Set sortSet(Map allMappings, Collection files) throws P4Exception { @@ -182,7 +185,7 @@ private static Set sortSet(Map allMappings, for (VirtualFile vf: files) { P4FileInfo info = allMappings.get(vf); if (info == null) { - LOG.warn("No retrieved mapping for " + vf.getPath()); + LOG.warn("No retrieved mapping for " + vf.getPath() + " (all mappings: " + allMappings + ")"); } else { ret.add(info); } @@ -195,6 +198,7 @@ private static Map sortMap(Map for (VirtualFile vf: files) { P4FileInfo info = allMappings.get(vf); if (info == null) { + LOG.warn("No retrieved mapping for " + vf.getPath() + " (all mappings: " + allMappings + ")"); throw new P4Exception(P4Bundle.message("error.copy.no-mapping", vf)); } ret.put(vf, info); @@ -205,35 +209,59 @@ private static Map sortMap(Map private Map mapVirtualFilesToClient( @NotNull Collection files, @NotNull P4Exec exec) throws VcsException { - // This is really slow, but allows for reuse of the invoked method - Map reverseLookup = new HashMap(); - for (VirtualFile vf : files) { - // make sure we have the correct name and path separators so it matches up with the FilePath value. - String path = (new File(vf.getPath())).getAbsolutePath(); - if (reverseLookup.containsKey(path)) { - throw new IllegalArgumentException(P4Bundle.message("error.move.duplicate", path)); + final List fileList; + if (files instanceof List) { + fileList = (List) files; + } else { + fileList = new ArrayList(files); + } + final Map ret = new HashMap(); + final List fileInfoList = + exec.loadFileInfo(project, FileSpecUtil.getFromVirtualFiles(fileList), fileInfoCache); + if (fileInfoList.size() == fileList.size()) { + // Everything worked as expected. + for (int i = 0; i < fileInfoList.size(); i++) { + ret.put(fileList.get(i), fileInfoList.get(i)); } - reverseLookup.put(path, vf); + // TODO debugging while looking at #62. + LOG.info("Mapped local to perforce: " + ret); + return ret; } - Map ret = new HashMap(); - for (P4FileInfo file : exec.loadFileInfo(project, FileSpecUtil.getFromVirtualFiles(reverseLookup.values()), fileInfoCache)) { + // Incorrect input file mapping. Related to #62. + // Include better logging for "when" it occurs again. + // NOTE: this seems to happen when the input file is a new + // file that's not in Perforce. + LOG.error("Could not map all input files (" + fileList + ") to Perforce depots (did find " + + fileInfoList + ")"); + + // So do the long matching process for what did work. + for (VirtualFile vf: fileList) { + boolean found = false; // Warning: for deleted files, fp.getPath() can be different than the actual file!!!! - // use this instead: getIOFile().getAbsolutePath() - VirtualFile vf = reverseLookup.remove(file.getPath().getIOFile().getAbsolutePath()); - if (vf == null) { - // It's a soft error, because we don't expect to get files we didn't request. - LOG.warn("ERROR: no vf mapping for " + file); - } else { - ret.put(vf, file); + // use this instead: getIOFile() + Iterator iter = fileInfoList.iterator(); + while (iter.hasNext()) { + P4FileInfo fileInfo = iter.next(); + // Warning: for deleted files, fp.getPath() can be different than the actual file!!!! + // use this instead: getIOFile() + File p4vf = fileInfo.getPath().getIOFile(); + if (FileUtil.filesEqual(p4vf, new File(vf.getCanonicalPath())) || fileInfo.getPath().equals(vf)) { + found = true; + ret.put(vf, fileInfo); + iter.remove(); + } + } + if (!found) { + LOG.warn("No P4 info match for local file " + vf); } } - if (!reverseLookup.isEmpty()) { - // This is a correct LOG.info statement. - LOG.info("No p4 files found for " + reverseLookup.values()); + if (!fileInfoList.isEmpty()) { + LOG.warn("No local file match for P4 files " + fileInfoList); } + LOG.info("Successfully mapped local to perforce: " + ret); return ret; } } diff --git a/plugin/src/net/groboclown/idea/p4ic/server/tasks/MoveRunner.java b/plugin/src/net/groboclown/idea/p4ic/server/tasks/MoveRunner.java index f32793df..8a42d674 100644 --- a/plugin/src/net/groboclown/idea/p4ic/server/tasks/MoveRunner.java +++ b/plugin/src/net/groboclown/idea/p4ic/server/tasks/MoveRunner.java @@ -15,6 +15,7 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.VcsException; import com.perforce.p4java.core.file.IFileSpec; @@ -22,6 +23,7 @@ import net.groboclown.idea.p4ic.server.exceptions.P4Exception; import org.jetbrains.annotations.NotNull; +import java.io.File; import java.util.*; import java.util.concurrent.CancellationException; @@ -206,15 +208,15 @@ private Map mapFilePathsToClient( for (FilePath fp : fileList) { boolean found = false; // Warning: for deleted files, fp.getPath() can be different than the actual file!!!! - // use this instead: getIOFile().getAbsolutePath() - String path = fp.getIOFile().getAbsolutePath(); + // use this instead: getIOFile() + File path = fp.getIOFile(); Iterator iter = fileInfoList.iterator(); while (iter.hasNext()) { P4FileInfo fileInfo = iter.next(); // Warning: for deleted files, fp.getPath() can be different than the actual file!!!! - // use this instead: getIOFile().getAbsolutePath() - String p4FilePath = fileInfo.getPath().getIOFile().getAbsolutePath(); - if (p4FilePath.equals(path) || fileInfo.getPath().equals(fp)) { + // use this instead: getIOFile() + File p4FilePath = fileInfo.getPath().getIOFile(); + if (FileUtil.filesEqual(p4FilePath, path) || fileInfo.getPath().equals(fp)) { found = true; ret.put(fp, fileInfo); iter.remove();