Mercurial > hg > mercurial
changeset 173:c3157374a356 remote-run/dmitry.neverov/uncompressed
merge TW-15195
author | Dmitry Neverov <dmitry.neverov@jetbrains.com> |
---|---|
date | Wed, 09 Feb 2011 14:01:26 +0300 |
parents | cfd745c297f1 (diff) ded3c1ad49be (current diff) |
children | d79286880dee |
files | mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java |
diffstat | 3 files changed, 1128 insertions(+), 1122 deletions(-) [+] |
line wrap: on
line diff
--- a/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/UpdateCommand.java Wed Feb 09 13:52:20 2011 +0300 +++ b/mercurial-common/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/command/UpdateCommand.java Wed Feb 09 14:01:26 2011 +0300 @@ -20,6 +20,9 @@ import org.jetbrains.annotations.NotNull; public class UpdateCommand extends BaseCommand { + + private static final int UPDATE_TIMEOUT_SECONDS = 8 * 3600;//8 hours + private String myToId; public UpdateCommand(@NotNull final Settings settings) { @@ -31,20 +34,15 @@ } public void execute() throws VcsException { - // switch working directory to current branch GeneralCommandLine cli = createCommandLine(); cli.addParameter("update"); cli.addParameter("-C"); - cli.addParameter(getSettings().getBranchName()); - runCommand(cli); - - cli = createCommandLine(); - cli.addParameter("update"); - cli.addParameter("-C"); + cli.addParameter("-r"); if (myToId != null) { - cli.addParameter("-r"); cli.addParameter(myToId); + } else { + cli.addParameter(getSettings().getBranchName()); } - runCommand(cli, 8*3600); // execution timeout is set to 8 hours for large repositories + runCommand(cli, UPDATE_TIMEOUT_SECONDS); } }
--- a/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java Wed Feb 09 13:52:20 2011 +0300 +++ b/mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java Wed Feb 09 14:01:26 2011 +0300 @@ -1,686 +1,682 @@ -/* - * Copyright 2000-2011 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.BuildAgent; -import jetbrains.buildServer.Used; -import jetbrains.buildServer.buildTriggers.vcs.AbstractVcsPropertiesProcessor; -import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*; -import jetbrains.buildServer.log.Loggers; -import jetbrains.buildServer.serverSide.*; -import jetbrains.buildServer.util.EventDispatcher; -import jetbrains.buildServer.util.FileUtil; -import jetbrains.buildServer.util.StringUtil; -import jetbrains.buildServer.util.filters.Filter; -import jetbrains.buildServer.util.filters.FilterUtil; -import jetbrains.buildServer.vcs.*; -import jetbrains.buildServer.vcs.patches.PatchBuilder; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.FileFilter; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -/** - * 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, BranchSupport { - private ConcurrentMap<String, Lock> myWorkDirLocks= new ConcurrentHashMap<String, Lock>(); - private VcsManager myVcsManager; - private File myDefaultWorkFolderParent; - - public MercurialVcsSupport(@NotNull final VcsManager vcsManager, - @NotNull ServerPaths paths, - @NotNull final SBuildServer server, - @NotNull EventDispatcher<BuildServerListener> dispatcher) { - myVcsManager = vcsManager; - myDefaultWorkFolderParent = new File(paths.getCachesDir(), "mercurial"); - dispatcher.addListener(new BuildServerAdapter() { - @Override - public void cleanupFinished() { - super.cleanupFinished(); - server.getExecutor().submit(new Runnable() { - public void run() { - removeOldWorkFolders(); - } - }); - } - - @Override - public void sourcesVersionReleased(@NotNull final BuildAgent agent) { - super.sourcesVersionReleased(agent); - server.getExecutor().submit(new Runnable() { - public void run() { - Set<File> clonedRepos = getAllClonedRepos(); - if (clonedRepos == null) return; - for (File f: clonedRepos) { - lockWorkDir(f); - try { - FileUtil.delete(f); - } finally { - unlockWorkDir(f); - } - } - } - }); - } - }); - } - - private Collection<ModifiedFile> computeModifiedFilesForMergeCommit(final Settings settings, final ChangeSet cur) throws VcsException { - ChangedFilesCommand cfc = new ChangedFilesCommand(settings); - cfc.setRevId(cur.getId()); - return cfc.execute(); - } - - private List<VcsChange> toVcsChanges(final List<ModifiedFile> modifiedFiles, String prevVer, String curVer, CheckoutRules rules) { - List<VcsChange> files = new ArrayList<VcsChange>(); - for (ModifiedFile mf: modifiedFiles) { - String normalizedPath = PathUtil.normalizeSeparator(mf.getPath()); - VcsChangeInfo.Type changeType = getChangeType(mf.getStatus()); - if (changeType == null) { - Loggers.VCS.warn("Unable to convert status: " + mf.getStatus() + " to VCS change type"); - changeType = VcsChangeInfo.Type.NOT_CHANGED; - } - files.add(new VcsChange(changeType, mf.getStatus().getName(), normalizedPath, rules.map(mf.getPath()), prevVer, curVer)); - } - return files; - } - - private VcsChangeInfo.Type getChangeType(final ModifiedFile.Status status) { - switch (status) { - case ADDED:return VcsChangeInfo.Type.ADDED; - case MODIFIED:return VcsChangeInfo.Type.CHANGED; - case REMOVED:return VcsChangeInfo.Type.REMOVED; - } - return null; - } - - @NotNull - public byte[] getContent(@NotNull final VcsModification vcsModification, - @NotNull final VcsChangeInfo change, - @NotNull final VcsChangeInfo.ContentType contentType, - @NotNull final VcsRoot vcsRoot) throws VcsException { - syncClonedRepository(vcsRoot); - String version = contentType == VcsChangeInfo.ContentType.AFTER_CHANGE ? change.getAfterChangeRevisionNumber() : change.getBeforeChangeRevisionNumber(); - return getContent(change.getRelativeFileName(), vcsRoot, version); - } - - @NotNull - public byte[] getContent(@NotNull final String filePath, @NotNull final VcsRoot vcsRoot, @NotNull final String version) throws VcsException { - syncClonedRepository(vcsRoot); - Settings settings = createSettings(vcsRoot); - CatCommand cc = new CatCommand(settings); - cc.setRevId(new ChangeSet(version).getId()); - File parentDir = cc.execute(Collections.singletonList(filePath)); - try { - File file = new File(parentDir, filePath); - if (file.isFile()) { - try { - return FileUtil.loadFileBytes(file); - } catch (IOException e) { - throw new VcsException("Failed to load content of file: " + file.getAbsolutePath(), e); - } - } else { - Loggers.VCS.warn("Unable to obtain content of the file: " + filePath); - } - } finally { - deleteTmpDir(parentDir); - } - return new byte[0]; - } - - @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")); - } - if (isEmpty(properties.get(Constants.SERVER_CLONE_PATH_PROP))) { - properties.put(Constants.SERVER_CLONE_PATH_PROP, myDefaultWorkFolderParent.getAbsolutePath()); - } - return result; - } - }; - } - - @NotNull - public String getVcsSettingsJspFilePath() { - return "mercurialSettings.jsp"; - } - - @NotNull - public String getCurrentVersion(@NotNull final VcsRoot root) throws VcsException { - // we will return full version of the most recent change as current version - syncClonedRepository(root); - Settings settings = createSettings(root); - BranchesCommand branches = new BranchesCommand(settings); - Map<String, ChangeSet> result = branches.execute(); - if (!result.containsKey(settings.getBranchName())) { - throw new VcsException("Unable to find current version for the branch: " + settings.getBranchName()); - } - - return result.get(settings.getBranchName()).getFullVersion(); - } - - public boolean sourcesUpdatePossibleIfChangesNotFound(@NotNull final VcsRoot root) { - return false; - } - - @NotNull - public String describeVcsRoot(final VcsRoot vcsRoot) { - return "mercurial: " + vcsRoot.getProperty(Constants.REPOSITORY_PROP); - } - - @Override - public TestConnectionSupport getTestConnectionSupport() { - return new TestConnectionSupport() { - public String testConnection(@NotNull final VcsRoot vcsRoot) throws VcsException { - Settings settings = createSettings(vcsRoot); - IdentifyCommand id = new IdentifyCommand(settings); - StringBuilder res = new StringBuilder(); - res.append(quoteIfNeeded(settings.getHgCommandPath())); - res.append(" identify "); - final String obfuscatedUrl = CommandUtil.removePrivateData(settings.getRepositoryUrl(), Collections.singleton(settings.getPassword())); - res.append(quoteIfNeeded(obfuscatedUrl)); - res.append('\n').append(id.execute()); - return res.toString(); - } - }; - } - - private String quoteIfNeeded(@NotNull String str) { - if (str.indexOf(' ') != -1) { - return "\"" + str + "\""; - } - - return str; - } - - @Nullable - public Map<String, String> getDefaultVcsProperties() { - Map<String, String> defaults = new HashMap<String, String>(); - defaults.put(Constants.HG_COMMAND_PATH_PROP, "hg"); - defaults.put(Constants.SERVER_CLONE_PATH_PROP, myDefaultWorkFolderParent.getAbsolutePath()); - defaults.put(Constants.UNCOMPRESSED_TRANSFER, "false"); - 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(final Settings settings, @NotNull final ChangeSet fromVer, @NotNull final ChangeSet toVer, final PatchBuilder builder, final CheckoutRules checkoutRules) - throws VcsException, IOException { - StatusCommand st = new StatusCommand(settings); - st.setFromRevId(fromVer.getId()); - st.setToRevId(toVer.getId()); - List<ModifiedFile> modifiedFiles = st.execute(); - List<String> notDeletedFiles = new ArrayList<String>(); - for (ModifiedFile f: modifiedFiles) { - if (f.getStatus() != ModifiedFile.Status.REMOVED) { - notDeletedFiles.add(f.getPath()); - } - } - - if (notDeletedFiles.isEmpty()) return; - - CatCommand cc = new CatCommand(settings); - cc.setRevId(toVer.getId()); - File parentDir = cc.execute(notDeletedFiles); - - try { - for (ModifiedFile f: modifiedFiles) { - String mappedPath = checkoutRules.map(f.getPath()); - if (mappedPath == null) continue; // skip - final File virtualFile = new File(mappedPath); - if (f.getStatus() == ModifiedFile.Status.REMOVED) { - builder.deleteFile(virtualFile, true); - } else { - File realFile = new File(parentDir, f.getPath()); - FileInputStream is = new FileInputStream(realFile); - try { - builder.changeOrCreateBinaryFile(virtualFile, null, is, realFile.length()); - } finally { - is.close(); - } - } - } - } finally { - deleteTmpDir(parentDir); - } - } - - private void deleteTmpDir(File parentDir) { - boolean dirDeleted = FileUtil.delete(parentDir); - if (!dirDeleted) { - Loggers.VCS.warn("Can not delete directory \"" + parentDir.getAbsolutePath() + "\""); - } - } - - // builds patch by exporting files using specified version - private void buildFullPatch(final Settings settings, @NotNull final ChangeSet toVer, final PatchBuilder builder, final CheckoutRules checkoutRules) - throws IOException, VcsException { - CloneCommand cl = new CloneCommand(settings); - // clone from the local repository - cl.setRepository(settings.getLocalRepositoryDir().getAbsolutePath()); - cl.setToId(toVer.getId()); - cl.setUpdateWorkingDir(false); - File tempDir = FileUtil.createTempDirectory("mercurial", toVer.getId()); - try { - final File repRoot = new File(tempDir, "rep"); - cl.setDestDir(repRoot.getAbsolutePath()); - cl.execute(); - - UpdateCommand up = new UpdateCommand(settings); - up.setWorkDirectory(repRoot.getAbsolutePath()); - up.setToId(toVer.getId()); - up.execute(); - - buildPatchFromDirectory(builder, repRoot, new FileFilter() { - public boolean accept(final File file) { - return !(file.isDirectory() && ".hg".equals(file.getName())); - } - }, checkoutRules); - } finally { - FileUtil.delete(tempDir); - } - } - - private void buildPatchFromDirectory(final PatchBuilder builder, final File repRoot, final FileFilter filter, final CheckoutRules checkoutRules) throws IOException { - buildPatchFromDirectory(repRoot, builder, repRoot, filter, checkoutRules); - } - - private void buildPatchFromDirectory(File curDir, final PatchBuilder builder, final File repRoot, final FileFilter filter, final CheckoutRules checkoutRules) 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, filter, checkoutRules); - } 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, filter, checkoutRules); - } - } - } - } - } - - // updates current working copy of repository by pulling changes from the repository specified in VCS root - private void syncClonedRepository(final VcsRoot root) throws VcsException { - Settings settings = createSettings(root); - File workDir = settings.getLocalRepositoryDir(); - lockWorkDir(workDir); - try { - if (settings.hasCopyOfRepository()) { - // update - PullCommand pull = new PullCommand(settings); - pull.execute(); - } else { - // clone - CloneCommand cl = new CloneCommand(settings); - cl.setDestDir(workDir.getAbsolutePath()); - cl.setUpdateWorkingDir(false); - cl.execute(); - } - } finally { - unlockWorkDir(workDir); - } - } - - @Override - public LabelingSupport getLabelingSupport() { - return this; - } - - @NotNull - public VcsFileContentProvider getContentProvider() { - return this; - } - - @NotNull - public String getRemoteRunOnBranchPattern() { - return "remote-run/{teamcity.user}/.+"; - } - - @NotNull - public Map<String, String> getBranchesRevisions(@NotNull VcsRoot root) throws VcsException { - syncClonedRepository(root); - Settings settings = createSettings(root); - BranchesCommand branches = new BranchesCommand(settings); - Map<String, String> result = new HashMap<String, String>(); - for (Map.Entry<String, ChangeSet> entry : branches.execute().entrySet()) { - result.put(entry.getKey(), entry.getValue().getId()); - } - return result; - } - - @NotNull - public Map<String, String> getBranchRootOptions(@NotNull VcsRoot root, @NotNull String branchName) { - final Map<String, String> options = new HashMap<String, String>(root.getProperties()); - options.put(Constants.BRANCH_NAME_PROP, branchName); - return options; - } - - public List<ModificationData> collectChanges(@NotNull VcsRoot fromRoot, @NotNull String fromRootRevision, - @NotNull VcsRoot toRoot, @Nullable String toRootRevision, - @NotNull CheckoutRules checkoutRules) throws VcsException { - //we get all branches while clone, if vcs roots are related it is doesn't matter in which one search for branch point - syncClonedRepository(fromRoot); - String branchPoint = getBranchPoint(fromRoot, fromRootRevision, toRootRevision); - return ((CollectChangesByCheckoutRules) getCollectChangesPolicy()).collectChanges(toRoot, branchPoint, toRootRevision, checkoutRules); - } - - private String getBranchPoint(@NotNull VcsRoot root, String branchOneRev, String branchTwoRev) throws VcsException { - Settings settings = createSettings(root); - LogCommand lc = new LogCommand(settings); - lc.setFromRevId(new ChangeSetRevision(branchOneRev).getId()); - lc.setToRevId(new ChangeSetRevision(branchTwoRev).getId()); - lc.setLimit(1); - List<ChangeSet> changeSets = lc.execute(); - ChangeSet cs = changeSets.get(0); - if (cs.isInitial()) { - return cs.getId(); - } else { - return cs.getParents().get(0).getId(); - } - } - - @NotNull - public CollectChangesPolicy getCollectChangesPolicy() { - return new CollectChangesByCheckoutRules() { - public List<ModificationData> collectChanges(@NotNull VcsRoot root, @NotNull String fromVersion, @Nullable String currentVersion, @NotNull CheckoutRules checkoutRules) throws VcsException { - syncClonedRepository(root); - - // first obtain changes between specified versions - List<ModificationData> result = new ArrayList<ModificationData>(); - if (currentVersion == null) return result; - - Settings settings = createSettings(root); - LogCommand lc = new LogCommand(settings); - String fromId = new ChangeSetRevision(fromVersion).getId(); - lc.setFromRevId(fromId); - lc.setToRevId(new ChangeSetRevision(currentVersion).getId()); - List<ChangeSet> changeSets = lc.execute(); - if (changeSets.isEmpty()) { - return result; - } - - // invoke status command for each changeset and determine what files were modified in these changesets - StatusCommand st = new StatusCommand(settings); - ChangeSet prev = new ChangeSet(fromVersion); - for (ChangeSet cur : changeSets) { - if (cur.getId().equals(fromId)) continue; // skip already reported changeset - - String prevId = prev.getId(); - List<ChangeSetRevision> curParents = cur.getParents(); - boolean merge = curParents != null && curParents.size() > 1; - if (curParents != null && !merge) { - prevId = curParents.get(0).getId(); - } - - List<ModifiedFile> modifiedFiles = new ArrayList<ModifiedFile>(); - if (merge) { - modifiedFiles.addAll(computeModifiedFilesForMergeCommit(settings, cur)); - } else { - st.setFromRevId(prevId); - st.setToRevId(cur.getId()); - modifiedFiles = st.execute(); - } - - // changeset full version will be set into VcsChange structure and - // stored in database (note that getContent method will be invoked with this version) - List<VcsChange> files = toVcsChanges(modifiedFiles, prev.getFullVersion(), cur.getFullVersion(), checkoutRules); - if (files.isEmpty() && !merge) continue; - ModificationData md = new ModificationData(cur.getTimestamp(), files, cur.getDescription(), cur.getUser(), root, cur.getFullVersion(), cur.getId()); - if (merge) { - md.setCanBeIgnored(false); - } - result.add(md); - prev = cur; - } - - return result; - } - }; - } - - @NotNull - public BuildPatchPolicy getBuildPatchPolicy() { - return new BuildPatchByCheckoutRules() { - 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 { - syncClonedRepository(root); - Settings settings = createSettings(root); - if (fromVersion == null) { - buildFullPatch(settings, new ChangeSet(toVersion), builder, checkoutRules); - } else { - buildIncrementalPatch(settings, new ChangeSet(fromVersion), new ChangeSet(toVersion), builder, checkoutRules); - } - } - }; - } - - private void lockWorkDir(@NotNull File workDir) { - getWorkDirLock(workDir).lock(); - } - - private void unlockWorkDir(@NotNull File workDir) { - getWorkDirLock(workDir).unlock(); - } - - @Override - public boolean allowSourceCaching() { - // since a copy of repository for each VCS root is already stored on disk - // we do not need separate cache for our patches - return false; - } - - private Lock getWorkDirLock(final File workDir) { - String path = workDir.getAbsolutePath(); - Lock lock = myWorkDirLocks.get(path); - if (lock == null) { - lock = new ReentrantLock(); - Lock curLock = myWorkDirLocks.putIfAbsent(path, lock); - if (curLock != null) { - lock = curLock; - } - } - return lock; - } - - private void removeOldWorkFolders() { - Set<File> workDirs = getAllClonedRepos(); - if (workDirs == null) return; - - for (VcsRoot vcsRoot: getMercurialVcsRoots()) { - try { - Settings s = createSettings(vcsRoot); - workDirs.remove(PathUtil.getCanonicalFile(s.getLocalRepositoryDir())); - } catch (VcsException e) { - Loggers.VCS.error(e); - } - } - - for (File f: workDirs) { - lockWorkDir(f); - try { - FileUtil.delete(f); - } finally { - unlockWorkDir(f); - } - } - } - - private Collection<VcsRoot> getMercurialVcsRoots() { - List<VcsRoot> res = new ArrayList<VcsRoot>(myVcsManager.getAllRegisteredVcsRoots()); - FilterUtil.filterCollection(res, new Filter<VcsRoot>() { - public boolean accept(@NotNull final VcsRoot data) { - return getName().equals(data.getVcsName()); - } - }); - return res; - } - - @Nullable - private Set<File> getAllClonedRepos() { - File workFoldersParent = myDefaultWorkFolderParent; - if (!workFoldersParent.isDirectory()) return null; - - Set<File> workDirs = new HashSet<File>(); - File[] files = workFoldersParent.listFiles(new FileFilter() { - public boolean accept(final File file) { - return file.isDirectory() && file.getName().startsWith(Settings.DEFAULT_WORK_DIR_PREFIX); - } - }); - if (files != null) { - for (File f: files) { - workDirs.add(PathUtil.getCanonicalFile(f)); - } - } - return workDirs; - } - - public String label(@NotNull String label, @NotNull String version, @NotNull VcsRoot root, @NotNull CheckoutRules checkoutRules) throws VcsException { - syncClonedRepository(root); - - Settings settings = createSettings(root); - - // I do not know why but hg tag does not work correctly if - // update command was not invoked for the current repo - // in such case if there were no tags before Mercurial attempts to - // create new head when tag is pushed to the parent repository - UpdateCommand uc = new UpdateCommand(settings); - uc.execute(); - - String fixedTagname = fixTagName(label); - TagCommand tc = new TagCommand(settings); - tc.setRevId(new ChangeSet(version).getId()); - tc.setTag(fixedTagname); - tc.execute(); - - PushCommand pc = new PushCommand(settings); -// pc.setForce(true); - pc.execute(); - return fixedTagname; - } - - 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', '_'); - } - - private Settings createSettings(final VcsRoot root) throws VcsException { - Settings settings = new Settings(myDefaultWorkFolderParent, root); - String customClonePath = root.getProperty(Constants.SERVER_CLONE_PATH_PROP); - if (!StringUtil.isEmptyOrSpaces(customClonePath) && !myDefaultWorkFolderParent.equals(new File(customClonePath).getAbsoluteFile())) { - File parentDir = new File(customClonePath); - createClonedRepositoryParentDir(parentDir); - - // take last part of repository path - String repPath = settings.getRepositoryUrl(); - String[] splitted = repPath.split("[/\\\\]"); - if (splitted.length > 0) { - repPath = splitted[splitted.length-1]; - } - - File customWorkingDir = new File(parentDir, repPath); - settings.setWorkingDir(customWorkingDir); - } else { - createClonedRepositoryParentDir(myDefaultWorkFolderParent); - } - return settings; - } - - private void createClonedRepositoryParentDir(final File parentDir) throws VcsException { - if (!parentDir.exists() && !parentDir.mkdirs()) { - throw new VcsException("Failed to create parent directory for cloned repository: " + parentDir.getAbsolutePath()); - } - } - - public boolean isAgentSideCheckoutAvailable() { - return true; - } -} +/* + * Copyright 2000-2011 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.BuildAgent; +import jetbrains.buildServer.Used; +import jetbrains.buildServer.buildTriggers.vcs.AbstractVcsPropertiesProcessor; +import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*; +import jetbrains.buildServer.log.Loggers; +import jetbrains.buildServer.serverSide.*; +import jetbrains.buildServer.util.EventDispatcher; +import jetbrains.buildServer.util.FileUtil; +import jetbrains.buildServer.util.StringUtil; +import jetbrains.buildServer.util.filters.Filter; +import jetbrains.buildServer.util.filters.FilterUtil; +import jetbrains.buildServer.vcs.*; +import jetbrains.buildServer.vcs.patches.PatchBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 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, BranchSupport { + private ConcurrentMap<String, Lock> myWorkDirLocks= new ConcurrentHashMap<String, Lock>(); + private VcsManager myVcsManager; + private File myDefaultWorkFolderParent; + + public MercurialVcsSupport(@NotNull final VcsManager vcsManager, + @NotNull ServerPaths paths, + @NotNull final SBuildServer server, + @NotNull EventDispatcher<BuildServerListener> dispatcher) { + myVcsManager = vcsManager; + myDefaultWorkFolderParent = new File(paths.getCachesDir(), "mercurial"); + dispatcher.addListener(new BuildServerAdapter() { + @Override + public void cleanupFinished() { + super.cleanupFinished(); + server.getExecutor().submit(new Runnable() { + public void run() { + removeOldWorkFolders(); + } + }); + } + + @Override + public void sourcesVersionReleased(@NotNull final BuildAgent agent) { + super.sourcesVersionReleased(agent); + server.getExecutor().submit(new Runnable() { + public void run() { + Set<File> clonedRepos = getAllClonedRepos(); + if (clonedRepos == null) return; + for (File f: clonedRepos) { + lockWorkDir(f); + try { + FileUtil.delete(f); + } finally { + unlockWorkDir(f); + } + } + } + }); + } + }); + } + + private Collection<ModifiedFile> computeModifiedFilesForMergeCommit(final Settings settings, final ChangeSet cur) throws VcsException { + ChangedFilesCommand cfc = new ChangedFilesCommand(settings); + cfc.setRevId(cur.getId()); + return cfc.execute(); + } + + private List<VcsChange> toVcsChanges(final List<ModifiedFile> modifiedFiles, String prevVer, String curVer, CheckoutRules rules) { + List<VcsChange> files = new ArrayList<VcsChange>(); + for (ModifiedFile mf: modifiedFiles) { + String normalizedPath = PathUtil.normalizeSeparator(mf.getPath()); + VcsChangeInfo.Type changeType = getChangeType(mf.getStatus()); + if (changeType == null) { + Loggers.VCS.warn("Unable to convert status: " + mf.getStatus() + " to VCS change type"); + changeType = VcsChangeInfo.Type.NOT_CHANGED; + } + files.add(new VcsChange(changeType, mf.getStatus().getName(), normalizedPath, rules.map(mf.getPath()), prevVer, curVer)); + } + return files; + } + + private VcsChangeInfo.Type getChangeType(final ModifiedFile.Status status) { + switch (status) { + case ADDED:return VcsChangeInfo.Type.ADDED; + case MODIFIED:return VcsChangeInfo.Type.CHANGED; + case REMOVED:return VcsChangeInfo.Type.REMOVED; + } + return null; + } + + @NotNull + public byte[] getContent(@NotNull final VcsModification vcsModification, + @NotNull final VcsChangeInfo change, + @NotNull final VcsChangeInfo.ContentType contentType, + @NotNull final VcsRoot vcsRoot) throws VcsException { + syncClonedRepository(vcsRoot); + String version = contentType == VcsChangeInfo.ContentType.AFTER_CHANGE ? change.getAfterChangeRevisionNumber() : change.getBeforeChangeRevisionNumber(); + return getContent(change.getRelativeFileName(), vcsRoot, version); + } + + @NotNull + public byte[] getContent(@NotNull final String filePath, @NotNull final VcsRoot vcsRoot, @NotNull final String version) throws VcsException { + syncClonedRepository(vcsRoot); + Settings settings = createSettings(vcsRoot); + CatCommand cc = new CatCommand(settings); + cc.setRevId(new ChangeSet(version).getId()); + File parentDir = cc.execute(Collections.singletonList(filePath)); + try { + File file = new File(parentDir, filePath); + if (file.isFile()) { + try { + return FileUtil.loadFileBytes(file); + } catch (IOException e) { + throw new VcsException("Failed to load content of file: " + file.getAbsolutePath(), e); + } + } else { + Loggers.VCS.warn("Unable to obtain content of the file: " + filePath); + } + } finally { + deleteTmpDir(parentDir); + } + return new byte[0]; + } + + @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"; + } + + @NotNull + public String getCurrentVersion(@NotNull final VcsRoot root) throws VcsException { + // we will return full version of the most recent change as current version + syncClonedRepository(root); + Settings settings = createSettings(root); + BranchesCommand branches = new BranchesCommand(settings); + Map<String, ChangeSet> result = branches.execute(); + if (!result.containsKey(settings.getBranchName())) { + throw new VcsException("Unable to find current version for the branch: " + settings.getBranchName()); + } + + return result.get(settings.getBranchName()).getFullVersion(); + } + + public boolean sourcesUpdatePossibleIfChangesNotFound(@NotNull final VcsRoot root) { + return false; + } + + @NotNull + public String describeVcsRoot(final VcsRoot vcsRoot) { + return "mercurial: " + vcsRoot.getProperty(Constants.REPOSITORY_PROP); + } + + @Override + public TestConnectionSupport getTestConnectionSupport() { + return new TestConnectionSupport() { + public String testConnection(@NotNull final VcsRoot vcsRoot) throws VcsException { + Settings settings = createSettings(vcsRoot); + IdentifyCommand id = new IdentifyCommand(settings); + StringBuilder res = new StringBuilder(); + res.append(quoteIfNeeded(settings.getHgCommandPath())); + res.append(" identify "); + final String obfuscatedUrl = CommandUtil.removePrivateData(settings.getRepositoryUrl(), Collections.singleton(settings.getPassword())); + res.append(quoteIfNeeded(obfuscatedUrl)); + res.append('\n').append(id.execute()); + return res.toString(); + } + }; + } + + private String quoteIfNeeded(@NotNull String str) { + if (str.indexOf(' ') != -1) { + return "\"" + str + "\""; + } + + return str; + } + + @Nullable + public Map<String, String> getDefaultVcsProperties() { + Map<String, String> defaults = new HashMap<String, String>(); + defaults.put(Constants.HG_COMMAND_PATH_PROP, "hg"); + defaults.put(Constants.UNCOMPRESSED_TRANSFER, "false"); + 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(final Settings settings, @NotNull final ChangeSet fromVer, @NotNull final ChangeSet toVer, final PatchBuilder builder, final CheckoutRules checkoutRules) + throws VcsException, IOException { + StatusCommand st = new StatusCommand(settings); + st.setFromRevId(fromVer.getId()); + st.setToRevId(toVer.getId()); + List<ModifiedFile> modifiedFiles = st.execute(); + List<String> notDeletedFiles = new ArrayList<String>(); + for (ModifiedFile f: modifiedFiles) { + if (f.getStatus() != ModifiedFile.Status.REMOVED) { + notDeletedFiles.add(f.getPath()); + } + } + + if (notDeletedFiles.isEmpty()) return; + + CatCommand cc = new CatCommand(settings); + cc.setRevId(toVer.getId()); + File parentDir = cc.execute(notDeletedFiles); + + try { + for (ModifiedFile f: modifiedFiles) { + String mappedPath = checkoutRules.map(f.getPath()); + if (mappedPath == null) continue; // skip + final File virtualFile = new File(mappedPath); + if (f.getStatus() == ModifiedFile.Status.REMOVED) { + builder.deleteFile(virtualFile, true); + } else { + File realFile = new File(parentDir, f.getPath()); + FileInputStream is = new FileInputStream(realFile); + try { + builder.changeOrCreateBinaryFile(virtualFile, null, is, realFile.length()); + } finally { + is.close(); + } + } + } + } finally { + deleteTmpDir(parentDir); + } + } + + private void deleteTmpDir(File parentDir) { + boolean dirDeleted = FileUtil.delete(parentDir); + if (!dirDeleted) { + Loggers.VCS.warn("Can not delete directory \"" + parentDir.getAbsolutePath() + "\""); + } + } + + // builds patch by exporting files using specified version + private void buildFullPatch(final Settings settings, @NotNull final ChangeSet toVer, final PatchBuilder builder, final CheckoutRules checkoutRules) + throws IOException, VcsException { + CloneCommand cl = new CloneCommand(settings); + // clone from the local repository + cl.setRepository(settings.getLocalRepositoryDir().getAbsolutePath()); + cl.setToId(toVer.getId()); + cl.setUpdateWorkingDir(false); + File tempDir = FileUtil.createTempDirectory("mercurial", toVer.getId()); + try { + final File repRoot = new File(tempDir, "rep"); + cl.setDestDir(repRoot.getAbsolutePath()); + cl.execute(); + + UpdateCommand up = new UpdateCommand(settings); + up.setWorkDirectory(repRoot.getAbsolutePath()); + up.setToId(toVer.getId()); + up.execute(); + + buildPatchFromDirectory(builder, repRoot, new FileFilter() { + public boolean accept(final File file) { + return !(file.isDirectory() && ".hg".equals(file.getName())); + } + }, checkoutRules); + } finally { + FileUtil.delete(tempDir); + } + } + + private void buildPatchFromDirectory(final PatchBuilder builder, final File repRoot, final FileFilter filter, final CheckoutRules checkoutRules) throws IOException { + buildPatchFromDirectory(repRoot, builder, repRoot, filter, checkoutRules); + } + + private void buildPatchFromDirectory(File curDir, final PatchBuilder builder, final File repRoot, final FileFilter filter, final CheckoutRules checkoutRules) 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, filter, checkoutRules); + } 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, filter, checkoutRules); + } + } + } + } + } + + // updates current working copy of repository by pulling changes from the repository specified in VCS root + private void syncClonedRepository(final VcsRoot root) throws VcsException { + Settings settings = createSettings(root); + File workDir = settings.getLocalRepositoryDir(); + lockWorkDir(workDir); + try { + if (settings.hasCopyOfRepository()) { + // update + PullCommand pull = new PullCommand(settings); + pull.execute(); + } else { + // clone + CloneCommand cl = new CloneCommand(settings); + cl.setDestDir(workDir.getAbsolutePath()); + cl.setUpdateWorkingDir(false); + cl.execute(); + } + } finally { + unlockWorkDir(workDir); + } + } + + @Override + public LabelingSupport getLabelingSupport() { + return this; + } + + @NotNull + public VcsFileContentProvider getContentProvider() { + return this; + } + + @NotNull + public String getRemoteRunOnBranchPattern() { + return "remote-run/{teamcity.username}/.+"; + } + + @NotNull + public Map<String, String> getBranchesRevisions(@NotNull VcsRoot root) throws VcsException { + syncClonedRepository(root); + Settings settings = createSettings(root); + BranchesCommand branches = new BranchesCommand(settings); + Map<String, String> result = new HashMap<String, String>(); + for (Map.Entry<String, ChangeSet> entry : branches.execute().entrySet()) { + result.put(entry.getKey(), entry.getValue().getId()); + } + return result; + } + + @NotNull + public Map<String, String> getBranchRootOptions(@NotNull VcsRoot root, @NotNull String branchName) { + final Map<String, String> options = new HashMap<String, String>(root.getProperties()); + options.put(Constants.BRANCH_NAME_PROP, branchName); + return options; + } + + public List<ModificationData> collectChanges(@NotNull VcsRoot fromRoot, @NotNull String fromRootRevision, + @NotNull VcsRoot toRoot, @Nullable String toRootRevision, + @NotNull CheckoutRules checkoutRules) throws VcsException { + //we get all branches while clone, if vcs roots are related it is doesn't matter in which one search for branch point + syncClonedRepository(fromRoot); + String branchPoint = getBranchPoint(fromRoot, fromRootRevision, toRootRevision); + return ((CollectChangesByCheckoutRules) getCollectChangesPolicy()).collectChanges(toRoot, branchPoint, toRootRevision, checkoutRules); + } + + private String getBranchPoint(@NotNull VcsRoot root, String branchOneRev, String branchTwoRev) throws VcsException { + Settings settings = createSettings(root); + LogCommand lc = new LogCommand(settings); + lc.setFromRevId(new ChangeSetRevision(branchOneRev).getId()); + lc.setToRevId(new ChangeSetRevision(branchTwoRev).getId()); + lc.setLimit(1); + List<ChangeSet> changeSets = lc.execute(); + ChangeSet cs = changeSets.get(0); + if (cs.isInitial()) { + return cs.getId(); + } else { + return cs.getParents().get(0).getId(); + } + } + + @NotNull + public CollectChangesPolicy getCollectChangesPolicy() { + return new CollectChangesByCheckoutRules() { + public List<ModificationData> collectChanges(@NotNull VcsRoot root, @NotNull String fromVersion, @Nullable String currentVersion, @NotNull CheckoutRules checkoutRules) throws VcsException { + syncClonedRepository(root); + + // first obtain changes between specified versions + List<ModificationData> result = new ArrayList<ModificationData>(); + if (currentVersion == null) return result; + + Settings settings = createSettings(root); + LogCommand lc = new LogCommand(settings); + String fromId = new ChangeSetRevision(fromVersion).getId(); + lc.setFromRevId(fromId); + lc.setToRevId(new ChangeSetRevision(currentVersion).getId()); + List<ChangeSet> changeSets = lc.execute(); + if (changeSets.isEmpty()) { + return result; + } + + // invoke status command for each changeset and determine what files were modified in these changesets + StatusCommand st = new StatusCommand(settings); + ChangeSet prev = new ChangeSet(fromVersion); + for (ChangeSet cur : changeSets) { + if (cur.getId().equals(fromId)) continue; // skip already reported changeset + + String prevId = prev.getId(); + List<ChangeSetRevision> curParents = cur.getParents(); + boolean merge = curParents != null && curParents.size() > 1; + if (curParents != null && !merge) { + prevId = curParents.get(0).getId(); + } + + List<ModifiedFile> modifiedFiles = new ArrayList<ModifiedFile>(); + if (merge) { + modifiedFiles.addAll(computeModifiedFilesForMergeCommit(settings, cur)); + } else { + st.setFromRevId(prevId); + st.setToRevId(cur.getId()); + modifiedFiles = st.execute(); + } + + // changeset full version will be set into VcsChange structure and + // stored in database (note that getContent method will be invoked with this version) + List<VcsChange> files = toVcsChanges(modifiedFiles, prev.getFullVersion(), cur.getFullVersion(), checkoutRules); + if (files.isEmpty() && !merge) continue; + ModificationData md = new ModificationData(cur.getTimestamp(), files, cur.getDescription(), cur.getUser(), root, cur.getFullVersion(), cur.getId()); + if (merge) { + md.setCanBeIgnored(false); + } + result.add(md); + prev = cur; + } + + return result; + } + }; + } + + @NotNull + public BuildPatchPolicy getBuildPatchPolicy() { + return new BuildPatchByCheckoutRules() { + 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 { + syncClonedRepository(root); + Settings settings = createSettings(root); + if (fromVersion == null) { + buildFullPatch(settings, new ChangeSet(toVersion), builder, checkoutRules); + } else { + buildIncrementalPatch(settings, new ChangeSet(fromVersion), new ChangeSet(toVersion), builder, checkoutRules); + } + } + }; + } + + private void lockWorkDir(@NotNull File workDir) { + getWorkDirLock(workDir).lock(); + } + + private void unlockWorkDir(@NotNull File workDir) { + getWorkDirLock(workDir).unlock(); + } + + @Override + public boolean allowSourceCaching() { + // since a copy of repository for each VCS root is already stored on disk + // we do not need separate cache for our patches + return false; + } + + private Lock getWorkDirLock(final File workDir) { + String path = workDir.getAbsolutePath(); + Lock lock = myWorkDirLocks.get(path); + if (lock == null) { + lock = new ReentrantLock(); + Lock curLock = myWorkDirLocks.putIfAbsent(path, lock); + if (curLock != null) { + lock = curLock; + } + } + return lock; + } + + private void removeOldWorkFolders() { + Set<File> workDirs = getAllClonedRepos(); + if (workDirs == null) return; + + for (VcsRoot vcsRoot: getMercurialVcsRoots()) { + try { + Settings s = createSettings(vcsRoot); + workDirs.remove(PathUtil.getCanonicalFile(s.getLocalRepositoryDir())); + } catch (VcsException e) { + Loggers.VCS.error(e); + } + } + + for (File f: workDirs) { + lockWorkDir(f); + try { + FileUtil.delete(f); + } finally { + unlockWorkDir(f); + } + } + } + + private Collection<VcsRoot> getMercurialVcsRoots() { + List<VcsRoot> res = new ArrayList<VcsRoot>(myVcsManager.getAllRegisteredVcsRoots()); + FilterUtil.filterCollection(res, new Filter<VcsRoot>() { + public boolean accept(@NotNull final VcsRoot data) { + return getName().equals(data.getVcsName()); + } + }); + return res; + } + + @Nullable + private Set<File> getAllClonedRepos() { + File workFoldersParent = myDefaultWorkFolderParent; + if (!workFoldersParent.isDirectory()) return null; + + Set<File> workDirs = new HashSet<File>(); + File[] files = workFoldersParent.listFiles(new FileFilter() { + public boolean accept(final File file) { + return file.isDirectory() && file.getName().startsWith(Settings.DEFAULT_WORK_DIR_PREFIX); + } + }); + if (files != null) { + for (File f: files) { + workDirs.add(PathUtil.getCanonicalFile(f)); + } + } + return workDirs; + } + + public String label(@NotNull String label, @NotNull String version, @NotNull VcsRoot root, @NotNull CheckoutRules checkoutRules) throws VcsException { + syncClonedRepository(root); + + Settings settings = createSettings(root); + + // I do not know why but hg tag does not work correctly if + // update command was not invoked for the current repo + // in such case if there were no tags before Mercurial attempts to + // create new head when tag is pushed to the parent repository + UpdateCommand uc = new UpdateCommand(settings); + uc.execute(); + + String fixedTagname = fixTagName(label); + TagCommand tc = new TagCommand(settings); + tc.setRevId(new ChangeSet(version).getId()); + tc.setTag(fixedTagname); + tc.execute(); + + PushCommand pc = new PushCommand(settings); +// pc.setForce(true); + pc.execute(); + return fixedTagname; + } + + 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', '_'); + } + + private Settings createSettings(final VcsRoot root) throws VcsException { + Settings settings = new Settings(myDefaultWorkFolderParent, root); + String customClonePath = root.getProperty(Constants.SERVER_CLONE_PATH_PROP); + if (!StringUtil.isEmptyOrSpaces(customClonePath) && !myDefaultWorkFolderParent.equals(new File(customClonePath).getAbsoluteFile())) { + File parentDir = new File(customClonePath); + createClonedRepositoryParentDir(parentDir); + + // take last part of repository path + String repPath = settings.getRepositoryUrl(); + String[] splitted = repPath.split("[/\\\\]"); + if (splitted.length > 0) { + repPath = splitted[splitted.length-1]; + } + + File customWorkingDir = new File(parentDir, repPath); + settings.setWorkingDir(customWorkingDir); + } else { + createClonedRepositoryParentDir(myDefaultWorkFolderParent); + } + return settings; + } + + private void createClonedRepositoryParentDir(final File parentDir) throws VcsException { + if (!parentDir.exists() && !parentDir.mkdirs()) { + throw new VcsException("Failed to create parent directory for cloned repository: " + parentDir.getAbsolutePath()); + } + } + + public boolean isAgentSideCheckoutAvailable() { + return true; + } +}
--- a/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java Wed Feb 09 13:52:20 2011 +0300 +++ b/mercurial-tests/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupportTest.java Wed Feb 09 14:01:26 2011 +0300 @@ -1,427 +1,439 @@ -/* - * Copyright 2000-2011 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 com.intellij.execution.configurations.GeneralCommandLine; -import jetbrains.buildServer.ExecResult; -import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandUtil; -import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings; -import jetbrains.buildServer.serverSide.BuildServerListener; -import jetbrains.buildServer.serverSide.SBuildServer; -import jetbrains.buildServer.serverSide.ServerPaths; -import jetbrains.buildServer.util.EventDispatcher; -import jetbrains.buildServer.vcs.*; -import jetbrains.buildServer.vcs.impl.VcsRootImpl; -import jetbrains.buildServer.vcs.patches.PatchBuilderImpl; -import junit.framework.Assert; -import org.jetbrains.annotations.NotNull; -import org.jmock.Mock; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FilenameFilter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; - -@Test -public class MercurialVcsSupportTest extends BaseMercurialTestCase { - private MercurialVcsSupport myVcs; - private ServerPaths myServerPaths; - - @BeforeMethod - protected void setUp() throws Exception { - super.setUp(); - - Mock vcsManagerMock = new Mock(VcsManager.class); - vcsManagerMock.stubs().method("registerVcsSupport"); - Mock serverMock = new Mock(SBuildServer.class); - ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); - serverMock.stubs().method("getExecutor").will(myMockSupport.returnValue(executor)); - - EventDispatcher<BuildServerListener> dispatcher = EventDispatcher.create(BuildServerListener.class); - - File systemDir = myTempFiles.createTempDir(); - myServerPaths = new ServerPaths(systemDir.getAbsolutePath(), systemDir.getAbsolutePath(), systemDir.getAbsolutePath()); - assertTrue(new File(myServerPaths.getCachesDir()).mkdirs()); - myVcs = new MercurialVcsSupport((VcsManager)vcsManagerMock.proxy(), myServerPaths, (SBuildServer)serverMock.proxy(), dispatcher); - } - - protected String getTestDataPath() { - return "mercurial-tests/testData"; - } - - public void test_get_current_version() throws Exception { - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - - assertEquals(myVcs.getCurrentVersion(vcsRoot), "10:9c6a6b4aede0"); - assertEquals("9c6a6b4aede0", myVcs.getVersionDisplayName("10:9c6a6b4aede0", vcsRoot)); - - assertEquals(myVcs.getCurrentVersion(createVcsRoot(simpleRepo(), "test_branch")), "8:04c3ae4c6312"); - - assertEquals(myVcs.getCurrentVersion(createVcsRoot(simpleRepo(), "name with space")), "9:9babcf2d5705"); - } - - private List<ModificationData> collectChanges(@NotNull VcsRoot vcsRoot, @NotNull String from, @NotNull String to, @NotNull CheckoutRules rules) throws VcsException { - return ((CollectChangesByCheckoutRules) myVcs.getCollectChangesPolicy()).collectChanges(vcsRoot, from, to, rules); - } - - public void test_collect_changes_between_two_same_roots() throws Exception { - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - VcsRootImpl sameVcsRoot = createVcsRoot(simpleRepo()); - List<ModificationData> changes = myVcs.collectChanges(vcsRoot, "0:9875b412a788", sameVcsRoot, "3:9522278aa38d", new CheckoutRules("")); - do_check_for_collect_changes(changes); - } - - public void test_collect_changes() throws Exception { - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - List<ModificationData> changes = collectChanges(vcsRoot, "0:9875b412a788", "3:9522278aa38d", new CheckoutRules("")); - do_check_for_collect_changes(changes); - } - - private void do_check_for_collect_changes(List<ModificationData> changes) throws Exception { - assertEquals(3, changes.size()); - - ModificationData md1 = changes.get(0); - ModificationData md2 = changes.get(1); - ModificationData md3 = changes.get(2); - assertEquals(md1.getVersion(), "1:1d446e82d356"); - assertEquals(md1.getDescription(), "new file added"); - List<VcsChange> files1 = md1.getChanges(); - assertEquals(1, files1.size()); - assertEquals(VcsChangeInfo.Type.ADDED, files1.get(0).getType()); - assertEquals(normalizePath(files1.get(0).getRelativeFileName()), "dir1/file3.txt"); - - assertEquals(md2.getVersion(), "2:7209b1f1d793"); - assertEquals(md2.getDescription(), "file4.txt added"); - List<VcsChange> files2 = md2.getChanges(); - assertEquals(1, files2.size()); - assertEquals(files2.get(0).getType(), VcsChangeInfo.Type.ADDED); - assertEquals(normalizePath(files2.get(0).getRelativeFileName()), "dir1/file4.txt"); - - assertEquals(md3.getVersion(), "3:9522278aa38d"); - assertEquals(md3.getDescription(), "file removed"); - List<VcsChange> files3 = md3.getChanges(); - assertEquals(1, files3.size()); - assertEquals(files3.get(0).getType(), VcsChangeInfo.Type.REMOVED); - assertEquals(normalizePath(files3.get(0).getRelativeFileName()), "dir1/file4.txt"); - } - - private ByteArrayOutputStream buildPatch(VcsRoot vcsRoot, String from, String to, CheckoutRules rules) throws IOException, VcsException { - final ByteArrayOutputStream output = new ByteArrayOutputStream(); - final PatchBuilderImpl builder = new PatchBuilderImpl(output); - - ((BuildPatchByCheckoutRules)myVcs.getBuildPatchPolicy()).buildPatch(vcsRoot, from, to, builder, rules); - builder.close(); - return output; - } - - @Test - public void test_build_patch() throws IOException, VcsException { - setName("cleanPatch1"); - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - - ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules("")); - checkPatchResult(output.toByteArray()); - - File clonedReposParentDir = new File(myServerPaths.getCachesDir(), "mercurial"); - assertTrue(clonedReposParentDir.isDirectory()); - assertTrue(1 == clonedReposParentDir.list(new FilenameFilter() { - public boolean accept(final File dir, final String name) { - return name.startsWith("hg_"); - } - }).length); - } - - public void test_build_incremental_patch() throws IOException, VcsException { - setName("patch1"); - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - - ByteArrayOutputStream output = buildPatch(vcsRoot, "3:9522278aa38d", "4:b06a290a363b", new CheckoutRules("")); - - checkPatchResult(output.toByteArray()); - } - - public void test_build_incremental_patch_checkout_rules() throws IOException, VcsException { - setName("patch1_checkout_rules"); - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - - ByteArrayOutputStream output = buildPatch(vcsRoot, "3:9522278aa38d", "4:b06a290a363b", new CheckoutRules("+:dir1=>path")); - - checkPatchResult(output.toByteArray()); - } - - public void test_build_clean_patch_checkout_rules() throws IOException, VcsException { - setName("cleanPatch1_checkout_rules"); - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - - ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules("+:dir1/subdir=>.")); - - checkPatchResult(output.toByteArray()); - } - - public void test_build_incremental_patch_file_with_space() throws IOException, VcsException { - setName("patch2"); - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - - ByteArrayOutputStream output = buildPatch(vcsRoot, "3:9522278aa38d", "6:b9deb9a1c6f4", new CheckoutRules("")); - - checkPatchResult(output.toByteArray()); - } - - public void test_get_content() throws IOException, VcsException { - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - - byte[] content = myVcs.getContent("dir1/subdir/file2.txt", vcsRoot, "4:b06a290a363b"); - assertEquals(new String(content), "bbb"); - content = myVcs.getContent("dir1/subdir/file2.txt", vcsRoot, "5:1d2cc6f3bc29"); - assertEquals(new String(content), "modified\r\nbbb"); - } - - public void test_get_content_in_branch() throws IOException, VcsException { - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); - - byte[] content = myVcs.getContent("file_in_branch.txt", vcsRoot, "8:04c3ae4c6312"); - assertEquals(new String(content), "file from the test_branch\r\nfile modified"); - } - - public void test_test_connection() throws IOException, VcsException { - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - - System.out.println(myVcs.getTestConnectionSupport().testConnection(vcsRoot)); - - vcsRoot.addProperty(Constants.REPOSITORY_PROP, "/some/non/existent/path"); - try { - myVcs.getTestConnectionSupport().testConnection(vcsRoot); - fail("Exception expected"); - } catch (VcsException e) { - } - } - - public void test_tag() throws IOException, VcsException { - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - cleanRepositoryAfterTest(simpleRepo()); - - String actualTag = myVcs.label("new:tag", "1:1d446e82d356", vcsRoot, new CheckoutRules("")); - assertEquals(actualTag, "new_tag"); - - // check the tag is pushed to the parent repository - GeneralCommandLine cli = new GeneralCommandLine(); - cli.setExePath(vcsRoot.getProperty(Constants.HG_COMMAND_PATH_PROP)); - cli.setWorkDirectory(vcsRoot.getProperty(Constants.REPOSITORY_PROP)); - cli.addParameter("tags"); - ExecResult res = CommandUtil.runCommand(cli); - assertTrue(res.getStdout().contains("new_tag")); - assertTrue(res.getStdout().contains("1:1d446e82d356")); - } - - public void test_tag_in_branch() throws IOException, VcsException { - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); - cleanRepositoryAfterTest(simpleRepo()); - - String actualTag = myVcs.label("branch_tag", "7:376dcf05cd2a", vcsRoot, new CheckoutRules("")); - assertEquals(actualTag, "branch_tag"); - - // check the tag is pushed to the parent repository - GeneralCommandLine cli = new GeneralCommandLine(); - cli.setExePath(vcsRoot.getProperty(Constants.HG_COMMAND_PATH_PROP)); - cli.setWorkDirectory(vcsRoot.getProperty(Constants.REPOSITORY_PROP)); - cli.addParameter("tags"); - ExecResult res = CommandUtil.runCommand(cli); - assertTrue(res.getStdout().contains("branch_tag")); - assertTrue(res.getStdout().contains("7:376dcf05cd2a")); - } - - public void test_collect_changes_in_branch() throws Exception { - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); - - // fromVersion(6:b9deb9a1c6f4) is not in the branch (it is in the default branch) - List<ModificationData> changes = collectChanges(vcsRoot, "6:b9deb9a1c6f4", "7:376dcf05cd2a", CheckoutRules.DEFAULT); - assertEquals(1, changes.size()); - - ModificationData md1 = changes.get(0); - assertEquals(md1.getVersion(), "7:376dcf05cd2a"); - assertEquals(md1.getDescription(), "new file added in the test_branch"); - List<VcsChange> files1 = md1.getChanges(); - assertEquals(1, files1.size()); - assertEquals(VcsChangeInfo.Type.ADDED, files1.get(0).getType()); - assertEquals(normalizePath(files1.get(0).getRelativeFileName()), "file_in_branch.txt"); - - changes = collectChanges(vcsRoot, "7:376dcf05cd2a", "8:04c3ae4c6312", CheckoutRules.DEFAULT); - assertEquals(1, changes.size()); - - md1 = changes.get(0); - assertEquals(md1.getVersion(), "8:04c3ae4c6312"); - assertEquals(md1.getDescription(), "file modified"); - } - - public void test_full_patch_from_branch() throws IOException, VcsException { - setName("patch3"); - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); - - ByteArrayOutputStream output = buildPatch(vcsRoot, null, "7:376dcf05cd2a", new CheckoutRules("")); - - checkPatchResult(output.toByteArray()); - } - - public void test_full_patch_from_branch_with_checkout_rules() throws IOException, VcsException { - setName("patch3_checkout_rules1"); - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); - - ByteArrayOutputStream output = buildPatch(vcsRoot, null, "7:376dcf05cd2a", new CheckoutRules("+:.=>path")); - - checkPatchResult(output.toByteArray()); - } - - public void test_full_patch_from_branch_with_checkout_rules_mapped_and_skipped() throws IOException, VcsException { - setName("patch3_checkout_rules2"); - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); - - ByteArrayOutputStream output = buildPatch(vcsRoot, null, "7:376dcf05cd2a", new CheckoutRules("+:dir1=>path/dir1\n+:dir with space")); - - checkPatchResult(output.toByteArray()); - } - - public void test_incremental_patch_from_branch() throws IOException, VcsException { - setName("patch4"); - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); - - ByteArrayOutputStream output = buildPatch(vcsRoot, "7:376dcf05cd2a", "8:04c3ae4c6312", new CheckoutRules("")); - - checkPatchResult(output.toByteArray()); - } - - @Test(enabled = false) - public void support_anchor_branch_notation() throws IOException { - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - String repPath = vcsRoot.getProperty(Constants.REPOSITORY_PROP); - vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath + "#test_branch"); - Settings settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot); - assertEquals("test_branch", settings.getBranchName()); - - vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath + "#"); - settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot); - assertEquals("default", settings.getBranchName()); - - vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath); - settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot); - assertEquals("default", settings.getBranchName()); - } - - public void build_patch_using_custom_clone_path() throws IOException, VcsException { - setName("cleanPatch1"); - VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); - File cloneDir = myTempFiles.createTempDir(); - vcsRoot.addProperty(Constants.SERVER_CLONE_PATH_PROP, cloneDir.getAbsolutePath()); - - ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules("")); - - checkPatchResult(output.toByteArray()); - - assertTrue(new File(cloneDir, new File(vcsRoot.getProperty(Constants.REPOSITORY_PROP)).getName()).isDirectory()); - } - - private String mergeCommittsRepo() { - return new File("mercurial-tests/testData/rep2").getAbsolutePath(); - } - - public void test_collect_changes_between_two_different_roots() throws Exception { - VcsRootImpl defaultRoot = createVcsRoot(mergeCommittsRepo()); - VcsRootImpl branchRoot = createVcsRoot(mergeCommittsRepo(), "test"); - List<ModificationData> changes = myVcs.collectChanges(defaultRoot, "11:48177654181c", branchRoot, "10:fc524efc2bc4", CheckoutRules.DEFAULT); - assertEquals(changes.size(), 2); - - assertEquals("9:8c44244d6645", changes.get(0).getVersion()); - assertEquals("10:fc524efc2bc4", changes.get(1).getVersion()); - } - - public void test_collect_changes_merge() throws Exception { - VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo()); - - List<ModificationData> changes = collectChanges(vcsRoot, "1:a3d15477d297", "4:6eeb8974fe67", CheckoutRules.DEFAULT); - assertEquals(changes.size(), 3); - - assertEquals("2:db8a04d262f3", changes.get(0).getVersion()); - assertEquals("3:2538c02bafeb", changes.get(1).getVersion()); - assertEquals("4:6eeb8974fe67", changes.get(2).getVersion()); - - assertFiles(Arrays.asList("A dir1/file1.txt"), changes.get(0)); - assertFiles(Arrays.asList("A dir2/file2.txt"), changes.get(1)); - assertFiles(Arrays.asList("A dir1/file1.txt", "A dir2/file2.txt"), changes.get(2)); - } - - public void test_collect_changes_merge_conflict() throws Exception { - VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo()); - - List<ModificationData> changes = collectChanges(vcsRoot, "6:6066b677d026", "8:b6e2d176fe8e", CheckoutRules.DEFAULT); - assertEquals(changes.size(), 2); - - assertFiles(Arrays.asList("A dir4/file41.txt"), changes.get(0)); - assertFiles(Arrays.asList("M dir4/file41.txt", "A dir4/file42.txt", "A dir4/file43.txt", "R dir3/file3.txt"), changes.get(1)); - } - - public void test_collect_changes_merge_conflict_named_branch() throws Exception { - VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo()); - - List<ModificationData> changes = collectChanges(vcsRoot, "8:b6e2d176fe8e", "12:1e620196c4b6", CheckoutRules.DEFAULT); - assertEquals(changes.size(), 2); - - assertFiles(Arrays.asList("A dir6/file6.txt"), changes.get(0)); - assertFiles(Arrays.asList("M dir6/file6.txt", "A dir5/file5.txt"), changes.get(1)); - } - - public void use_compressed_transfer_by_default() { - VcsRootImpl root = new VcsRootImpl(1, Constants.VCS_NAME); - root.addAllProperties(myVcs.getDefaultVcsProperties()); - root.addProperty(Constants.REPOSITORY_PROP, "http://host.com/path"); - Settings settings = new Settings(new File("."), root); - assertFalse(settings.isUncompressedTransfer()); - } - - private void assertFiles(final List<String> expectedFiles, final ModificationData modificationData) { - List<String> actualFiles = new ArrayList<String>(); - for (VcsChange vc: modificationData.getChanges()) { - actualFiles.add(toFileStatus(vc.getType()) + " " + vc.getRelativeFileName()); - } - Assert.assertEquals("Actual files: " + actualFiles.toString(), expectedFiles, actualFiles); - } - - private String toFileStatus(VcsChange.Type type) { - switch (type) { - case ADDED: - return "A"; - case REMOVED: - return "R"; - case CHANGED: - return "M"; - } - return "?"; - } - - private Object normalizePath(final String path) { - return path.replace(File.separatorChar, '/'); - } - - - public void test_collect_changes_using_checkout_rules() { - assertTrue(myVcs.getCollectChangesPolicy() instanceof CollectChangesByCheckoutRules); - } -} +/* + * Copyright 2000-2011 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 com.intellij.execution.configurations.GeneralCommandLine; +import jetbrains.buildServer.ExecResult; +import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.CommandUtil; +import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.Settings; +import jetbrains.buildServer.serverSide.BuildServerListener; +import jetbrains.buildServer.serverSide.SBuildServer; +import jetbrains.buildServer.serverSide.ServerPaths; +import jetbrains.buildServer.util.EventDispatcher; +import jetbrains.buildServer.vcs.*; +import jetbrains.buildServer.vcs.impl.VcsRootImpl; +import jetbrains.buildServer.vcs.patches.PatchBuilderImpl; +import junit.framework.Assert; +import org.jetbrains.annotations.NotNull; +import org.jmock.Mock; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@Test +public class MercurialVcsSupportTest extends BaseMercurialTestCase { + private MercurialVcsSupport myVcs; + private ServerPaths myServerPaths; + + @BeforeMethod + protected void setUp() throws Exception { + super.setUp(); + + Mock vcsManagerMock = new Mock(VcsManager.class); + vcsManagerMock.stubs().method("registerVcsSupport"); + Mock serverMock = new Mock(SBuildServer.class); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + serverMock.stubs().method("getExecutor").will(myMockSupport.returnValue(executor)); + + EventDispatcher<BuildServerListener> dispatcher = EventDispatcher.create(BuildServerListener.class); + + File systemDir = myTempFiles.createTempDir(); + myServerPaths = new ServerPaths(systemDir.getAbsolutePath(), systemDir.getAbsolutePath(), systemDir.getAbsolutePath()); + assertTrue(new File(myServerPaths.getCachesDir()).mkdirs()); + myVcs = new MercurialVcsSupport((VcsManager)vcsManagerMock.proxy(), myServerPaths, (SBuildServer)serverMock.proxy(), dispatcher); + } + + protected String getTestDataPath() { + return "mercurial-tests/testData"; + } + + public void test_get_current_version() throws Exception { + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + + assertEquals(myVcs.getCurrentVersion(vcsRoot), "10:9c6a6b4aede0"); + assertEquals("9c6a6b4aede0", myVcs.getVersionDisplayName("10:9c6a6b4aede0", vcsRoot)); + + assertEquals(myVcs.getCurrentVersion(createVcsRoot(simpleRepo(), "test_branch")), "8:04c3ae4c6312"); + + assertEquals(myVcs.getCurrentVersion(createVcsRoot(simpleRepo(), "name with space")), "9:9babcf2d5705"); + } + + private List<ModificationData> collectChanges(@NotNull VcsRoot vcsRoot, @NotNull String from, @NotNull String to, @NotNull CheckoutRules rules) throws VcsException { + return ((CollectChangesByCheckoutRules) myVcs.getCollectChangesPolicy()).collectChanges(vcsRoot, from, to, rules); + } + + public void test_collect_changes_between_two_same_roots() throws Exception { + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + VcsRootImpl sameVcsRoot = createVcsRoot(simpleRepo()); + List<ModificationData> changes = myVcs.collectChanges(vcsRoot, "0:9875b412a788", sameVcsRoot, "3:9522278aa38d", new CheckoutRules("")); + do_check_for_collect_changes(changes); + } + + public void test_collect_changes() throws Exception { + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + List<ModificationData> changes = collectChanges(vcsRoot, "0:9875b412a788", "3:9522278aa38d", new CheckoutRules("")); + do_check_for_collect_changes(changes); + } + + private void do_check_for_collect_changes(List<ModificationData> changes) throws Exception { + assertEquals(3, changes.size()); + + ModificationData md1 = changes.get(0); + ModificationData md2 = changes.get(1); + ModificationData md3 = changes.get(2); + assertEquals(md1.getVersion(), "1:1d446e82d356"); + assertEquals(md1.getDescription(), "new file added"); + List<VcsChange> files1 = md1.getChanges(); + assertEquals(1, files1.size()); + assertEquals(VcsChangeInfo.Type.ADDED, files1.get(0).getType()); + assertEquals(normalizePath(files1.get(0).getRelativeFileName()), "dir1/file3.txt"); + + assertEquals(md2.getVersion(), "2:7209b1f1d793"); + assertEquals(md2.getDescription(), "file4.txt added"); + List<VcsChange> files2 = md2.getChanges(); + assertEquals(1, files2.size()); + assertEquals(files2.get(0).getType(), VcsChangeInfo.Type.ADDED); + assertEquals(normalizePath(files2.get(0).getRelativeFileName()), "dir1/file4.txt"); + + assertEquals(md3.getVersion(), "3:9522278aa38d"); + assertEquals(md3.getDescription(), "file removed"); + List<VcsChange> files3 = md3.getChanges(); + assertEquals(1, files3.size()); + assertEquals(files3.get(0).getType(), VcsChangeInfo.Type.REMOVED); + assertEquals(normalizePath(files3.get(0).getRelativeFileName()), "dir1/file4.txt"); + } + + private ByteArrayOutputStream buildPatch(VcsRoot vcsRoot, String from, String to, CheckoutRules rules) throws IOException, VcsException { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + final PatchBuilderImpl builder = new PatchBuilderImpl(output); + + ((BuildPatchByCheckoutRules)myVcs.getBuildPatchPolicy()).buildPatch(vcsRoot, from, to, builder, rules); + builder.close(); + return output; + } + + @Test + public void test_build_patch() throws IOException, VcsException { + setName("cleanPatch1"); + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + + ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules("")); + checkPatchResult(output.toByteArray()); + + File clonedReposParentDir = new File(myServerPaths.getCachesDir(), "mercurial"); + assertTrue(clonedReposParentDir.isDirectory()); + assertTrue(1 == clonedReposParentDir.list(new FilenameFilter() { + public boolean accept(final File dir, final String name) { + return name.startsWith("hg_"); + } + }).length); + } + + public void test_build_incremental_patch() throws IOException, VcsException { + setName("patch1"); + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + + ByteArrayOutputStream output = buildPatch(vcsRoot, "3:9522278aa38d", "4:b06a290a363b", new CheckoutRules("")); + + checkPatchResult(output.toByteArray()); + } + + public void test_build_incremental_patch_checkout_rules() throws IOException, VcsException { + setName("patch1_checkout_rules"); + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + + ByteArrayOutputStream output = buildPatch(vcsRoot, "3:9522278aa38d", "4:b06a290a363b", new CheckoutRules("+:dir1=>path")); + + checkPatchResult(output.toByteArray()); + } + + public void test_build_clean_patch_checkout_rules() throws IOException, VcsException { + setName("cleanPatch1_checkout_rules"); + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + + ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules("+:dir1/subdir=>.")); + + checkPatchResult(output.toByteArray()); + } + + public void test_build_incremental_patch_file_with_space() throws IOException, VcsException { + setName("patch2"); + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + + ByteArrayOutputStream output = buildPatch(vcsRoot, "3:9522278aa38d", "6:b9deb9a1c6f4", new CheckoutRules("")); + + checkPatchResult(output.toByteArray()); + } + + public void test_get_content() throws IOException, VcsException { + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + + byte[] content = myVcs.getContent("dir1/subdir/file2.txt", vcsRoot, "4:b06a290a363b"); + assertEquals(new String(content), "bbb"); + content = myVcs.getContent("dir1/subdir/file2.txt", vcsRoot, "5:1d2cc6f3bc29"); + assertEquals(new String(content), "modified\r\nbbb"); + } + + public void test_get_content_in_branch() throws IOException, VcsException { + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); + + byte[] content = myVcs.getContent("file_in_branch.txt", vcsRoot, "8:04c3ae4c6312"); + assertEquals(new String(content), "file from the test_branch\r\nfile modified"); + } + + public void test_test_connection() throws IOException, VcsException { + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + + System.out.println(myVcs.getTestConnectionSupport().testConnection(vcsRoot)); + + vcsRoot.addProperty(Constants.REPOSITORY_PROP, "/some/non/existent/path"); + try { + myVcs.getTestConnectionSupport().testConnection(vcsRoot); + fail("Exception expected"); + } catch (VcsException e) { + } + } + + public void test_tag() throws IOException, VcsException { + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + cleanRepositoryAfterTest(simpleRepo()); + + String actualTag = myVcs.label("new:tag", "1:1d446e82d356", vcsRoot, new CheckoutRules("")); + assertEquals(actualTag, "new_tag"); + + // check the tag is pushed to the parent repository + GeneralCommandLine cli = new GeneralCommandLine(); + cli.setExePath(vcsRoot.getProperty(Constants.HG_COMMAND_PATH_PROP)); + cli.setWorkDirectory(vcsRoot.getProperty(Constants.REPOSITORY_PROP)); + cli.addParameter("tags"); + ExecResult res = CommandUtil.runCommand(cli); + assertTrue(res.getStdout().contains("new_tag")); + assertTrue(res.getStdout().contains("1:1d446e82d356")); + } + + public void test_tag_in_branch() throws IOException, VcsException { + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); + cleanRepositoryAfterTest(simpleRepo()); + + String actualTag = myVcs.label("branch_tag", "7:376dcf05cd2a", vcsRoot, new CheckoutRules("")); + assertEquals(actualTag, "branch_tag"); + + // check the tag is pushed to the parent repository + GeneralCommandLine cli = new GeneralCommandLine(); + cli.setExePath(vcsRoot.getProperty(Constants.HG_COMMAND_PATH_PROP)); + cli.setWorkDirectory(vcsRoot.getProperty(Constants.REPOSITORY_PROP)); + cli.addParameter("tags"); + ExecResult res = CommandUtil.runCommand(cli); + assertTrue(res.getStdout().contains("branch_tag")); + assertTrue(res.getStdout().contains("7:376dcf05cd2a")); + } + + public void test_collect_changes_in_branch() throws Exception { + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); + + // fromVersion(6:b9deb9a1c6f4) is not in the branch (it is in the default branch) + List<ModificationData> changes = collectChanges(vcsRoot, "6:b9deb9a1c6f4", "7:376dcf05cd2a", CheckoutRules.DEFAULT); + assertEquals(1, changes.size()); + + ModificationData md1 = changes.get(0); + assertEquals(md1.getVersion(), "7:376dcf05cd2a"); + assertEquals(md1.getDescription(), "new file added in the test_branch"); + List<VcsChange> files1 = md1.getChanges(); + assertEquals(1, files1.size()); + assertEquals(VcsChangeInfo.Type.ADDED, files1.get(0).getType()); + assertEquals(normalizePath(files1.get(0).getRelativeFileName()), "file_in_branch.txt"); + + changes = collectChanges(vcsRoot, "7:376dcf05cd2a", "8:04c3ae4c6312", CheckoutRules.DEFAULT); + assertEquals(1, changes.size()); + + md1 = changes.get(0); + assertEquals(md1.getVersion(), "8:04c3ae4c6312"); + assertEquals(md1.getDescription(), "file modified"); + } + + public void test_full_patch_from_branch() throws IOException, VcsException { + setName("patch3"); + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); + + ByteArrayOutputStream output = buildPatch(vcsRoot, null, "7:376dcf05cd2a", new CheckoutRules("")); + + checkPatchResult(output.toByteArray()); + } + + public void test_full_patch_from_branch_with_checkout_rules() throws IOException, VcsException { + setName("patch3_checkout_rules1"); + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); + + ByteArrayOutputStream output = buildPatch(vcsRoot, null, "7:376dcf05cd2a", new CheckoutRules("+:.=>path")); + + checkPatchResult(output.toByteArray()); + } + + public void test_full_patch_from_branch_with_checkout_rules_mapped_and_skipped() throws IOException, VcsException { + setName("patch3_checkout_rules2"); + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); + + ByteArrayOutputStream output = buildPatch(vcsRoot, null, "7:376dcf05cd2a", new CheckoutRules("+:dir1=>path/dir1\n+:dir with space")); + + checkPatchResult(output.toByteArray()); + } + + public void test_incremental_patch_from_branch() throws IOException, VcsException { + setName("patch4"); + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo(), "test_branch"); + + ByteArrayOutputStream output = buildPatch(vcsRoot, "7:376dcf05cd2a", "8:04c3ae4c6312", new CheckoutRules("")); + + checkPatchResult(output.toByteArray()); + } + + @Test(enabled = false) + public void support_anchor_branch_notation() throws IOException { + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + String repPath = vcsRoot.getProperty(Constants.REPOSITORY_PROP); + vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath + "#test_branch"); + Settings settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot); + assertEquals("test_branch", settings.getBranchName()); + + vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath + "#"); + settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot); + assertEquals("default", settings.getBranchName()); + + vcsRoot.addProperty(Constants.REPOSITORY_PROP, repPath); + settings = new Settings(new File(myServerPaths.getCachesDir()), vcsRoot); + assertEquals("default", settings.getBranchName()); + } + + public void build_patch_using_custom_clone_path() throws IOException, VcsException { + setName("cleanPatch1"); + VcsRootImpl vcsRoot = createVcsRoot(simpleRepo()); + File cloneDir = myTempFiles.createTempDir(); + vcsRoot.addProperty(Constants.SERVER_CLONE_PATH_PROP, cloneDir.getAbsolutePath()); + + ByteArrayOutputStream output = buildPatch(vcsRoot, null, "4:b06a290a363b", new CheckoutRules("")); + + checkPatchResult(output.toByteArray()); + + assertTrue(new File(cloneDir, new File(vcsRoot.getProperty(Constants.REPOSITORY_PROP)).getName()).isDirectory()); + } + + private String mergeCommittsRepo() { + return new File("mercurial-tests/testData/rep2").getAbsolutePath(); + } + + public void test_collect_changes_between_two_different_roots() throws Exception { + VcsRootImpl defaultRoot = createVcsRoot(mergeCommittsRepo()); + VcsRootImpl branchRoot = createVcsRoot(mergeCommittsRepo(), "test"); + List<ModificationData> changes = myVcs.collectChanges(defaultRoot, "11:48177654181c", branchRoot, "10:fc524efc2bc4", CheckoutRules.DEFAULT); + assertEquals(changes.size(), 2); + + assertEquals("9:8c44244d6645", changes.get(0).getVersion()); + assertEquals("10:fc524efc2bc4", changes.get(1).getVersion()); + } + + public void test_collect_changes_merge() throws Exception { + VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo()); + + List<ModificationData> changes = collectChanges(vcsRoot, "1:a3d15477d297", "4:6eeb8974fe67", CheckoutRules.DEFAULT); + assertEquals(changes.size(), 3); + + assertEquals("2:db8a04d262f3", changes.get(0).getVersion()); + assertEquals("3:2538c02bafeb", changes.get(1).getVersion()); + assertEquals("4:6eeb8974fe67", changes.get(2).getVersion()); + + assertFiles(Arrays.asList("A dir1/file1.txt"), changes.get(0)); + assertFiles(Arrays.asList("A dir2/file2.txt"), changes.get(1)); + assertFiles(Arrays.asList("A dir1/file1.txt", "A dir2/file2.txt"), changes.get(2)); + } + + public void test_collect_changes_merge_conflict() throws Exception { + VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo()); + + List<ModificationData> changes = collectChanges(vcsRoot, "6:6066b677d026", "8:b6e2d176fe8e", CheckoutRules.DEFAULT); + assertEquals(changes.size(), 2); + + assertFiles(Arrays.asList("A dir4/file41.txt"), changes.get(0)); + assertFiles(Arrays.asList("M dir4/file41.txt", "A dir4/file42.txt", "A dir4/file43.txt", "R dir3/file3.txt"), changes.get(1)); + } + + public void test_collect_changes_merge_conflict_named_branch() throws Exception { + VcsRootImpl vcsRoot = createVcsRoot(mergeCommittsRepo()); + + List<ModificationData> changes = collectChanges(vcsRoot, "8:b6e2d176fe8e", "12:1e620196c4b6", CheckoutRules.DEFAULT); + assertEquals(changes.size(), 2); + + assertFiles(Arrays.asList("A dir6/file6.txt"), changes.get(0)); + assertFiles(Arrays.asList("M dir6/file6.txt", "A dir5/file5.txt"), changes.get(1)); + } + + //TW-10172 + public void should_not_fill_server_clone_path() { + assertFalse(myVcs.getDefaultVcsProperties().containsKey(Constants.SERVER_CLONE_PATH_PROP)); + + Map<String, String> rootProperties = new HashMap<String, String>() {{ + put(Constants.HG_COMMAND_PATH_PROP, "hg"); + put(Constants.REPOSITORY_PROP, "http://somewhere.com/path"); + }}; + + assertFalse(rootProperties.containsKey(Constants.SERVER_CLONE_PATH_PROP)); + myVcs.getVcsPropertiesProcessor().process(rootProperties); + assertFalse(rootProperties.containsKey(Constants.SERVER_CLONE_PATH_PROP)); + } + + public void use_compressed_transfer_by_default() { + VcsRootImpl root = new VcsRootImpl(1, Constants.VCS_NAME); + root.addAllProperties(myVcs.getDefaultVcsProperties()); + root.addProperty(Constants.REPOSITORY_PROP, "http://host.com/path"); + Settings settings = new Settings(new File("."), root); + assertFalse(settings.isUncompressedTransfer()); + } + + private void assertFiles(final List<String> expectedFiles, final ModificationData modificationData) { + List<String> actualFiles = new ArrayList<String>(); + for (VcsChange vc: modificationData.getChanges()) { + actualFiles.add(toFileStatus(vc.getType()) + " " + vc.getRelativeFileName()); + } + Assert.assertEquals("Actual files: " + actualFiles.toString(), expectedFiles, actualFiles); + } + + private String toFileStatus(VcsChange.Type type) { + switch (type) { + case ADDED: + return "A"; + case REMOVED: + return "R"; + case CHANGED: + return "M"; + } + return "?"; + } + + private Object normalizePath(final String path) { + return path.replace(File.separatorChar, '/'); + } + + + public void test_collect_changes_using_checkout_rules() { + assertTrue(myVcs.getCollectChangesPolicy() instanceof CollectChangesByCheckoutRules); + } +}