/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnFileSystemListener.java

https://bitbucket.org/nbargnesi/idea · Java · 1037 lines · 898 code · 81 blank · 58 comment · 234 complexity · afa2b6794207fd58f31488ab7df3c13f MD5 · raw file

  1. /*
  2. * Copyright 2000-2012 JetBrains s.r.o.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.jetbrains.idea.svn;
  17. import com.intellij.openapi.application.ApplicationManager;
  18. import com.intellij.openapi.command.CommandAdapter;
  19. import com.intellij.openapi.command.CommandEvent;
  20. import com.intellij.openapi.command.undo.UndoManager;
  21. import com.intellij.openapi.diagnostic.Logger;
  22. import com.intellij.openapi.progress.ProgressManager;
  23. import com.intellij.openapi.project.Project;
  24. import com.intellij.openapi.project.ProjectManager;
  25. import com.intellij.openapi.util.Comparing;
  26. import com.intellij.openapi.util.Pair;
  27. import com.intellij.openapi.util.io.FileUtil;
  28. import com.intellij.openapi.vcs.*;
  29. import com.intellij.openapi.vcs.actions.VcsContextFactory;
  30. import com.intellij.openapi.vcs.changes.ChangeListManager;
  31. import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
  32. import com.intellij.openapi.vfs.LocalFileOperationsHandler;
  33. import com.intellij.openapi.vfs.LocalFileSystem;
  34. import com.intellij.openapi.vfs.VirtualFile;
  35. import com.intellij.openapi.vfs.newvfs.RefreshQueue;
  36. import com.intellij.util.Processor;
  37. import com.intellij.util.ThrowableConsumer;
  38. import com.intellij.util.containers.MultiMap;
  39. import com.intellij.vcsUtil.ActionWithTempFile;
  40. import org.jetbrains.annotations.NotNull;
  41. import org.jetbrains.annotations.Nullable;
  42. import org.tmatesoft.svn.core.SVNErrorCode;
  43. import org.tmatesoft.svn.core.SVNErrorMessage;
  44. import org.tmatesoft.svn.core.SVNException;
  45. import org.tmatesoft.svn.core.SVNNodeKind;
  46. import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
  47. import org.tmatesoft.svn.core.wc.*;
  48. import java.io.File;
  49. import java.io.IOException;
  50. import java.util.*;
  51. public class SvnFileSystemListener extends CommandAdapter implements LocalFileOperationsHandler {
  52. private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.svn.SvnFileSystemListener");
  53. private final LocalFileSystem myLfs;
  54. private static class AddedFileInfo {
  55. private final VirtualFile myDir;
  56. private final String myName;
  57. @Nullable private final File myCopyFrom;
  58. private final boolean myRecursive;
  59. public AddedFileInfo(final VirtualFile dir, final String name, @Nullable final File copyFrom, boolean recursive) {
  60. myDir = dir;
  61. myName = name;
  62. myCopyFrom = copyFrom;
  63. myRecursive = recursive;
  64. }
  65. }
  66. private static class MovedFileInfo {
  67. private final Project myProject;
  68. private final File mySrc;
  69. private final File myDst;
  70. private MovedFileInfo(final Project project, final File src, final File dst) {
  71. myProject = project;
  72. mySrc = src;
  73. myDst = dst;
  74. }
  75. }
  76. private final MultiMap<Project, AddedFileInfo> myAddedFiles = new MultiMap<Project, AddedFileInfo>();
  77. private final MultiMap<Project, File> myDeletedFiles = new MultiMap<Project, File>();
  78. private final List<MovedFileInfo> myMovedFiles = new ArrayList<MovedFileInfo>();
  79. private final Map<Project, List<VcsException>> myMoveExceptions = new HashMap<Project, List<VcsException>>();
  80. private final List<VirtualFile> myFilesToRefresh = new ArrayList<VirtualFile>();
  81. @Nullable private File myStorageForUndo;
  82. private final List<Pair<File, File>> myUndoStorageContents = new ArrayList<Pair<File, File>>();
  83. private boolean myUndoingMove = false;
  84. public SvnFileSystemListener() {
  85. myLfs = LocalFileSystem.getInstance();
  86. }
  87. private void addToMoveExceptions(final Project project, final SVNException e) {
  88. List<VcsException> exceptionList = myMoveExceptions.get(project);
  89. if (exceptionList == null) {
  90. exceptionList = new ArrayList<VcsException>();
  91. myMoveExceptions.put(project, exceptionList);
  92. }
  93. VcsException vcsException;
  94. if (SVNErrorCode.ENTRY_EXISTS.equals(e.getErrorMessage().getErrorCode())) {
  95. vcsException = new VcsException(Arrays.asList("Target of move operation is already under version control.",
  96. "Subversion move had not been performed. ", e.getMessage()));
  97. } else {
  98. vcsException = new VcsException(e);
  99. }
  100. exceptionList.add(vcsException);
  101. }
  102. @Nullable
  103. public File copy(final VirtualFile file, final VirtualFile toDir, final String copyName) throws IOException {
  104. SvnVcs vcs = getVCS(toDir);
  105. if (vcs == null) {
  106. vcs = getVCS(file);
  107. }
  108. if (vcs == null) {
  109. return null;
  110. }
  111. File srcFile = new File(file.getPath());
  112. File destFile = new File(new File(toDir.getPath()), copyName);
  113. final boolean dstDirUnderControl = SvnUtil.isSvnVersioned(vcs.getProject(), destFile.getParentFile());
  114. if (! dstDirUnderControl && !isPendingAdd(vcs.getProject(), toDir)) {
  115. return null;
  116. }
  117. if (! SvnUtil.isSvnVersioned(vcs.getProject(), srcFile.getParentFile())) {
  118. myAddedFiles.putValue(vcs.getProject(), new AddedFileInfo(toDir, copyName, null, false));
  119. return null;
  120. }
  121. final SVNStatus fileStatus = getFileStatus(vcs, srcFile);
  122. if (fileStatus != null && SvnVcs.svnStatusIs(fileStatus, SVNStatusType.STATUS_ADDED)) {
  123. myAddedFiles.putValue(vcs.getProject(), new AddedFileInfo(toDir, copyName, null, false));
  124. return null;
  125. }
  126. if (sameRoot(vcs, file.getParent(), toDir)) {
  127. myAddedFiles.putValue(vcs.getProject(), new AddedFileInfo(toDir, copyName, srcFile, false));
  128. return null;
  129. }
  130. myAddedFiles.putValue(vcs.getProject(), new AddedFileInfo(toDir, copyName, null, false));
  131. return null;
  132. }
  133. private boolean sameRoot(final SvnVcs vcs, final VirtualFile srcDir, final VirtualFile dstDir) {
  134. final UUIDHelper helper = new UUIDHelper(vcs);
  135. final String srcUUID = helper.getRepositoryUUID(vcs.getProject(), srcDir);
  136. final String dstUUID = helper.getRepositoryUUID(vcs.getProject(), dstDir);
  137. return srcUUID != null && dstUUID != null && srcUUID.equals(dstUUID);
  138. }
  139. private class UUIDHelper {
  140. private final SVNWCClient myWcClient;
  141. private UUIDHelper(final SvnVcs vcs) {
  142. myWcClient = vcs.createWCClient();
  143. }
  144. /**
  145. * passed dir must be under VC control (it is assumed)
  146. */
  147. @Nullable
  148. public String getRepositoryUUID(final Project project, final VirtualFile dir) {
  149. try {
  150. final SVNInfo info1 = new RepeatSvnActionThroughBusy() {
  151. @Override
  152. protected void executeImpl() throws SVNException {
  153. myT = myWcClient.doInfo(new File(dir.getPath()), SVNRevision.UNDEFINED);
  154. }
  155. }.compute();
  156. if (info1 == null || info1.getRepositoryUUID() == null) {
  157. // go deeper if current parent was added (if parent was added, it theoretically could NOT know its repo UUID)
  158. final VirtualFile parent = dir.getParent();
  159. if (parent == null) {
  160. return null;
  161. }
  162. if (isPendingAdd(project, parent)) {
  163. return getRepositoryUUID(project, parent);
  164. }
  165. } else {
  166. return info1.getRepositoryUUID();
  167. }
  168. } catch (SVNException e) {
  169. // go to return default
  170. }
  171. return null;
  172. }
  173. }
  174. public boolean move(VirtualFile file, VirtualFile toDir) throws IOException {
  175. File srcFile = getIOFile(file);
  176. File dstFile = new File(getIOFile(toDir), file.getName());
  177. final SvnVcs vcs = getVCS(toDir);
  178. final SvnVcs sourceVcs = getVCS(file);
  179. if (vcs == null && sourceVcs == null) return false;
  180. if (vcs == null) {
  181. return false;
  182. }
  183. if (sourceVcs == null) {
  184. return createItem(toDir, file.getName(), file.isDirectory(), true);
  185. }
  186. if (isPendingAdd(vcs.getProject(), toDir)) {
  187. myMovedFiles.add(new MovedFileInfo(sourceVcs.getProject(), srcFile, dstFile));
  188. return true;
  189. }
  190. else {
  191. final VirtualFile oldParent = file.getParent();
  192. myFilesToRefresh.add(oldParent);
  193. myFilesToRefresh.add(toDir);
  194. return doMove(sourceVcs, srcFile, dstFile);
  195. }
  196. }
  197. public boolean rename(VirtualFile file, String newName) throws IOException {
  198. File srcFile = getIOFile(file);
  199. File dstFile = new File(srcFile.getParentFile(), newName);
  200. SvnVcs vcs = getVCS(file);
  201. if (vcs != null) {
  202. myFilesToRefresh.add(file.getParent());
  203. return doMove(vcs, srcFile, dstFile);
  204. }
  205. return false;
  206. }
  207. private boolean doMove(@NotNull SvnVcs vcs, final File src, final File dst) {
  208. long srcTime = src.lastModified();
  209. try {
  210. final boolean isUndo = isUndo(vcs);
  211. final String list = isUndo ? null : SvnChangelistListener.getCurrentMapping(vcs.getProject(), src);
  212. final boolean is17 = SvnUtil.is17CopyPart(src);
  213. if (is17) {
  214. SVNStatus srcStatus = getFileStatus(src);
  215. SVNStatus dstStatus = getFileStatus(dst);
  216. if ((srcStatus == null || SvnVcs.svnStatusIsUnversioned(srcStatus)) && (dstStatus == null || SvnVcs.svnStatusIsUnversioned(dstStatus))) {
  217. return false;
  218. }
  219. if (for17move(vcs, src, dst, isUndo)) return false;
  220. } else {
  221. if (for16move(vcs, src, dst, isUndo)) return false;
  222. }
  223. if (! isUndo && list != null) {
  224. SvnChangelistListener.putUnderList(vcs.getProject(), list, dst);
  225. }
  226. dst.setLastModified(srcTime);
  227. }
  228. catch (SVNException e) {
  229. addToMoveExceptions(vcs.getProject(), e);
  230. return false;
  231. }
  232. return true;
  233. }
  234. private boolean for17move(SvnVcs vcs, final File src, final File dst, boolean undo) throws SVNException {
  235. if (undo) {
  236. myUndoingMove = true;
  237. final SVNWCClient wcClient = vcs.createWCClient();
  238. new RepeatSvnActionThroughBusy() {
  239. @Override
  240. protected void executeImpl() throws SVNException {
  241. wcClient.doRevert(dst, true);
  242. }
  243. }.execute();
  244. copyUnversionedMembersOfDirectory(src, dst);
  245. final SVNStatus srcStatus = getFileStatus(src);
  246. if (srcStatus == null || SvnVcs.svnStatusIsUnversioned(srcStatus)) {
  247. FileUtil.delete(src);
  248. } else {
  249. new RepeatSvnActionThroughBusy() {
  250. @Override
  251. protected void executeImpl() throws SVNException {
  252. wcClient.doRevert(src, true);
  253. }
  254. }.execute();
  255. }
  256. restoreFromUndoStorage(dst);
  257. } else {
  258. if (doUsualMove(vcs, src)) return true;
  259. // check destination directory
  260. final SVNStatus dstParentStatus = getFileStatus(dst.getParentFile());
  261. if (dstParentStatus == null || SvnVcs.svnStatusIsUnversioned(dstParentStatus)) {
  262. try {
  263. copyFileOrDir(src, dst);
  264. }
  265. catch (IOException e) {
  266. throw new SVNException(SVNErrorMessage.create(SVNErrorCode.IO_ERROR), e);
  267. }
  268. final SVNWCClient wcClient = vcs.createWCClient();
  269. new RepeatSvnActionThroughBusy() {
  270. @Override
  271. protected void executeImpl() throws SVNException {
  272. wcClient.doDelete(src, true, false);
  273. }
  274. }.execute();
  275. return false;
  276. }
  277. final SVNCopyClient copyClient = vcs.createCopyClient();
  278. final SVNCopySource svnCopySource = new SVNCopySource(SVNRevision.UNDEFINED, SVNRevision.WORKING, src);
  279. new RepeatSvnActionThroughBusy() {
  280. @Override
  281. protected void executeImpl() throws SVNException {
  282. copyClient.doCopy(new SVNCopySource[]{svnCopySource}, dst, true, false, true);
  283. }
  284. }.execute();
  285. }
  286. return false;
  287. }
  288. private void copyUnversionedMembersOfDirectory(final File src, final File dst) throws SVNException {
  289. if (src.isDirectory()) {
  290. final SVNException[] exc = new SVNException[1];
  291. FileUtil.processFilesRecursively(src, new Processor<File>() {
  292. @Override
  293. public boolean process(File file) {
  294. String relativePath = FileUtil.getRelativePath(src, file);
  295. File newFile = new File(dst, relativePath);
  296. if (!newFile.exists()) {
  297. try {
  298. copyFileOrDir(src, dst);
  299. }
  300. catch (IOException e) {
  301. exc[0] = new SVNException(SVNErrorMessage.create(SVNErrorCode.IO_ERROR), e);
  302. return false;
  303. }
  304. }
  305. return true;
  306. }
  307. });
  308. if (exc[0] != null) {
  309. throw exc[0];
  310. }
  311. }
  312. }
  313. private void copyFileOrDir(File src, File dst) throws IOException {
  314. if (src.isDirectory()) {
  315. FileUtil.copyDir(src, dst);
  316. } else {
  317. FileUtil.copy(src, dst);
  318. }
  319. }
  320. private boolean doUsualMove(SvnVcs vcs, File src) {
  321. // if src is not under version control, do usual move.
  322. SVNStatus srcStatus = getFileStatus(vcs, src);
  323. if (srcStatus == null || SvnVcs.svnStatusIsUnversioned(srcStatus) ||
  324. SvnVcs.svnStatusIs(srcStatus, SVNStatusType.STATUS_OBSTRUCTED) ||
  325. SvnVcs.svnStatusIs(srcStatus, SVNStatusType.STATUS_MISSING) ||
  326. SvnVcs.svnStatusIs(srcStatus, SVNStatusType.STATUS_EXTERNAL)) {
  327. return true;
  328. }
  329. return false;
  330. }
  331. private boolean for16move(SvnVcs vcs, final File src, final File dst, boolean undo) throws SVNException {
  332. final SVNMoveClient mover = vcs.createMoveClient();
  333. if (undo) {
  334. myUndoingMove = true;
  335. restoreFromUndoStorage(dst);
  336. new RepeatSvnActionThroughBusy() {
  337. @Override
  338. protected void executeImpl() throws SVNException {
  339. mover.undoMove(src, dst);
  340. }
  341. }.execute();
  342. }
  343. else {
  344. // if src is not under version control, do usual move.
  345. if (doUsualMove(vcs, src)) return true;
  346. new RepeatSvnActionThroughBusy() {
  347. @Override
  348. protected void executeImpl() throws SVNException {
  349. mover.doMove(src, dst);
  350. }
  351. }.execute();
  352. }
  353. return false;
  354. }
  355. private void restoreFromUndoStorage(final File dst) {
  356. String normPath = FileUtil.toSystemIndependentName(dst.getPath());
  357. for (Iterator<Pair<File, File>> it = myUndoStorageContents.iterator(); it.hasNext();) {
  358. Pair<File, File> e = it.next();
  359. final String p = FileUtil.toSystemIndependentName(e.first.getPath());
  360. if (p.startsWith(normPath)) {
  361. try {
  362. FileUtil.rename(e.second, e.first);
  363. }
  364. catch (IOException ex) {
  365. LOG.error(ex);
  366. FileUtil.asyncDelete(e.second);
  367. }
  368. it.remove();
  369. }
  370. }
  371. if (myStorageForUndo != null) {
  372. final File[] files = myStorageForUndo.listFiles();
  373. if (files == null || files.length == 0) {
  374. FileUtil.asyncDelete(myStorageForUndo);
  375. myStorageForUndo = null;
  376. }
  377. }
  378. }
  379. public boolean createFile(VirtualFile dir, String name) throws IOException {
  380. return createItem(dir, name, false, false);
  381. }
  382. public boolean createDirectory(VirtualFile dir, String name) throws IOException {
  383. return createItem(dir, name, true, false);
  384. }
  385. /**
  386. * delete file or directory (both 'undo' and 'do' modes)
  387. * unversioned: do nothing, return false
  388. * obstructed: do nothing, return false
  389. * external or wc root: do nothing, return false
  390. * missing: do nothing, return false
  391. * <p/>
  392. * versioned: schedule for deletion, return true
  393. * added: schedule for deletion (make unversioned), return true
  394. * copied, but not scheduled: schedule for deletion, return true
  395. * replaced: schedule for deletion, return true
  396. * <p/>
  397. * deleted: do nothing, return true (strange)
  398. */
  399. public boolean delete(VirtualFile file) throws IOException {
  400. SvnVcs vcs = getVCS(file);
  401. if (vcs != null && SvnUtil.isAdminDirectory(file)) {
  402. return true;
  403. }
  404. if (vcs == null) return false;
  405. final File ioFile = getIOFile(file);
  406. if (! SvnUtil.isSvnVersioned(vcs.getProject(), ioFile.getParentFile())) {
  407. return false;
  408. }
  409. try {
  410. if (SVNWCUtil.isWorkingCopyRoot(ioFile)) {
  411. return false;
  412. }
  413. } catch (SVNException e) {
  414. //
  415. }
  416. SVNStatus status = getFileStatus(ioFile);
  417. if (status == null ||
  418. SvnVcs.svnStatusIsUnversioned(status) ||
  419. SvnVcs.svnStatusIs(status, SVNStatusType.STATUS_OBSTRUCTED) ||
  420. SvnVcs.svnStatusIs(status, SVNStatusType.STATUS_MISSING) ||
  421. SvnVcs.svnStatusIs(status, SVNStatusType.STATUS_EXTERNAL) ||
  422. SvnVcs.svnStatusIs(status, SVNStatusType.STATUS_IGNORED)) {
  423. return false;
  424. } else if (SvnVcs.svnStatusIs(status, SVNStatusType.STATUS_DELETED)) {
  425. if (isUndo(vcs)) {
  426. moveToUndoStorage(file);
  427. }
  428. return true;
  429. }
  430. else {
  431. if (vcs != null) {
  432. if (isAboveSourceOfCopyOrMove(vcs.getProject(), ioFile)) {
  433. myDeletedFiles.putValue(vcs.getProject(), ioFile);
  434. return true;
  435. }
  436. if (SvnVcs.svnStatusIs(status, SVNStatusType.STATUS_ADDED)) {
  437. try {
  438. final SVNWCClient wcClient = vcs.createWCClient();
  439. new RepeatSvnActionThroughBusy() {
  440. @Override
  441. protected void executeImpl() throws SVNException {
  442. wcClient.doRevert(ioFile, false);
  443. }
  444. }.execute();
  445. }
  446. catch (SVNException e) {
  447. // ignore
  448. }
  449. }
  450. else {
  451. myDeletedFiles.putValue(vcs.getProject(), ioFile);
  452. // packages deleted from disk should not be deleted from svn (IDEADEV-16066)
  453. if (file.isDirectory() || isUndo(vcs)) return true;
  454. }
  455. }
  456. return false;
  457. }
  458. }
  459. private boolean isAboveSourceOfCopyOrMove(final Project p, File ioFile) {
  460. for (MovedFileInfo file : myMovedFiles) {
  461. if (FileUtil.isAncestor(ioFile, file.mySrc, false)) return true;
  462. }
  463. for (AddedFileInfo info : myAddedFiles.get(p)) {
  464. if (info.myCopyFrom != null && FileUtil.isAncestor(ioFile, info.myCopyFrom, false)) return true;
  465. }
  466. return false;
  467. }
  468. private void moveToUndoStorage(final VirtualFile file) {
  469. if (myStorageForUndo == null) {
  470. try {
  471. myStorageForUndo = FileUtil.createTempDirectory("svnUndoStorage", "");
  472. }
  473. catch (IOException e) {
  474. LOG.error(e);
  475. return;
  476. }
  477. }
  478. final File tmpFile = FileUtil.findSequentNonexistentFile(myStorageForUndo, "tmp", "");
  479. myUndoStorageContents.add(0, new Pair<File, File>(new File(file.getPath()), tmpFile));
  480. new File(file.getPath()).renameTo(tmpFile);
  481. }
  482. /**
  483. * add file or directory:
  484. * <p/>
  485. * parent directory is:
  486. * unversioned: do nothing, return false
  487. * versioned:
  488. * entry is:
  489. * null: create entry, schedule for addition
  490. * missing: do nothing, return false
  491. * deleted, 'do' mode: try to create entry and it schedule for addition if kind is the same, otherwise do nothing, return false.
  492. * deleted: 'undo' mode: try to revert non-recursively, if kind is the same, otherwise do nothing, return false.
  493. * anything else: return false.
  494. */
  495. private boolean createItem(VirtualFile dir, String name, boolean directory, final boolean recursive) {
  496. SvnVcs vcs = getVCS(dir);
  497. if (vcs == null) {
  498. return false;
  499. }
  500. if (isUndo(vcs) && SvnUtil.isAdminDirectory(dir, name)) {
  501. return false;
  502. }
  503. File ioDir = getIOFile(dir);
  504. boolean pendingAdd = isPendingAdd(vcs.getProject(), dir);
  505. if (! SvnUtil.isSvnVersioned(vcs.getProject(), ioDir) && ! pendingAdd) {
  506. return false;
  507. }
  508. final SVNWCClient wcClient = vcs.createWCClient();
  509. final File targetFile = new File(ioDir, name);
  510. SVNStatus status = getFileStatus(vcs, targetFile);
  511. if (status == null || status.getContentsStatus() == SVNStatusType.STATUS_NONE ||
  512. status.getContentsStatus() == SVNStatusType.STATUS_UNVERSIONED) {
  513. myAddedFiles.putValue(vcs.getProject(), new AddedFileInfo(dir, name, null, recursive));
  514. return false;
  515. }
  516. else if (SvnVcs.svnStatusIs(status, SVNStatusType.STATUS_MISSING)) {
  517. return false;
  518. }
  519. else if (SvnVcs.svnStatusIs(status, SVNStatusType.STATUS_DELETED)) {
  520. SVNNodeKind kind = status.getKind();
  521. // kind differs.
  522. if (directory && kind != SVNNodeKind.DIR || !directory && kind != SVNNodeKind.FILE) {
  523. return false;
  524. }
  525. try {
  526. if (isUndo(vcs)) {
  527. new RepeatSvnActionThroughBusy() {
  528. @Override
  529. protected void executeImpl() throws SVNException {
  530. wcClient.doRevert(targetFile, false);
  531. }
  532. }.execute();
  533. return true;
  534. }
  535. myAddedFiles.putValue(vcs.getProject(), new AddedFileInfo(dir, name, null, recursive));
  536. return false;
  537. }
  538. catch (SVNException e) {
  539. SVNFileUtil.deleteAll(targetFile, true);
  540. return false;
  541. }
  542. }
  543. return false;
  544. }
  545. private boolean isPendingAdd(final Project project, final VirtualFile dir) {
  546. final Collection<AddedFileInfo> addedFileInfos = myAddedFiles.get(project);
  547. for(AddedFileInfo i: addedFileInfos) {
  548. if (Comparing.equal(i.myDir, dir.getParent()) && i.myName.equals(dir.getName())) {
  549. return true;
  550. }
  551. }
  552. return false;
  553. }
  554. public void commandStarted(CommandEvent event) {
  555. myUndoingMove = false;
  556. final Project project = event.getProject();
  557. if (project == null) return;
  558. commandStarted(project);
  559. }
  560. void commandStarted(final Project project) {
  561. myUndoingMove = false;
  562. myMoveExceptions.remove(project);
  563. }
  564. public void commandFinished(CommandEvent event) {
  565. final Project project = event.getProject();
  566. if (project == null) return;
  567. commandFinished(project);
  568. }
  569. void commandFinished(final Project project) {
  570. checkOverwrites(project);
  571. if (myAddedFiles.containsKey(project)) {
  572. processAddedFiles(project);
  573. }
  574. processMovedFiles(project);
  575. if (myDeletedFiles.containsKey(project)) {
  576. processDeletedFiles(project);
  577. }
  578. final List<VcsException> exceptionList = myMoveExceptions.get(project);
  579. if (exceptionList != null && ! exceptionList.isEmpty()) {
  580. AbstractVcsHelper.getInstance(project).showErrors(exceptionList, SvnBundle.message("move.files.errors.title"));
  581. }
  582. if (!myFilesToRefresh.isEmpty()) {
  583. refreshFiles(project);
  584. }
  585. }
  586. private void checkOverwrites(final Project project) {
  587. final Collection<AddedFileInfo> addedFileInfos = myAddedFiles.get(project);
  588. final Collection<File> deletedFiles = myDeletedFiles.get(project);
  589. if (addedFileInfos.isEmpty() || deletedFiles.isEmpty()) return;
  590. final Iterator<AddedFileInfo> iterator = addedFileInfos.iterator();
  591. while (iterator.hasNext()) {
  592. AddedFileInfo addedFileInfo = iterator.next();
  593. final File ioFile = new File(addedFileInfo.myDir.getPath(), addedFileInfo.myName);
  594. if (deletedFiles.remove(ioFile)) {
  595. iterator.remove();
  596. }
  597. }
  598. }
  599. private void refreshFiles(final Project project) {
  600. final List<VirtualFile> toRefreshFiles = new ArrayList<VirtualFile>();
  601. final List<VirtualFile> toRefreshDirs = new ArrayList<VirtualFile>();
  602. for (VirtualFile file : myFilesToRefresh) {
  603. if (file == null) continue;
  604. if (file.isDirectory()) {
  605. toRefreshDirs.add(file);
  606. } else {
  607. toRefreshFiles.add(file);
  608. }
  609. }
  610. // if refresh asynchronously, local changes would also be notified that they are dirty asynchronously,
  611. // and commit could be executed while not all changes are visible
  612. filterOutInvalid(myFilesToRefresh);
  613. RefreshQueue.getInstance().refresh(true, true, new Runnable() {
  614. public void run() {
  615. if (project.isDisposed()) return;
  616. filterOutInvalid(toRefreshFiles);
  617. filterOutInvalid(toRefreshDirs);
  618. final VcsDirtyScopeManager vcsDirtyScopeManager = VcsDirtyScopeManager.getInstance(project);
  619. vcsDirtyScopeManager.filesDirty(toRefreshFiles, toRefreshDirs);
  620. }
  621. }, myFilesToRefresh);
  622. myFilesToRefresh.clear();
  623. }
  624. private static void filterOutInvalid(final Collection<VirtualFile> files) {
  625. for (Iterator<VirtualFile> iterator = files.iterator(); iterator.hasNext();) {
  626. final VirtualFile file = iterator.next();
  627. if (! file.isValid() || ! file.exists()) {
  628. LOG.info("Refresh root is not valid: " + file.getPath());
  629. iterator.remove();
  630. }
  631. }
  632. }
  633. private void processAddedFiles(Project project) {
  634. SvnVcs vcs = SvnVcs.getInstance(project);
  635. List<VirtualFile> addedVFiles = new ArrayList<VirtualFile>();
  636. Map<VirtualFile, File> copyFromMap = new HashMap<VirtualFile, File>();
  637. final Set<VirtualFile> recursiveItems = new HashSet<VirtualFile>();
  638. fillAddedFiles(project, vcs, addedVFiles, copyFromMap, recursiveItems);
  639. if (addedVFiles.isEmpty()) return;
  640. final VcsShowConfirmationOption.Value value = vcs.getAddConfirmation().getValue();
  641. if (value != VcsShowConfirmationOption.Value.DO_NOTHING_SILENTLY) {
  642. final AbstractVcsHelper vcsHelper = AbstractVcsHelper.getInstance(project);
  643. final Collection<VirtualFile> filesToProcess = promptAboutAddition(vcs, addedVFiles, value, vcsHelper);
  644. if (filesToProcess != null && !filesToProcess.isEmpty()) {
  645. final List<VcsException> exceptions = new ArrayList<VcsException>();
  646. runInBackground(project, "Adding files to Subversion",
  647. createAdditionRunnable(project, vcs, copyFromMap, filesToProcess, exceptions));
  648. if (!exceptions.isEmpty()) {
  649. vcsHelper.showErrors(exceptions, SvnBundle.message("add.files.errors.title"));
  650. }
  651. }
  652. }
  653. }
  654. private void runInBackground(final Project project, final String name, final Runnable runnable) {
  655. if (ApplicationManager.getApplication().isDispatchThread()) {
  656. ProgressManager.getInstance().runProcessWithProgressSynchronously(runnable, name, false, project);
  657. } else {
  658. runnable.run();
  659. }
  660. }
  661. private Runnable createAdditionRunnable(final Project project,
  662. final SvnVcs vcs,
  663. final Map<VirtualFile, File> copyFromMap,
  664. final Collection<VirtualFile> filesToProcess,
  665. final List<VcsException> exceptions) {
  666. return new Runnable() {
  667. @Override
  668. public void run() {
  669. final SVNWCClient wcClient = vcs.createWCClient();
  670. final SVNCopyClient copyClient = vcs.createCopyClient();
  671. for(VirtualFile file: filesToProcess) {
  672. final File ioFile = new File(file.getPath());
  673. try {
  674. final File copyFrom = copyFromMap.get(file);
  675. if (copyFrom != null) {
  676. try {
  677. new ActionWithTempFile(ioFile) {
  678. protected void executeInternal() throws VcsException {
  679. try {
  680. // not recursive
  681. final SVNCopySource[] copySource = {new SVNCopySource(SVNRevision.WORKING, SVNRevision.WORKING, copyFrom)};
  682. new RepeatSvnActionThroughBusy() {
  683. @Override
  684. protected void executeImpl() throws SVNException {
  685. copyClient.doCopy(copySource, ioFile, false, true, true);
  686. }
  687. }.execute();
  688. }
  689. catch (SVNException e) {
  690. throw new VcsException(e);
  691. }
  692. }
  693. }.execute();
  694. }
  695. catch (VcsException e) {
  696. exceptions.add(e);
  697. }
  698. }
  699. else {
  700. new RepeatSvnActionThroughBusy() {
  701. @Override
  702. protected void executeImpl() throws SVNException {
  703. wcClient.doAdd(ioFile, true, false, false, true);
  704. }
  705. }.execute();
  706. }
  707. VcsDirtyScopeManager.getInstance(project).fileDirty(file);
  708. }
  709. catch (SVNException e) {
  710. exceptions.add(new VcsException(e));
  711. }
  712. }
  713. }
  714. };
  715. }
  716. private Collection<VirtualFile> promptAboutAddition(SvnVcs vcs,
  717. List<VirtualFile> addedVFiles,
  718. VcsShowConfirmationOption.Value value,
  719. AbstractVcsHelper vcsHelper) {
  720. Collection<VirtualFile> filesToProcess;
  721. if (value == VcsShowConfirmationOption.Value.DO_ACTION_SILENTLY) {
  722. filesToProcess = addedVFiles;
  723. }
  724. else {
  725. final String singleFilePrompt;
  726. if (addedVFiles.size() == 1 && addedVFiles.get(0).isDirectory()) {
  727. singleFilePrompt = SvnBundle.getString("confirmation.text.add.dir");
  728. }
  729. else {
  730. singleFilePrompt = SvnBundle.getString("confirmation.text.add.file");
  731. }
  732. filesToProcess = vcsHelper.selectFilesToProcess(addedVFiles, SvnBundle.message("confirmation.title.add.multiple.files"),
  733. null,
  734. SvnBundle.message("confirmation.title.add.file"), singleFilePrompt,
  735. vcs.getAddConfirmation());
  736. }
  737. return filesToProcess;
  738. }
  739. private void fillAddedFiles(Project project,
  740. SvnVcs vcs,
  741. List<VirtualFile> addedVFiles,
  742. Map<VirtualFile, File> copyFromMap,
  743. Set<VirtualFile> recursiveItems) {
  744. final Collection<AddedFileInfo> addedFileInfos = myAddedFiles.remove(project);
  745. final ChangeListManager changeListManager = ChangeListManager.getInstance(project);
  746. for (AddedFileInfo addedFileInfo : addedFileInfos) {
  747. final File ioFile = new File(getIOFile(addedFileInfo.myDir), addedFileInfo.myName);
  748. VirtualFile addedFile = addedFileInfo.myDir.findChild(addedFileInfo.myName);
  749. if (addedFile == null) {
  750. addedFile = myLfs.refreshAndFindFileByIoFile(ioFile);
  751. }
  752. if (addedFile != null) {
  753. final SVNStatus fileStatus = getFileStatus(vcs, ioFile);
  754. if (fileStatus == null || ! SvnVcs.svnStatusIs(fileStatus, SVNStatusType.STATUS_IGNORED)) {
  755. boolean isIgnored = changeListManager.isIgnoredFile(addedFile);
  756. if (!isIgnored) {
  757. addedVFiles.add(addedFile);
  758. copyFromMap.put(addedFile, addedFileInfo.myCopyFrom);
  759. if (addedFileInfo.myRecursive) {
  760. recursiveItems.add(addedFile);
  761. }
  762. }
  763. }
  764. }
  765. }
  766. }
  767. private void processDeletedFiles(Project project) {
  768. final List<FilePath> deletedFiles = new ArrayList<FilePath>();
  769. final Collection<FilePath> filesToProcess = new ArrayList<FilePath>();
  770. fillDeletedFiles(project, deletedFiles, filesToProcess);
  771. if (deletedFiles.isEmpty() && filesToProcess.isEmpty() || myUndoingMove) return;
  772. SvnVcs vcs = SvnVcs.getInstance(project);
  773. final VcsShowConfirmationOption.Value value = vcs.getDeleteConfirmation().getValue();
  774. if (value != VcsShowConfirmationOption.Value.DO_NOTHING_SILENTLY) {
  775. final AbstractVcsHelper vcsHelper = AbstractVcsHelper.getInstance(project);
  776. if (! deletedFiles.isEmpty()) {
  777. final Collection<FilePath> confirmed = promptAboutDeletion(deletedFiles, vcs, value, vcsHelper);
  778. if (confirmed != null) {
  779. filesToProcess.addAll(confirmed);
  780. }
  781. }
  782. if (filesToProcess != null && !filesToProcess.isEmpty()) {
  783. List<VcsException> exceptions = new ArrayList<VcsException>();
  784. runInBackground(project, "Deleting files from Subversion", createDeleteRunnable(project, vcs, filesToProcess, exceptions));
  785. if (!exceptions.isEmpty()) {
  786. vcsHelper.showErrors(exceptions, SvnBundle.message("delete.files.errors.title"));
  787. }
  788. }
  789. for (FilePath file : deletedFiles) {
  790. final FilePath parent = file.getParentPath();
  791. if (parent != null) {
  792. myFilesToRefresh.add(parent.getVirtualFile());
  793. }
  794. }
  795. if (filesToProcess != null) {
  796. deletedFiles.removeAll(filesToProcess);
  797. }
  798. for (FilePath file : deletedFiles) {
  799. FileUtil.delete(file.getIOFile());
  800. }
  801. }
  802. }
  803. private Runnable createDeleteRunnable(final Project project,
  804. final SvnVcs vcs,
  805. final Collection<FilePath> filesToProcess,
  806. final List<VcsException> exceptions) {
  807. return new Runnable() {
  808. public void run() {
  809. final SVNWCClient wcClient = vcs.createWCClient();
  810. for(FilePath file: filesToProcess) {
  811. VirtualFile vFile = file.getVirtualFile(); // for deleted directories
  812. final File ioFile = new File(file.getPath());
  813. try {
  814. new RepeatSvnActionThroughBusy() {
  815. @Override
  816. protected void executeImpl() throws SVNException {
  817. wcClient.doDelete(ioFile, true, false);
  818. }
  819. }.execute();
  820. if (vFile != null && vFile.isValid() && vFile.isDirectory()) {
  821. vFile.refresh(true, true);
  822. VcsDirtyScopeManager.getInstance(project).dirDirtyRecursively(vFile);
  823. }
  824. else {
  825. VcsDirtyScopeManager.getInstance(project).fileDirty(file);
  826. }
  827. }
  828. catch (SVNException e) {
  829. exceptions.add(new VcsException(e));
  830. }
  831. }
  832. }
  833. };
  834. }
  835. private Collection<FilePath> promptAboutDeletion(List<FilePath> deletedFiles,
  836. SvnVcs vcs,
  837. VcsShowConfirmationOption.Value value,
  838. AbstractVcsHelper vcsHelper) {
  839. Collection<FilePath> filesToProcess;
  840. if (value == VcsShowConfirmationOption.Value.DO_ACTION_SILENTLY) {
  841. filesToProcess = new ArrayList<FilePath>(deletedFiles);
  842. }
  843. else {
  844. final String singleFilePrompt;
  845. if (deletedFiles.size() == 1 && deletedFiles.get(0).isDirectory()) {
  846. singleFilePrompt = SvnBundle.getString("confirmation.text.delete.dir");
  847. }
  848. else {
  849. singleFilePrompt = SvnBundle.getString("confirmation.text.delete.file");
  850. }
  851. final Collection<FilePath> files = vcsHelper
  852. .selectFilePathsToProcess(deletedFiles, SvnBundle.message("confirmation.title.delete.multiple.files"), null,
  853. SvnBundle.message("confirmation.title.delete.file"), singleFilePrompt, vcs.getDeleteConfirmation());
  854. filesToProcess = files == null ? null : new ArrayList<FilePath>(files);
  855. }
  856. return filesToProcess;
  857. }
  858. private void fillDeletedFiles(Project project, List<FilePath> deletedFiles, Collection<FilePath> deleteAnyway) {
  859. final SvnVcs vcs = SvnVcs.getInstance(project);
  860. final SVNStatusClient sc = vcs.createStatusClient();
  861. final Collection<File> files = myDeletedFiles.remove(project);
  862. for (final File file : files) {
  863. boolean isAdded = false;
  864. try {
  865. final SVNStatus status = new RepeatSvnActionThroughBusy() {
  866. @Override
  867. protected void executeImpl() throws SVNException {
  868. myT = sc.doStatus(file, false);
  869. }
  870. }.compute();
  871. isAdded = SVNStatusType.STATUS_ADDED.equals(status.getNodeStatus());
  872. }
  873. catch (SVNException e) {
  874. //
  875. }
  876. final FilePath filePath = VcsContextFactory.SERVICE.getInstance().createFilePathOn(file);
  877. if (isAdded) {
  878. deleteAnyway.add(filePath);
  879. } else {
  880. deletedFiles.add(filePath);
  881. }
  882. }
  883. }
  884. private void processMovedFiles(final Project project) {
  885. if (myMovedFiles.isEmpty()) return;
  886. final Runnable runnable = new Runnable() {
  887. public void run() {
  888. for (Iterator<MovedFileInfo> iterator = myMovedFiles.iterator(); iterator.hasNext();) {
  889. MovedFileInfo movedFileInfo = iterator.next();
  890. if (movedFileInfo.myProject == project) {
  891. doMove(SvnVcs.getInstance(project), movedFileInfo.mySrc, movedFileInfo.myDst);
  892. iterator.remove();
  893. }
  894. }
  895. }
  896. };
  897. runInBackground(project, "Moving files in Subversion", runnable);
  898. }
  899. @Nullable
  900. private static SvnVcs getVCS(VirtualFile file) {
  901. Project[] projects = ProjectManager.getInstance().getOpenProjects();
  902. for (Project project : projects) {
  903. AbstractVcs vcs = ProjectLevelVcsManager.getInstance(project).getVcsFor(file);
  904. if (vcs instanceof SvnVcs) {
  905. return (SvnVcs)vcs;
  906. }
  907. }
  908. return null;
  909. }
  910. private static File getIOFile(VirtualFile vf) {
  911. return new File(vf.getPath()).getAbsoluteFile();
  912. }
  913. @Nullable
  914. private static SVNStatus getFileStatus(File file) {
  915. final SVNClientManager clientManager = SVNClientManager.newInstance();
  916. try {
  917. SVNStatusClient stClient = clientManager.getStatusClient();
  918. return getFileStatus(file, stClient);
  919. }
  920. finally {
  921. clientManager.dispose();
  922. }
  923. }
  924. @Nullable
  925. private static SVNStatus getFileStatus(SvnVcs vcs, File file) {
  926. SVNStatusClient stClient = vcs.createStatusClient();
  927. return getFileStatus(file, stClient);
  928. }
  929. @Nullable
  930. private static SVNStatus getFileStatus(final File file, final SVNStatusClient stClient) {
  931. try {
  932. return new RepeatSvnActionThroughBusy() {
  933. @Override
  934. protected void executeImpl() throws SVNException {
  935. myT = stClient.doStatus(file, false);
  936. }
  937. }.compute();
  938. }
  939. catch (SVNException e) {
  940. return null;
  941. }
  942. }
  943. private static boolean isUndoOrRedo(@NotNull final Project project) {
  944. final UndoManager undoManager = UndoManager.getInstance(project);
  945. return undoManager.isUndoInProgress() || undoManager.isRedoInProgress();
  946. }
  947. private static boolean isUndo(SvnVcs vcs) {
  948. if (vcs == null || vcs.getProject() == null) {
  949. return false;
  950. }
  951. Project p = vcs.getProject();
  952. return UndoManager.getInstance(p).isUndoInProgress();
  953. }
  954. public void afterDone(final ThrowableConsumer<LocalFileOperationsHandler, IOException> invoker) {
  955. }
  956. }