Pavel@25: /* dmitry@160: * Copyright 2000-2011 JetBrains s.r.o. Pavel@25: * Pavel@25: * Licensed under the Apache License, Version 2.0 (the "License"); Pavel@25: * you may not use this file except in compliance with the License. Pavel@25: * You may obtain a copy of the License at Pavel@25: * Pavel@25: * http://www.apache.org/licenses/LICENSE-2.0 Pavel@25: * Pavel@25: * Unless required by applicable law or agreed to in writing, software Pavel@25: * distributed under the License is distributed on an "AS IS" BASIS, Pavel@25: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Pavel@25: * See the License for the specific language governing permissions and Pavel@25: * limitations under the License. Pavel@25: */ Pavel@0: package jetbrains.buildServer.buildTriggers.vcs.mercurial; Pavel@0: Pavel@103: import jetbrains.buildServer.BuildAgent; Pavel@0: import jetbrains.buildServer.Used; Pavel@0: import jetbrains.buildServer.buildTriggers.vcs.AbstractVcsPropertiesProcessor; Pavel@0: import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*; Pavel@0: import jetbrains.buildServer.log.Loggers; Pavel@103: import jetbrains.buildServer.serverSide.*; Pavel@103: import jetbrains.buildServer.util.EventDispatcher; Pavel@0: import jetbrains.buildServer.util.FileUtil; Pavel@62: import jetbrains.buildServer.util.StringUtil; Pavel@103: import jetbrains.buildServer.util.filters.Filter; Pavel@103: import jetbrains.buildServer.util.filters.FilterUtil; Pavel@0: import jetbrains.buildServer.vcs.*; Pavel@0: import jetbrains.buildServer.vcs.patches.PatchBuilder; Pavel@0: import org.jetbrains.annotations.NotNull; Pavel@0: import org.jetbrains.annotations.Nullable; Pavel@0: Pavel@1: import java.io.File; Pavel@1: import java.io.FileFilter; Pavel@1: import java.io.FileInputStream; Pavel@1: import java.io.IOException; Pavel@1: import java.util.*; Pavel@21: import java.util.concurrent.ConcurrentHashMap; Pavel@21: import java.util.concurrent.ConcurrentMap; Pavel@21: import java.util.concurrent.locks.Lock; Pavel@21: import java.util.concurrent.locks.ReentrantLock; Pavel@1: Pavel@19: /** Pavel@19: * Mercurial VCS plugin for TeamCity works as follows: Pavel@19: * Pavel@19: * Pavel@57: *

Working copy of repository is created in the $TEAMCITY_DATA_PATH/system/caches/hg_<hash code> folder. Pavel@19: *

Personal builds (remote runs) are not yet supported, they require corresponding functionality from the IDE. Pavel@19: */ dmitry@148: public class MercurialVcsSupport extends ServerVcsSupport implements LabelingSupport, VcsFileContentProvider, BranchSupport { Pavel@21: private ConcurrentMap myWorkDirLocks= new ConcurrentHashMap(); Pavel@21: private VcsManager myVcsManager; Pavel@27: private File myDefaultWorkFolderParent; Pavel@0: Pavel@21: public MercurialVcsSupport(@NotNull final VcsManager vcsManager, Pavel@21: @NotNull ServerPaths paths, Pavel@103: @NotNull final SBuildServer server, Pavel@103: @NotNull EventDispatcher dispatcher) { Pavel@21: myVcsManager = vcsManager; Pavel@62: myDefaultWorkFolderParent = new File(paths.getCachesDir(), "mercurial"); Pavel@103: dispatcher.addListener(new BuildServerAdapter() { Pavel@103: @Override Pavel@121: public void cleanupFinished() { Pavel@121: super.cleanupFinished(); Pavel@121: server.getExecutor().submit(new Runnable() { Pavel@121: public void run() { Pavel@121: removeOldWorkFolders(); Pavel@121: } Pavel@121: }); Pavel@121: } Pavel@121: Pavel@121: @Override Pavel@103: public void sourcesVersionReleased(@NotNull final BuildAgent agent) { Pavel@103: super.sourcesVersionReleased(agent); Pavel@103: server.getExecutor().submit(new Runnable() { Pavel@103: public void run() { Pavel@103: Set clonedRepos = getAllClonedRepos(); Pavel@103: if (clonedRepos == null) return; Pavel@103: for (File f: clonedRepos) { Pavel@103: lockWorkDir(f); Pavel@103: try { Pavel@103: FileUtil.delete(f); Pavel@103: } finally { Pavel@103: unlockWorkDir(f); Pavel@103: } Pavel@103: } Pavel@103: } Pavel@103: }); Pavel@103: } Pavel@103: }); Pavel@0: } Pavel@0: Pavel@90: private Collection computeModifiedFilesForMergeCommit(final Settings settings, final ChangeSet cur) throws VcsException { Pavel@90: ChangedFilesCommand cfc = new ChangedFilesCommand(settings); Pavel@90: cfc.setRevId(cur.getId()); Pavel@90: return cfc.execute(); Pavel@90: } Pavel@90: dmitry@145: private List toVcsChanges(final List modifiedFiles, String prevVer, String curVer, CheckoutRules rules) { Pavel@0: List files = new ArrayList(); Pavel@0: for (ModifiedFile mf: modifiedFiles) { dmitry@145: String normalizedPath = PathUtil.normalizeSeparator(mf.getPath()); Pavel@0: VcsChangeInfo.Type changeType = getChangeType(mf.getStatus()); Pavel@0: if (changeType == null) { Pavel@0: Loggers.VCS.warn("Unable to convert status: " + mf.getStatus() + " to VCS change type"); Pavel@0: changeType = VcsChangeInfo.Type.NOT_CHANGED; Pavel@0: } dmitry@145: files.add(new VcsChange(changeType, mf.getStatus().getName(), normalizedPath, rules.map(mf.getPath()), prevVer, curVer)); Pavel@0: } Pavel@0: return files; Pavel@0: } Pavel@0: Pavel@0: private VcsChangeInfo.Type getChangeType(final ModifiedFile.Status status) { Pavel@0: switch (status) { Pavel@0: case ADDED:return VcsChangeInfo.Type.ADDED; Pavel@0: case MODIFIED:return VcsChangeInfo.Type.CHANGED; Pavel@0: case REMOVED:return VcsChangeInfo.Type.REMOVED; Pavel@0: } Pavel@0: return null; Pavel@0: } Pavel@0: Pavel@0: @NotNull Pavel@86: public byte[] getContent(@NotNull final VcsModification vcsModification, Pavel@86: @NotNull final VcsChangeInfo change, Pavel@86: @NotNull final VcsChangeInfo.ContentType contentType, Pavel@86: @NotNull final VcsRoot vcsRoot) throws VcsException { Pavel@44: syncClonedRepository(vcsRoot); Pavel@12: String version = contentType == VcsChangeInfo.ContentType.AFTER_CHANGE ? change.getAfterChangeRevisionNumber() : change.getBeforeChangeRevisionNumber(); Pavel@12: return getContent(change.getRelativeFileName(), vcsRoot, version); Pavel@0: } Pavel@0: Pavel@0: @NotNull Pavel@86: public byte[] getContent(@NotNull final String filePath, @NotNull final VcsRoot vcsRoot, @NotNull final String version) throws VcsException { Pavel@44: syncClonedRepository(vcsRoot); Pavel@44: Settings settings = createSettings(vcsRoot); Pavel@12: CatCommand cc = new CatCommand(settings); Pavel@59: cc.setRevId(new ChangeSet(version).getId()); Pavel@12: File parentDir = cc.execute(Collections.singletonList(filePath)); Pavel@30: try { Pavel@30: File file = new File(parentDir, filePath); Pavel@30: if (file.isFile()) { Pavel@30: try { Pavel@30: return FileUtil.loadFileBytes(file); Pavel@30: } catch (IOException e) { Pavel@30: throw new VcsException("Failed to load content of file: " + file.getAbsolutePath(), e); Pavel@30: } Pavel@30: } else { Pavel@30: Loggers.VCS.warn("Unable to obtain content of the file: " + filePath); Pavel@12: } Pavel@30: } finally { nd@118: deleteTmpDir(parentDir); Pavel@12: } Pavel@0: return new byte[0]; Pavel@0: } Pavel@0: Pavel@86: @NotNull Pavel@0: public String getName() { Pavel@28: return Constants.VCS_NAME; Pavel@0: } Pavel@0: Pavel@86: @NotNull Pavel@0: @Used("jsp") Pavel@0: public String getDisplayName() { Pavel@0: return "Mercurial"; Pavel@0: } Pavel@0: Pavel@0: @Nullable Pavel@0: public PropertiesProcessor getVcsPropertiesProcessor() { Pavel@0: return new AbstractVcsPropertiesProcessor() { Pavel@0: public Collection process(final Map properties) { Pavel@0: List result = new ArrayList(); Pavel@0: if (isEmpty(properties.get(Constants.HG_COMMAND_PATH_PROP))) { Pavel@0: result.add(new InvalidProperty(Constants.HG_COMMAND_PATH_PROP, "Path to 'hg' command must be specified")); Pavel@0: } Pavel@0: if (isEmpty(properties.get(Constants.REPOSITORY_PROP))) { Pavel@0: result.add(new InvalidProperty(Constants.REPOSITORY_PROP, "Repository must be specified")); Pavel@0: } Pavel@62: if (isEmpty(properties.get(Constants.SERVER_CLONE_PATH_PROP))) { Pavel@62: properties.put(Constants.SERVER_CLONE_PATH_PROP, myDefaultWorkFolderParent.getAbsolutePath()); Pavel@62: } Pavel@0: return result; Pavel@0: } Pavel@0: }; Pavel@0: } Pavel@0: Pavel@86: @NotNull Pavel@0: public String getVcsSettingsJspFilePath() { Pavel@0: return "mercurialSettings.jsp"; Pavel@0: } Pavel@0: Pavel@0: @NotNull Pavel@86: public String getCurrentVersion(@NotNull final VcsRoot root) throws VcsException { Pavel@19: // we will return full version of the most recent change as current version Pavel@44: syncClonedRepository(root); Pavel@44: Settings settings = createSettings(root); Pavel@57: BranchesCommand branches = new BranchesCommand(settings); Pavel@57: Map result = branches.execute(); Pavel@57: if (!result.containsKey(settings.getBranchName())) { Pavel@57: throw new VcsException("Unable to find current version for the branch: " + settings.getBranchName()); Pavel@57: } Pavel@57: Pavel@57: return result.get(settings.getBranchName()).getFullVersion(); Pavel@0: } Pavel@0: Pavel@106: public boolean sourcesUpdatePossibleIfChangesNotFound(@NotNull final VcsRoot root) { Pavel@106: return false; Pavel@106: } Pavel@106: Pavel@86: @NotNull Pavel@0: public String describeVcsRoot(final VcsRoot vcsRoot) { Pavel@0: return "mercurial: " + vcsRoot.getProperty(Constants.REPOSITORY_PROP); Pavel@0: } Pavel@0: Pavel@106: @Override Pavel@106: public TestConnectionSupport getTestConnectionSupport() { Pavel@106: return new TestConnectionSupport() { Pavel@106: public String testConnection(@NotNull final VcsRoot vcsRoot) throws VcsException { Pavel@106: Settings settings = createSettings(vcsRoot); Pavel@106: IdentifyCommand id = new IdentifyCommand(settings); Pavel@106: StringBuilder res = new StringBuilder(); Pavel@106: res.append(quoteIfNeeded(settings.getHgCommandPath())); Pavel@106: res.append(" identify "); Pavel@106: final String obfuscatedUrl = CommandUtil.removePrivateData(settings.getRepositoryUrl(), Collections.singleton(settings.getPassword())); Pavel@106: res.append(quoteIfNeeded(obfuscatedUrl)); Pavel@106: res.append('\n').append(id.execute()); Pavel@106: return res.toString(); Pavel@106: } Pavel@106: }; Pavel@0: } Pavel@0: Pavel@17: private String quoteIfNeeded(@NotNull String str) { Pavel@17: if (str.indexOf(' ') != -1) { Pavel@17: return "\"" + str + "\""; Pavel@17: } Pavel@17: Pavel@17: return str; Pavel@17: } Pavel@17: Pavel@0: @Nullable Pavel@0: public Map getDefaultVcsProperties() { Pavel@62: Map defaults = new HashMap(); Pavel@62: defaults.put(Constants.HG_COMMAND_PATH_PROP, "hg"); Pavel@62: defaults.put(Constants.SERVER_CLONE_PATH_PROP, myDefaultWorkFolderParent.getAbsolutePath()); dmitry@169: defaults.put(Constants.UNCOMPRESSED_TRANSFER, "false"); Pavel@62: return defaults; Pavel@0: } Pavel@0: Pavel@86: public String getVersionDisplayName(@NotNull final String version, @NotNull final VcsRoot root) throws VcsException { Pavel@59: return new ChangeSet(version).getId(); Pavel@0: } Pavel@0: Pavel@0: @NotNull Pavel@0: public Comparator getVersionComparator() { Pavel@19: // comparator is called when TeamCity needs to sort modifications in the order of their appearance, Pavel@19: // currently we sort changes by revision number, not sure however that this is a good idea, Pavel@19: // probably it would be better to sort them by timestamp (and to add timestamp into the version). Pavel@0: return new Comparator() { Pavel@0: public int compare(final String o1, final String o2) { Pavel@0: try { Pavel@0: return new ChangeSet(o1).getRevNumber() - new ChangeSet(o2).getRevNumber(); Pavel@0: } catch (Exception e) { Pavel@0: return 1; Pavel@0: } Pavel@0: } Pavel@0: }; Pavel@0: } Pavel@0: Pavel@19: // builds patch from version to version Pavel@79: private void buildIncrementalPatch(final Settings settings, @NotNull final ChangeSet fromVer, @NotNull final ChangeSet toVer, final PatchBuilder builder, final CheckoutRules checkoutRules) Pavel@0: throws VcsException, IOException { Pavel@0: StatusCommand st = new StatusCommand(settings); Pavel@0: st.setFromRevId(fromVer.getId()); Pavel@0: st.setToRevId(toVer.getId()); Pavel@0: List modifiedFiles = st.execute(); Pavel@0: List notDeletedFiles = new ArrayList(); Pavel@0: for (ModifiedFile f: modifiedFiles) { Pavel@0: if (f.getStatus() != ModifiedFile.Status.REMOVED) { Pavel@0: notDeletedFiles.add(f.getPath()); Pavel@0: } Pavel@0: } Pavel@0: Pavel@47: if (notDeletedFiles.isEmpty()) return; Pavel@47: Pavel@0: CatCommand cc = new CatCommand(settings); Pavel@0: cc.setRevId(toVer.getId()); Pavel@0: File parentDir = cc.execute(notDeletedFiles); Pavel@0: Pavel@0: try { Pavel@0: for (ModifiedFile f: modifiedFiles) { Pavel@79: String mappedPath = checkoutRules.map(f.getPath()); Pavel@79: if (mappedPath == null) continue; // skip Pavel@79: final File virtualFile = new File(mappedPath); Pavel@0: if (f.getStatus() == ModifiedFile.Status.REMOVED) { Pavel@0: builder.deleteFile(virtualFile, true); Pavel@0: } else { Pavel@0: File realFile = new File(parentDir, f.getPath()); Pavel@0: FileInputStream is = new FileInputStream(realFile); Pavel@0: try { Pavel@11: builder.changeOrCreateBinaryFile(virtualFile, null, is, realFile.length()); Pavel@0: } finally { Pavel@0: is.close(); Pavel@0: } Pavel@0: } Pavel@0: } Pavel@0: } finally { nd@118: deleteTmpDir(parentDir); nd@118: } nd@118: } nd@118: nd@118: private void deleteTmpDir(File parentDir) { nd@118: boolean dirDeleted = FileUtil.delete(parentDir); nd@118: if (!dirDeleted) { nd@118: Loggers.VCS.warn("Can not delete directory \"" + parentDir.getAbsolutePath() + "\""); Pavel@0: } Pavel@0: } Pavel@0: Pavel@19: // builds patch by exporting files using specified version Pavel@79: private void buildFullPatch(final Settings settings, @NotNull final ChangeSet toVer, final PatchBuilder builder, final CheckoutRules checkoutRules) Pavel@0: throws IOException, VcsException { Pavel@0: CloneCommand cl = new CloneCommand(settings); Pavel@57: // clone from the local repository Pavel@67: cl.setRepository(settings.getLocalRepositoryDir().getAbsolutePath()); Pavel@0: cl.setToId(toVer.getId()); Pavel@57: cl.setUpdateWorkingDir(false); Pavel@0: File tempDir = FileUtil.createTempDirectory("mercurial", toVer.getId()); Pavel@0: try { Pavel@0: final File repRoot = new File(tempDir, "rep"); Pavel@0: cl.setDestDir(repRoot.getAbsolutePath()); Pavel@0: cl.execute(); Pavel@57: Pavel@57: UpdateCommand up = new UpdateCommand(settings); Pavel@57: up.setWorkDirectory(repRoot.getAbsolutePath()); Pavel@57: up.setToId(toVer.getId()); Pavel@57: up.execute(); Pavel@57: Pavel@0: buildPatchFromDirectory(builder, repRoot, new FileFilter() { Pavel@0: public boolean accept(final File file) { Pavel@0: return !(file.isDirectory() && ".hg".equals(file.getName())); Pavel@0: } Pavel@79: }, checkoutRules); Pavel@0: } finally { Pavel@0: FileUtil.delete(tempDir); Pavel@0: } Pavel@0: } Pavel@0: Pavel@79: private void buildPatchFromDirectory(final PatchBuilder builder, final File repRoot, final FileFilter filter, final CheckoutRules checkoutRules) throws IOException { Pavel@79: buildPatchFromDirectory(repRoot, builder, repRoot, filter, checkoutRules); Pavel@0: } Pavel@0: Pavel@79: private void buildPatchFromDirectory(File curDir, final PatchBuilder builder, final File repRoot, final FileFilter filter, final CheckoutRules checkoutRules) throws IOException { Pavel@0: File[] files = curDir.listFiles(filter); Pavel@0: if (files != null) { Pavel@0: for (File realFile: files) { Pavel@0: String relPath = realFile.getAbsolutePath().substring(repRoot.getAbsolutePath().length()); Pavel@79: String mappedPath = checkoutRules.map(relPath); Pavel@97: if (mappedPath != null && mappedPath.length() > 0) { Pavel@79: final File virtualFile = new File(mappedPath); Pavel@79: if (realFile.isDirectory()) { Pavel@79: builder.createDirectory(virtualFile); Pavel@79: buildPatchFromDirectory(realFile, builder, repRoot, filter, checkoutRules); Pavel@79: } else { Pavel@79: final FileInputStream is = new FileInputStream(realFile); Pavel@79: try { Pavel@79: builder.createBinaryFile(virtualFile, null, is, realFile.length()); Pavel@79: } finally { Pavel@79: is.close(); Pavel@79: } Pavel@0: } Pavel@97: } else { Pavel@97: if (realFile.isDirectory()) { Pavel@97: buildPatchFromDirectory(realFile, builder, repRoot, filter, checkoutRules); Pavel@97: } Pavel@0: } Pavel@0: } Pavel@0: } Pavel@0: } Pavel@0: Pavel@19: // updates current working copy of repository by pulling changes from the repository specified in VCS root Pavel@44: private void syncClonedRepository(final VcsRoot root) throws VcsException { Pavel@44: Settings settings = createSettings(root); Pavel@57: File workDir = settings.getLocalRepositoryDir(); Pavel@21: lockWorkDir(workDir); Pavel@21: try { Pavel@29: if (settings.hasCopyOfRepository()) { Pavel@0: // update Pavel@8: PullCommand pull = new PullCommand(settings); Pavel@8: pull.execute(); Pavel@0: } else { Pavel@0: // clone Pavel@0: CloneCommand cl = new CloneCommand(settings); Pavel@22: cl.setDestDir(workDir.getAbsolutePath()); Pavel@18: cl.setUpdateWorkingDir(false); Pavel@0: cl.execute(); Pavel@0: } Pavel@21: } finally { Pavel@21: unlockWorkDir(workDir); Pavel@0: } Pavel@0: } Pavel@0: Pavel@44: @Override Pavel@44: public LabelingSupport getLabelingSupport() { Pavel@44: return this; Pavel@44: } Pavel@44: Pavel@106: @NotNull Pavel@106: public VcsFileContentProvider getContentProvider() { Pavel@106: return this; Pavel@106: } Pavel@106: dmitry@156: @NotNull dmitry@156: public String getRemoteRunOnBranchPattern() { dmitry@156: return "remote-run/{teamcity.user}/.+"; dmitry@156: } dmitry@156: dmitry@156: @NotNull dmitry@156: public Map getBranchesRevisions(@NotNull VcsRoot root) throws VcsException { dmitry@156: syncClonedRepository(root); dmitry@156: Settings settings = createSettings(root); dmitry@156: BranchesCommand branches = new BranchesCommand(settings); dmitry@156: Map result = new HashMap(); dmitry@156: for (Map.Entry entry : branches.execute().entrySet()) { dmitry@156: result.put(entry.getKey(), entry.getValue().getId()); dmitry@156: } dmitry@156: return result; dmitry@156: } dmitry@156: dmitry@156: @NotNull dmitry@156: public Map getBranchRootOptions(@NotNull VcsRoot root, @NotNull String branchName) { dmitry@156: final Map options = new HashMap(root.getProperties()); dmitry@156: options.put(Constants.BRANCH_NAME_PROP, branchName); dmitry@156: return options; dmitry@156: } dmitry@156: dmitry@148: public List collectChanges(@NotNull VcsRoot fromRoot, @NotNull String fromRootRevision, dmitry@148: @NotNull VcsRoot toRoot, @Nullable String toRootRevision, dmitry@148: @NotNull CheckoutRules checkoutRules) throws VcsException { dmitry@148: //we get all branches while clone, if vcs roots are related it is doesn't matter in which one search for branch point dmitry@148: syncClonedRepository(fromRoot); dmitry@148: String branchPoint = getBranchPoint(fromRoot, fromRootRevision, toRootRevision); dmitry@155: return ((CollectChangesByCheckoutRules) getCollectChangesPolicy()).collectChanges(toRoot, branchPoint, toRootRevision, checkoutRules); dmitry@148: } dmitry@148: dmitry@148: private String getBranchPoint(@NotNull VcsRoot root, String branchOneRev, String branchTwoRev) throws VcsException { dmitry@148: Settings settings = createSettings(root); dmitry@148: LogCommand lc = new LogCommand(settings); dmitry@148: lc.setFromRevId(new ChangeSetRevision(branchOneRev).getId()); dmitry@148: lc.setToRevId(new ChangeSetRevision(branchTwoRev).getId()); dmitry@148: lc.setLimit(1); dmitry@148: List changeSets = lc.execute(); dmitry@151: ChangeSet cs = changeSets.get(0); dmitry@151: if (cs.isInitial()) { dmitry@151: return cs.getId(); dmitry@151: } else { dmitry@151: return cs.getParents().get(0).getId(); dmitry@151: } dmitry@148: } dmitry@148: Pavel@106: @NotNull Pavel@106: public CollectChangesPolicy getCollectChangesPolicy() { dmitry@144: return new CollectChangesByCheckoutRules() { dmitry@144: public List collectChanges(@NotNull VcsRoot root, @NotNull String fromVersion, @Nullable String currentVersion, @NotNull CheckoutRules checkoutRules) throws VcsException { dmitry@144: syncClonedRepository(root); Pavel@106: dmitry@144: // first obtain changes between specified versions dmitry@144: List result = new ArrayList(); dmitry@144: if (currentVersion == null) return result; Pavel@106: dmitry@144: Settings settings = createSettings(root); dmitry@144: LogCommand lc = new LogCommand(settings); dmitry@144: String fromId = new ChangeSetRevision(fromVersion).getId(); dmitry@144: lc.setFromRevId(fromId); dmitry@144: lc.setToRevId(new ChangeSetRevision(currentVersion).getId()); dmitry@144: List changeSets = lc.execute(); dmitry@144: if (changeSets.isEmpty()) { dmitry@144: return result; dmitry@144: } Pavel@106: dmitry@144: // invoke status command for each changeset and determine what files were modified in these changesets dmitry@144: StatusCommand st = new StatusCommand(settings); dmitry@144: ChangeSet prev = new ChangeSet(fromVersion); dmitry@144: for (ChangeSet cur : changeSets) { dmitry@144: if (cur.getId().equals(fromId)) continue; // skip already reported changeset Pavel@106: dmitry@144: String prevId = prev.getId(); dmitry@144: List curParents = cur.getParents(); dmitry@144: boolean merge = curParents != null && curParents.size() > 1; dmitry@144: if (curParents != null && !merge) { dmitry@144: prevId = curParents.get(0).getId(); Pavel@106: } Pavel@106: dmitry@144: List modifiedFiles = new ArrayList(); dmitry@144: if (merge) { dmitry@144: modifiedFiles.addAll(computeModifiedFilesForMergeCommit(settings, cur)); dmitry@144: } else { dmitry@144: st.setFromRevId(prevId); dmitry@144: st.setToRevId(cur.getId()); dmitry@144: modifiedFiles = st.execute(); Pavel@106: } dmitry@144: dmitry@144: // changeset full version will be set into VcsChange structure and dmitry@144: // stored in database (note that getContent method will be invoked with this version) dmitry@145: List files = toVcsChanges(modifiedFiles, prev.getFullVersion(), cur.getFullVersion(), checkoutRules); dmitry@144: if (files.isEmpty() && !merge) continue; dmitry@144: ModificationData md = new ModificationData(cur.getTimestamp(), files, cur.getDescription(), cur.getUser(), root, cur.getFullVersion(), cur.getId()); dmitry@144: if (merge) { dmitry@144: md.setCanBeIgnored(false); dmitry@144: } dmitry@144: result.add(md); dmitry@144: prev = cur; dmitry@144: } dmitry@144: dmitry@144: return result; Pavel@106: } Pavel@106: }; Pavel@106: } Pavel@106: Pavel@106: @NotNull Pavel@106: public BuildPatchPolicy getBuildPatchPolicy() { Pavel@106: return new BuildPatchByCheckoutRules() { Pavel@106: public void buildPatch(@NotNull final VcsRoot root, Pavel@106: @Nullable final String fromVersion, Pavel@106: @NotNull final String toVersion, Pavel@106: @NotNull final PatchBuilder builder, Pavel@106: @NotNull final CheckoutRules checkoutRules) throws IOException, VcsException { Pavel@106: syncClonedRepository(root); Pavel@106: Settings settings = createSettings(root); Pavel@106: if (fromVersion == null) { Pavel@106: buildFullPatch(settings, new ChangeSet(toVersion), builder, checkoutRules); Pavel@106: } else { Pavel@106: buildIncrementalPatch(settings, new ChangeSet(fromVersion), new ChangeSet(toVersion), builder, checkoutRules); Pavel@106: } Pavel@106: } Pavel@106: }; Pavel@106: } Pavel@106: Pavel@22: private void lockWorkDir(@NotNull File workDir) { Pavel@21: getWorkDirLock(workDir).lock(); Pavel@21: } Pavel@21: Pavel@22: private void unlockWorkDir(@NotNull File workDir) { Pavel@21: getWorkDirLock(workDir).unlock(); Pavel@21: } Pavel@21: Pavel@31: @Override Pavel@106: public boolean allowSourceCaching() { Pavel@31: // since a copy of repository for each VCS root is already stored on disk Pavel@106: // we do not need separate cache for our patches Pavel@106: return false; Pavel@31: } Pavel@31: Pavel@22: private Lock getWorkDirLock(final File workDir) { Pavel@22: String path = workDir.getAbsolutePath(); Pavel@22: Lock lock = myWorkDirLocks.get(path); Pavel@21: if (lock == null) { Pavel@21: lock = new ReentrantLock(); Pavel@22: Lock curLock = myWorkDirLocks.putIfAbsent(path, lock); Pavel@21: if (curLock != null) { Pavel@21: lock = curLock; Pavel@21: } Pavel@21: } Pavel@21: return lock; Pavel@21: } Pavel@21: Pavel@23: private void removeOldWorkFolders() { Pavel@103: Set workDirs = getAllClonedRepos(); Pavel@103: if (workDirs == null) return; Pavel@103: Pavel@103: for (VcsRoot vcsRoot: getMercurialVcsRoots()) { Pavel@103: try { Pavel@103: Settings s = createSettings(vcsRoot); Pavel@103: workDirs.remove(PathUtil.getCanonicalFile(s.getLocalRepositoryDir())); Pavel@103: } catch (VcsException e) { Pavel@103: Loggers.VCS.error(e); Pavel@103: } Pavel@103: } Pavel@103: Pavel@103: for (File f: workDirs) { Pavel@103: lockWorkDir(f); Pavel@103: try { Pavel@103: FileUtil.delete(f); Pavel@103: } finally { Pavel@103: unlockWorkDir(f); Pavel@103: } Pavel@103: } Pavel@103: } Pavel@103: Pavel@103: private Collection getMercurialVcsRoots() { Pavel@103: List res = new ArrayList(myVcsManager.getAllRegisteredVcsRoots()); Pavel@103: FilterUtil.filterCollection(res, new Filter() { Pavel@103: public boolean accept(@NotNull final VcsRoot data) { Pavel@103: return getName().equals(data.getVcsName()); Pavel@103: } Pavel@103: }); Pavel@103: return res; Pavel@103: } Pavel@103: Pavel@103: @Nullable Pavel@103: private Set getAllClonedRepos() { Pavel@62: File workFoldersParent = myDefaultWorkFolderParent; Pavel@103: if (!workFoldersParent.isDirectory()) return null; Pavel@23: Pavel@23: Set workDirs = new HashSet(); Pavel@23: File[] files = workFoldersParent.listFiles(new FileFilter() { Pavel@23: public boolean accept(final File file) { Pavel@23: return file.isDirectory() && file.getName().startsWith(Settings.DEFAULT_WORK_DIR_PREFIX); Pavel@23: } Pavel@23: }); Pavel@23: if (files != null) { Pavel@23: for (File f: files) { Pavel@46: workDirs.add(PathUtil.getCanonicalFile(f)); Pavel@23: } Pavel@23: } Pavel@103: return workDirs; Pavel@23: } Pavel@44: Pavel@44: public String label(@NotNull String label, @NotNull String version, @NotNull VcsRoot root, @NotNull CheckoutRules checkoutRules) throws VcsException { Pavel@44: syncClonedRepository(root); Pavel@44: Pavel@44: Settings settings = createSettings(root); Pavel@44: Pavel@44: // I do not know why but hg tag does not work correctly if Pavel@44: // update command was not invoked for the current repo Pavel@44: // in such case if there were no tags before Mercurial attempts to Pavel@44: // create new head when tag is pushed to the parent repository Pavel@44: UpdateCommand uc = new UpdateCommand(settings); Pavel@44: uc.execute(); Pavel@44: Pavel@44: String fixedTagname = fixTagName(label); Pavel@44: TagCommand tc = new TagCommand(settings); Pavel@44: tc.setRevId(new ChangeSet(version).getId()); Pavel@44: tc.setTag(fixedTagname); Pavel@44: tc.execute(); Pavel@44: Pavel@44: PushCommand pc = new PushCommand(settings); Pavel@101: // pc.setForce(true); Pavel@44: pc.execute(); Pavel@44: return fixedTagname; Pavel@44: } Pavel@44: Pavel@44: private String fixTagName(final String label) { Pavel@44: // according to Mercurial documentation http://hgbook.red-bean.com/hgbookch8.html#x12-1570008 Pavel@44: // tag name must not contain: nd@118: // Colon (ASCII 58, �:�) nd@118: // Carriage return (ASCII 13, �\r�) nd@118: // Newline (ASCII 10, �\n�) Pavel@44: // all these characters will be replaced with _ (underscore) Pavel@44: return label.replace(':', '_').replace('\r', '_').replace('\n', '_'); Pavel@44: } Pavel@44: Pavel@62: private Settings createSettings(final VcsRoot root) throws VcsException { Pavel@62: Settings settings = new Settings(myDefaultWorkFolderParent, root); Pavel@62: String customClonePath = root.getProperty(Constants.SERVER_CLONE_PATH_PROP); Pavel@62: if (!StringUtil.isEmptyOrSpaces(customClonePath) && !myDefaultWorkFolderParent.equals(new File(customClonePath).getAbsoluteFile())) { Pavel@62: File parentDir = new File(customClonePath); Pavel@62: createClonedRepositoryParentDir(parentDir); Pavel@62: Pavel@62: // take last part of repository path Pavel@67: String repPath = settings.getRepositoryUrl(); Pavel@62: String[] splitted = repPath.split("[/\\\\]"); Pavel@62: if (splitted.length > 0) { Pavel@62: repPath = splitted[splitted.length-1]; Pavel@62: } Pavel@62: Pavel@62: File customWorkingDir = new File(parentDir, repPath); Pavel@62: settings.setWorkingDir(customWorkingDir); Pavel@62: } else { Pavel@62: createClonedRepositoryParentDir(myDefaultWorkFolderParent); Pavel@62: } Pavel@62: return settings; Pavel@62: } Pavel@62: Pavel@62: private void createClonedRepositoryParentDir(final File parentDir) throws VcsException { Pavel@62: if (!parentDir.exists() && !parentDir.mkdirs()) { Pavel@62: throw new VcsException("Failed to create parent directory for cloned repository: " + parentDir.getAbsolutePath()); Pavel@62: } Pavel@44: } Pavel@48: Pavel@48: public boolean isAgentSideCheckoutAvailable() { Pavel@48: return true; Pavel@48: } Pavel@0: }