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