2 * Copyright 2000-2009 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package org.jetbrains.idea.svn;
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.api.NodeKind;
34 import org.jetbrains.idea.svn.branchConfig.SvnBranchConfigurationManager;
35 import org.jetbrains.idea.svn.history.SimplePropertyRevision;
36 import org.jetbrains.idea.svn.info.Info;
37 import org.jetbrains.idea.svn.status.Status;
38 import org.jetbrains.idea.svn.status.StatusType;
39 import org.tmatesoft.svn.core.SVNException;
40 import org.tmatesoft.svn.core.SVNURL;
41 import org.tmatesoft.svn.core.wc.SVNRevision;
44 import java.util.List;
47 import static org.jetbrains.idea.svn.actions.ShowPropertiesDiffAction.getPropertyList;
49 class SvnChangeProviderContext implements StatusReceiver {
50 private static final Logger LOG = Logger.getInstance("org.jetbrains.idea.svn.SvnChangeProviderContext");
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();
62 @Nullable private final ProgressIndicator myProgress;
64 public SvnChangeProviderContext(@NotNull SvnVcs vcs, @NotNull ChangelistBuilder changelistBuilder, @Nullable ProgressIndicator progress) {
66 myChangelistBuilder = changelistBuilder;
67 myProgress = progress;
68 myBranchConfigurationManager = SvnBranchConfigurationManager.getInstance(myVcs.getProject());
71 public void process(FilePath path, Status status) throws SVNException {
73 processStatusFirstPass(path, status);
77 public void processIgnored(VirtualFile vFile) {
78 myChangelistBuilder.processIgnoredFile(vFile);
81 public void processUnversioned(VirtualFile vFile) {
82 myChangelistBuilder.processUnversionedFile(vFile);
86 public void processCopyRoot(VirtualFile file, SVNURL url, WorkingCopyFormat format, SVNURL rootURL) {
90 public void bewareRoot(VirtualFile vf, SVNURL url) {
94 public void finish() {
95 LocalFileSystem.getInstance().refreshIoFiles(filesToRefresh, true, false, null);
99 public ChangelistBuilder getBuilder() {
100 return myChangelistBuilder;
103 public void reportTreeConflict(@NotNull Status status) {
104 myTreeConflicted.put(status.getFile().getAbsolutePath(), status);
108 public Status getTreeConflictStatus(@NotNull File file) {
109 return myTreeConflicted.get(file.getAbsolutePath());
113 public List<SvnChangedFile> getCopiedFiles() {
114 return myCopiedFiles;
118 public List<SvnChangedFile> getDeletedFiles() {
119 return myDeletedFiles;
122 public boolean isDeleted(@NotNull FilePath path) {
123 for (SvnChangedFile deletedFile : myDeletedFiles) {
124 if (Comparing.equal(path, deletedFile.getFilePath())) {
131 public void checkCanceled() {
132 if (myProgress != null) {
133 myProgress.checkCanceled();
138 * If the specified filepath or its parent was added with history, returns the URL of the copy source for this filepath.
140 * @param filePath the original filepath
141 * @return the copy source url, or null if the file isn't a copy of anything
144 public String getParentCopyFromURL(@NotNull FilePath filePath) {
145 String result = null;
146 FilePath parent = filePath;
148 while (parent != null && !myCopyFromURLs.containsKey(parent)) {
149 parent = parent.getParentPath();
152 if (parent != null) {
153 String copyFromUrl = myCopyFromURLs.get(parent);
155 //noinspection ConstantConditions
156 result = parent == filePath
158 : SvnUtil.appendMultiParts(copyFromUrl, FileUtil.getRelativePath(parent.getIOFile(), filePath.getIOFile()));
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);
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));
173 if (status.getLocalLock() != null) {
174 myChangelistBuilder.processLogicallyLockedFolder(filePath.getVirtualFile(), status.getLocalLock().toLogicalLock(true));
176 if (filePath.isDirectory() && status.isLocked()) {
177 myChangelistBuilder.processLockedFolder(filePath.getVirtualFile());
179 if ((status.is(StatusType.STATUS_ADDED) || StatusType.STATUS_MODIFIED.equals(status.getNodeStatus())) &&
180 status.getCopyFromURL() != null) {
181 addCopiedFile(filePath, status, status.getCopyFromURL());
183 else if (status.is(StatusType.STATUS_DELETED)) {
184 myDeletedFiles.add(new SvnChangedFile(filePath, status));
187 String parentCopyFromURL = getParentCopyFromURL(filePath);
188 if (parentCopyFromURL != null) {
189 addCopiedFile(filePath, status, parentCopyFromURL);
192 processStatus(filePath, status);
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);
203 FileStatus fStatus = SvnStatusConvertor.convertStatus(status);
205 final StatusType statusType = status.getContentsStatus();
206 if (status.is(StatusType.STATUS_UNVERSIONED, StatusType.UNKNOWN)) {
207 final VirtualFile file = filePath.getVirtualFile();
209 myChangelistBuilder.processUnversionedFile(file);
212 else if (status.is(StatusType.STATUS_ADDED)) {
213 processChangeInList(null, CurrentContentRevision.create(filePath), fStatus, status);
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,
219 checkSwitched(filePath, status, fStatus);
221 else if (status.is(StatusType.STATUS_DELETED)) {
222 processChangeInList(SvnContentRevision.createBaseRevision(myVcs, filePath, status), null, fStatus, status);
224 else if (status.is(StatusType.STATUS_MISSING)) {
225 myChangelistBuilder.processLocallyDeletedFile(new SvnLocallyDeletedChange(filePath, getState(status)));
227 else if (status.is(StatusType.STATUS_IGNORED)) {
228 VirtualFile file = filePath.getVirtualFile();
230 file = LocalFileSystem.getInstance().refreshAndFindFileByPath(filePath.getPath());
233 LOG.error("No virtual file for ignored file: " + filePath.getPresentableUrl() + ", isNonLocal: " + filePath.isNonLocal());
235 else if (!myVcs.isWcRoot(filePath)) {
236 myChangelistBuilder.processIgnoredFile(filePath.getVirtualFile());
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);
245 else if (status.getTreeConflict() != null) {
246 myChangelistBuilder.processChange(createChange(SvnContentRevision.createBaseRevision(myVcs, filePath, status),
247 CurrentContentRevision.create(filePath), FileStatus.MODIFIED, status),
250 checkSwitched(filePath, status, fStatus);
254 public void addModifiedNotSavedChange(@NotNull VirtualFile file) throws SVNException {
255 final FilePath filePath = VcsUtil.getFilePath(file);
256 final Info svnInfo = myVcs.getInfo(file);
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);
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);
273 myChangelistBuilder.processChangeInList(change, SvnUtil.getChangelistName(status), SvnVcs.getKey());
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);
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)
293 * @param filePath the path of a changed file.
295 private void loadEntriesFile(@NotNull FilePath filePath) {
296 final FilePath parentPath = filePath.getParentPath();
297 if (parentPath == null) {
300 refreshDotSvnAndEntries(parentPath);
301 if (filePath.isDirectory()) {
302 refreshDotSvnAndEntries(filePath);
306 private void refreshDotSvnAndEntries(@NotNull FilePath filePath) {
307 final File svn = new File(filePath.getPath(), SvnUtil.SVN_ADMIN_DIR_NAME);
309 filesToRefresh.add(svn);
310 filesToRefresh.add(new File(svn, SvnUtil.ENTRIES_FILE_NAME));
313 // seems here we can only have a tree conflict; which can be marked on either path (?)
314 // .. ok try to merge states
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);
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());
342 change.setIsPhantom(StatusType.STATUS_DELETED.equals(svnStatus.getNodeStatus()) && !svnStatus.getRevision().isValid());
343 change.setBeforeDescription(svnStatus.getTreeConflict());
344 patchWithPropertyChange(change, svnStatus, null);
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));
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;
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;
371 deletedStatus != null ? FileStatus.MODIFIED : SvnStatusConvertor.convertPropertyStatus(svnStatus.getPropertiesStatus());
373 return new Change(beforeRevision, afterRevision, status);
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;
383 return new SimplePropertyRevision(getPropertyList(myVcs, file, revision), path, getRevisionNumber(contentRevision));
387 private static String getRevisionNumber(@Nullable ContentRevision revision) {
388 return revision != null ? revision.getRevisionNumber().asString() : null;
392 private ConflictState getState(@Nullable Status svnStatus) {
393 ConflictState result = svnStatus != null ? ConflictState.from(svnStatus) : ConflictState.none;
395 if (result.isTree()) {
396 //noinspection ConstantConditions
397 reportTreeConflict(svnStatus);