Mercurial > hg > mercurial
annotate mercurial/src/jetbrains/buildServer/buildTriggers/vcs/mercurial/MercurialVcsSupport.java @ 25:7047f643747f
license added, preamble added to source code
author | Pavel.Sher |
---|---|
date | Mon, 21 Jul 2008 21:15:24 +0400 |
parents | 3ddf410c2fd6 |
children |
rev | line source |
---|---|
25 | 1 /* |
2 * Copyright 2000-2007 JetBrains s.r.o. | |
3 * | |
4 * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 * you may not use this file except in compliance with the License. | |
6 * You may obtain a copy of the License at | |
7 * | |
8 * http://www.apache.org/licenses/LICENSE-2.0 | |
9 * | |
10 * Unless required by applicable law or agreed to in writing, software | |
11 * distributed under the License is distributed on an "AS IS" BASIS, | |
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 * See the License for the specific language governing permissions and | |
14 * limitations under the License. | |
15 */ | |
0 | 16 package jetbrains.buildServer.buildTriggers.vcs.mercurial; |
17 | |
18 import jetbrains.buildServer.CollectChangesByIncludeRule; | |
19 import jetbrains.buildServer.Used; | |
20 import jetbrains.buildServer.buildTriggers.vcs.AbstractVcsPropertiesProcessor; | |
21 import jetbrains.buildServer.buildTriggers.vcs.mercurial.command.*; | |
22 import jetbrains.buildServer.log.Loggers; | |
23 import jetbrains.buildServer.serverSide.InvalidProperty; | |
24 import jetbrains.buildServer.serverSide.PropertiesProcessor; | |
21 | 25 import jetbrains.buildServer.serverSide.SBuildServer; |
0 | 26 import jetbrains.buildServer.serverSide.ServerPaths; |
27 import jetbrains.buildServer.util.FileUtil; | |
28 import jetbrains.buildServer.vcs.*; | |
29 import jetbrains.buildServer.vcs.patches.PatchBuilder; | |
30 import org.jetbrains.annotations.NotNull; | |
31 import org.jetbrains.annotations.Nullable; | |
32 | |
1 | 33 import java.io.File; |
34 import java.io.FileFilter; | |
35 import java.io.FileInputStream; | |
36 import java.io.IOException; | |
37 import java.util.*; | |
21 | 38 import java.util.concurrent.ConcurrentHashMap; |
39 import java.util.concurrent.ConcurrentMap; | |
40 import java.util.concurrent.TimeUnit; | |
41 import java.util.concurrent.locks.Lock; | |
42 import java.util.concurrent.locks.ReentrantLock; | |
1 | 43 |
19 | 44 /** |
45 * Mercurial VCS plugin for TeamCity works as follows: | |
46 * <ul> | |
47 * <li>clones repository to internal storage | |
48 * <li>before any operation with working copy of repository pulls changes from the original repository | |
49 * <li>executes corresponding hg command | |
50 * </ul> | |
51 * | |
52 * <p>Working copy of repository is created in the $TEAMCITY_DATA_PATH/system/caches/mercurial folder. | |
53 * <p>Personal builds (remote runs) are not yet supported, they require corresponding functionality from the IDE. | |
54 * <p>Checkout on agent mode is not yet supported too. | |
55 */ | |
0 | 56 public class MercurialVcsSupport extends VcsSupport implements CollectChangesByIncludeRule { |
57 private ServerPaths myServerPaths; | |
21 | 58 private ConcurrentMap<String, Lock> myWorkDirLocks= new ConcurrentHashMap<String, Lock>(); |
59 private static final int OLD_WORK_DIRS_CLEANUP_PERIOD = 600; | |
60 private VcsManager myVcsManager; | |
0 | 61 |
21 | 62 public MercurialVcsSupport(@NotNull final VcsManager vcsManager, |
63 @NotNull ServerPaths paths, | |
64 @NotNull SBuildServer server) { | |
0 | 65 vcsManager.registerVcsSupport(this); |
66 myServerPaths = paths; | |
21 | 67 myVcsManager = vcsManager; |
68 server.getExecutor().scheduleAtFixedRate(new Runnable() { | |
69 public void run() { | |
70 removeOldWorkFolders(); | |
71 } | |
72 }, 0, OLD_WORK_DIRS_CLEANUP_PERIOD, TimeUnit.SECONDS); | |
0 | 73 } |
74 | |
75 public List<ModificationData> collectBuildChanges(final VcsRoot root, | |
76 @NotNull final String fromVersion, | |
77 @NotNull final String currentVersion, | |
78 final CheckoutRules checkoutRules) throws VcsException { | |
79 updateWorkingDirectory(root); | |
80 return VcsSupportUtil.collectBuildChanges(root, fromVersion, currentVersion, checkoutRules, this); | |
81 } | |
82 | |
83 public List<ModificationData> collectBuildChanges(final VcsRoot root, | |
84 final String fromVersion, | |
85 final String currentVersion, | |
86 final IncludeRule includeRule) throws VcsException { | |
19 | 87 // first obtain changes between specified versions |
0 | 88 List<ModificationData> result = new ArrayList<ModificationData>(); |
89 Settings settings = new Settings(myServerPaths, root); | |
90 LogCommand lc = new LogCommand(settings); | |
91 lc.setFromRevId(new ChangeSet(fromVersion).getId()); | |
92 lc.setToRevId(new ChangeSet(currentVersion).getId()); | |
93 List<ChangeSet> changeSets = lc.execute(); | |
94 if (changeSets.isEmpty()) { | |
95 return result; | |
96 } | |
97 | |
19 | 98 // invoke status command for each changeset and determine what files were modified in these changesets |
0 | 99 Iterator<ChangeSet> it = changeSets.iterator(); |
100 ChangeSet prev = it.next(); // skip first changeset (cause it was already reported) | |
101 StatusCommand st = new StatusCommand(settings); | |
102 while (it.hasNext()) { | |
103 ChangeSet cur = it.next(); | |
104 st.setFromRevId(prev.getId()); | |
105 st.setToRevId(cur.getId()); | |
106 List<ModifiedFile> modifiedFiles = st.execute(); | |
19 | 107 // changeset full version will be set into VcsChange structure and |
20 | 108 // stored in database (note that getContent method will be invoked with this version) |
0 | 109 List<VcsChange> files = toVcsChanges(modifiedFiles, prev.getFullVersion(), cur.getFullVersion(), includeRule); |
110 if (files.isEmpty()) continue; | |
111 ModificationData md = new ModificationData(cur.getTimestamp(), files, cur.getSummary(), cur.getUser(), root, cur.getFullVersion(), cur.getFullVersion()); | |
112 result.add(md); | |
113 prev = cur; | |
114 } | |
115 | |
116 return result; | |
117 } | |
118 | |
119 private List<VcsChange> toVcsChanges(final List<ModifiedFile> modifiedFiles, String prevVer, String curVer, final IncludeRule includeRule) { | |
120 List<VcsChange> files = new ArrayList<VcsChange>(); | |
121 for (ModifiedFile mf: modifiedFiles) { | |
21 | 122 String normalizedPath = PathUtil.normalizeSeparator(mf.getPath()); |
12 | 123 if (!normalizedPath.startsWith(includeRule.getFrom())) continue; // skip files which do not match include rule |
0 | 124 |
125 VcsChangeInfo.Type changeType = getChangeType(mf.getStatus()); | |
126 if (changeType == null) { | |
127 Loggers.VCS.warn("Unable to convert status: " + mf.getStatus() + " to VCS change type"); | |
128 changeType = VcsChangeInfo.Type.NOT_CHANGED; | |
129 } | |
12 | 130 files.add(new VcsChange(changeType, mf.getStatus().getName(), normalizedPath, normalizedPath, prevVer, curVer)); |
0 | 131 } |
132 return files; | |
133 } | |
134 | |
135 private VcsChangeInfo.Type getChangeType(final ModifiedFile.Status status) { | |
136 switch (status) { | |
137 case ADDED:return VcsChangeInfo.Type.ADDED; | |
138 case MODIFIED:return VcsChangeInfo.Type.CHANGED; | |
139 case REMOVED:return VcsChangeInfo.Type.REMOVED; | |
140 } | |
141 return null; | |
142 } | |
143 | |
144 @NotNull | |
145 public byte[] getContent(final VcsModification vcsModification, | |
146 final VcsChangeInfo change, | |
147 final VcsChangeInfo.ContentType contentType, | |
148 final VcsRoot vcsRoot) throws VcsException { | |
12 | 149 updateWorkingDirectory(vcsRoot); |
150 String version = contentType == VcsChangeInfo.ContentType.AFTER_CHANGE ? change.getAfterChangeRevisionNumber() : change.getBeforeChangeRevisionNumber(); | |
151 return getContent(change.getRelativeFileName(), vcsRoot, version); | |
0 | 152 } |
153 | |
154 @NotNull | |
12 | 155 public byte[] getContent(final String filePath, final VcsRoot vcsRoot, final String version) throws VcsException { |
156 updateWorkingDirectory(vcsRoot); | |
157 Settings settings = new Settings(myServerPaths, vcsRoot); | |
158 CatCommand cc = new CatCommand(settings); | |
159 ChangeSet cs = new ChangeSet(version); | |
160 cc.setRevId(cs.getId()); | |
161 File parentDir = cc.execute(Collections.singletonList(filePath)); | |
162 File file = new File(parentDir, filePath); | |
163 if (file.isFile()) { | |
164 try { | |
165 return FileUtil.loadFileBytes(file); | |
166 } catch (IOException e) { | |
167 throw new VcsException("Failed to load content of file: " + file.getAbsolutePath(), e); | |
168 } | |
169 } else { | |
170 Loggers.VCS.warn("Unable to obtain content of the file: " + filePath); | |
171 } | |
0 | 172 return new byte[0]; |
173 } | |
174 | |
175 public String getName() { | |
176 return "mercurial"; | |
177 } | |
178 | |
179 @Used("jsp") | |
180 public String getDisplayName() { | |
181 return "Mercurial"; | |
182 } | |
183 | |
184 @Nullable | |
185 public PropertiesProcessor getVcsPropertiesProcessor() { | |
186 return new AbstractVcsPropertiesProcessor() { | |
187 public Collection<InvalidProperty> process(final Map<String, String> properties) { | |
188 List<InvalidProperty> result = new ArrayList<InvalidProperty>(); | |
189 if (isEmpty(properties.get(Constants.HG_COMMAND_PATH_PROP))) { | |
190 result.add(new InvalidProperty(Constants.HG_COMMAND_PATH_PROP, "Path to 'hg' command must be specified")); | |
191 } | |
192 if (isEmpty(properties.get(Constants.REPOSITORY_PROP))) { | |
193 result.add(new InvalidProperty(Constants.REPOSITORY_PROP, "Repository must be specified")); | |
194 } | |
195 return result; | |
196 } | |
197 }; | |
198 } | |
199 | |
200 public String getVcsSettingsJspFilePath() { | |
201 return "mercurialSettings.jsp"; | |
202 } | |
203 | |
204 @NotNull | |
205 public String getCurrentVersion(final VcsRoot root) throws VcsException { | |
19 | 206 // we will return full version of the most recent change as current version |
0 | 207 updateWorkingDirectory(root); |
208 Settings settings = new Settings(myServerPaths, root); | |
9
7dadebd03515
use tip command instead of log in getCurrentVersion method
Pavel.Sher
parents:
8
diff
changeset
|
209 TipCommand lc = new TipCommand(settings); |
7dadebd03515
use tip command instead of log in getCurrentVersion method
Pavel.Sher
parents:
8
diff
changeset
|
210 ChangeSet changeSet = lc.execute(); |
0 | 211 return changeSet.getFullVersion(); |
212 } | |
213 | |
214 public String describeVcsRoot(final VcsRoot vcsRoot) { | |
215 return "mercurial: " + vcsRoot.getProperty(Constants.REPOSITORY_PROP); | |
216 } | |
217 | |
218 public boolean isTestConnectionSupported() { | |
16
7aa397165fa0
identify command added, test connection now uses identify command
Pavel.Sher
parents:
13
diff
changeset
|
219 return true; |
0 | 220 } |
221 | |
222 @Nullable | |
223 public String testConnection(final VcsRoot vcsRoot) throws VcsException { | |
16
7aa397165fa0
identify command added, test connection now uses identify command
Pavel.Sher
parents:
13
diff
changeset
|
224 Settings settings = new Settings(myServerPaths, vcsRoot); |
7aa397165fa0
identify command added, test connection now uses identify command
Pavel.Sher
parents:
13
diff
changeset
|
225 IdentifyCommand id = new IdentifyCommand(settings); |
7aa397165fa0
identify command added, test connection now uses identify command
Pavel.Sher
parents:
13
diff
changeset
|
226 StringBuilder res = new StringBuilder(); |
17 | 227 res.append(quoteIfNeeded(settings.getHgCommandPath())); |
228 res.append(" identify "); | |
229 res.append(quoteIfNeeded(settings.getRepository())); | |
16
7aa397165fa0
identify command added, test connection now uses identify command
Pavel.Sher
parents:
13
diff
changeset
|
230 res.append('\n').append(id.execute()); |
7aa397165fa0
identify command added, test connection now uses identify command
Pavel.Sher
parents:
13
diff
changeset
|
231 return res.toString(); |
0 | 232 } |
233 | |
17 | 234 private String quoteIfNeeded(@NotNull String str) { |
235 if (str.indexOf(' ') != -1) { | |
236 return "\"" + str + "\""; | |
237 } | |
238 | |
239 return str; | |
240 } | |
241 | |
0 | 242 @Nullable |
243 public Map<String, String> getDefaultVcsProperties() { | |
19 | 244 return Collections.singletonMap(Constants.HG_COMMAND_PATH_PROP, "hg"); |
0 | 245 } |
246 | |
247 public String getVersionDisplayName(final String version, final VcsRoot root) throws VcsException { | |
248 return version; | |
249 } | |
250 | |
251 @NotNull | |
252 public Comparator<String> getVersionComparator() { | |
19 | 253 // comparator is called when TeamCity needs to sort modifications in the order of their appearance, |
254 // currently we sort changes by revision number, not sure however that this is a good idea, | |
255 // probably it would be better to sort them by timestamp (and to add timestamp into the version). | |
0 | 256 return new Comparator<String>() { |
257 public int compare(final String o1, final String o2) { | |
258 try { | |
259 return new ChangeSet(o1).getRevNumber() - new ChangeSet(o2).getRevNumber(); | |
260 } catch (Exception e) { | |
261 return 1; | |
262 } | |
263 } | |
264 }; | |
265 } | |
266 | |
267 public void buildPatch(final VcsRoot root, | |
268 @Nullable final String fromVersion, | |
269 @NotNull final String toVersion, | |
270 final PatchBuilder builder, | |
271 final CheckoutRules checkoutRules) throws IOException, VcsException { | |
272 updateWorkingDirectory(root); | |
273 Settings settings = new Settings(myServerPaths, root); | |
274 if (fromVersion == null) { | |
275 buildFullPatch(settings, new ChangeSet(toVersion), builder); | |
276 } else { | |
277 buildIncrementalPatch(settings, new ChangeSet(fromVersion), new ChangeSet(toVersion), builder); | |
278 } | |
279 } | |
280 | |
19 | 281 // builds patch from version to version |
0 | 282 private void buildIncrementalPatch(final Settings settings, @NotNull final ChangeSet fromVer, @NotNull final ChangeSet toVer, final PatchBuilder builder) |
283 throws VcsException, IOException { | |
284 StatusCommand st = new StatusCommand(settings); | |
285 st.setFromRevId(fromVer.getId()); | |
286 st.setToRevId(toVer.getId()); | |
287 List<ModifiedFile> modifiedFiles = st.execute(); | |
288 List<String> notDeletedFiles = new ArrayList<String>(); | |
289 for (ModifiedFile f: modifiedFiles) { | |
290 if (f.getStatus() != ModifiedFile.Status.REMOVED) { | |
291 notDeletedFiles.add(f.getPath()); | |
292 } | |
293 } | |
294 | |
295 CatCommand cc = new CatCommand(settings); | |
296 cc.setRevId(toVer.getId()); | |
297 File parentDir = cc.execute(notDeletedFiles); | |
298 | |
299 try { | |
300 for (ModifiedFile f: modifiedFiles) { | |
301 final File virtualFile = new File(f.getPath()); | |
302 if (f.getStatus() == ModifiedFile.Status.REMOVED) { | |
303 builder.deleteFile(virtualFile, true); | |
304 } else { | |
305 File realFile = new File(parentDir, f.getPath()); | |
306 FileInputStream is = new FileInputStream(realFile); | |
307 try { | |
11 | 308 builder.changeOrCreateBinaryFile(virtualFile, null, is, realFile.length()); |
0 | 309 } finally { |
310 is.close(); | |
311 } | |
312 } | |
313 } | |
314 } finally { | |
315 FileUtil.delete(parentDir); | |
316 } | |
317 } | |
318 | |
19 | 319 // builds patch by exporting files using specified version |
0 | 320 private void buildFullPatch(final Settings settings, @NotNull final ChangeSet toVer, final PatchBuilder builder) |
321 throws IOException, VcsException { | |
322 CloneCommand cl = new CloneCommand(settings); | |
323 cl.setToId(toVer.getId()); | |
324 File tempDir = FileUtil.createTempDirectory("mercurial", toVer.getId()); | |
325 try { | |
326 final File repRoot = new File(tempDir, "rep"); | |
327 cl.setDestDir(repRoot.getAbsolutePath()); | |
328 cl.execute(); | |
329 buildPatchFromDirectory(builder, repRoot, new FileFilter() { | |
330 public boolean accept(final File file) { | |
331 return !(file.isDirectory() && ".hg".equals(file.getName())); | |
332 } | |
333 }); | |
334 } finally { | |
335 FileUtil.delete(tempDir); | |
336 } | |
337 } | |
338 | |
339 private void buildPatchFromDirectory(final PatchBuilder builder, final File repRoot, final FileFilter filter) throws IOException { | |
340 buildPatchFromDirectory(repRoot, builder, repRoot, filter); | |
341 } | |
342 | |
343 private void buildPatchFromDirectory(File curDir, final PatchBuilder builder, final File repRoot, final FileFilter filter) throws IOException { | |
344 File[] files = curDir.listFiles(filter); | |
345 if (files != null) { | |
346 for (File realFile: files) { | |
347 String relPath = realFile.getAbsolutePath().substring(repRoot.getAbsolutePath().length()); | |
348 final File virtualFile = new File(relPath); | |
349 if (realFile.isDirectory()) { | |
350 builder.createDirectory(virtualFile); | |
351 buildPatchFromDirectory(realFile, builder, repRoot, filter); | |
352 } else { | |
353 final FileInputStream is = new FileInputStream(realFile); | |
354 try { | |
355 builder.createBinaryFile(virtualFile, null, is, realFile.length()); | |
356 } finally { | |
357 is.close(); | |
358 } | |
359 } | |
360 } | |
361 } | |
362 } | |
363 | |
19 | 364 // updates current working copy of repository by pulling changes from the repository specified in VCS root |
0 | 365 private void updateWorkingDirectory(final VcsRoot root) throws VcsException { |
366 Settings settings = new Settings(myServerPaths, root); | |
22 | 367 File workDir = settings.getWorkingDir(); |
21 | 368 lockWorkDir(workDir); |
369 try { | |
22 | 370 if (hasRepositoryCopy(workDir)) { |
0 | 371 // update |
8 | 372 PullCommand pull = new PullCommand(settings); |
373 pull.execute(); | |
0 | 374 } else { |
375 // clone | |
376 CloneCommand cl = new CloneCommand(settings); | |
22 | 377 cl.setDestDir(workDir.getAbsolutePath()); |
18 | 378 cl.setUpdateWorkingDir(false); |
0 | 379 cl.execute(); |
380 } | |
21 | 381 } finally { |
382 unlockWorkDir(workDir); | |
0 | 383 } |
384 } | |
385 | |
22 | 386 private void lockWorkDir(@NotNull File workDir) { |
21 | 387 getWorkDirLock(workDir).lock(); |
388 } | |
389 | |
22 | 390 private void unlockWorkDir(@NotNull File workDir) { |
21 | 391 getWorkDirLock(workDir).unlock(); |
392 } | |
393 | |
22 | 394 private Lock getWorkDirLock(final File workDir) { |
395 String path = workDir.getAbsolutePath(); | |
396 Lock lock = myWorkDirLocks.get(path); | |
21 | 397 if (lock == null) { |
398 lock = new ReentrantLock(); | |
22 | 399 Lock curLock = myWorkDirLocks.putIfAbsent(path, lock); |
21 | 400 if (curLock != null) { |
401 lock = curLock; | |
402 } | |
403 } | |
404 return lock; | |
405 } | |
406 | |
0 | 407 private boolean hasRepositoryCopy(final File workDir) { |
19 | 408 // need better way to check that repository copy is ok |
0 | 409 return workDir.isDirectory() && new File(workDir, ".hg").isDirectory(); |
410 } | |
411 | |
23 | 412 private void removeOldWorkFolders() { |
413 File workFoldersParent = new File(myServerPaths.getCachesDir(), "mercurial"); | |
414 if (!workFoldersParent.isDirectory()) return; | |
415 | |
416 Set<File> workDirs = new HashSet<File>(); | |
417 File[] files = workFoldersParent.listFiles(new FileFilter() { | |
418 public boolean accept(final File file) { | |
419 return file.isDirectory() && file.getName().startsWith(Settings.DEFAULT_WORK_DIR_PREFIX); | |
420 } | |
421 }); | |
422 if (files != null) { | |
423 for (File f: files) { | |
424 workDirs.add(FileUtil.getCanonicalFile(f)); | |
425 } | |
426 } | |
427 | |
428 for (VcsRoot vcsRoot: myVcsManager.getAllRegisteredVcsRoots()) { | |
429 if (getName().equals(vcsRoot.getVcsName())) { | |
430 Settings s = new Settings(myServerPaths, vcsRoot); | |
431 workDirs.remove(FileUtil.getCanonicalFile(s.getWorkingDir())); | |
432 } | |
433 } | |
434 | |
435 for (File f: workDirs) { | |
436 lockWorkDir(f); | |
437 try { | |
438 FileUtil.delete(f); | |
439 } finally { | |
440 unlockWorkDir(f); | |
441 } | |
442 } | |
443 } | |
0 | 444 } |