Mercurial > hg > mercurial
view mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java @ 157:43f4c91d5eaa
Implement BranchSupport interface
author | Dmitry Neverov <dmitry.neverov@jetbrains.com> |
---|---|
date | Tue, 11 Jan 2011 20:38:46 +0300 |
parents | 3a8af53dea6b 7e36394a9c00 |
children | 5198b02fc5e9 |
line wrap: on
line source
/* * Copyright 2000-2010 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()); 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; } }