Mercurial > hg > mercurial
view mercurial-server/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java @ 31:1c11478f515b
disable cache
author | Pavel.Sher |
---|---|
date | Thu, 24 Jul 2008 14:59:48 +0400 |
parents | 007c63ae45b0 |
children | 1490e2981799 |
line wrap: on
line source
/* * Copyright 2000-2007 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.CollectChangesByIncludeRule; 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.InvalidProperty; import jetbrains.buildServer.serverSide.PropertiesProcessor; import jetbrains.buildServer.serverSide.SBuildServer; import jetbrains.buildServer.serverSide.ServerPaths; import jetbrains.buildServer.util.FileUtil; 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.TimeUnit; 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/mercurial folder. * <p>Personal builds (remote runs) are not yet supported, they require corresponding functionality from the IDE. * <p>Checkout on agent mode is not yet supported too. */ public class MercurialVcsSupport extends VcsSupport implements CollectChangesByIncludeRule { private ConcurrentMap<String, Lock> myWorkDirLocks= new ConcurrentHashMap<String, Lock>(); private static final int OLD_WORK_DIRS_CLEANUP_PERIOD = 600; private VcsManager myVcsManager; private File myDefaultWorkFolderParent; public MercurialVcsSupport(@NotNull final VcsManager vcsManager, @NotNull ServerPaths paths, @NotNull SBuildServer server) { vcsManager.registerVcsSupport(this); myVcsManager = vcsManager; server.getExecutor().scheduleAtFixedRate(new Runnable() { public void run() { removeOldWorkFolders(); } }, 0, OLD_WORK_DIRS_CLEANUP_PERIOD, TimeUnit.SECONDS); myDefaultWorkFolderParent = new File(paths.getCachesDir()); } public List<ModificationData> collectBuildChanges(final VcsRoot root, @NotNull final String fromVersion, @NotNull final String currentVersion, final CheckoutRules checkoutRules) throws VcsException { updateWorkingDirectory(root); return VcsSupportUtil.collectBuildChanges(root, fromVersion, currentVersion, checkoutRules, this); } public List<ModificationData> collectBuildChanges(final VcsRoot root, final String fromVersion, final String currentVersion, final IncludeRule includeRule) throws VcsException { // first obtain changes between specified versions List<ModificationData> result = new ArrayList<ModificationData>(); Settings settings = new Settings(myDefaultWorkFolderParent, root); LogCommand lc = new LogCommand(settings); lc.setFromRevId(new ChangeSet(fromVersion).getId()); lc.setToRevId(new ChangeSet(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 Iterator<ChangeSet> it = changeSets.iterator(); ChangeSet prev = it.next(); // skip first changeset (cause it was already reported) StatusCommand st = new StatusCommand(settings); while (it.hasNext()) { ChangeSet cur = it.next(); st.setFromRevId(prev.getId()); st.setToRevId(cur.getId()); List<ModifiedFile> 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(), includeRule); if (files.isEmpty()) continue; ModificationData md = new ModificationData(cur.getTimestamp(), files, cur.getSummary(), cur.getUser(), root, cur.getFullVersion(), cur.getFullVersion()); result.add(md); prev = cur; } return result; } private List<VcsChange> toVcsChanges(final List<ModifiedFile> modifiedFiles, String prevVer, String curVer, final IncludeRule includeRule) { List<VcsChange> files = new ArrayList<VcsChange>(); for (ModifiedFile mf: modifiedFiles) { String normalizedPath = PathUtil.normalizeSeparator(mf.getPath()); if (!normalizedPath.startsWith(includeRule.getFrom())) continue; // skip files which do not match include rule 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, normalizedPath, 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(final VcsModification vcsModification, final VcsChangeInfo change, final VcsChangeInfo.ContentType contentType, final VcsRoot vcsRoot) throws VcsException { updateWorkingDirectory(vcsRoot); String version = contentType == VcsChangeInfo.ContentType.AFTER_CHANGE ? change.getAfterChangeRevisionNumber() : change.getBeforeChangeRevisionNumber(); return getContent(change.getRelativeFileName(), vcsRoot, version); } @NotNull public byte[] getContent(final String filePath, final VcsRoot vcsRoot, final String version) throws VcsException { updateWorkingDirectory(vcsRoot); Settings settings = new Settings(myDefaultWorkFolderParent, vcsRoot); CatCommand cc = new CatCommand(settings); ChangeSet cs = new ChangeSet(version); cc.setRevId(cs.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 { FileUtil.delete(parentDir); } return new byte[0]; } public String getName() { return Constants.VCS_NAME; } @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; } }; } public String getVcsSettingsJspFilePath() { return "mercurialSettings.jsp"; } @NotNull public String getCurrentVersion(final VcsRoot root) throws VcsException { // we will return full version of the most recent change as current version updateWorkingDirectory(root); Settings settings = new Settings(myDefaultWorkFolderParent, root); TipCommand lc = new TipCommand(settings); ChangeSet changeSet = lc.execute(); return changeSet.getFullVersion(); } public String describeVcsRoot(final VcsRoot vcsRoot) { return "mercurial: " + vcsRoot.getProperty(Constants.REPOSITORY_PROP); } public boolean isTestConnectionSupported() { return true; } @Nullable public String testConnection(final VcsRoot vcsRoot) throws VcsException { Settings settings = new Settings(myDefaultWorkFolderParent, vcsRoot); IdentifyCommand id = new IdentifyCommand(settings); StringBuilder res = new StringBuilder(); res.append(quoteIfNeeded(settings.getHgCommandPath())); res.append(" identify "); res.append(quoteIfNeeded(settings.getRepository())); 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() { return Collections.singletonMap(Constants.HG_COMMAND_PATH_PROP, "hg"); } public String getVersionDisplayName(final String version, final VcsRoot root) throws VcsException { return version; } @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; } } }; } public void buildPatch(final VcsRoot root, @Nullable final String fromVersion, @NotNull final String toVersion, final PatchBuilder builder, final CheckoutRules checkoutRules) throws IOException, VcsException { updateWorkingDirectory(root); Settings settings = new Settings(myDefaultWorkFolderParent, root); if (fromVersion == null) { buildFullPatch(settings, new ChangeSet(toVersion), builder); } else { buildIncrementalPatch(settings, new ChangeSet(fromVersion), new ChangeSet(toVersion), builder); } } // builds patch from version to version private void buildIncrementalPatch(final Settings settings, @NotNull final ChangeSet fromVer, @NotNull final ChangeSet toVer, final PatchBuilder builder) 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()); } } CatCommand cc = new CatCommand(settings); cc.setRevId(toVer.getId()); File parentDir = cc.execute(notDeletedFiles); try { for (ModifiedFile f: modifiedFiles) { final File virtualFile = new File(f.getPath()); 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 { FileUtil.delete(parentDir); } } // builds patch by exporting files using specified version private void buildFullPatch(final Settings settings, @NotNull final ChangeSet toVer, final PatchBuilder builder) throws IOException, VcsException { CloneCommand cl = new CloneCommand(settings); cl.setToId(toVer.getId()); File tempDir = FileUtil.createTempDirectory("mercurial", toVer.getId()); try { final File repRoot = new File(tempDir, "rep"); cl.setDestDir(repRoot.getAbsolutePath()); cl.execute(); buildPatchFromDirectory(builder, repRoot, new FileFilter() { public boolean accept(final File file) { return !(file.isDirectory() && ".hg".equals(file.getName())); } }); } finally { FileUtil.delete(tempDir); } } private void buildPatchFromDirectory(final PatchBuilder builder, final File repRoot, final FileFilter filter) throws IOException { buildPatchFromDirectory(repRoot, builder, repRoot, filter); } private void buildPatchFromDirectory(File curDir, final PatchBuilder builder, final File repRoot, final FileFilter filter) throws IOException { File[] files = curDir.listFiles(filter); if (files != null) { for (File realFile: files) { String relPath = realFile.getAbsolutePath().substring(repRoot.getAbsolutePath().length()); final File virtualFile = new File(relPath); if (realFile.isDirectory()) { builder.createDirectory(virtualFile); buildPatchFromDirectory(realFile, builder, repRoot, filter); } else { final FileInputStream is = new FileInputStream(realFile); try { builder.createBinaryFile(virtualFile, null, is, realFile.length()); } finally { is.close(); } } } } } // updates current working copy of repository by pulling changes from the repository specified in VCS root private void updateWorkingDirectory(final VcsRoot root) throws VcsException { Settings settings = new Settings(myDefaultWorkFolderParent, root); File workDir = settings.getWorkingDir(); 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); } } private void lockWorkDir(@NotNull File workDir) { getWorkDirLock(workDir).lock(); } private void unlockWorkDir(@NotNull File workDir) { getWorkDirLock(workDir).unlock(); } @Override public boolean ignoreServerCachesFor(final VcsRoot root) { // since a copy of repository for each VCS root is already stored on disk // we do not need separate cache for our patches return true; } 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() { File workFoldersParent = new File(myDefaultWorkFolderParent, "mercurial"); if (!workFoldersParent.isDirectory()) return; 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(FileUtil.getCanonicalFile(f)); } } for (VcsRoot vcsRoot: myVcsManager.getAllRegisteredVcsRoots()) { if (getName().equals(vcsRoot.getVcsName())) { Settings s = new Settings(myDefaultWorkFolderParent, vcsRoot); workDirs.remove(FileUtil.getCanonicalFile(s.getWorkingDir())); } } for (File f: workDirs) { lockWorkDir(f); try { FileUtil.delete(f); } finally { unlockWorkDir(f); } } } }