b4363e3482bd88f627d73c103a71ef6aae6c39b1
[idea/community.git] / plugins / svn4idea / src / org / jetbrains / idea / svn / SvnChangeProviderContext.java
1 /*
2  * Copyright 2000-2009 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
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.fileEditor.FileDocumentManager;
20 import com.intellij.openapi.progress.ProgressIndicator;
21 import com.intellij.openapi.util.Comparing;
22 import com.intellij.openapi.util.io.FileUtil;
23 import com.intellij.openapi.vcs.FilePath;
24 import com.intellij.openapi.vcs.FileStatus;
25 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
26 import com.intellij.openapi.vcs.changes.*;
27 import com.intellij.openapi.vfs.LocalFileSystem;
28 import com.intellij.openapi.vfs.VirtualFile;
29 import com.intellij.util.containers.ContainerUtil;
30 import com.intellij.vcsUtil.VcsUtil;
31 import org.jetbrains.annotations.NotNull;
32 import org.jetbrains.annotations.Nullable;
33 import org.jetbrains.idea.svn.actions.AbstractShowPropertiesDiffAction;
34 import org.jetbrains.idea.svn.api.NodeKind;
35 import org.jetbrains.idea.svn.branchConfig.SvnBranchConfigurationManager;
36 import org.jetbrains.idea.svn.history.SimplePropertyRevision;
37 import org.jetbrains.idea.svn.info.Info;
38 import org.jetbrains.idea.svn.properties.PropertyData;
39 import org.jetbrains.idea.svn.status.Status;
40 import org.jetbrains.idea.svn.status.StatusType;
41 import org.tmatesoft.svn.core.SVNException;
42 import org.tmatesoft.svn.core.SVNURL;
43 import org.tmatesoft.svn.core.wc.SVNRevision;
44
45 import java.io.File;
46 import java.util.List;
47 import java.util.Map;
48
49 class SvnChangeProviderContext implements StatusReceiver {
50   private static final Logger LOG = Logger.getInstance("org.jetbrains.idea.svn.SvnChangeProviderContext");
51
52   @NotNull private final ChangelistBuilder myChangelistBuilder;
53   @NotNull private final List<SvnChangedFile> myCopiedFiles = ContainerUtil.newArrayList();
54   @NotNull private final List<SvnChangedFile> myDeletedFiles = ContainerUtil.newArrayList();
55   // for files moved in a subtree, which were the targets of merge (for instance).
56   @NotNull private final Map<String, Status> myTreeConflicted = ContainerUtil.newHashMap();
57   @NotNull private final Map<FilePath, String> myCopyFromURLs = ContainerUtil.newHashMap();
58   @NotNull private final SvnVcs myVcs;
59   private final SvnBranchConfigurationManager myBranchConfigurationManager;
60   @NotNull private final List<File> filesToRefresh = ContainerUtil.newArrayList();
61
62   @Nullable private final ProgressIndicator myProgress;
63
64   public SvnChangeProviderContext(@NotNull SvnVcs vcs, @NotNull ChangelistBuilder changelistBuilder, @Nullable ProgressIndicator progress) {
65     myVcs = vcs;
66     myChangelistBuilder = changelistBuilder;
67     myProgress = progress;
68     myBranchConfigurationManager = SvnBranchConfigurationManager.getInstance(myVcs.getProject());
69   }
70
71   public void process(FilePath path, Status status) throws SVNException {
72     if (status != null) {
73       processStatusFirstPass(path, status);
74     }
75   }
76
77   public void processIgnored(VirtualFile vFile) {
78     myChangelistBuilder.processIgnoredFile(vFile);
79   }
80
81   public void processUnversioned(VirtualFile vFile) {
82     myChangelistBuilder.processUnversionedFile(vFile);
83   }
84
85   @Override
86   public void processCopyRoot(VirtualFile file, SVNURL url, WorkingCopyFormat format, SVNURL rootURL) {
87   }
88
89   @Override
90   public void bewareRoot(VirtualFile vf, SVNURL url) {
91   }
92
93   @Override
94   public void finish() {
95     LocalFileSystem.getInstance().refreshIoFiles(filesToRefresh, true, false, null);
96   }
97
98   @NotNull
99   public ChangelistBuilder getBuilder() {
100     return myChangelistBuilder;
101   }
102
103   public void reportTreeConflict(@NotNull Status status) {
104     myTreeConflicted.put(status.getFile().getAbsolutePath(), status);
105   }
106
107   @Nullable
108   public Status getTreeConflictStatus(@NotNull File file) {
109     return myTreeConflicted.get(file.getAbsolutePath());
110   }
111
112   @NotNull
113   public List<SvnChangedFile> getCopiedFiles() {
114     return myCopiedFiles;
115   }
116
117   @NotNull
118   public List<SvnChangedFile> getDeletedFiles() {
119     return myDeletedFiles;
120   }
121
122   public boolean isDeleted(@NotNull FilePath path) {
123     for (SvnChangedFile deletedFile : myDeletedFiles) {
124       if (Comparing.equal(path, deletedFile.getFilePath())) {
125         return true;
126       }
127     }
128     return false;
129   }
130
131   public void checkCanceled() {
132     if (myProgress != null) {
133       myProgress.checkCanceled();
134     }
135   }
136
137   /**
138    * If the specified filepath or its parent was added with history, returns the URL of the copy source for this filepath.
139    *
140    * @param filePath the original filepath
141    * @return the copy source url, or null if the file isn't a copy of anything
142    */
143   @Nullable
144   public String getParentCopyFromURL(@NotNull FilePath filePath) {
145     String result = null;
146     FilePath parent = filePath;
147
148     while (parent != null && !myCopyFromURLs.containsKey(parent)) {
149       parent = parent.getParentPath();
150     }
151
152     if (parent != null) {
153       String copyFromUrl = myCopyFromURLs.get(parent);
154
155       //noinspection ConstantConditions
156       result = parent == filePath
157                ? copyFromUrl
158                : SvnUtil.appendMultiParts(copyFromUrl, FileUtil.getRelativePath(parent.getIOFile(), filePath.getIOFile()));
159     }
160
161     return result;
162   }
163
164   public void addCopiedFile(@NotNull FilePath filePath, @NotNull Status status, @NotNull String copyFromURL) {
165     myCopiedFiles.add(new SvnChangedFile(filePath, status, copyFromURL));
166     ContainerUtil.putIfNotNull(filePath, status.getCopyFromURL(), myCopyFromURLs);
167   }
168
169   void processStatusFirstPass(@NotNull FilePath filePath, @NotNull Status status) throws SVNException {
170     if (status.getRemoteLock() != null) {
171       myChangelistBuilder.processLogicallyLockedFolder(filePath.getVirtualFile(), status.getRemoteLock().toLogicalLock(false));
172     }
173     if (status.getLocalLock() != null) {
174       myChangelistBuilder.processLogicallyLockedFolder(filePath.getVirtualFile(), status.getLocalLock().toLogicalLock(true));
175     }
176     if (filePath.isDirectory() && status.isLocked()) {
177       myChangelistBuilder.processLockedFolder(filePath.getVirtualFile());
178     }
179     if ((status.is(StatusType.STATUS_ADDED) || StatusType.STATUS_MODIFIED.equals(status.getNodeStatus())) &&
180         status.getCopyFromURL() != null) {
181       addCopiedFile(filePath, status, status.getCopyFromURL());
182     }
183     else if (status.is(StatusType.STATUS_DELETED)) {
184       myDeletedFiles.add(new SvnChangedFile(filePath, status));
185     }
186     else {
187       String parentCopyFromURL = getParentCopyFromURL(filePath);
188       if (parentCopyFromURL != null) {
189         addCopiedFile(filePath, status, parentCopyFromURL);
190       }
191       else {
192         processStatus(filePath, status);
193       }
194     }
195   }
196
197   void processStatus(@NotNull FilePath filePath, @NotNull Status status) throws SVNException {
198     WorkingCopyFormat format = myVcs.getWorkingCopyFormat(filePath.getIOFile());
199     if (!WorkingCopyFormat.UNKNOWN.equals(format) && format.less(WorkingCopyFormat.ONE_DOT_SEVEN)) {
200       loadEntriesFile(filePath);
201     }
202
203     FileStatus fStatus = SvnStatusConvertor.convertStatus(status);
204
205     final StatusType statusType = status.getContentsStatus();
206     if (status.is(StatusType.STATUS_UNVERSIONED, StatusType.UNKNOWN)) {
207       final VirtualFile file = filePath.getVirtualFile();
208       if (file != null) {
209         myChangelistBuilder.processUnversionedFile(file);
210       }
211     }
212     else if (status.is(StatusType.STATUS_ADDED)) {
213       processChangeInList(null, CurrentContentRevision.create(filePath), fStatus, status);
214     }
215     else if (status.is(StatusType.STATUS_CONFLICTED, StatusType.STATUS_MODIFIED, StatusType.STATUS_REPLACED) ||
216              status.isProperty(StatusType.STATUS_MODIFIED, StatusType.STATUS_CONFLICTED)) {
217       processChangeInList(SvnContentRevision.createBaseRevision(myVcs, filePath, status), CurrentContentRevision.create(filePath), fStatus,
218                           status);
219       checkSwitched(filePath, status, fStatus);
220     }
221     else if (status.is(StatusType.STATUS_DELETED)) {
222       processChangeInList(SvnContentRevision.createBaseRevision(myVcs, filePath, status), null, fStatus, status);
223     }
224     else if (status.is(StatusType.STATUS_MISSING)) {
225       myChangelistBuilder.processLocallyDeletedFile(new SvnLocallyDeletedChange(filePath, getState(status)));
226     }
227     else if (status.is(StatusType.STATUS_IGNORED)) {
228       VirtualFile file = filePath.getVirtualFile();
229       if (file == null) {
230         file = LocalFileSystem.getInstance().refreshAndFindFileByPath(filePath.getPath());
231       }
232       if (file == null) {
233         LOG.error("No virtual file for ignored file: " + filePath.getPresentableUrl() + ", isNonLocal: " + filePath.isNonLocal());
234       }
235       else if (!myVcs.isWcRoot(filePath)) {
236         myChangelistBuilder.processIgnoredFile(filePath.getVirtualFile());
237       }
238     }
239     else if ((fStatus == FileStatus.NOT_CHANGED || fStatus == FileStatus.SWITCHED) && statusType != StatusType.STATUS_NONE) {
240       VirtualFile file = filePath.getVirtualFile();
241       if (file != null && FileDocumentManager.getInstance().isFileModified(file)) {
242         processChangeInList(SvnContentRevision.createBaseRevision(myVcs, filePath, status), CurrentContentRevision.create(filePath),
243                             FileStatus.MODIFIED, status);
244       }
245       else if (status.getTreeConflict() != null) {
246         myChangelistBuilder.processChange(createChange(SvnContentRevision.createBaseRevision(myVcs, filePath, status),
247                                                        CurrentContentRevision.create(filePath), FileStatus.MODIFIED, status),
248                                           SvnVcs.getKey());
249       }
250       checkSwitched(filePath, status, fStatus);
251     }
252   }
253
254   public void addModifiedNotSavedChange(@NotNull VirtualFile file) throws SVNException {
255     final FilePath filePath = VcsUtil.getFilePath(file);
256     final Info svnInfo = myVcs.getInfo(file);
257
258     if (svnInfo != null) {
259       final Status svnStatus = new Status();
260       svnStatus.setRevision(svnInfo.getRevision());
261       svnStatus.setKind(NodeKind.from(filePath.isDirectory()));
262       processChangeInList(SvnContentRevision.createBaseRevision(myVcs, filePath, svnInfo.getRevision()),
263                           CurrentContentRevision.create(filePath), FileStatus.MODIFIED, svnStatus);
264     }
265   }
266
267   private void processChangeInList(@Nullable ContentRevision beforeRevision,
268                                    @Nullable ContentRevision afterRevision,
269                                    @NotNull FileStatus fileStatus,
270                                    @NotNull Status status) throws SVNException {
271     Change change = createChange(beforeRevision, afterRevision, fileStatus, status);
272
273     myChangelistBuilder.processChangeInList(change, SvnUtil.getChangelistName(status), SvnVcs.getKey());
274   }
275
276   private void checkSwitched(@NotNull FilePath filePath, @NotNull Status status, @NotNull FileStatus convertedStatus) {
277     if (status.isSwitched() || (convertedStatus == FileStatus.SWITCHED)) {
278       final VirtualFile virtualFile = filePath.getVirtualFile();
279       if (virtualFile == null) return;
280       final String switchUrl = status.getURL().toString();
281       final VirtualFile vcsRoot = ProjectLevelVcsManager.getInstance(myVcs.getProject()).getVcsRootFor(virtualFile);
282       if (vcsRoot != null) {  // it will be null if we walked into an excluded directory
283         String baseUrl = myBranchConfigurationManager.get(vcsRoot).getBaseName(switchUrl);
284         myChangelistBuilder.processSwitchedFile(virtualFile, baseUrl == null ? switchUrl : baseUrl, true);
285       }
286     }
287   }
288
289   /**
290    * Ensures that the contents of the 'entries' file is cached in the VFS, so that the VFS will send
291    * correct events when the 'entries' file is changed externally (to be received by SvnEntriesFileListener)
292    *
293    * @param filePath the path of a changed file.
294    */
295   private void loadEntriesFile(@NotNull FilePath filePath) {
296     final FilePath parentPath = filePath.getParentPath();
297     if (parentPath == null) {
298       return;
299     }
300     refreshDotSvnAndEntries(parentPath);
301     if (filePath.isDirectory()) {
302       refreshDotSvnAndEntries(filePath);
303     }
304   }
305
306   private void refreshDotSvnAndEntries(@NotNull FilePath filePath) {
307     final File svn = new File(filePath.getPath(), SvnUtil.SVN_ADMIN_DIR_NAME);
308
309     filesToRefresh.add(svn);
310     filesToRefresh.add(new File(svn, SvnUtil.ENTRIES_FILE_NAME));
311   }
312
313   // seems here we can only have a tree conflict; which can be marked on either path (?)
314   // .. ok try to merge states
315   @NotNull
316   Change createMovedChange(@NotNull ContentRevision before,
317                            @NotNull ContentRevision after,
318                            @Nullable Status copiedStatus,
319                            @NotNull Status deletedStatus) throws SVNException {
320     // todo no convertion needed for the contents status?
321     ConflictedSvnChange change =
322       new ConflictedSvnChange(before, after, ConflictState.mergeState(getState(copiedStatus), getState(deletedStatus)),
323                               ((copiedStatus != null) && (copiedStatus.getTreeConflict() != null)) ? after.getFile() : before.getFile());
324     change.setBeforeDescription(deletedStatus.getTreeConflict());
325     if (copiedStatus != null) {
326       change.setAfterDescription(copiedStatus.getTreeConflict());
327       patchWithPropertyChange(change, copiedStatus, deletedStatus);
328     }
329
330     return change;
331   }
332
333   @NotNull
334   private Change createChange(@Nullable ContentRevision before,
335                               @Nullable ContentRevision after,
336                               @NotNull FileStatus fStatus,
337                               @NotNull Status svnStatus)
338     throws SVNException {
339     ConflictedSvnChange change =
340       new ConflictedSvnChange(before, after, fStatus, getState(svnStatus), after == null ? before.getFile() : after.getFile());
341
342     change.setIsPhantom(StatusType.STATUS_DELETED.equals(svnStatus.getNodeStatus()) && !svnStatus.getRevision().isValid());
343     change.setBeforeDescription(svnStatus.getTreeConflict());
344     patchWithPropertyChange(change, svnStatus, null);
345
346     return change;
347   }
348
349   private void patchWithPropertyChange(@NotNull Change change, @NotNull Status svnStatus, @Nullable Status deletedStatus)
350     throws SVNException {
351     if (svnStatus.isProperty(StatusType.STATUS_CONFLICTED, StatusType.CHANGED, StatusType.STATUS_ADDED, StatusType.STATUS_DELETED,
352                              StatusType.STATUS_MODIFIED, StatusType.STATUS_REPLACED, StatusType.MERGED)) {
353       change.addAdditionalLayerElement(SvnChangeProvider.PROPERTY_LAYER, createPropertyChange(change, svnStatus, deletedStatus));
354     }
355   }
356
357   @NotNull
358   private Change createPropertyChange(@NotNull Change change, @NotNull Status svnStatus, @Nullable Status deletedStatus)
359     throws SVNException {
360     final File ioFile = ChangesUtil.getFilePath(change).getIOFile();
361     final File beforeFile = deletedStatus != null ? deletedStatus.getFile() : ioFile;
362
363     // TODO: There are cases when status output is like (on newly added file with some properties that is locally deleted)
364     // <entry path="some_path"> <wc-status item="missing" revision="-1" props="modified"> </wc-status> </entry>
365     // TODO: For such cases in current logic we'll have Change with before revision containing SVNRevision.UNDEFINED
366     // TODO: Analyze if this logic is OK or we should update flow somehow (for instance, to have null before revision)
367     ContentRevision beforeRevision =
368       !svnStatus.isProperty(StatusType.STATUS_ADDED) || deletedStatus != null ? createPropertyRevision(change, beforeFile, true) : null;
369     ContentRevision afterRevision = !svnStatus.isProperty(StatusType.STATUS_DELETED) ? createPropertyRevision(change, ioFile, false) : null;
370     FileStatus status =
371       deletedStatus != null ? FileStatus.MODIFIED : SvnStatusConvertor.convertPropertyStatus(svnStatus.getPropertiesStatus());
372
373     return new Change(beforeRevision, afterRevision, status);
374   }
375
376   @Nullable
377   private ContentRevision createPropertyRevision(@NotNull Change change, @NotNull File file, boolean isBeforeRevision)
378     throws SVNException {
379     FilePath path = ChangesUtil.getFilePath(change);
380     ContentRevision contentRevision = isBeforeRevision ? change.getBeforeRevision() : change.getAfterRevision();
381     SVNRevision revision = isBeforeRevision ? SVNRevision.BASE : SVNRevision.WORKING;
382
383     return new SimplePropertyRevision(getProperties(file, revision), path, getRevisionNumber(contentRevision));
384   }
385
386   @Nullable
387   private static String getRevisionNumber(@Nullable ContentRevision revision) {
388     return revision != null ? revision.getRevisionNumber().asString() : null;
389   }
390
391   @NotNull
392   private List<PropertyData> getProperties(@NotNull File file, @NotNull SVNRevision revision) throws SVNException {
393     return AbstractShowPropertiesDiffAction.getPropertyList(myVcs, file, revision);
394   }
395
396   @NotNull
397   private ConflictState getState(@Nullable Status svnStatus) {
398     ConflictState result = svnStatus != null ? ConflictState.from(svnStatus) : ConflictState.none;
399
400     if (result.isTree()) {
401       //noinspection ConstantConditions
402       reportTreeConflict(svnStatus);
403     }
404
405     return result;
406   }
407 }