Mercurial > hg > mercurial
view mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java @ 980:1168c4c64d49
Merge branch Indore-2017.2.x
author | Dmitry Neverov <dmitry.neverov@gmail.com> |
---|---|
date | Wed, 24 Jan 2018 17:38:56 +0100 |
parents | 7bf4d943d5bb 38adef4f1b8f |
children | f342d25311c1 |
line wrap: on
line source
/* * Copyright 2000-2018 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 jetbrains.buildServer.buildTriggers.vcs.mercurial; import jetbrains.buildServer.Used; import jetbrains.buildServer.buildTriggers.vcs.AbstractVcsPropertiesProcessor; import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*; import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.AbandonedTransactionFound; import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.UnrelatedRepositoryException; import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.exception.WrongSubrepoUrlException; import jetbrains.buildServer.log.Loggers; import jetbrains.buildServer.serverSide.InvalidProperty; import jetbrains.buildServer.serverSide.PropertiesProcessor; import jetbrains.buildServer.serverSide.ServerListener; import jetbrains.buildServer.serverSide.ServerListenerAdapter; import jetbrains.buildServer.util.EventDispatcher; import jetbrains.buildServer.util.FileUtil; import jetbrains.buildServer.util.cache.ResetCacheRegister; import jetbrains.buildServer.vcs.*; import jetbrains.buildServer.vcs.patches.PatchBuilder; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.*; import java.util.*; import static com.intellij.openapi.util.text.StringUtil.isEmpty; import static jetbrains.buildServer.buildTriggers.vcs.mercurial.HgFileUtil.deleteDir; /** * Mercurial VCS plugin for TeamCity works as follows: * <ul> * <li>clones repository to internal storage * <li>before any operation with working copy of repository pulls changes from the original repository * <li>executes corresponding hg command * </ul> * * <p>Working copy of repository is created in the $TEAMCITY_DATA_PATH/system/caches/hg_<hash code> folder. * <p>Personal builds (remote runs) are not yet supported, they require corresponding functionality from the IDE. */ public class MercurialVcsSupport extends ServerVcsSupport implements LabelingSupport, VcsFileContentProvider, BuildPatchByCheckoutRules { private final VcsOperationProgressProvider myProgressProvider; private final MirrorManager myMirrorManager; private final ServerPluginConfig myConfig; private final RepoFactory myRepoFactory; private final HgVcsRootFactory myHgVcsRootFactory; private final FileFilter myIgnoreDotHgFilter = new IgnoreDotHgFilter(); private final FileFilter myAcceptAllFilter = new AcceptAllFilter(); private final HgTestConnectionSupport myTestConnection; private final SubrepoCheckoutRulesProvider mySubrepoCheckoutRulesProvider; private final Collection<MercurialServerExtension> myExtensions = new ArrayList<MercurialServerExtension>(); public MercurialVcsSupport(@NotNull final VcsOperationProgressProvider progressProvider, @NotNull final EventDispatcher<ServerListener> dispatcher, @NotNull final ResetCacheRegister resetCacheHandlerManager, @NotNull final ServerPluginConfig config, @NotNull final RepoFactory repoFactory, @NotNull final MirrorManager mirrorManager, @NotNull final HgVcsRootFactory hgVcsRootFactory, @NotNull final HgTestConnectionSupport testConnection, @NotNull final SubrepoCheckoutRulesProvider subrepoCheckoutRulesProvider) { myProgressProvider = progressProvider; myConfig = config; myMirrorManager = mirrorManager; myRepoFactory = repoFactory; myHgVcsRootFactory = hgVcsRootFactory; myTestConnection = testConnection; mySubrepoCheckoutRulesProvider = subrepoCheckoutRulesProvider; resetCacheHandlerManager.registerHandler(new MercurialResetCacheHandler(myMirrorManager)); dispatcher.addListener(new ServerListenerAdapter() { @Override public void serverShutdown() { myRepoFactory.dispose(); } }); logUsedHg(); } public void addExtensions(@NotNull Collection<MercurialServerExtension> extensions) { myExtensions.addAll(extensions); } public void addExtension(@NotNull MercurialServerExtension extension) { myExtensions.add(extension); } private void logUsedHg() { String hgPath = myConfig.getHgPath(); if (hgPath != null) Loggers.VCS.info("Use server-wide hg path " + hgPath + ", path in the VCS root settings will be ignored"); else Loggers.VCS.info("Server-wide hg path is not set, will use path from the VCS root settings"); } @NotNull public byte[] getContent(@NotNull final VcsModification vcsModification, @NotNull final VcsChangeInfo change, @NotNull final VcsChangeInfo.ContentType contentType, @NotNull final VcsRoot vcsRoot) throws VcsException { String version = contentType == VcsChangeInfo.ContentType.AFTER_CHANGE ? change.getAfterChangeRevisionNumber() : change.getBeforeChangeRevisionNumber(); return getContent(change.getFileName(), vcsRoot, version); } @NotNull public byte[] getContent(@NotNull final String filePath, @NotNull final VcsRoot vcsRoot, @NotNull final String version) throws VcsException { ChangeSet cset = new ChangeSet(version); HgVcsRoot root = getHgRoot(vcsRoot); syncRepository(root, cset); HgRepo repo = createRepo(root); File parentDir = repo.cat().files(filePath).atRevision(cset).call(); File file = new File(parentDir, filePath); try { return FileUtil.loadFileBytes(file); } catch (IOException e) { throw new VcsException("Failed to load content of file " + filePath + " at revision " + version, e); } finally { deleteDir(parentDir, Loggers.VCS); } } @NotNull public HgVcsRoot getHgRoot(@NotNull final VcsRoot vcsRoot) throws VcsException { return myHgVcsRootFactory.createHgRoot(vcsRoot); } @NotNull public String getName() { return Constants.VCS_NAME; } @NotNull @Used("jsp") public String getDisplayName() { return "Mercurial"; } @Nullable public PropertiesProcessor getVcsPropertiesProcessor() { return new AbstractVcsPropertiesProcessor() { public Collection<InvalidProperty> process(final Map<String, String> properties) { List<InvalidProperty> result = new ArrayList<InvalidProperty>(); if (isEmpty(properties.get(Constants.HG_COMMAND_PATH_PROP))) { result.add(new InvalidProperty(Constants.HG_COMMAND_PATH_PROP, "Path to 'hg' command must be specified")); } if (isEmpty(properties.get(Constants.REPOSITORY_PROP))) { result.add(new InvalidProperty(Constants.REPOSITORY_PROP, "Repository must be specified")); } return result; } }; } @NotNull public String getVcsSettingsJspFilePath() { return "mercurialSettings.jsp"; } public boolean sourcesUpdatePossibleIfChangesNotFound(@NotNull final VcsRoot root) { return false; } @NotNull public String describeVcsRoot(@NotNull final VcsRoot vcsRoot) { return "mercurial: " + vcsRoot.getProperty(Constants.REPOSITORY_PROP); } @NotNull public String describeVcsRoot(@NotNull final HgVcsRoot vcsRoot) { return "mercurial: " + vcsRoot.getProperty(Constants.REPOSITORY_PROP); } @Override @NotNull public TestConnectionSupport getTestConnectionSupport() { return myTestConnection; } @Nullable public Map<String, String> getDefaultVcsProperties() { Map<String, String> defaults = new HashMap<String, String>(); defaults.put(Constants.BRANCH_NAME_PROP, "default"); defaults.put(Constants.HG_COMMAND_PATH_PROP, "hg"); defaults.put(Constants.UNCOMPRESSED_TRANSFER, "true"); return defaults; } public String getVersionDisplayName(@NotNull final String version, @NotNull final VcsRoot root) throws VcsException { return new ChangeSet(version).getId(); } @NotNull public Comparator<String> getVersionComparator() { // comparator is called when TeamCity needs to sort modifications in the order of their appearance, // currently we sort changes by revision number, not sure however that this is a good idea, // probably it would be better to sort them by timestamp (and to add timestamp into the version). return new Comparator<String>() { public int compare(final String o1, final String o2) { try { return new ChangeSet(o1).getRevNumber() - new ChangeSet(o2).getRevNumber(); } catch (Exception e) { return 1; } } }; } // builds patch from version to version private void buildIncrementalPatch(@NotNull final HgVcsRoot root, @NotNull final ChangeSet fromVer, @NotNull final ChangeSet toVer, @NotNull final PatchBuilder builder, @NotNull final CheckoutRules checkoutRules) throws VcsException, IOException { final HgRepo repo = createRepo(root); final List<FileStatus> modifiedFiles = repo.status().fromRevision(fromVer).toRevision(toVer).call(); final List<String> notDeletedFiles = new ArrayList<String>(); for (FileStatus f: modifiedFiles) { if (f.getStatus() != Status.REMOVED) { notDeletedFiles.add(f.getPath()); } } File parentDir = null; try { if (root.useArchiveForPatch()) { parentDir = HgFileUtil.createTempDir(); final File archFile = new File(parentDir, "arch.tar"); buildIncrementalPatchWithArchive(builder, repo, toVer, checkoutRules, modifiedFiles, notDeletedFiles, archFile); } else { parentDir = repo.cat().files(notDeletedFiles).atRevision(toVer).call(); for (FileStatus f: modifiedFiles) { String mappedPath = checkoutRules.map(f.getPath()); if (mappedPath == null) continue; // skip final File virtualFile = new File(mappedPath); if (f.getStatus() == Status.REMOVED) { builder.deleteFile(virtualFile, true); } else { final File realFile = new File(parentDir, f.getPath()); final InputStream is = new BufferedInputStream(new FileInputStream(realFile)); try { builder.changeOrCreateBinaryFile(virtualFile, null, is, realFile.length()); } finally { FileUtil.close(is); } } } } if (root.includeSubreposInPatch()) buildSubrepoPatch(root, fromVer, toVer, builder, checkoutRules, repo); } catch (Exception e) { if (e instanceof VcsException) throw (VcsException) e; throw new VcsException("Error while building an incremental patch", e); } finally { if (parentDir != null) deleteDir(parentDir, Loggers.VCS); } } private void buildIncrementalPatchWithArchive(@NotNull final PatchBuilder builder, @NotNull final HgRepo repo, @NotNull final ChangeSet toVer, @NotNull final CheckoutRules checkoutRules, @NotNull final List<FileStatus> modifiedFiles, @NotNull final List<String> notDeletedFiles, @NotNull final File archiveFile) throws VcsException, IOException { ArchiveCommand archive = repo.archive().revision(toVer).type("tar").destination(archiveFile); int i = 0; while (i < notDeletedFiles.size()) { String mappedPath = checkoutRules.map(notDeletedFiles.get(i)); if (mappedPath == null) { i++; continue; } if (archive.addIncludePathRule(/*"path:" + */notDeletedFiles.get(i))) { i++; continue; } //archive command is full, call it archive.call(); buildPatchFromArchive(builder, archiveFile, checkoutRules, new ExcludeHgArchival()); FileUtil.delete(archiveFile); archive = repo.archive().revision(toVer).type("tar").destination(archiveFile); } if (!notDeletedFiles.isEmpty()) { archive.call(); buildPatchFromArchive(builder, archiveFile, checkoutRules, new ExcludeHgArchival()); FileUtil.delete(archiveFile); } //delete removed files for (FileStatus f: modifiedFiles) { if (f.getStatus() != Status.REMOVED) continue; //other files processed below final String mappedPath = checkoutRules.map(f.getPath()); if (mappedPath == null) continue; // skip builder.deleteFile(new File(mappedPath), true); } } private void buildSubrepoPatch(@NotNull HgVcsRoot mainRoot, @NotNull ChangeSet mainRootFromRevision, @NotNull ChangeSet mainRootToRevision, @NotNull PatchBuilder builder, @NotNull CheckoutRules mainRootRules, @NotNull HgRepo mainRepo) throws IOException, VcsException { List<HgSubrepoConfigChange> subrepoConfigChanges = mainRepo.getSubrepoConfigChanges(mainRootFromRevision.getId(), mainRootToRevision.getId()); for (HgSubrepoConfigChange configChange : subrepoConfigChanges) { try { String subrepoPath = configChange.getPath(); if (!mainRootRules.shouldInclude(subrepoPath)) continue; SubRepo currentSubrepo = configChange.getCurrent(); if (configChange.subrepoAdded()) { assert currentSubrepo != null; String subrepoUrl = currentSubrepo.resolveUrl(mainRoot.getRepository()); HgVcsRoot subrepoRoot = mainRoot.withUrl(subrepoUrl); String subrepoFromRevision = null; String subrepoToRevision = currentSubrepo.revision(); CheckoutRules subrepoRules = mySubrepoCheckoutRulesProvider.getSubrepoRules(mainRootRules, subrepoPath); buildPatch(subrepoRoot, subrepoFromRevision, subrepoToRevision, builder, subrepoRules); } else if (configChange.subrepoRemoved()) { deleteDirInPatch(builder, mainRootRules, subrepoPath); } else if (configChange.subrepoUrlChanged()) { assert currentSubrepo != null; deleteDirInPatch(builder, mainRootRules, subrepoPath); String subrepoUrl = currentSubrepo.resolveUrl(mainRoot.getRepository()); HgVcsRoot subrepoRoot = mainRoot.withUrl(subrepoUrl); String subrepoFromRevision = null; String subrepoToRevision = currentSubrepo.revision(); CheckoutRules subrepoRules = mySubrepoCheckoutRulesProvider.getSubrepoRules(mainRootRules, subrepoPath); buildPatch(subrepoRoot, subrepoFromRevision, subrepoToRevision, builder, subrepoRules); } else { assert currentSubrepo != null; String subrepoUrl = currentSubrepo.resolveUrl(mainRoot.getRepository()); HgVcsRoot subrepoRoot = mainRoot.withUrl(subrepoUrl); String subrepoFromRevision = configChange.getPrevious().get(0).revision(); String subrepoToRevision = currentSubrepo.revision(); CheckoutRules subrepoRules = mySubrepoCheckoutRulesProvider.getSubrepoRules(mainRootRules, subrepoPath); buildPatch(subrepoRoot, subrepoFromRevision, subrepoToRevision, builder, subrepoRules); } } catch (WrongSubrepoUrlException e) { throw new VcsException("Error while resolving subrepo url", e); } } } private void deleteDirInPatch(@NotNull PatchBuilder builder, @NotNull CheckoutRules rules, @NotNull String unMappedPath) throws IOException { for (IncludeRule rule : rules.getRootIncludeRules()) { String mappedPath = rule.map(unMappedPath); if (mappedPath != null) builder.deleteDirectory(new File(mappedPath), true); } } // builds patch by exporting files using specified version private void buildFullPatch(@NotNull final HgVcsRoot root, @NotNull final ChangeSet toVer, @NotNull final PatchBuilder builder, @NotNull final CheckoutRules checkoutRules) throws IOException, VcsException { final File tempDir = HgFileUtil.createTempDir(); try { final HgRepo repo = createRepo(root); if (root.includeSubreposInPatch()) { final Map<String, SubRepo> subrepos = repo.getSubrepositories(toVer); if (!subrepos.isEmpty()) { Loggers.VCS.debug("Repository '" + root.getRepository() + "' has subrepos at revision " + toVer.getId() + ", use 'hg clone' to build clean patch"); final File mirrorDir = getWorkingDir(root); final HgRepo cloneOfTheMirror = createRepo(root, tempDir); cloneOfTheMirror.init().call(); cloneOfTheMirror.setDefaultPath(root.getRepository()); cloneOfTheMirror.setTeamCityConfig(root.getCustomHgConfig()); cloneOfTheMirror.pull().fromRepository(mirrorDir).call(); cloneSubrepos(root, tempDir, subrepos); cloneOfTheMirror.update().toRevision(toVer).call(); buildPatchFromDirectory(builder, tempDir, checkoutRules, myIgnoreDotHgFilter); } else { Loggers.VCS.debug("Repository '" + root.getRepository() + "' doesn't have subrepos at revision " + toVer.getId() + ", use 'hg archive' to build clean patch"); if (root.useArchiveForPatch()) { File archive = new File(tempDir, "arch.tar"); repo.archive().revision(toVer).type("tar").destination(archive).call(); buildPatchFromArchive(builder, archive, checkoutRules, new ExcludeHgArchival()); } else { repo.archive().revision(toVer).destination(tempDir).call(); buildPatchFromDirectory(builder, tempDir, checkoutRules, myAcceptAllFilter); } } } else { Loggers.VCS.debug("Subrepos disabled in VCS root, use 'hg archive' to build clean patch"); if (root.useArchiveForPatch()) { final File archive = new File(tempDir, "arch.tar"); repo.archive().revision(toVer).type("tar").destination(archive).call(); buildPatchFromArchive(builder, archive, checkoutRules, new ExcludeHgArchival()); } else { repo.archive().revision(toVer).destination(tempDir).call(); buildPatchFromDirectory(builder, tempDir, checkoutRules, myAcceptAllFilter); } } } finally { deleteDir(tempDir, Loggers.VCS); } } private void cloneSubrepos(@NotNull HgVcsRoot mainRoot, @NotNull File mainRootDir, @NotNull Map<String, SubRepo> subrepos) throws VcsException, IOException { for (Map.Entry<String, SubRepo> e : subrepos.entrySet()) { String path = e.getKey(); SubRepo subrepoConfig = e.getValue(); if (subrepoConfig.vcsType() != SubRepo.VcsType.hg) continue; String parentRepositoryUrl = mainRoot.getRepository(); try { String subrepoUrl = subrepoConfig.resolveUrl(parentRepositoryUrl); HgVcsRoot subrepoRoot = mainRoot.withUrl(subrepoUrl); HgRepo subrepo = createRepo(subrepoRoot); if (!subrepo.containsRevision(subrepoConfig.revision())) syncRepository(subrepoRoot); File subrepoMirrorDir = getWorkingDir(subrepoRoot); File subrepoDir = new File(mainRootDir, path); HgRepo cloneOfSubrepoMirror = createRepo(subrepoRoot, subrepoDir); cloneOfSubrepoMirror.init().call(); cloneOfSubrepoMirror.setDefaultPath(subrepoUrl); cloneOfSubrepoMirror.setTeamCityConfig(mainRoot.getCustomHgConfig()); cloneOfSubrepoMirror.pull().fromRepository(subrepoMirrorDir).call(); Map<String, SubRepo> subSubrepos = subrepo.getSubrepositories(subrepoConfig.revision()); if (!subSubrepos.isEmpty()) cloneSubrepos(subrepoRoot, subrepoDir, subSubrepos); } catch (WrongSubrepoUrlException error) { //ignore it, will try to clone from network during main repository update } } } private void buildPatchFromArchive(@NotNull final PatchBuilder builder, @NotNull final File archive, @NotNull final CheckoutRules checkoutRules, @NotNull final FileFilter filter) throws IOException { InputStream fis = null; ArchiveInputStream is = null; try { fis = new BufferedInputStream(new FileInputStream(archive)); is = new TarArchiveInputStream(fis); ArchiveEntry entry; while ((entry = is.getNextEntry()) != null) { if (entry.isDirectory()) continue; String fileName = entry.getName(); //TODO: does it work if I have arch/ in my repo? if (fileName.startsWith("arch/")) fileName = fileName.substring(5); if (!filter.accept(new File(fileName))) continue; final String mappedFile = checkoutRules.map(fileName); if (!isEmpty(mappedFile)) { builder.createBinaryFile(new File(mappedFile), null, is, entry.getSize()); } } } finally { FileUtil.closeAll(is, fis); } } private void buildPatchFromDirectory(@NotNull final PatchBuilder builder, @NotNull final File repRoot, @NotNull final CheckoutRules checkoutRules, @NotNull final FileFilter filter) throws IOException { buildPatchFromDirectory(repRoot, builder, repRoot, checkoutRules, filter); } private void buildPatchFromDirectory(@NotNull final File curDir, @NotNull final PatchBuilder builder, @NotNull final File repRoot, @NotNull final CheckoutRules checkoutRules, @NotNull final FileFilter filter) throws IOException { File[] files = curDir.listFiles(filter); if (files != null) { for (File realFile: files) { String relPath = realFile.getAbsolutePath().substring(repRoot.getAbsolutePath().length()); String mappedPath = checkoutRules.map(relPath); if (mappedPath != null && mappedPath.length() > 0) { final File virtualFile = new File(mappedPath); if (realFile.isDirectory()) { builder.createDirectory(virtualFile); buildPatchFromDirectory(realFile, builder, repRoot, checkoutRules, filter); } else { final FileInputStream is = new FileInputStream(realFile); try { builder.createBinaryFile(virtualFile, null, is, realFile.length()); } finally { is.close(); } } } else { if (realFile.isDirectory()) { buildPatchFromDirectory(realFile, builder, repRoot, checkoutRules, filter); } } } } } /* clone the repo if it doesn't exist, pull the repo if it doesn't contain specified changeSet */ private void syncRepository(@NotNull final HgVcsRoot root, @NotNull final ChangeSet cset) throws VcsException { File workingDir = getWorkingDir(root); lockWorkDir(workingDir); HgRepo repo = createRepo(root); try { if (!repo.isValidRepository()) repo.init().call(); if (repo.containsRevision(cset)) return; repo.setDefaultPath(root.getRepository()); repo.setTeamCityConfig(root.getCustomHgConfig()); try { repo.pull().fromRepository(root.getRepository()) .withTimeout(myConfig.getPullTimeout()) .withProfile(myConfig.runWithProfile(root)) .call(); } catch (UnrelatedRepositoryException e) { Loggers.VCS.warn("Repository at " + root.getRepository() + " is unrelated, clone it again"); myMirrorManager.forgetDir(workingDir); syncRepository(root, cset); } } finally { unlockWorkDir(workingDir); } } public void syncRepository(@NotNull final VcsRoot root) throws VcsException { syncRepository(getHgRoot(root)); } public void syncRepository(@NotNull final HgVcsRoot root) throws VcsException { syncRepository(root, new SyncSettings<Void>(VcsCallable.NO_OP)); } public <T> T syncRepository(@NotNull HgVcsRoot root, @NotNull SyncSettings<T> settings) throws VcsException { return syncRepository(root, settings, null); } public <T> T syncRepository(@NotNull HgVcsRoot root, @NotNull SyncSettings<T> settings, @Nullable OperationContext context) throws VcsException { boolean customWorkingDir = root.getCustomWorkingDir() != null; File workingDir = getWorkingDir(root); int attemptsLeft = 3; VcsException lastError = null; while (attemptsLeft-- > 0) { try { return syncRepositoryOnce(root, settings, workingDir, context); } catch (UnrelatedRepositoryException e) { if (customWorkingDir) throw new VcsException(e.getMessage() + ". VCS root uses a custom clone dir, manual recovery is required.", e); Loggers.VCS.warn("Repository at " + workingDir.getAbsolutePath() + " is unrelated to " + root.getRepository() + ". Clone it again, attempts left " + attemptsLeft); myMirrorManager.forgetDir(workingDir); lastError = e; } catch (AbandonedTransactionFound e) { if (customWorkingDir) throw new VcsException(e.getMessage() + ". VCS root uses a custom clone dir, manual recovery is required.", e); Loggers.VCS.warn("Abandoned transaction found in repository " + root.getRepository() + " at " + workingDir.getAbsolutePath() + ". Clone it again, attempts left " + attemptsLeft); myMirrorManager.forgetDir(workingDir); lastError = e; } } throw lastError; } private <T> T syncRepositoryOnce(@NotNull HgVcsRoot root, @NotNull SyncSettings<T> settings, @NotNull File workingDir, @Nullable OperationContext context) throws VcsException { lockWorkDir(workingDir, settings.getProgressConsumer()); HgRepo repo = context != null ? context.createRepo(root) : createRepo(root); try { if (!repo.isValidRepository()) repo.init().call(); repo.setDefaultPath(root.getRepository()); repo.setTeamCityConfig(root.getCustomHgConfig()); resetBookmarks(repo); repo.pull().fromRepository(root.getRepository()) .withTimeout(myConfig.getPullTimeout()) .withProfile(myConfig.runWithProfile(root)) .call(); return settings.getCmd().call(); } finally { unlockWorkDir(workingDir); } } private void resetBookmarks(HgRepo repo) throws VcsException { if (!myConfig.bookmarksEnabled()) return; HgVersion v = repo.version().call(); if (v.isEqualsOrGreaterThan(BookmarksCommand.REQUIRED_HG_VERSION)) repo.resetBookmarks(); } @Override public LabelingSupport getLabelingSupport() { return this; } @NotNull public VcsFileContentProvider getContentProvider() { return this; } @NotNull public MercurialCollectChangesPolicy getCollectChangesPolicy() { return new MercurialCollectChangesPolicy(myProgressProvider, this, myConfig, myHgVcsRootFactory, myRepoFactory); } @NotNull public BuildPatchPolicy getBuildPatchPolicy() { return this; } public void buildPatch(@NotNull final VcsRoot root, @Nullable final String fromVersion, @NotNull final String toVersion, @NotNull final PatchBuilder builder, @NotNull final CheckoutRules checkoutRules) throws IOException, VcsException { final HgVcsRoot hgRoot = getHgRoot(root); buildPatch(hgRoot, fromVersion, toVersion, builder, checkoutRules); } public void buildPatch(@NotNull final HgVcsRoot hgRoot, @Nullable final String fromVersion, @NotNull final String toVersion, @NotNull final PatchBuilder builder, @NotNull final CheckoutRules checkoutRules) throws IOException, VcsException { syncRepository(hgRoot); final ChangeSet to = new ChangeSet(toVersion); if (fromVersion == null) { buildFullPatch(hgRoot, to, builder, checkoutRules); return; } final ChangeSet from = new ChangeSet(fromVersion); final HgRepo repo = createRepo(hgRoot); if (repo.containsRevision(from)) { buildIncrementalPatch(hgRoot, from, to, builder, checkoutRules); return; } Loggers.VCS.info("Cannot find revision " + fromVersion + " in repository " + hgRoot.getRepository() + ", will build a full patch"); cleanCheckoutDir(builder, checkoutRules); buildFullPatch(hgRoot, to, builder, checkoutRules); } private void cleanCheckoutDir(@NotNull PatchBuilder builder, @NotNull CheckoutRules checkoutRules) throws IOException { builder.deleteDirectory(new File(checkoutRules.map("")), true); } private void lockWorkDir(@NotNull File workDir) { lockWorkDir(workDir, null); } private void lockWorkDir(@NotNull File workDir, @Nullable ProgressParser.ProgressConsumer progressConsumer) { if (progressConsumer != null) progressConsumer.consume(-1f, "Acquire repository lock"); myMirrorManager.lockDir(workDir); if (progressConsumer != null) progressConsumer.consume(-1f, "Repository lock acquired"); } private void unlockWorkDir(@NotNull File workDir) { myMirrorManager.unlockDir(workDir); } @Override public boolean allowSourceCaching() { return myConfig.allowSourceCaching(); } @NotNull public String label(@NotNull String label, @NotNull String version, @NotNull VcsRoot root, @NotNull CheckoutRules checkoutRules) throws VcsException { File tmpDir = null; try { tmpDir = createLabelingTmpDir(); HgVcsRoot hgRoot = getHgRoot(root); hgRoot.setCustomWorkingDir(tmpDir); syncRepository(hgRoot); HgRepo repo = createRepo(hgRoot); String branchName = getCommitBranch(repo, version); HgVersion hgVersion = repo.version().call(); if (hgVersion.isEqualsOrGreaterThan(ServerHgRepo.REVSET_HG_VERSION)) { repo.update().branch("branch('" + branchName + "') and head()").call(); } else { repo.update().branch(branchName).call(); } String fixedTagname = fixTagName(label); try { repo.tag().revision(version) .tagName(fixedTagname) .byUser(hgRoot.getUserForTag()) .call(); } catch (VcsException e) { String msg = e.getMessage(); if (msg != null && msg.contains("not at a branch head") && hgVersion.isLessThan(ServerHgRepo.REVSET_HG_VERSION)) { Loggers.VCS.warn("Please upgrade mercurial to the version supporting revsets(" + ServerHgRepo.REVSET_HG_VERSION + "+), current version: " + hgVersion); } throw e; } repo.push().toRepository(hgRoot.getRepository()).call(); return fixedTagname; } finally { deleteDir(tmpDir, Loggers.VCS); } } @NotNull private String getCommitBranch(@NotNull HgRepo repo, @NotNull String cset) throws VcsException { return repo.id().inLocalRepository().revision(cset).showBranch().call(); } private String fixTagName(final String label) { // according to Mercurial documentation http://hgbook.red-bean.com/hgbookch8.html#x12-1570008 // tag name must not contain: // Colon (ASCII 58, ':') // Carriage return (ASCII 13, '\r') // Newline (ASCII 10, '\n') // all these characters will be replaced with _ (underscore) return label.replace(':', '_').replace('\r', '_').replace('\n', '_'); } public File getWorkingDir(HgVcsRoot root) { File customDir = root.getCustomWorkingDir(); return customDir != null ? customDir : myMirrorManager.getMirrorDir(root.getRepository()); } public boolean isAgentSideCheckoutAvailable() { return true; } private File createLabelingTmpDir() throws VcsException { try { return HgFileUtil.createTempDir(); } catch (IOException e) { throw new VcsException("Unable to create temporary directory"); } } /* for tests only */ public MirrorManager getMirrorManager() { return myMirrorManager; } @Override public boolean isDAGBasedVcs() { return true; } private static class IgnoreDotHgFilter implements FileFilter { public boolean accept(final File file) { return !(file.isDirectory() && ".hg".equals(file.getName())); } } private static class AcceptAllFilter implements FileFilter { public boolean accept(File pathname) { return true; } } @NotNull public ServerHgRepo createRepo(@NotNull HgVcsRoot root) throws VcsException { return myRepoFactory.createRepo(root, getWorkingDir(root)); } @NotNull public ServerHgRepo createRepo(@NotNull HgVcsRoot root, @NotNull File customDir) throws VcsException { return myRepoFactory.createRepo(root, customDir); } @NotNull @Override public Map<String, String> getCheckoutProperties(@NotNull VcsRoot root) { Map<String, String> rootProperties = root.getProperties(); Map<String, String> repositoryProperties = new HashMap<String, String>(); repositoryProperties.put(Constants.REPOSITORY_PROP, rootProperties.get(Constants.REPOSITORY_PROP)); repositoryProperties.put(Constants.INCLUDE_SUBREPOS_IN_PATCH, rootProperties.get(Constants.INCLUDE_SUBREPOS_IN_PATCH)); String customHgConfig = rootProperties.get(Constants.CUSTOM_HG_CONFIG_PROP); if (!isEmpty(customHgConfig)) repositoryProperties.put(Constants.CUSTOM_HG_CONFIG_PROP, customHgConfig); return repositoryProperties; } @Override public ListFilesPolicy getListFilesPolicy() { return new ListFilesSupport(this, myConfig, myHgVcsRootFactory); } @NotNull public UrlSupport getUrlSupport() { return new MercurialUrlSupport(this); } @Override @Nullable protected <T extends VcsExtension> T getVcsCustomExtension(@NotNull final Class<T> extensionClass) { for (MercurialServerExtension e : myExtensions) { if (extensionClass.isInstance(e)) return extensionClass.cast(e); } return super.getVcsCustomExtension(extensionClass); } private static class ExcludeHgArchival implements FileFilter { public boolean accept(File f) { return !f.getName().equals(".hg_archival.txt"); } } }