f98d4de249fa17d95bdadacc938a97f7b055860f
[idea/community.git] / plugins / svn4idea / src / org / jetbrains / idea / svn / history / SvnChangeList.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
17 /*
18  * Created by IntelliJ IDEA.
19  * User: yole
20  * Date: 28.11.2006
21  * Time: 17:20:32
22  */
23 package org.jetbrains.idea.svn.history;
24
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.util.Pair;
27 import com.intellij.openapi.util.text.StringUtil;
28 import com.intellij.openapi.vcs.AbstractVcs;
29 import com.intellij.openapi.vcs.FilePath;
30 import com.intellij.openapi.vcs.VcsException;
31 import com.intellij.openapi.vcs.changes.*;
32 import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.util.ConstantFunction;
35 import com.intellij.util.NotNullFunction;
36 import com.intellij.util.UriUtil;
37 import com.intellij.util.containers.ContainerUtil;
38 import com.intellij.vcsUtil.VcsUtil;
39 import org.jetbrains.annotations.NotNull;
40 import org.jetbrains.annotations.Nullable;
41 import org.jetbrains.idea.svn.*;
42 import org.jetbrains.idea.svn.api.Depth;
43 import org.jetbrains.idea.svn.browse.DirectoryEntry;
44 import org.jetbrains.idea.svn.browse.DirectoryEntryConsumer;
45 import org.jetbrains.idea.svn.commandLine.SvnBindException;
46 import org.jetbrains.idea.svn.info.Info;
47 import org.tmatesoft.svn.core.SVNException;
48 import org.tmatesoft.svn.core.SVNURL;
49 import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
50 import org.tmatesoft.svn.core.wc.SVNRevision;
51 import org.tmatesoft.svn.core.wc2.SvnTarget;
52
53 import java.io.DataInput;
54 import java.io.DataOutput;
55 import java.io.File;
56 import java.io.IOException;
57 import java.util.*;
58
59 public class SvnChangeList implements CommittedChangeList {
60   private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.svn.history");
61
62   private final SvnVcs myVcs;
63   private final SvnRepositoryLocation myLocation;
64   private String myRepositoryRoot;
65   private long myRevision;
66   private String myAuthor;
67   private Date myDate;
68   private String myMessage;
69   private final Set<String> myChangedPaths = new HashSet<String>();
70   private final Set<String> myAddedPaths = new HashSet<String>();
71   private final Set<String> myDeletedPaths = new HashSet<String>();
72   private final Set<String> myReplacedPaths = new HashSet<String>();
73
74   private ChangesListCreationHelper myListsHolder;
75
76   private SVNURL myBranchUrl;
77
78   private boolean myCachedInfoLoaded;
79
80   // key: added path, value: copied-from
81   private final TreeMap<String, String> myCopiedAddedPaths = new TreeMap<String, String>();
82   private RootUrlInfo myWcRoot;
83   private final CommonPathSearcher myCommonPathSearcher;
84   private final Set<String> myKnownAsDirectories;
85
86   public SvnChangeList(@NotNull final List<CommittedChangeList> lists, @NotNull final SvnRepositoryLocation location) {
87
88     final SvnChangeList sample = (SvnChangeList) lists.get(0);
89     myVcs = sample.myVcs;
90     myLocation = location;
91     myRevision = sample.myRevision;
92     myAuthor = sample.myAuthor;
93     myDate = sample.myDate;
94     myMessage = sample.myMessage;
95     myRepositoryRoot = sample.myRepositoryRoot;
96     myCommonPathSearcher = new CommonPathSearcher();
97
98     for (CommittedChangeList list : lists) {
99       final SvnChangeList svnList = (SvnChangeList) list;
100       myChangedPaths.addAll(svnList.myChangedPaths);
101       myAddedPaths.addAll(svnList.myAddedPaths);
102       myDeletedPaths.addAll(svnList.myDeletedPaths);
103       myReplacedPaths.addAll(svnList.myReplacedPaths);
104     }
105     myKnownAsDirectories = new HashSet<String>(0);
106   }
107
108   public SvnChangeList(SvnVcs vcs, @NotNull final SvnRepositoryLocation location, final LogEntry logEntry, String repositoryRoot) {
109     myVcs = vcs;
110     myLocation = location;
111     myRevision = logEntry.getRevision();
112     myAuthor = StringUtil.notNullize(logEntry.getAuthor());
113     myDate = logEntry.getDate();
114     myMessage = StringUtil.notNullize(logEntry.getMessage());
115     myRepositoryRoot = UriUtil.trimTrailingSlashes(repositoryRoot);
116
117     myCommonPathSearcher = new CommonPathSearcher();
118
119     myKnownAsDirectories = new HashSet<String>(0);
120     for(LogEntryPath entry : logEntry.getChangedPaths().values()) {
121       final String path = entry.getPath();
122
123       if (entry.isDirectory()) {
124         myKnownAsDirectories.add(path);
125       }
126
127       myCommonPathSearcher.next(path);
128       
129       if (entry.getType() == 'A') {
130         if (entry.getCopyPath() != null) {
131           myCopiedAddedPaths.put(path, entry.getCopyPath());
132         }
133         myAddedPaths.add(path);
134       }
135       else if (entry.getType() == 'D') {
136         myDeletedPaths.add(path);
137       }
138       else {
139         if (entry.getType() == 'R') {
140           myReplacedPaths.add(path);
141         }
142         myChangedPaths.add(path);
143       }
144     }
145   }
146
147   public SvnChangeList(SvnVcs vcs, @NotNull SvnRepositoryLocation location, @NotNull DataInput stream, final boolean supportsCopyFromInfo,
148                        final boolean supportsReplaced) throws IOException {
149     myVcs = vcs;
150     myLocation = location;
151     myKnownAsDirectories = new HashSet<String>();
152     readFromStream(stream, supportsCopyFromInfo, supportsReplaced);
153     myCommonPathSearcher = new CommonPathSearcher();
154     myCommonPathSearcher.next(myAddedPaths);
155     myCommonPathSearcher.next(myDeletedPaths);
156     myCommonPathSearcher.next(myChangedPaths);
157   }
158
159   public Change getByPath(final String path) {
160     if (myListsHolder == null) {
161       createLists();
162     }
163     return myListsHolder.getByPath(path);
164   }
165
166   public String getCommitterName() {
167     return myAuthor;
168   }
169
170   public Date getCommitDate() {
171     return myDate;
172   }
173
174
175   public Collection<Change> getChanges() {
176     if (myListsHolder == null) {
177       createLists();
178     }
179     return myListsHolder.getList();
180   }
181
182   private void createLists() {
183     myListsHolder = new ChangesListCreationHelper();
184     
185     // key: copied-from
186     final Map<String, ExternallyRenamedChange> copiedAddedChanges = new HashMap<String, ExternallyRenamedChange>();
187
188     correctBeforePaths();
189     final List<String> copyDeleted = new ArrayList<String>(myDeletedPaths);
190
191     for(String path: myAddedPaths) {
192       final Change addedChange;
193       if (myCopiedAddedPaths.containsKey(path)) {
194         final String copyTarget = myCopiedAddedPaths.get(path);
195         if (copyDeleted.contains(copyTarget)) {
196           addedChange = new ExternallyRenamedChange(myListsHolder.createRevisionLazily(copyTarget, true),
197                                                     myListsHolder.createRevisionLazily(path, false), copyTarget);
198           addedChange.getMoveRelativePath(myVcs.getProject());
199           ((ExternallyRenamedChange) addedChange).setCopied(false);
200           copyDeleted.remove(copyTarget);
201         } else {
202           addedChange = new ExternallyRenamedChange(null, myListsHolder.createRevisionLazily(path, false), copyTarget);
203         }
204         copiedAddedChanges.put(copyTarget, (ExternallyRenamedChange) addedChange);
205       } else {
206         addedChange = new Change(null, myListsHolder.createRevisionLazily(path, false));
207       }
208       myListsHolder.add(path, addedChange);
209     }
210     for(String path: copyDeleted) {
211       final Change deletedChange;
212       if (copiedAddedChanges.containsKey(path)) {
213         // seems never occurs any more
214         final ExternallyRenamedChange addedChange = copiedAddedChanges.get(path);
215         final FilePath source = addedChange.getAfterRevision().getFile();
216         deletedChange = new ExternallyRenamedChange(myListsHolder.createDeletedItemRevision(path, true), null, path);
217         ((ExternallyRenamedChange) deletedChange).setCopied(false);
218         //noinspection ConstantConditions
219         //addedChange.setRenamedOrMovedTarget(deletedChange.getBeforeRevision().getFile());
220         //noinspection ConstantConditions
221         ((ExternallyRenamedChange) deletedChange).setRenamedOrMovedTarget(source);
222       } else {
223         deletedChange = new Change(myListsHolder.createDeletedItemRevision(path, true), null);
224       }
225       myListsHolder.add(path, deletedChange);
226     }
227     for(String path: myChangedPaths) {
228       boolean moveAndChange = false;
229       final boolean replaced = myReplacedPaths.contains(path);
230
231       // this piece: for copied-from (or moved) and further modified
232       for (String addedPath : myAddedPaths) {
233         String copyFromPath = myCopiedAddedPaths.get(addedPath);
234         if ((copyFromPath != null) && (SVNPathUtil.isAncestor(addedPath, path))) {
235           if (addedPath.length() < path.length()) {
236             final String relative = SVNPathUtil.getRelativePath(addedPath, path);
237             copyFromPath = SVNPathUtil.append(copyFromPath, relative);
238           }
239           final ExternallyRenamedChange renamedChange = new ExternallyRenamedChange(myListsHolder.createRevisionLazily(copyFromPath, true),
240                                                      myListsHolder.createRevisionLazily(path, false), copyFromPath);
241           moveAndChange = true;
242           renamedChange.getMoveRelativePath(myVcs.getProject());
243           renamedChange.setIsReplaced(replaced);
244
245           final ExternallyRenamedChange addedChange = copiedAddedChanges.get(myCopiedAddedPaths.get(addedPath));
246           renamedChange.setCopied(addedChange != null && addedChange.isCopied());
247
248           myListsHolder.add(path, renamedChange);
249           break;
250         }
251       }
252       if (! moveAndChange) {
253         final ExternallyRenamedChange renamedChange =
254           new ExternallyRenamedChange(myListsHolder.createRevisionLazily(path, true), myListsHolder.createRevisionLazily(path, false),
255                                       null);
256         renamedChange.setIsReplaced(replaced);
257         renamedChange.setCopied(false);
258         myListsHolder.add(path, renamedChange);
259       }
260     }
261   }
262
263   private void correctBeforePaths() {
264     processDeletedForBeforePaths(myDeletedPaths);
265     processModifiedForBeforePaths(myChangedPaths);
266     processModifiedForBeforePaths(myReplacedPaths);
267   }
268
269   private void processModifiedForBeforePaths(Set<String> paths) {
270     final RenameHelper helper = new RenameHelper();
271     for (String s : paths) {
272       final String converted = helper.convertBeforePath(s, myCopiedAddedPaths);
273       if (! s.equals(converted)) {
274         myCopiedAddedPaths.put(s, converted);
275       }
276     }
277   }
278
279   private void processDeletedForBeforePaths(Set<String> paths) {
280     final RenameHelper helper = new RenameHelper();
281     final HashSet<String> copy = new HashSet<String>(paths);
282     paths.clear();
283     for (String s : copy) {
284       paths.add(helper.convertBeforePath(s, myCopiedAddedPaths));
285     }
286   }
287
288   @Nullable
289   private FilePath getLocalPath(final String path, final NotNullFunction<File, Boolean> detector) {
290     return SvnRepositoryLocation.getLocalPath(myRepositoryRoot + path, detector, myVcs);
291   }
292
293   private long getRevision(final boolean isBeforeRevision) {
294     return isBeforeRevision ? (myRevision - 1) : myRevision;
295   }
296
297   public SvnRepositoryLocation getLocation() {
298     return myLocation;
299   }
300
301   /**
302    * needed to track in which changes non-local files live
303    */
304   private class ChangesListCreationHelper {
305     private final List<Change> myList;
306     private final Map<String, Change> myPathToChangeMapping;
307     private List<Change> myDetailedList;
308     private final List<Pair<Integer, Boolean>> myWithoutDirStatus;
309
310     private ChangesListCreationHelper() {
311       myList = new ArrayList<Change>();
312       myWithoutDirStatus = new ArrayList<Pair<Integer, Boolean>>();
313       myPathToChangeMapping = new HashMap<String, Change>();
314     }
315
316     public void add(final String path, final Change change) {
317       patchChange(change, path);
318       myList.add(change);
319       myPathToChangeMapping.put(path, change);
320     }
321
322     public Change getByPath(final String path) {
323       return myPathToChangeMapping.get(path);
324     }
325
326     private FilePath localDeletedPath(final String fullPath, final boolean isDir) {
327       final SvnFileUrlMapping urlMapping = myVcs.getSvnFileUrlMapping();
328       final String path = urlMapping.getLocalPath(fullPath);
329       if (path != null) {
330         File file = new File(path);
331         return VcsUtil.getFilePathForDeletedFile(path, isDir || file.isDirectory());
332       }
333
334       return null;
335     }
336
337     public SvnRepositoryContentRevision createDeletedItemRevision(final String path, final boolean isBeforeRevision) {
338       final boolean knownAsDirectory = myKnownAsDirectories.contains(path);
339       final String fullPath = myRepositoryRoot + path;
340       if (! knownAsDirectory) {
341         myWithoutDirStatus.add(Pair.create(myList.size(), isBeforeRevision));
342       }
343       return SvnRepositoryContentRevision.create(myVcs, myRepositoryRoot, path, localDeletedPath(fullPath, knownAsDirectory),
344                                                  getRevision(isBeforeRevision));
345     }
346
347     public SvnRepositoryContentRevision createRevisionLazily(final String path, final boolean isBeforeRevision) {
348       final boolean knownAsDirectory = myKnownAsDirectories.contains(path);
349       final FilePath localPath = getLocalPath(path, new NotNullFunction<File, Boolean>() {
350         @NotNull
351         public Boolean fun(final File file) {
352           if (knownAsDirectory) return Boolean.TRUE;
353           // list will be next
354           myWithoutDirStatus.add(new Pair<Integer, Boolean>(myList.size(), isBeforeRevision));
355           return Boolean.FALSE;
356         }
357       });
358       long revision = getRevision(isBeforeRevision);
359       return localPath == null
360              ? SvnRepositoryContentRevision.createForRemotePath(myVcs, myRepositoryRoot, path, knownAsDirectory, revision)
361              : SvnRepositoryContentRevision.create(myVcs, myRepositoryRoot, path, localPath, revision);
362     }
363
364     public List<Change> getList() {
365       return myList;
366     }
367
368     public List<Change> getDetailedList() {
369       if (myDetailedList == null) {
370         myDetailedList = new ArrayList<Change>(myList);
371
372         try {
373           doRemoteDetails();
374           uploadDeletedRenamedChildren();
375           ContainerUtil.removeDuplicates(myDetailedList);
376         }
377         catch (SVNException e) {
378           LOG.info(e);
379         }
380         catch (VcsException e) {
381           LOG.info(e);
382         }
383       }
384       return myDetailedList;
385     }
386
387     private void doRemoteDetails() throws SVNException, SvnBindException {
388       for (Pair<Integer, Boolean> idxData : myWithoutDirStatus) {
389         final Change sourceChange = myDetailedList.get(idxData.first.intValue());
390         final SvnRepositoryContentRevision revision = (SvnRepositoryContentRevision)
391             (idxData.second.booleanValue() ? sourceChange.getBeforeRevision() : sourceChange.getAfterRevision());
392         if (revision == null) {
393           continue;
394         }
395         // TODO: Logic with detecting "isDirectory" status is not clear enough. Why we can't just collect this info from logEntry and
396         // TODO: if loading from disk - use cached values? Not to invoke separate call here.
397         SVNRevision beforeRevision = SVNRevision.create(getRevision(idxData.second.booleanValue()));
398         Info info = myVcs.getInfo(SvnUtil.createUrl(revision.getFullPath()), beforeRevision, beforeRevision);
399         boolean isDirectory = info != null && info.isDirectory();
400         Change replacingChange = new Change(createRevision((SvnRepositoryContentRevision)sourceChange.getBeforeRevision(), isDirectory),
401                                             createRevision((SvnRepositoryContentRevision)sourceChange.getAfterRevision(), isDirectory));
402         replacingChange.setIsReplaced(sourceChange.isIsReplaced());
403         myDetailedList.set(idxData.first.intValue(), replacingChange);
404       }
405
406       myWithoutDirStatus.clear();
407     }
408
409     @Nullable
410     private SvnRepositoryContentRevision createRevision(final SvnRepositoryContentRevision previousRevision, final boolean isDir) {
411       return previousRevision == null ? null :
412              SvnRepositoryContentRevision.create(myVcs, previousRevision.getFullPath(),
413                                                  VcsUtil.getFilePath(previousRevision.getFile().getIOFile(), isDir),
414                                                  previousRevision.getRevisionNumber().getRevision().getNumber());
415     }
416
417     private void uploadDeletedRenamedChildren() throws VcsException {
418       Set<Pair<Boolean, String>> duplicates = collectDuplicates();
419       List<Change> preprocessed = ChangesPreprocess.preprocessChangesRemoveDeletedForDuplicateMoved(myDetailedList);
420
421       myDetailedList.addAll(collectDetails(preprocessed, duplicates));
422     }
423
424     private List<Change> collectDetails(@NotNull List<Change> changes, @NotNull Set<Pair<Boolean, String>> duplicates)
425       throws VcsException {
426       List<Change> result = ContainerUtil.newArrayList();
427
428       for (Change change : changes) {
429         // directory statuses are already uploaded
430         if ((change.getAfterRevision() == null) && (change.getBeforeRevision().getFile().isDirectory())) {
431           result.addAll(getChildrenAsChanges(change.getBeforeRevision(), true, duplicates));
432         } else if ((change.getBeforeRevision() == null) && (change.getAfterRevision().getFile().isDirectory())) {
433           // look for renamed folders contents
434           if (myCopiedAddedPaths.containsKey(getRelativePath(change.getAfterRevision()))) {
435             result.addAll(getChildrenAsChanges(change.getAfterRevision(), false, duplicates));
436           }
437         } else if ((change.isIsReplaced() || change.isMoved() || change.isRenamed()) && change.getAfterRevision().getFile().isDirectory()) {
438           result.addAll(getChildrenAsChanges(change.getBeforeRevision(), true, duplicates));
439           result.addAll(getChildrenAsChanges(change.getAfterRevision(), false, duplicates));
440         }
441       }
442
443       return result;
444     }
445
446     private Set<Pair<Boolean, String>> collectDuplicates() {
447       Set<Pair<Boolean, String>> result = ContainerUtil.newHashSet();
448
449       for (Change change : myDetailedList) {
450         addDuplicate(result, true, change.getBeforeRevision());
451         addDuplicate(result, false, change.getAfterRevision());
452       }
453
454       return result;
455     }
456
457     private void addDuplicate(@NotNull Set<Pair<Boolean, String>> duplicates,
458                               boolean isBefore,
459                               @Nullable ContentRevision revision) {
460       if (revision != null) {
461         duplicates.add(Pair.create(isBefore, getRelativePath(revision)));
462       }
463     }
464
465     @NotNull
466     private String getRelativePath(@NotNull ContentRevision revision) {
467       return ((SvnRepositoryContentRevision)revision).getRelativePath(myRepositoryRoot);
468     }
469
470     @NotNull
471     private Collection<Change> getChildrenAsChanges(@NotNull ContentRevision contentRevision,
472                                                     final boolean isBefore,
473                                                     @NotNull final Set<Pair<Boolean, String>> duplicates)
474       throws VcsException {
475       final List<Change> result = new ArrayList<Change>();
476
477       final String path = getRelativePath(contentRevision);
478       SVNURL fullPath = SvnUtil.createUrl(((SvnRepositoryContentRevision)contentRevision).getFullPath());
479       SVNRevision revisionNumber = SVNRevision.create(getRevision(isBefore));
480       SvnTarget target = SvnTarget.fromURL(fullPath, revisionNumber);
481
482       myVcs.getFactory(target).createBrowseClient().list(target, revisionNumber, Depth.INFINITY, new DirectoryEntryConsumer() {
483
484         @Override
485         public void consume(final DirectoryEntry entry) throws SVNException {
486           final String childPath = path + '/' + entry.getRelativePath();
487
488           if (!duplicates.contains(Pair.create(isBefore, childPath))) {
489             final ContentRevision contentRevision = createRevision(childPath, isBefore, entry.isDirectory());
490             result.add(new Change(isBefore ? contentRevision : null, isBefore ? null : contentRevision));
491           }
492         }
493       });
494
495       return result;
496     }
497
498     private SvnRepositoryContentRevision createRevision(final String path, final boolean isBeforeRevision, final boolean isDir) {
499       return SvnRepositoryContentRevision.create(myVcs, myRepositoryRoot, path,
500                                                  getLocalPath(path, new ConstantFunction<File, Boolean>(isDir)), getRevision(isBeforeRevision));
501     }
502   }
503
504   private static class RenameHelper {
505
506     public String convertBeforePath(final String path, final TreeMap<String, String> after2before) {
507       String current = path;
508       // backwards
509       for (String key : after2before.descendingKeySet()) {
510         if (SVNPathUtil.isAncestor(key, current)) {
511           final String relativePath = SVNPathUtil.getRelativePath(key, current);
512           current = SVNPathUtil.append(after2before.get(key), relativePath);
513         }
514       }
515       return current;
516     }
517   }
518
519   private void patchChange(Change change, final String path) {
520     final SVNURL becameUrl;
521     SVNURL wasUrl;
522     try {
523       becameUrl = SVNURL.parseURIEncoded(SVNPathUtil.append(myRepositoryRoot, path));
524       wasUrl = becameUrl;
525
526       if (change instanceof ExternallyRenamedChange && change.getBeforeRevision() != null) {
527         String originUrl = ((ExternallyRenamedChange)change).getOriginUrl();
528
529         if (originUrl != null) {
530           // use another url for origin
531           wasUrl = SVNURL.parseURIEncoded(SVNPathUtil.append(myRepositoryRoot, originUrl));
532         }
533       }
534     }
535     catch (SVNException e) {
536       // nothing to do
537       LOG.info(e);
538       return;
539     }
540
541     final FilePath filePath = ChangesUtil.getFilePath(change);
542     final Change additional = new Change(createPropertyRevision(filePath, change.getBeforeRevision(), wasUrl),
543                                          createPropertyRevision(filePath, change.getAfterRevision(), becameUrl));
544     change.addAdditionalLayerElement(SvnChangeProvider.PROPERTY_LAYER, additional);
545   }
546
547   @Nullable
548   private SvnLazyPropertyContentRevision createPropertyRevision(@NotNull FilePath filePath,
549                                                                 @Nullable ContentRevision revision,
550                                                                 @NotNull SVNURL url) {
551     return revision == null ? null : new SvnLazyPropertyContentRevision(filePath, revision.getRevisionNumber(), myVcs.getProject(), url);
552   }
553
554   @NotNull
555   public String getName() {
556     return myMessage;
557   }
558
559   public String getComment() {
560     return myMessage;
561   }
562
563   public long getNumber() {
564     return myRevision;
565   }
566
567   @Override
568   public String getBranch() {
569     return null;
570   }
571
572   public AbstractVcs getVcs() {
573     return myVcs;
574   }
575
576   public Collection<Change> getChangesWithMovedTrees() {
577     if (myListsHolder == null) {
578       createLists();
579     }
580
581     return myListsHolder.getDetailedList();
582   }
583
584   @Override
585   public boolean isModifiable() {
586     return true;
587   }
588
589   @Override
590   public void setDescription(String newMessage) {
591     myMessage = newMessage;
592   }
593
594   public boolean equals(final Object o) {
595     if (this == o) return true;
596     if (o == null || getClass() != o.getClass()) return false;
597
598     final SvnChangeList that = (SvnChangeList)o;
599
600     if (myRevision != that.myRevision) return false;
601     if (myAuthor != null ? !myAuthor.equals(that.myAuthor) : that.myAuthor != null) return false;
602     if (myDate != null ? !myDate.equals(that.myDate) : that.myDate != null) return false;
603     if (myMessage != null ? !myMessage.equals(that.myMessage) : that.myMessage != null) return false;
604
605     return true;
606   }
607
608   public int hashCode() {
609     int result;
610     result = (int)(myRevision ^ (myRevision >>> 32));
611     result = 31 * result + (myAuthor != null ? myAuthor.hashCode() : 0);
612     result = 31 * result + (myDate != null ? myDate.hashCode() : 0);
613     result = 31 * result + (myMessage != null ? myMessage.hashCode() : 0);
614     return result;
615   }
616
617   public String toString() {
618     return myMessage;
619   }
620
621   public void writeToStream(@NotNull DataOutput stream) throws IOException {
622     stream.writeUTF(myRepositoryRoot);
623     stream.writeLong(myRevision);
624     stream.writeUTF(myAuthor);
625     stream.writeLong(myDate.getTime());
626     writeUTFTruncated(stream, myMessage);
627     writeFiles(stream, myChangedPaths);
628     writeFiles(stream, myAddedPaths);
629     writeFiles(stream, myDeletedPaths);
630     writeMap(stream, myCopiedAddedPaths);
631     writeFiles(stream, myReplacedPaths);
632
633     stream.writeInt(myKnownAsDirectories.size());
634     for (String directory : myKnownAsDirectories) {
635       stream.writeUTF(directory);
636     }
637   }
638
639   // to be able to update plugin only
640   public static void writeUTFTruncated(final DataOutput stream, final String text) throws IOException {
641     // we should not compare number of symbols to 65635 -> it is number of bytes what should be compared
642     // ? 4 bytes per symbol - rough estimation
643     if (text.length() > 16383) {
644       stream.writeUTF(text.substring(0, 16383));
645     }
646     else {
647       stream.writeUTF(text);
648     }
649   }
650
651   private static void writeFiles(final DataOutput stream, final Set<String> paths) throws IOException {
652     stream.writeInt(paths.size());
653     for(String s: paths) {
654       stream.writeUTF(s);
655     }
656   }
657
658   private void readFromStream(@NotNull DataInput stream, final boolean supportsCopyFromInfo, final boolean supportsReplaced)
659     throws IOException {
660     myRepositoryRoot = stream.readUTF();
661     myRevision = stream.readLong();
662     myAuthor = stream.readUTF();
663     myDate = new Date(stream.readLong());
664     myMessage = stream.readUTF();
665     readFiles(stream, myChangedPaths);
666     readFiles(stream, myAddedPaths);
667     readFiles(stream, myDeletedPaths);
668
669     if (supportsCopyFromInfo) {
670       readMap(stream, myCopiedAddedPaths);
671     }
672
673     if (supportsReplaced) {
674       readFiles(stream, myReplacedPaths);
675     }
676
677     final int size = stream.readInt();
678     for (int i = 0; i < size; i++) {
679       myKnownAsDirectories.add(stream.readUTF());
680     }
681   }
682
683   private static void writeMap(final DataOutput stream, final Map<String, String> map) throws IOException {
684     stream.writeInt(map.size());
685     for (Map.Entry<String, String> entry : map.entrySet()) {
686       stream.writeUTF(entry.getKey());
687       stream.writeUTF(entry.getValue());
688     }
689   }
690
691   private static void readMap(final DataInput stream, final Map<String, String> map) throws IOException {
692     int count = stream.readInt();
693     for (int i = 0; i < count; i++) {
694       map.put(stream.readUTF(), stream.readUTF());
695     }
696   }
697
698   private static void readFiles(final DataInput stream, final Set<String> paths) throws IOException {
699     int count = stream.readInt();
700     for(int i=0; i<count; i++) {
701       paths.add(stream.readUTF());
702     }
703   }
704
705   public SVNURL getBranchUrl() {
706     ensureCacheUpdated();
707
708     return myBranchUrl;
709   }
710
711   @Nullable
712   public VirtualFile getVcsRoot() {
713     ensureCacheUpdated();
714
715     return myWcRoot == null ? null : myWcRoot.getRoot();
716   }
717
718   @Nullable
719   public VirtualFile getRoot() {
720     ensureCacheUpdated();
721
722     return myWcRoot == null ? null : myWcRoot.getVirtualFile();
723   }
724
725   public RootUrlInfo getWcRootInfo() {
726     ensureCacheUpdated();
727
728     return myWcRoot;
729   }
730
731   private void ensureCacheUpdated() {
732     if (!myCachedInfoLoaded) {
733       updateCachedInfo();
734     }
735   }
736
737   private static class CommonPathSearcher {
738     private String myCommon;
739
740     public void next(Iterable<String> values) {
741       for (String value : values) {
742         next(value);
743       }
744     }
745
746     public void next(final String value) {
747       if (value == null) {
748         return;
749       }
750       if (myCommon == null) {
751         myCommon = value;
752         return;
753       }
754
755       if (value.startsWith(myCommon)) {
756         return;
757       }
758
759       myCommon = SVNPathUtil.getCommonPathAncestor(myCommon, value);
760     }
761
762     public String getCommon() {
763       return myCommon;
764     }
765   }
766
767   private void updateCachedInfo() {
768     myCachedInfoLoaded = true;
769
770     final String commonPath = myCommonPathSearcher.getCommon();
771     if (commonPath != null) {
772       final SvnFileUrlMapping urlMapping = myVcs.getSvnFileUrlMapping();
773       if (urlMapping.isEmpty()) {
774         myCachedInfoLoaded = false;
775         return;
776       }
777       final String absoluteUrl = SVNPathUtil.append(myRepositoryRoot, commonPath);
778       myWcRoot = urlMapping.getWcRootForUrl(absoluteUrl);
779       if (myWcRoot != null) {
780         myBranchUrl = SvnUtil.getBranchForUrl(myVcs, myWcRoot.getVirtualFile(), absoluteUrl);
781       }
782     }
783   }
784
785   public void forceReloadCachedInfo(final boolean reloadRoot) {
786     myCachedInfoLoaded = false;
787     myBranchUrl = null;
788
789     if (reloadRoot) {
790       myWcRoot = null;
791     }
792   }
793
794   @NotNull
795   public Set<String> getAffectedPaths() {
796     return ContainerUtil.newHashSet(ContainerUtil.concat(myAddedPaths, myDeletedPaths, myChangedPaths));
797   }
798
799   @Nullable
800   public String getWcPath() {
801     final RootUrlInfo rootInfo = getWcRootInfo();
802
803     return rootInfo == null ? null : rootInfo.getIoFile().getAbsolutePath();
804   }
805
806   public boolean allPathsUnder(final String path) {
807     final String commonRelative = myCommonPathSearcher.getCommon();
808
809     return commonRelative != null && SVNPathUtil.isAncestor(path, SVNPathUtil.append(myRepositoryRoot, commonRelative));
810   }
811 }