Merge remote-tracking branch 'origin/master'
[idea/community.git] / plugins / svn4idea / src / org / jetbrains / idea / svn / SvnUtil.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.idea.svn;
17
18 import com.intellij.notification.NotificationType;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.progress.ProcessCanceledException;
22 import com.intellij.openapi.progress.ProgressIndicator;
23 import com.intellij.openapi.progress.ProgressManager;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.util.Computable;
26 import com.intellij.openapi.util.Pair;
27 import com.intellij.openapi.util.Ref;
28 import com.intellij.openapi.util.io.FileUtil;
29 import com.intellij.openapi.util.io.FileUtilRt;
30 import com.intellij.openapi.util.text.StringUtil;
31 import com.intellij.openapi.vcs.AbstractVcsHelper;
32 import com.intellij.openapi.vcs.FilePath;
33 import com.intellij.openapi.vcs.VcsException;
34 import com.intellij.openapi.vcs.changes.Change;
35 import com.intellij.openapi.vcs.changes.ChangesUtil;
36 import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
37 import com.intellij.openapi.vfs.LocalFileSystem;
38 import com.intellij.openapi.vfs.VfsUtilCore;
39 import com.intellij.openapi.vfs.VirtualFile;
40 import com.intellij.openapi.vfs.VirtualFileManager;
41 import com.intellij.openapi.wm.impl.status.StatusBarUtil;
42 import com.intellij.util.ArrayUtil;
43 import com.intellij.util.containers.ContainerUtil;
44 import com.intellij.util.containers.Convertor;
45 import org.jetbrains.annotations.NonNls;
46 import org.jetbrains.annotations.NotNull;
47 import org.jetbrains.annotations.Nullable;
48 import org.jetbrains.idea.svn.api.Depth;
49 import org.jetbrains.idea.svn.api.EventAction;
50 import org.jetbrains.idea.svn.api.ProgressEvent;
51 import org.jetbrains.idea.svn.api.ProgressTracker;
52 import org.jetbrains.idea.svn.branchConfig.SvnBranchConfigurationManager;
53 import org.jetbrains.idea.svn.branchConfig.SvnBranchConfigurationNew;
54 import org.jetbrains.idea.svn.browse.DirectoryEntry;
55 import org.jetbrains.idea.svn.browse.DirectoryEntryConsumer;
56 import org.jetbrains.idea.svn.commandLine.SvnBindException;
57 import org.jetbrains.idea.svn.dialogs.LockDialog;
58 import org.jetbrains.idea.svn.info.Info;
59 import org.jetbrains.idea.svn.status.Status;
60 import org.tmatesoft.sqljet.core.SqlJetException;
61 import org.tmatesoft.sqljet.core.table.SqlJetDb;
62 import org.tmatesoft.svn.core.SVNErrorCode;
63 import org.tmatesoft.svn.core.SVNErrorMessage;
64 import org.tmatesoft.svn.core.SVNException;
65 import org.tmatesoft.svn.core.SVNURL;
66 import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
67 import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
68 import org.tmatesoft.svn.core.internal.wc2.SvnWcGeneration;
69 import org.tmatesoft.svn.core.wc.SVNRevision;
70 import org.tmatesoft.svn.core.wc.SVNWCUtil;
71 import org.tmatesoft.svn.core.wc2.SvnOperationFactory;
72 import org.tmatesoft.svn.core.wc2.SvnTarget;
73
74 import java.io.File;
75 import java.net.URI;
76 import java.nio.channels.NonWritableChannelException;
77 import java.util.*;
78 import java.util.regex.Matcher;
79 import java.util.regex.Pattern;
80
81 public class SvnUtil {
82   // TODO: ASP.NET hack behavior should be supported - http://svn.apache.org/repos/asf/subversion/trunk/notes/asp-dot-net-hack.txt
83   // TODO: Remember this when moving out SVNKit classes.
84   @NonNls public static final String SVN_ADMIN_DIR_NAME = SVNFileUtil.getAdminDirectoryName();
85   @NonNls public static final String ENTRIES_FILE_NAME = "entries";
86   @NonNls public static final String WC_DB_FILE_NAME = "wc.db";
87   @NonNls public static final String PATH_TO_LOCK_FILE = SVN_ADMIN_DIR_NAME + "/lock";
88   public static final int DEFAULT_PORT_INDICATOR = -1;
89   private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.svn.SvnUtil");
90
91   public static final Pattern ERROR_PATTERN = Pattern.compile("^svn: (E(\\d+)): (.*)$", Pattern.MULTILINE);
92   public static final Pattern WARNING_PATTERN = Pattern.compile("^svn: warning: (W(\\d+)): (.*)$", Pattern.MULTILINE);
93
94   private static final Pair<SVNURL, WorkingCopyFormat> UNKNOWN_REPOSITORY_AND_FORMAT = Pair.create(null, WorkingCopyFormat.UNKNOWN);
95
96   private SvnUtil() { }
97
98   @Nullable
99   public static SVNErrorMessage parseWarning(@NotNull String text) {
100     Matcher matcher = WARNING_PATTERN.matcher(text);
101     SVNErrorMessage error = null;
102
103     // currently treating only first warning
104     if (matcher.find()) {
105       error = SVNErrorMessage
106         .create(SVNErrorCode.getErrorCode(Integer.parseInt(matcher.group(2))), matcher.group(3), SVNErrorMessage.TYPE_WARNING);
107     }
108
109     return error;
110   }
111
112   public static boolean isSvnVersioned(final Project project, File parent) {
113     return isSvnVersioned(SvnVcs.getInstance(project), parent);
114   }
115
116   public static boolean isSvnVersioned(final @NotNull SvnVcs vcs, File parent) {
117     final Info info = vcs.getInfo(parent);
118
119     return info != null;
120   }
121
122   public static Collection<VirtualFile> crawlWCRoots(final Project project, File path, SvnWCRootCrawler callback, ProgressIndicator progress) {
123     final LocalFileSystem lfs = LocalFileSystem.getInstance();
124     VirtualFile vf = lfs.findFileByIoFile(path);
125     if (vf == null) {
126       vf = lfs.refreshAndFindFileByIoFile(path);
127     }
128     if (vf == null) return Collections.emptyList();
129     return crawlWCRoots(project, vf, callback, progress);
130   }
131
132   private static Collection<VirtualFile> crawlWCRoots(final Project project, VirtualFile vf, SvnWCRootCrawler callback, ProgressIndicator progress) {
133     final Collection<VirtualFile> result = new HashSet<VirtualFile>();
134     final boolean isDirectory = vf.isDirectory();
135     VirtualFile parent = ! isDirectory || !vf.exists() ? vf.getParent() : vf;
136
137     final File parentIo = new File(parent.getPath());
138     if (isSvnVersioned(project, parentIo)) {
139       checkCanceled(progress);
140       File ioFile = new File(vf.getPath());
141       callback.handleWorkingCopyRoot(ioFile, progress);
142       checkCanceled(progress);
143       result.add(parent);
144     } else if (isDirectory) {
145       checkCanceled(progress);
146       final VirtualFile[] childrenVF = parent.getChildren();
147       for (VirtualFile file : childrenVF) {
148         checkCanceled(progress);
149         if (file.isDirectory()) {
150           result.addAll(crawlWCRoots(project, file, callback, progress));
151         }
152       }
153     }
154     return result;
155   }
156
157   private static void checkCanceled(final ProgressIndicator progress) {
158     if (progress != null && progress.isCanceled()) {
159       throw new ProcessCanceledException();
160     }
161   }
162
163   @Nullable
164   public static String getExactLocation(final SvnVcs vcs, File path) {
165     Info info = vcs.getInfo(path);
166     return info != null && info.getURL() != null ? info.getURL().toString() : null;
167   }
168
169   public static void doLockFiles(Project project, final SvnVcs activeVcs, @NotNull final File[] ioFiles) throws VcsException {
170     final String lockMessage;
171     final boolean force;
172     // TODO[yole]: check for shift pressed
173     if (activeVcs.getCheckoutOptions().getValue()) {
174       LockDialog dialog = new LockDialog(project, true, ioFiles.length > 1);
175       if (!dialog.showAndGet()) {
176         return;
177       }
178       lockMessage = dialog.getComment();
179       force = dialog.isForce();
180     }
181     else {
182       lockMessage = "";
183       force = false;
184     }
185
186     final VcsException[] exception = new VcsException[1];
187     final Collection<String> failedLocks = new ArrayList<String>();
188     final int[] count = new int[]{ioFiles.length};
189     final ProgressTracker eventHandler = new ProgressTracker() {
190       public void consume(ProgressEvent event) {
191         if (event.getAction() == EventAction.LOCK_FAILED) {
192           failedLocks.add(event.getErrorMessage() != null ?
193                           event.getErrorMessage().getFullMessage() :
194                           event.getFile().getAbsolutePath());
195           count[0]--;
196         }
197       }
198
199       public void checkCancelled() {
200       }
201     };
202
203     Runnable command = new Runnable() {
204       public void run() {
205         ProgressIndicator progress = ProgressManager.getInstance().getProgressIndicator();
206
207         try {
208           if (progress != null) {
209             progress.setText(SvnBundle.message("progress.text.locking.files"));
210           }
211           for (File ioFile : ioFiles) {
212             if (progress != null) {
213               progress.checkCanceled();
214             }
215             if (progress != null) {
216               progress.setText2(SvnBundle.message("progress.text2.processing.file", ioFile.getName()));
217             }
218             activeVcs.getFactory(ioFile).createLockClient().lock(ioFile, force, lockMessage, eventHandler);
219           }
220         }
221         catch (VcsException e) {
222           exception[0] = e;
223         }
224       }
225     };
226
227     ProgressManager.getInstance().runProcessWithProgressSynchronously(command, SvnBundle.message("progress.title.lock.files"), false, project);
228     if (!failedLocks.isEmpty()) {
229       String[] failedFiles = ArrayUtil.toStringArray(failedLocks);
230       List<VcsException> exceptions = new ArrayList<VcsException>();
231       for (String file : failedFiles) {
232         exceptions.add(new VcsException(SvnBundle.message("exception.text.locking.file.failed", file)));
233       }
234       final StringBuilder sb = new StringBuilder(SvnBundle.message("message.text.files.lock.failed", failedFiles.length == 1 ? 0 : 1));
235       for (VcsException vcsException : exceptions) {
236         if (sb.length() > 0) sb.append('\n');
237         sb.append(vcsException.getMessage());
238       }
239       //AbstractVcsHelper.getInstance(project).showErrors(exceptions, SvnBundle.message("message.title.lock.failures"));
240       throw new VcsException(sb.toString());
241     }
242
243     StatusBarUtil.setStatusBarInfo(project, SvnBundle.message("message.text.files.locked", count[0]));
244     if (exception[0] != null) {
245       throw exception[0];
246     }
247   }
248
249   public static void doUnlockFiles(Project project, final SvnVcs activeVcs, final File[] ioFiles) throws VcsException {
250     final boolean force = true;
251     final VcsException[] exception = new VcsException[1];
252     final Collection<String> failedUnlocks = new ArrayList<String>();
253     final int[] count = new int[]{ioFiles.length};
254     final ProgressTracker eventHandler = new ProgressTracker() {
255       public void consume(ProgressEvent event) {
256         if (event.getAction() == EventAction.UNLOCK_FAILED) {
257           failedUnlocks.add(event.getErrorMessage() != null ?
258                             event.getErrorMessage().getFullMessage() :
259                             event.getFile().getAbsolutePath());
260           count[0]--;
261         }
262       }
263
264       public void checkCancelled() {
265       }
266     };
267
268     Runnable command = new Runnable() {
269       public void run() {
270         ProgressIndicator progress = ProgressManager.getInstance().getProgressIndicator();
271
272         try {
273           if (progress != null) {
274             progress.setText(SvnBundle.message("progress.text.unlocking.files"));
275           }
276           for (File ioFile : ioFiles) {
277             if (progress != null) {
278               progress.checkCanceled();
279             }
280             if (progress != null) {
281               progress.setText2(SvnBundle.message("progress.text2.processing.file", ioFile.getName()));
282             }
283             activeVcs.getFactory(ioFile).createLockClient().unlock(ioFile, force, eventHandler);
284           }
285         }
286         catch (VcsException e) {
287           exception[0] = e;
288         }
289       }
290     };
291
292     ProgressManager.getInstance().runProcessWithProgressSynchronously(command, SvnBundle.message("progress.title.unlock.files"), false, project);
293     if (!failedUnlocks.isEmpty()) {
294       String[] failedFiles = ArrayUtil.toStringArray(failedUnlocks);
295       List<VcsException> exceptions = new ArrayList<VcsException>();
296
297       for (String file : failedFiles) {
298         exceptions.add(new VcsException(SvnBundle.message("exception.text.failed.to.unlock.file", file)));
299       }
300       AbstractVcsHelper.getInstance(project).showErrors(exceptions, SvnBundle.message("message.title.unlock.failures"));
301     }
302
303     StatusBarUtil.setStatusBarInfo(project, SvnBundle.message("message.text.files.unlocked", count[0]));
304     if (exception[0] != null) {
305       throw new VcsException(exception[0]);
306     }
307   }
308
309   @NotNull
310   public static Map<Pair<SVNURL, WorkingCopyFormat>, Set<Change>> splitChangesIntoWc(@NotNull SvnVcs vcs, @NotNull List<Change> changes) {
311     return splitIntoRepositoriesMap(vcs, changes, new Convertor<Change, FilePath>() {
312       @Override
313       public FilePath convert(@NotNull Change change) {
314         return ChangesUtil.getFilePath(change);
315       }
316     });
317   }
318
319   @NotNull
320   public static <T> Map<Pair<SVNURL, WorkingCopyFormat>, Set<T>> splitIntoRepositoriesMap(@NotNull final SvnVcs vcs,
321                                                                                           @NotNull List<T> items,
322                                                                                           @NotNull final Convertor<T, FilePath> converter) {
323     return ContainerUtil.classify(items.iterator(), new Convertor<T, Pair<SVNURL, WorkingCopyFormat>>() {
324       @Override
325       public Pair<SVNURL, WorkingCopyFormat> convert(@NotNull T item) {
326         RootUrlInfo path = vcs.getSvnFileUrlMapping().getWcRootForFilePath(converter.convert(item).getIOFile());
327
328         return path == null ? UNKNOWN_REPOSITORY_AND_FORMAT : Pair.create(path.getRepositoryUrlUrl(), path.getFormat());
329       }
330     });
331   }
332
333   /**
334    * Gets working copy internal format. Works for 1.7 and 1.8.
335    *
336    * @param path
337    * @return
338    */
339   @NotNull
340   public static WorkingCopyFormat getFormat(final File path) {
341     WorkingCopyFormat result = null;
342     File dbFile = resolveDatabase(path);
343
344     if (dbFile != null) {
345       result = FileUtilRt.doIOOperation(new WorkingCopyFormatOperation(dbFile));
346
347       if (result == null) {
348         notifyDatabaseError();
349       }
350     }
351
352     return result != null ? result : WorkingCopyFormat.UNKNOWN;
353   }
354
355   private static void close(@Nullable SqlJetDb db) {
356     if (db != null) {
357       try {
358         db.close();
359       }
360       catch (SqlJetException e) {
361         notifyDatabaseError();
362       }
363     }
364   }
365
366   private static void notifyDatabaseError() {
367     VcsBalloonProblemNotifier.NOTIFICATION_GROUP
368       .createNotification("Some errors occurred while accessing svn working copy database.", NotificationType.ERROR).notify(null);
369   }
370
371   private static File resolveDatabase(final File path) {
372     File dbFile = getWcDb(path);
373     File result = null;
374
375     try {
376       if (dbFile.exists() && dbFile.isFile()) {
377         result = dbFile;
378       }
379     } catch (SecurityException e) {
380       LOG.error("Failed to access working copy database", e);
381     }
382
383     return result;
384   }
385
386   @Nullable
387   public static String getRepositoryUUID(final SvnVcs vcs, final File file) {
388     final Info info = vcs.getInfo(file);
389     return info != null ? info.getRepositoryUUID() : null;
390   }
391
392   @Nullable
393   public static String getRepositoryUUID(final SvnVcs vcs, final SVNURL url) {
394     try {
395       final Info info = vcs.getInfo(url, SVNRevision.UNDEFINED);
396
397       return (info == null) ? null : info.getRepositoryUUID();
398     }
399     catch (SvnBindException e) {
400       return null;
401     }
402   }
403
404   @Nullable
405   public static SVNURL getRepositoryRoot(final SvnVcs vcs, final File file) {
406     final Info info = vcs.getInfo(file);
407     return info != null ? info.getRepositoryRootURL() : null;
408   }
409
410   @Nullable
411   public static SVNURL getRepositoryRoot(final SvnVcs vcs, final String url) {
412     try {
413       return getRepositoryRoot(vcs, createUrl(url));
414     }
415     catch (SvnBindException e) {
416       return null;
417     }
418   }
419
420   @Nullable
421   public static SVNURL getRepositoryRoot(final SvnVcs vcs, final SVNURL url) throws SvnBindException {
422     Info info = vcs.getInfo(url, SVNRevision.HEAD);
423
424     return (info == null) ? null : info.getRepositoryRootURL();
425   }
426
427   public static boolean isWorkingCopyRoot(final File file) {
428     return FileUtil.filesEqual(file, getWorkingCopyRootNew(file));
429   }
430
431   @Nullable
432   public static File getWorkingCopyRoot(final File inFile) {
433     File file = inFile;
434     while ((file != null) && (file.isFile() || (! file.exists()))) {
435       file = file.getParentFile();
436     }
437
438     if (file == null) {
439       return null;
440     }
441
442     File workingCopyRoot = null;
443     try {
444       workingCopyRoot = SVNWCUtil.getWorkingCopyRoot(file, true);
445     } catch (SVNException e) {
446       //
447     }
448     if (workingCopyRoot == null) {
449      workingCopyRoot = getWcCopyRootIf17(file, null);
450     }
451     return workingCopyRoot;
452   }
453
454   @NotNull
455   public static File fileFromUrl(final File baseDir, final String baseUrl, final String fullUrl) {
456     assert fullUrl.startsWith(baseUrl);
457
458     final String part = fullUrl.substring(baseUrl.length()).replace('/', File.separatorChar).replace('\\', File.separatorChar);
459     return new File(baseDir, part);
460   }
461
462   public static VirtualFile getVirtualFile(final String filePath) {
463     @NonNls final String path = VfsUtilCore.pathToUrl(filePath.replace(File.separatorChar, '/'));
464     return ApplicationManager.getApplication().runReadAction(new Computable<VirtualFile>() {
465       @Nullable
466       public VirtualFile compute() {
467         return VirtualFileManager.getInstance().findFileByUrl(path);
468       }
469     });
470   }
471
472   @Nullable
473   public static SVNURL getBranchForUrl(@NotNull SvnVcs vcs, @NotNull VirtualFile vcsRoot, @NotNull String urlValue) {
474     SVNURL url = null;
475
476     try {
477       url = createUrl(urlValue);
478     }
479     catch (SvnBindException e) {
480       LOG.debug(e);
481     }
482
483     return url != null ? getBranchForUrl(vcs, vcsRoot, url) : null;
484   }
485
486   @Nullable
487   public static SVNURL getBranchForUrl(@NotNull SvnVcs vcs, @NotNull VirtualFile vcsRoot, @NotNull SVNURL url) {
488     SVNURL result = null;
489     SvnBranchConfigurationNew configuration = SvnBranchConfigurationManager.getInstance(vcs.getProject()).get(vcsRoot);
490
491     try {
492       result = configuration.getWorkingBranch(url);
493     }
494     catch (SvnBindException e) {
495       LOG.debug(e);
496     }
497
498     return result;
499   }
500
501   public static boolean checkRepositoryVersion15(@NotNull SvnVcs vcs, @NotNull String url) {
502     // Merge info tracking is supported in repositories since svn 1.5 (June 2008) - see http://subversion.apache.org/docs/release-notes/.
503     // But still some users use 1.4 repositories and currently we need to know if repository supports merge info for some code flows.
504
505     boolean result = false;
506
507     try {
508       result = vcs.getFactory().createRepositoryFeaturesClient().supportsMergeTracking(createUrl(url));
509     }
510     catch (VcsException e) {
511       LOG.info(e);
512       // TODO: Exception is thrown when url just not exist (was deleted, for instance) => and false is returned which seems not to be correct.
513     }
514
515     return result;
516   }
517
518   @Nullable
519   public static Status getStatus(@NotNull final SvnVcs vcs, @NotNull final File file) {
520     try {
521       return vcs.getFactory(file).createStatusClient().doStatus(file, false);
522     }
523     catch (SvnBindException e) {
524       return null;
525     }
526   }
527
528   public static Depth getDepth(final SvnVcs vcs, final File file) {
529     Info info = vcs.getInfo(file);
530
531     return info != null && info.getDepth() != null ? info.getDepth() : Depth.UNKNOWN;
532   }
533
534   public static boolean seemsLikeVersionedDir(final VirtualFile file) {
535     final String adminName = SVNFileUtil.getAdminDirectoryName();
536     final VirtualFile child = file.findChild(adminName);
537     return child != null && child.isDirectory();
538   }
539
540   public static boolean isAdminDirectory(final VirtualFile file) {
541     return isAdminDirectory(file.getParent(), file.getName());
542   }
543
544   public static boolean isAdminDirectory(VirtualFile parent, String name) {
545     // never allow to delete admin directories by themselves (this can happen during VCS undo,
546     // which deletes created directories from bottom to top)
547     if (name.equals(SVN_ADMIN_DIR_NAME)) {
548       return true;
549     }
550     if (parent != null) {
551       if (parent.getName().equals(SVN_ADMIN_DIR_NAME)) {
552         return true;
553       }
554       parent = parent.getParent();
555       if (parent != null && parent.getName().equals(SVN_ADMIN_DIR_NAME)) {
556         return true;
557       }
558     }
559     return false;
560   }
561
562   @Nullable
563   public static SVNURL getUrl(final SvnVcs vcs, final File file) {
564     // todo for moved items?
565     final Info info = vcs.getInfo(file);
566
567     return info == null ? null : info.getURL();
568   }
569
570   public static boolean remoteFolderIsEmpty(final SvnVcs vcs, final String url) throws VcsException {
571     SvnTarget target = SvnTarget.fromURL(createUrl(url));
572     final Ref<Boolean> result = new Ref<Boolean>(true);
573     DirectoryEntryConsumer handler = new DirectoryEntryConsumer() {
574
575       @Override
576       public void consume(final DirectoryEntry entry) throws SVNException {
577         if (entry != null) {
578           result.set(false);
579         }
580       }
581     };
582
583     vcs.getFactory(target).createBrowseClient().list(target, null, Depth.IMMEDIATES, handler);
584     return result.get();
585   }
586
587   public static File getWcDb(final File file) {
588     return new File(file, SVN_ADMIN_DIR_NAME + "/wc.db");
589   }
590
591   @Nullable
592   public static File getWcCopyRootIf17(final File file, @Nullable final File upperBound) {
593     File current = getParentWithDb(file);
594     if (current == null) return null;
595
596     while (current != null) {
597       try {
598         final SvnWcGeneration svnWcGeneration = SvnOperationFactory.detectWcGeneration(current, false);
599         if (SvnWcGeneration.V17.equals(svnWcGeneration)) return current;
600         if (SvnWcGeneration.V16.equals(svnWcGeneration)) return null;
601         if (upperBound != null && FileUtil.filesEqual(upperBound, current)) return null;
602         current = current.getParentFile();
603       }
604       catch (SVNException e) {
605         return null;
606       }
607     }
608     return null;
609   }
610
611   /**
612    * Utility method that deals also with 1.8 working copies.
613    * TODO: Should be renamed when all parts updated for 1.8.
614    *
615    * @param file
616    * @return
617    */
618   @Nullable
619   public static File getWorkingCopyRootNew(final File file) {
620     File current = getParentWithDb(file);
621     if (current == null) return getWorkingCopyRoot(file);
622
623     WorkingCopyFormat format = getFormat(current);
624
625     return format.isOrGreater(WorkingCopyFormat.ONE_DOT_SEVEN) ? current : getWorkingCopyRoot(file);
626   }
627
628   private static File getParentWithDb(File file) {
629     File current = file;
630     boolean wcDbFound = false;
631     while (current != null) {
632       File wcDb;
633       if ((wcDb = getWcDb(current)).exists() && ! wcDb.isDirectory()) {
634         wcDbFound = true;
635         break;
636       }
637       current = current.getParentFile();
638     }
639     if (! wcDbFound) return null;
640     return current;
641   }
642
643   public static String getRelativeUrl(@NotNull String parentUrl, @NotNull String childUrl) {
644     return FileUtilRt.getRelativePath(parentUrl, childUrl, '/', true);
645   }
646
647   public static String getRelativePath(@NotNull String parentPath, @NotNull String childPath) {
648     return  FileUtilRt.getRelativePath(FileUtil.toSystemIndependentName(parentPath), FileUtil.toSystemIndependentName(childPath), '/');
649   }
650
651   public static String ensureStartSlash(@NotNull String path) {
652     return StringUtil.startsWithChar(path, '/') ? path : '/' + path;
653   }
654
655   @NotNull
656   public static String join(@NotNull final String... parts) {
657     return StringUtil.join(parts, "/");
658   }
659
660   public static String appendMultiParts(@NotNull final String base, @NotNull final String subPath) {
661     if (StringUtil.isEmpty(subPath)) return base;
662     final List<String> parts = StringUtil.split(subPath.replace('\\', '/'), "/", true);
663     String result = base;
664     for (String part : parts) {
665       result = SVNPathUtil.append(result, part);
666     }
667     return result;
668   }
669
670   public static SVNURL appendMultiParts(@NotNull final SVNURL base, @NotNull final String subPath) throws SVNException {
671     if (StringUtil.isEmpty(subPath)) return base;
672     final List<String> parts = StringUtil.split(subPath.replace('\\', '/'), "/", true);
673     SVNURL result = base;
674     for (String part : parts) {
675       result = result.appendPath(part, false);
676     }
677     return result;
678   }
679
680   @NotNull
681   public static SVNURL removePathTail(@NotNull SVNURL url) throws SvnBindException {
682     return createUrl(SVNPathUtil.removeTail(url.toDecodedString()));
683   }
684
685   @NotNull
686   public static SVNRevision getHeadRevision(@NotNull SvnVcs vcs, @NotNull SVNURL url) throws SvnBindException {
687     Info info = vcs.getInfo(url, SVNRevision.HEAD);
688
689     if (info == null) {
690       throw new SvnBindException("Could not get info for " + url);
691     }
692     if (info.getRevision() == null) {
693       throw new SvnBindException("Could not get revision for " + url);
694     }
695
696     return info.getRevision();
697   }
698
699   public static byte[] getFileContents(@NotNull final SvnVcs vcs,
700                                        @NotNull final SvnTarget target,
701                                        @Nullable final SVNRevision revision,
702                                        @Nullable final SVNRevision pegRevision)
703     throws VcsException {
704     return vcs.getFactory(target).createContentClient().getContent(target, revision, pegRevision);
705   }
706
707   public static boolean hasDefaultPort(@NotNull SVNURL result) {
708     return !result.hasPort() || SVNURL.getDefaultPortNumber(result.getProtocol()) == result.getPort();
709   }
710
711   /**
712    * When creating SVNURL with default port, some negative value should be specified as port number, otherwise specified port value (even
713    * if equals to default) will occur in toString() result.
714    */
715   public static int resolvePort(@NotNull SVNURL url) {
716     return !hasDefaultPort(url) ? url.getPort() : DEFAULT_PORT_INDICATOR;
717   }
718
719   @NotNull
720   public static SVNURL createUrl(@NotNull String url) throws SvnBindException {
721     return createUrl(url, true);
722   }
723
724   @NotNull
725   public static SVNURL createUrl(@NotNull String url, boolean encoded) throws SvnBindException {
726     try {
727       SVNURL result = encoded ? SVNURL.parseURIEncoded(url) : SVNURL.parseURIDecoded(url);
728
729       // explicitly check if port corresponds to default port and recreate url specifying default port indicator
730       if (result.hasPort() && hasDefaultPort(result)) {
731         result = SVNURL
732           .create(result.getProtocol(), result.getUserInfo(), result.getHost(), DEFAULT_PORT_INDICATOR, result.getURIEncodedPath(), true);
733       }
734
735       return result;
736     }
737     catch (SVNException e) {
738       throw new SvnBindException(e);
739     }
740   }
741
742   public static SVNURL parseUrl(@NotNull String url) {
743     try {
744       return SVNURL.parseURIEncoded(url);
745     }
746     catch (SVNException e) {
747       throw createIllegalArgument(e);
748     }
749   }
750
751   public static SVNURL append(@NotNull SVNURL parent, String child) {
752     try {
753       return parent.appendPath(child, false);
754     }
755     catch (SVNException e) {
756       throw createIllegalArgument(e);
757     }
758   }
759
760   public static IllegalArgumentException createIllegalArgument(SVNException e) {
761     IllegalArgumentException runtimeException = new IllegalArgumentException();
762     runtimeException.initCause(e);
763     return runtimeException;
764   }
765
766   @Nullable
767   public static String getChangelistName(@NotNull final Status status) {
768     // no explicit check on working copy format supports change lists as they are supported from svn 1.5
769     // and anyway status.getChangelistName() should just return null if change lists are not supported.
770     return status.getKind().isFile() ? status.getChangelistName() : null;
771   }
772
773   public static boolean isUnversionedOrNotFound(@NotNull SvnBindException e) {
774     return e.contains(SVNErrorCode.WC_PATH_NOT_FOUND) ||
775            e.contains(SVNErrorCode.UNVERSIONED_RESOURCE) ||
776            e.contains(SVNErrorCode.WC_NOT_WORKING_COPY) ||
777            // thrown when getting info from repository for non-existent item - like HEAD revision for deleted file
778            e.contains(SVNErrorCode.ILLEGAL_TARGET) ||
779            // for svn 1.6
780            StringUtil.containsIgnoreCase(e.getMessage(), "(not a versioned resource)");
781   }
782
783   // TODO: Create custom Target class and implement append there
784   @NotNull
785   public static SvnTarget append(@NotNull SvnTarget target, @NotNull String path) throws SvnBindException {
786     return append(target, path, false);
787   }
788
789   @NotNull
790   public static SvnTarget append(@NotNull SvnTarget target, @NotNull String path, boolean checkAbsolute) throws SvnBindException {
791     SvnTarget result;
792
793     if (target.isFile()) {
794       result = SvnTarget.fromFile(resolvePath(target.getFile(), path));
795     }
796     else {
797       try {
798         result = SvnTarget
799           .fromURL(checkAbsolute && URI.create(path).isAbsolute() ? SVNURL.parseURIEncoded(path) : target.getURL().appendPath(path, false));
800       }
801       catch (SVNException e) {
802         throw new SvnBindException(e);
803       }
804     }
805
806     return result;
807   }
808
809   @NotNull
810   public static File resolvePath(@NotNull File base, @NotNull String path) {
811     File result = new File(path);
812
813     if (!result.isAbsolute()) {
814       result = ".".equals(path) ? base : new File(base, path);
815     }
816
817     return result;
818   }
819
820   /**
821    * {@code SvnTarget.getPathOrUrlDecodedString} does not correctly work for URL targets - {@code SVNURL.toString} instead of
822    * {@code SVNURL.toDecodedString} is used.
823    * <p/>
824    * Current utility method fixes this case.
825    */
826   @NotNull
827   public static String toDecodedString(@NotNull SvnTarget target) {
828     return target.isFile() ? target.getFile().getPath() : target.getURL().toDecodedString();
829   }
830
831   private static class WorkingCopyFormatOperation implements FileUtilRt.RepeatableIOOperation<WorkingCopyFormat, RuntimeException> {
832     @NotNull private final File myDbFile;
833
834     public WorkingCopyFormatOperation(@NotNull File dbFile) {
835       myDbFile = dbFile;
836     }
837
838     @Nullable
839     @Override
840     public WorkingCopyFormat execute(boolean lastAttempt) {
841       // TODO: rewrite it using sqlite jdbc driver
842       SqlJetDb db = null;
843       WorkingCopyFormat result = null;
844       try {
845         // "write" access is requested here for now as workaround - see some details
846         // in https://code.google.com/p/sqljet/issues/detail?id=25 and http://issues.tmatesoft.com/issue/SVNKIT-418.
847         // BUSY error is currently handled same way as others.
848         db = SqlJetDb.open(myDbFile, true);
849         result = WorkingCopyFormat.getInstance(db.getOptions().getUserVersion());
850       }
851       catch (NonWritableChannelException e) {
852         // Such exceptions could be thrown when db is opened in "read" mode, but the db file is readonly (for instance, locked
853         // by other process). See links above for some details.
854         // handle this exception type separately - not to break execution flow
855         LOG.info(e);
856       }
857       catch (SqlJetException e) {
858         LOG.info(e);
859       }
860       finally {
861         close(db);
862       }
863       return result;
864     }
865   }
866 }