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