/* * Copyright 2000-2014 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jetbrains.idea.svn; import com.intellij.notification.NotificationType; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.AbstractVcsHelper; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangesUtil; import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.openapi.wm.impl.status.StatusBarUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.Convertor; import com.intellij.util.containers.MultiMap; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.svn.api.Depth; import org.jetbrains.idea.svn.api.EventAction; import org.jetbrains.idea.svn.api.ProgressEvent; import org.jetbrains.idea.svn.api.ProgressTracker; import org.jetbrains.idea.svn.branchConfig.SvnBranchConfigurationNew; import org.jetbrains.idea.svn.browse.DirectoryEntry; import org.jetbrains.idea.svn.browse.DirectoryEntryConsumer; import org.jetbrains.idea.svn.commandLine.SvnBindException; import org.jetbrains.idea.svn.dialogs.LockDialog; import org.jetbrains.idea.svn.info.Info; import org.jetbrains.idea.svn.status.Status; import org.tmatesoft.sqljet.core.SqlJetException; import org.tmatesoft.sqljet.core.table.SqlJetDb; import org.tmatesoft.svn.core.*; import org.tmatesoft.svn.core.internal.util.SVNPathUtil; import org.tmatesoft.svn.core.internal.wc.SVNFileUtil; import org.tmatesoft.svn.core.internal.wc2.SvnWcGeneration; import org.tmatesoft.svn.core.wc.*; import org.tmatesoft.svn.core.wc2.SvnOperationFactory; import org.tmatesoft.svn.core.wc2.SvnTarget; import java.io.File; import java.net.URI; import java.nio.channels.NonWritableChannelException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class SvnUtil { // TODO: ASP.NET hack behavior should be supported - http://svn.apache.org/repos/asf/subversion/trunk/notes/asp-dot-net-hack.txt // TODO: Remember this when moving out SVNKit classes. @NonNls public static final String SVN_ADMIN_DIR_NAME = SVNFileUtil.getAdminDirectoryName(); @NonNls public static final String ENTRIES_FILE_NAME = "entries"; @NonNls public static final String WC_DB_FILE_NAME = "wc.db"; @NonNls public static final String PATH_TO_LOCK_FILE = SVN_ADMIN_DIR_NAME + "/lock"; public static final int DEFAULT_PORT_INDICATOR = -1; private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.svn.SvnUtil"); public static final Pattern ERROR_PATTERN = Pattern.compile("^svn: (E(\\d+)): (.*)$", Pattern.MULTILINE); public static final Pattern WARNING_PATTERN = Pattern.compile("^svn: warning: (W(\\d+)): (.*)$", Pattern.MULTILINE); private SvnUtil() { } @Nullable public static SVNErrorMessage parseWarning(@NotNull String text) { Matcher matcher = WARNING_PATTERN.matcher(text); SVNErrorMessage error = null; // currently treating only first warning if (matcher.find()) { error = SVNErrorMessage .create(SVNErrorCode.getErrorCode(Integer.parseInt(matcher.group(2))), matcher.group(3), SVNErrorMessage.TYPE_WARNING); } return error; } public static boolean isSvnVersioned(final Project project, File parent) { return isSvnVersioned(SvnVcs.getInstance(project), parent); } public static boolean isSvnVersioned(final @NotNull SvnVcs vcs, File parent) { final Info info = vcs.getInfo(parent); return info != null; } public static List toFiles(Iterable paths) { List result = ContainerUtil.newArrayList(); for (String path : paths) { result.add(new File(path)); } return result; } public static Collection crawlWCRoots(final Project project, File path, SvnWCRootCrawler callback, ProgressIndicator progress) { final LocalFileSystem lfs = LocalFileSystem.getInstance(); VirtualFile vf = lfs.findFileByIoFile(path); if (vf == null) { vf = lfs.refreshAndFindFileByIoFile(path); } if (vf == null) return Collections.emptyList(); return crawlWCRoots(project, vf, callback, progress); } private static Collection crawlWCRoots(final Project project, VirtualFile vf, SvnWCRootCrawler callback, ProgressIndicator progress) { final Collection result = new HashSet(); final boolean isDirectory = vf.isDirectory(); VirtualFile parent = ! isDirectory || !vf.exists() ? vf.getParent() : vf; final File parentIo = new File(parent.getPath()); if (isSvnVersioned(project, parentIo)) { checkCanceled(progress); File ioFile = new File(vf.getPath()); callback.handleWorkingCopyRoot(ioFile, progress); checkCanceled(progress); result.add(parent); } else if (isDirectory) { checkCanceled(progress); final VirtualFile[] childrenVF = parent.getChildren(); for (VirtualFile file : childrenVF) { checkCanceled(progress); if (file.isDirectory()) { result.addAll(crawlWCRoots(project, file, callback, progress)); } } } return result; } private static void checkCanceled(final ProgressIndicator progress) { if (progress != null && progress.isCanceled()) { throw new ProcessCanceledException(); } } @Nullable public static String getExactLocation(final SvnVcs vcs, File path) { Info info = vcs.getInfo(path); return info != null && info.getURL() != null ? info.getURL().toString() : null; } public static void doLockFiles(Project project, final SvnVcs activeVcs, @NotNull final File[] ioFiles) throws VcsException { final String lockMessage; final boolean force; // TODO[yole]: check for shift pressed if (activeVcs.getCheckoutOptions().getValue()) { LockDialog dialog = new LockDialog(project, true, ioFiles.length > 1); dialog.show(); if (!dialog.isOK()) { return; } lockMessage = dialog.getComment(); force = dialog.isForce(); } else { lockMessage = ""; force = false; } final VcsException[] exception = new VcsException[1]; final Collection failedLocks = new ArrayList(); final int[] count = new int[]{ioFiles.length}; final ProgressTracker eventHandler = new ProgressTracker() { public void consume(ProgressEvent event) { if (event.getAction() == EventAction.LOCK_FAILED) { failedLocks.add(event.getErrorMessage() != null ? event.getErrorMessage().getFullMessage() : event.getFile().getAbsolutePath()); count[0]--; } } public void checkCancelled() { } }; Runnable command = new Runnable() { public void run() { ProgressIndicator progress = ProgressManager.getInstance().getProgressIndicator(); try { if (progress != null) { progress.setText(SvnBundle.message("progress.text.locking.files")); } for (File ioFile : ioFiles) { if (progress != null) { progress.checkCanceled(); } if (progress != null) { progress.setText2(SvnBundle.message("progress.text2.processing.file", ioFile.getName())); } activeVcs.getFactory(ioFile).createLockClient().lock(ioFile, force, lockMessage, eventHandler); } } catch (VcsException e) { exception[0] = e; } } }; ProgressManager.getInstance().runProcessWithProgressSynchronously(command, SvnBundle.message("progress.title.lock.files"), false, project); if (!failedLocks.isEmpty()) { String[] failedFiles = ArrayUtil.toStringArray(failedLocks); List exceptions = new ArrayList(); for (String file : failedFiles) { exceptions.add(new VcsException(SvnBundle.message("exception.text.locking.file.failed", file))); } final StringBuilder sb = new StringBuilder(SvnBundle.message("message.text.files.lock.failed", failedFiles.length == 1 ? 0 : 1)); for (VcsException vcsException : exceptions) { if (sb.length() > 0) sb.append('\n'); sb.append(vcsException.getMessage()); } //AbstractVcsHelper.getInstance(project).showErrors(exceptions, SvnBundle.message("message.title.lock.failures")); throw new VcsException(sb.toString()); } StatusBarUtil.setStatusBarInfo(project, SvnBundle.message("message.text.files.locked", count[0])); if (exception[0] != null) { throw exception[0]; } } public static void doUnlockFiles(Project project, final SvnVcs activeVcs, final File[] ioFiles) throws VcsException { final boolean force = true; final VcsException[] exception = new VcsException[1]; final Collection failedUnlocks = new ArrayList(); final int[] count = new int[]{ioFiles.length}; final ProgressTracker eventHandler = new ProgressTracker() { public void consume(ProgressEvent event) { if (event.getAction() == EventAction.UNLOCK_FAILED) { failedUnlocks.add(event.getErrorMessage() != null ? event.getErrorMessage().getFullMessage() : event.getFile().getAbsolutePath()); count[0]--; } } public void checkCancelled() { } }; Runnable command = new Runnable() { public void run() { ProgressIndicator progress = ProgressManager.getInstance().getProgressIndicator(); try { if (progress != null) { progress.setText(SvnBundle.message("progress.text.unlocking.files")); } for (File ioFile : ioFiles) { if (progress != null) { progress.checkCanceled(); } if (progress != null) { progress.setText2(SvnBundle.message("progress.text2.processing.file", ioFile.getName())); } activeVcs.getFactory(ioFile).createLockClient().unlock(ioFile, force, eventHandler); } } catch (VcsException e) { exception[0] = e; } } }; ProgressManager.getInstance().runProcessWithProgressSynchronously(command, SvnBundle.message("progress.title.unlock.files"), false, project); if (!failedUnlocks.isEmpty()) { String[] failedFiles = ArrayUtil.toStringArray(failedUnlocks); List exceptions = new ArrayList(); for (String file : failedFiles) { exceptions.add(new VcsException(SvnBundle.message("exception.text.failed.to.unlock.file", file))); } AbstractVcsHelper.getInstance(project).showErrors(exceptions, SvnBundle.message("message.title.unlock.failures")); } StatusBarUtil.setStatusBarInfo(project, SvnBundle.message("message.text.files.unlocked", count[0])); if (exception[0] != null) { throw new VcsException(exception[0]); } } public static Collection> splitChangesIntoWc(final SvnVcs vcs, final List changes) { return splitIntoRepositories(vcs, changes, new Convertor() { @Override public File convert(Change o) { return ChangesUtil.getFilePath(o).getIOFile(); } }); } public static Collection> splitIntoRepositories(final SvnVcs vcs, final List committables, Convertor convertor) { if (committables.size() == 1) { return Collections.singletonList(committables); } final MultiMap, T> result = splitIntoRepositoriesMap(vcs, committables, convertor); if (result.size() == 1) { return Collections.singletonList(committables); } final Collection> result2 = new ArrayList>(); for (Map.Entry, Collection> entry : result.entrySet()) { result2.add((List)entry.getValue()); } return result2; } public static MultiMap, T> splitIntoRepositoriesMap(SvnVcs vcs, List committables, Convertor convertor) { final MultiMap, T> result = MultiMap.create(); for (T committable : committables) { final RootUrlInfo path = vcs.getSvnFileUrlMapping().getWcRootForFilePath(convertor.convert(committable)); if (path == null) { result.putValue(new Pair(null, null), committable); } else { result.putValue(Pair.create(path.getRepositoryUrlUrl(), path.getFormat()), committable); } } return result; } /** * Gets working copy internal format. Works for 1.7 and 1.8. * * @param path * @return */ @NotNull public static WorkingCopyFormat getFormat(final File path) { WorkingCopyFormat result = null; File dbFile = resolveDatabase(path); if (dbFile != null) { result = FileUtilRt.doIOOperation(new WorkingCopyFormatOperation(dbFile)); if (result == null) { notifyDatabaseError(); } } return result != null ? result : WorkingCopyFormat.UNKNOWN; } private static void close(@Nullable SqlJetDb db) { if (db != null) { try { db.close(); } catch (SqlJetException e) { notifyDatabaseError(); } } } private static void notifyDatabaseError() { VcsBalloonProblemNotifier.NOTIFICATION_GROUP .createNotification("Some errors occurred while accessing svn working copy database.", NotificationType.ERROR).notify(null); } private static File resolveDatabase(final File path) { File dbFile = getWcDb(path); File result = null; try { if (dbFile.exists() && dbFile.isFile()) { result = dbFile; } } catch (SecurityException e) { LOG.error("Failed to access working copy database", e); } return result; } @Nullable public static String getRepositoryUUID(final SvnVcs vcs, final File file) { final Info info = vcs.getInfo(file); return info != null ? info.getRepositoryUUID() : null; } @Nullable public static String getRepositoryUUID(final SvnVcs vcs, final SVNURL url) { try { final Info info = vcs.getInfo(url, SVNRevision.UNDEFINED); return (info == null) ? null : info.getRepositoryUUID(); } catch (SvnBindException e) { return null; } } @Nullable public static SVNURL getRepositoryRoot(final SvnVcs vcs, final File file) { final Info info = vcs.getInfo(file); return info != null ? info.getRepositoryRootURL() : null; } @Nullable public static SVNURL getRepositoryRoot(final SvnVcs vcs, final String url) { try { return getRepositoryRoot(vcs, createUrl(url)); } catch (SvnBindException e) { return null; } } @Nullable public static SVNURL getRepositoryRoot(final SvnVcs vcs, final SVNURL url) throws SvnBindException { Info info = vcs.getInfo(url, SVNRevision.HEAD); return (info == null) ? null : info.getRepositoryRootURL(); } public static boolean isWorkingCopyRoot(final File file) { return FileUtil.filesEqual(file, getWorkingCopyRootNew(file)); } @Nullable public static File getWorkingCopyRoot(final File inFile) { File file = inFile; while ((file != null) && (file.isFile() || (! file.exists()))) { file = file.getParentFile(); } if (file == null) { return null; } File workingCopyRoot = null; try { workingCopyRoot = SVNWCUtil.getWorkingCopyRoot(file, true); } catch (SVNException e) { // } if (workingCopyRoot == null) { workingCopyRoot = getWcCopyRootIf17(file, null); } return workingCopyRoot; } public static File fileFromUrl(final File baseDir, final String baseUrl, final String fullUrl) throws SVNException { assert fullUrl.startsWith(baseUrl); final String part = fullUrl.substring(baseUrl.length()).replace('/', File.separatorChar).replace('\\', File.separatorChar); return new File(baseDir, part); } public static VirtualFile getVirtualFile(final String filePath) { @NonNls final String path = VfsUtilCore.pathToUrl(filePath.replace(File.separatorChar, '/')); return ApplicationManager.getApplication().runReadAction(new Computable() { @Nullable public VirtualFile compute() { return VirtualFileManager.getInstance().findFileByUrl(path); } }); } @Nullable public static SVNURL getBranchForUrl(final SvnVcs vcs, final VirtualFile vcsRoot, final String urlPath) { final SvnBranchConfigurationNew configuration; try { final SVNURL url = SVNURL.parseURIEncoded(urlPath); configuration = SvnBranchConfigurationManager.getInstance(vcs.getProject()).get(vcsRoot); return (configuration == null) ? null : configuration.getWorkingBranch(url); } catch (SVNException e) { return null; } catch (VcsException e1) { return null; } } @Nullable public static VirtualFile correctRoot(final Project project, final VirtualFile file) { if (file.getPath().length() == 0) { // project root return project.getBaseDir(); } return file; } public static boolean checkRepositoryVersion15(@NotNull SvnVcs vcs, @NotNull String url) { // Merge info tracking is supported in repositories since svn 1.5 (June 2008) - see http://subversion.apache.org/docs/release-notes/. // But still some users use 1.4 repositories and currently we need to know if repository supports merge info for some code flows. boolean result = false; try { result = vcs.getFactory().createRepositoryFeaturesClient().supportsMergeTracking(createUrl(url)); } catch (VcsException e) { LOG.info(e); // TODO: Exception is thrown when url just not exist (was deleted, for instance) => and false is returned which seems not to be correct. } return result; } @Nullable public static Status getStatus(@NotNull final SvnVcs vcs, @NotNull final File file) { try { return vcs.getFactory(file).createStatusClient().doStatus(file, false); } catch (SvnBindException e) { return null; } } public static Depth getDepth(final SvnVcs vcs, final File file) { Info info = vcs.getInfo(file); return info != null && info.getDepth() != null ? info.getDepth() : Depth.UNKNOWN; } public static boolean seemsLikeVersionedDir(final VirtualFile file) { final String adminName = SVNFileUtil.getAdminDirectoryName(); final VirtualFile child = file.findChild(adminName); return child != null && child.isDirectory(); } public static boolean isAdminDirectory(final VirtualFile file) { return isAdminDirectory(file.getParent(), file.getName()); } public static boolean isAdminDirectory(VirtualFile parent, String name) { // never allow to delete admin directories by themselves (this can happen during VCS undo, // which deletes created directories from bottom to top) if (name.equals(SVN_ADMIN_DIR_NAME)) { return true; } if (parent != null) { if (parent.getName().equals(SVN_ADMIN_DIR_NAME)) { return true; } parent = parent.getParent(); if (parent != null && parent.getName().equals(SVN_ADMIN_DIR_NAME)) { return true; } } return false; } @Nullable public static SVNURL getUrl(final SvnVcs vcs, final File file) { // todo for moved items? final Info info = vcs.getInfo(file); return info == null ? null : info.getURL(); } public static boolean remoteFolderIsEmpty(final SvnVcs vcs, final String url) throws VcsException { SvnTarget target = SvnTarget.fromURL(createUrl(url)); final Ref result = new Ref(true); DirectoryEntryConsumer handler = new DirectoryEntryConsumer() { @Override public void consume(final DirectoryEntry entry) throws SVNException { if (entry != null) { result.set(false); } } }; vcs.getFactory(target).createBrowseClient().list(target, null, Depth.IMMEDIATES, handler); return result.get(); } public static File getWcDb(final File file) { return new File(file, SVN_ADMIN_DIR_NAME + "/wc.db"); } @Nullable public static File getWcCopyRootIf17(final File file, @Nullable final File upperBound) { File current = getParentWithDb(file); if (current == null) return null; while (current != null) { try { final SvnWcGeneration svnWcGeneration = SvnOperationFactory.detectWcGeneration(current, false); if (SvnWcGeneration.V17.equals(svnWcGeneration)) return current; if (SvnWcGeneration.V16.equals(svnWcGeneration)) return null; if (upperBound != null && FileUtil.filesEqual(upperBound, current)) return null; current = current.getParentFile(); } catch (SVNException e) { return null; } } return null; } /** * Utility method that deals also with 1.8 working copies. * TODO: Should be renamed when all parts updated for 1.8. * * @param file * @return */ @Nullable public static File getWorkingCopyRootNew(final File file) { File current = getParentWithDb(file); if (current == null) return getWorkingCopyRoot(file); WorkingCopyFormat format = getFormat(current); return format.isOrGreater(WorkingCopyFormat.ONE_DOT_SEVEN) ? current : getWorkingCopyRoot(file); } private static File getParentWithDb(File file) { File current = file; boolean wcDbFound = false; while (current != null) { File wcDb; if ((wcDb = getWcDb(current)).exists() && ! wcDb.isDirectory()) { wcDbFound = true; break; } current = current.getParentFile(); } if (! wcDbFound) return null; return current; } public static String getRelativeUrl(@NotNull String parentUrl, @NotNull String childUrl) { return FileUtilRt.getRelativePath(parentUrl, childUrl, '/', true); } public static String getRelativePath(@NotNull String parentPath, @NotNull String childPath) { return FileUtilRt.getRelativePath(FileUtil.toSystemIndependentName(parentPath), FileUtil.toSystemIndependentName(childPath), '/'); } public static String ensureStartSlash(@NotNull String path) { return StringUtil.startsWithChar(path, '/') ? path : '/' + path; } @NotNull public static String join(@NotNull final String... parts) { return StringUtil.join(parts, "/"); } public static String appendMultiParts(@NotNull final String base, @NotNull final String subPath) { if (StringUtil.isEmpty(subPath)) return base; final List parts = StringUtil.split(subPath.replace('\\', '/'), "/", true); String result = base; for (String part : parts) { result = SVNPathUtil.append(result, part); } return result; } public static SVNURL appendMultiParts(@NotNull final SVNURL base, @NotNull final String subPath) throws SVNException { if (StringUtil.isEmpty(subPath)) return base; final List parts = StringUtil.split(subPath.replace('\\', '/'), "/", true); SVNURL result = base; for (String part : parts) { result = result.appendPath(part, false); } return result; } @NotNull public static SVNURL removePathTail(@NotNull SVNURL url) throws SvnBindException { return createUrl(SVNPathUtil.removeTail(url.toDecodedString())); } @NotNull public static SVNRevision getHeadRevision(@NotNull SvnVcs vcs, @NotNull SVNURL url) throws SvnBindException { Info info = vcs.getInfo(url, SVNRevision.HEAD); if (info == null) { throw new SvnBindException("Could not get info for " + url); } if (info.getRevision() == null) { throw new SvnBindException("Could not get revision for " + url); } return info.getRevision(); } public static byte[] getFileContents(@NotNull final SvnVcs vcs, @NotNull final SvnTarget target, @Nullable final SVNRevision revision, @Nullable final SVNRevision pegRevision) throws VcsException { return vcs.getFactory(target).createContentClient().getContent(target, revision, pegRevision); } public static boolean hasDefaultPort(@NotNull SVNURL result) { return !result.hasPort() || SVNURL.getDefaultPortNumber(result.getProtocol()) == result.getPort(); } /** * When creating SVNURL with default port, some negative value should be specified as port number, otherwise specified port value (even * if equals to default) will occur in toString() result. */ public static int resolvePort(@NotNull SVNURL url) { return !hasDefaultPort(url) ? url.getPort() : DEFAULT_PORT_INDICATOR; } @NotNull public static SVNURL createUrl(@NotNull String url) throws SvnBindException { try { SVNURL result = SVNURL.parseURIEncoded(url); // explicitly check if port corresponds to default port and recreate url specifying default port indicator if (result.hasPort() && hasDefaultPort(result)) { result = SVNURL .create(result.getProtocol(), result.getUserInfo(), result.getHost(), DEFAULT_PORT_INDICATOR, result.getURIEncodedPath(), true); } return result; } catch (SVNException e) { throw new SvnBindException(e); } } public static SVNURL parseUrl(@NotNull String url) { try { return SVNURL.parseURIEncoded(url); } catch (SVNException e) { throw createIllegalArgument(e); } } public static SVNURL append(@NotNull SVNURL parent, String child) { try { return parent.appendPath(child, false); } catch (SVNException e) { throw createIllegalArgument(e); } } public static IllegalArgumentException createIllegalArgument(SVNException e) { IllegalArgumentException runtimeException = new IllegalArgumentException(); runtimeException.initCause(e); return runtimeException; } @Nullable public static String getChangelistName(@NotNull final Status status) { // no explicit check on working copy format supports change lists as they are supported from svn 1.5 // and anyway status.getChangelistName() should just return null if change lists are not supported. return status.getKind().isFile() ? status.getChangelistName() : null; } public static boolean isUnversionedOrNotFound(@NotNull SvnBindException e) { return e.contains(SVNErrorCode.WC_PATH_NOT_FOUND) || e.contains(SVNErrorCode.UNVERSIONED_RESOURCE) || e.contains(SVNErrorCode.WC_NOT_WORKING_COPY) || // thrown when getting info from repository for non-existent item - like HEAD revision for deleted file e.contains(SVNErrorCode.ILLEGAL_TARGET); } // TODO: Create custom Target class and implement append there @NotNull public static SvnTarget append(@NotNull SvnTarget target, @NotNull String path) throws SvnBindException { return append(target, path, false); } @NotNull public static SvnTarget append(@NotNull SvnTarget target, @NotNull String path, boolean checkAbsolute) throws SvnBindException { SvnTarget result; if (target.isFile()) { result = SvnTarget.fromFile(resolvePath(target.getFile(), path)); } else { try { result = SvnTarget .fromURL(checkAbsolute && URI.create(path).isAbsolute() ? SVNURL.parseURIEncoded(path) : target.getURL().appendPath(path, false)); } catch (SVNException e) { throw new SvnBindException(e); } } return result; } @NotNull public static File resolvePath(@NotNull File base, @NotNull String path) { File result = new File(path); if (!result.isAbsolute()) { result = ".".equals(path) ? base : new File(base, path); } return result; } /** * {@code SvnTarget.getPathOrUrlDecodedString} does not correctly work for URL targets - {@code SVNURL.toString} instead of * {@code SVNURL.toDecodedString} is used. *

* Current utility method fixes this case. */ @NotNull public static String toDecodedString(@NotNull SvnTarget target) { return target.isFile() ? target.getFile().getPath() : target.getURL().toDecodedString(); } private static class WorkingCopyFormatOperation implements FileUtilRt.RepeatableIOOperation { @NotNull private final File myDbFile; public WorkingCopyFormatOperation(@NotNull File dbFile) { myDbFile = dbFile; } @Nullable @Override public WorkingCopyFormat execute(boolean lastAttempt) { // TODO: rewrite it using sqlite jdbc driver SqlJetDb db = null; WorkingCopyFormat result = null; try { // "write" access is requested here for now as workaround - see some details // in https://code.google.com/p/sqljet/issues/detail?id=25 and http://issues.tmatesoft.com/issue/SVNKIT-418. // BUSY error is currently handled same way as others. db = SqlJetDb.open(myDbFile, true); result = WorkingCopyFormat.getInstance(db.getOptions().getUserVersion()); } catch (NonWritableChannelException e) { // Such exceptions could be thrown when db is opened in "read" mode, but the db file is readonly (for instance, locked // by other process). See links above for some details. // handle this exception type separately - not to break execution flow LOG.info(e); } catch (SqlJetException e) { LOG.info(e); } finally { close(db); } return result; } } }