1
2
3
4
5
6
7
8
9
10 package org.jenkinsci.plugins.darcs;
11
12 import org.jenkinsci.plugins.darcs.browsers.DarcsRepositoryBrowser;
13 import hudson.EnvVars;
14 import hudson.FilePath;
15 import hudson.FilePath.FileCallable;
16 import hudson.Launcher;
17 import hudson.Launcher.LocalLauncher;
18 import hudson.init.InitMilestone;
19 import hudson.init.Initializer;
20 import hudson.model.AbstractBuild;
21 import hudson.model.AbstractProject;
22 import hudson.model.BuildListener;
23 import hudson.model.TaskListener;
24 import hudson.remoting.VirtualChannel;
25 import hudson.scm.ChangeLogParser;
26 import hudson.scm.PollingResult;
27 import hudson.scm.PollingResult.Change;
28 import hudson.scm.SCM;
29 import hudson.scm.SCMRevisionState;
30 import hudson.util.IOUtils;
31 import java.io.ByteArrayOutputStream;
32 import java.io.File;
33 import java.io.FileOutputStream;
34 import java.io.IOException;
35 import java.io.PrintStream;
36 import java.io.PrintWriter;
37 import java.io.Serializable;
38 import java.io.StringWriter;
39 import java.util.logging.Logger;
40 import jenkins.model.Jenkins;
41 import org.kohsuke.stapler.DataBoundConstructor;
42 import org.xml.sax.SAXException;
43
44
45
46
47
48
49
50
51
52
53
54 public class DarcsScm extends SCM implements Serializable {
55
56
57
58
59 private static final long serialVersionUID = 3L;
60
61
62
63 private static final Logger LOGGER = Logger.getLogger(DarcsScm.class.getName());
64
65
66
67 private final String source;
68
69
70
71 private final String localDir;
72
73
74
75 private final boolean clean;
76
77
78
79 private final DarcsRepositoryBrowser browser;
80
81
82
83
84
85
86
87
88 public DarcsScm(final String source) throws SAXException {
89 this(source, "", false, null);
90 }
91
92
93
94
95
96
97
98
99
100 @DataBoundConstructor
101 public DarcsScm(final String source, final String localDir, final boolean clean, final DarcsRepositoryBrowser browser) {
102 super();
103 this.source = source;
104 this.clean = clean;
105 this.browser = browser;
106 this.localDir = localDir;
107 }
108
109
110
111
112
113
114 public String getSource() {
115 return source;
116 }
117
118
119
120
121
122
123 public String getLocalDir() {
124 return localDir;
125 }
126
127
128
129
130
131
132 public boolean isClean() {
133 return clean;
134 }
135
136 @Override
137 public DarcsRepositoryBrowser getBrowser() {
138 return browser;
139 }
140
141 @Override
142 public boolean supportsPolling() {
143 return false;
144 }
145
146 @Override
147 public boolean requiresWorkspaceForPolling() {
148 return false;
149 }
150
151 @Override
152 public SCMRevisionState calcRevisionsFromBuild(final AbstractBuild<?, ?> build, final Launcher launcher,
153 final TaskListener listener) throws IOException, InterruptedException {
154 final FilePath localPath = createLocalPath(build.getWorkspace());
155 final DarcsRevisionState local = getRevisionState(launcher, listener, localPath.getRemote(), build.getWorkspace());
156
157 if (null == local) {
158 listener.getLogger().println(String.format("[poll] Got <null> as revision state."));
159 return SCMRevisionState.NONE;
160 }
161
162 listener.getLogger().println(String.format("[poll] Calculate revison from build %s.", local));
163 return local;
164 }
165
166 @Override
167 protected PollingResult compareRemoteRevisionWith(final AbstractProject<?, ?> project, final Launcher launcher,
168 final FilePath workspace, final TaskListener listener, final SCMRevisionState baseline)
169 throws IOException, InterruptedException {
170 final PrintStream logger = listener.getLogger();
171 final SCMRevisionState localRevisionState;
172
173 if (baseline instanceof DarcsRevisionState) {
174 localRevisionState = (DarcsRevisionState) baseline;
175 } else if (null != project && null != project.getLastBuild()) {
176 localRevisionState = calcRevisionsFromBuild(project.getLastBuild(), launcher, listener);
177 } else {
178 localRevisionState = new DarcsRevisionState();
179 }
180
181 if (null != project && null != project.getLastBuild()) {
182 logger.println("[poll] Last Build : #" + project.getLastBuild().getNumber());
183 } else {
184
185 logger.println("[poll] No previous build, so forcing an initial build.");
186
187 return PollingResult.BUILD_NOW;
188 }
189
190 final Change change;
191 final DarcsRevisionState remoteRevisionState = getRevisionState(launcher, listener, source, workspace);
192
193 logger.printf("[poll] Current remote revision is %s. Local revision is %s.%n",
194 remoteRevisionState, localRevisionState);
195
196 if (SCMRevisionState.NONE.equals(localRevisionState)) {
197 logger.println("[poll] Does not have a local revision state.");
198 change = Change.SIGNIFICANT;
199 } else if (localRevisionState.getClass() != DarcsRevisionState.class) {
200
201
202 logger.println("[poll] local revision state is not of type darcs.");
203 change = Change.SIGNIFICANT;
204 } else if (null != remoteRevisionState && !remoteRevisionState.equals(localRevisionState)) {
205 logger.println("[poll] Local revision state differs from remote.");
206
207 if (remoteRevisionState.getChanges().size()
208 < ((DarcsRevisionState) localRevisionState).getChanges().size()) {
209 final FilePath ws = project.getLastBuild().getWorkspace();
210
211 logger.printf("[poll] Remote repo has less patches than local: remote(%s) vs. local(%s). Will wipe "
212 + "workspace %s...%n",
213 remoteRevisionState.getChanges().size(),
214 ((DarcsRevisionState) localRevisionState).getChanges().size(),
215 (null != ws) ? ws.getRemote() : "null");
216
217 if (null != ws) {
218 ws.deleteRecursive();
219 }
220 }
221
222 change = Change.SIGNIFICANT;
223 } else {
224 change = Change.NONE;
225 }
226
227 return new PollingResult(localRevisionState, remoteRevisionState, change);
228 }
229
230
231
232
233
234
235
236
237
238
239 DarcsRevisionState getRevisionState(final Launcher launcher, final TaskListener listener, final String repo, final FilePath workspace)
240 throws InterruptedException {
241 final DarcsCmd cmd;
242
243 if (null == launcher) {
244
245
246
247 cmd = new DarcsCmd(new LocalLauncher(listener), EnvVars.masterEnvVars, getDescriptor().getDarcsExe(), workspace);
248 } else {
249 cmd = new DarcsCmd(launcher, EnvVars.masterEnvVars, getDescriptor().getDarcsExe(), workspace);
250 }
251
252 DarcsRevisionState rev = null;
253
254 try {
255 final ByteArrayOutputStream changes = cmd.allChanges(repo);
256 rev = new DarcsRevisionState(((DarcsChangeLogParser) createChangeLogParser()).parse(changes));
257 } catch (Exception e) {
258 listener.getLogger().println(String.format("[warning] Failed to get revision state for repository: %s", repo));
259 }
260
261 return rev;
262 }
263
264
265
266
267
268
269
270
271
272
273 private void createChangeLog(final Launcher launcher, final int numPatches, final FilePath workspace,
274 final File changeLog, final BuildListener listener) throws InterruptedException {
275 if (0 == numPatches) {
276 LOGGER.info("Creating empty changelog.");
277 createEmptyChangeLog(changeLog, listener, "changelog");
278 return;
279 }
280
281 final DarcsCmd cmd = new DarcsCmd(launcher, EnvVars.masterEnvVars, getDescriptor().getDarcsExe(), workspace.getParent());
282 FileOutputStream fos = null;
283
284 try {
285 fos = new FileOutputStream(changeLog);
286 final FilePath localPath = createLocalPath(workspace);
287 final ByteArrayOutputStream changes = cmd.lastSummarizedChanges(localPath.getRemote(), numPatches);
288 changes.writeTo(fos);
289 } catch (Exception e) {
290 final StringWriter w = new StringWriter();
291 e.printStackTrace(new PrintWriter(w));
292 LOGGER.warning(String.format("Failed to get log from repository: %s", w));
293 } finally {
294 IOUtils.closeQuietly(fos);
295 }
296 }
297
298 @Override
299 public boolean checkout(final AbstractBuild<?, ?> build, final Launcher launcher, final FilePath workspace,
300 final BuildListener listener, final File changelogFile) throws IOException, InterruptedException {
301 final FilePath localPath = createLocalPath(workspace);
302 final boolean existsRepoinWorkspace = localPath.act(new FileCallable<Boolean>() {
303 private static final long serialVersionUID = 1L;
304
305 public Boolean invoke(File ws, VirtualChannel channel) throws IOException {
306 final File file = new File(ws, "_darcs");
307 return file.exists();
308 }
309 });
310
311 if (existsRepoinWorkspace && !isClean()) {
312 return pullRepo(build, launcher, workspace, listener, changelogFile);
313 } else {
314 return getRepo(build, launcher, workspace, listener, changelogFile);
315 }
316 }
317
318
319
320
321
322
323
324
325
326
327
328
329 private int countPatches(final AbstractBuild<?, ?> build, final Launcher launcher, final FilePath workspace,
330 final BuildListener listener) {
331 try {
332 final DarcsCmd cmd = new DarcsCmd(launcher, build.getEnvironment(listener), getDescriptor().getDarcsExe(), workspace.getParent());
333 final FilePath localPath = createLocalPath(workspace);
334 return cmd.countChanges(localPath.getRemote());
335 } catch (Exception e) {
336 listener.error("Failed to count patches in workspace repo:%n", e.toString());
337 return 0;
338 }
339 }
340
341
342
343
344
345
346
347
348
349
350
351
352
353 private boolean pullRepo(final AbstractBuild<?, ?> build, final Launcher launcher, final FilePath workspace,
354 final BuildListener listener, final File changelogFile) throws InterruptedException, IOException {
355 LOGGER.info(String.format("Pulling repo from: %s", source));
356 final int preCnt = countPatches(build, launcher, workspace, listener);
357 LOGGER.info(String.format("Count of patches pre pulling is %d", preCnt));
358
359 try {
360 final DarcsCmd cmd = new DarcsCmd(launcher, build.getEnvironment(listener), getDescriptor().getDarcsExe(), workspace.getParent());
361 final FilePath localPath = createLocalPath(workspace);
362 cmd.pull(localPath.getRemote(), source);
363 } catch (Exception e) {
364 listener.error("Failed to pull: " + e.toString());
365 return false;
366 }
367
368 final int postCnt = countPatches(build, launcher, workspace, listener);
369 LOGGER.info(String.format("Count of patches post pulling is %d", preCnt));
370 createChangeLog(launcher, postCnt - preCnt, workspace, changelogFile, listener);
371
372 return true;
373 }
374
375
376
377
378
379
380
381
382
383
384
385
386 private boolean getRepo(final AbstractBuild<?, ?> build, final Launcher launcher, final FilePath workspace,
387 final BuildListener listener, final File changeLog) throws InterruptedException {
388 LOGGER.info(String.format("Getting repo from: %s", source));
389
390 try {
391 final FilePath localPath = createLocalPath(workspace);
392 localPath.deleteRecursive();
393 } catch (IOException e) {
394 e.printStackTrace(listener.error("Failed to clean the workspace"));
395 return false;
396 }
397
398 try {
399 final DarcsCmd cmd = new DarcsCmd(launcher, build.getEnvironment(listener), getDescriptor().getDarcsExe(), workspace.getParent());
400 final FilePath localPath = createLocalPath(workspace);
401 cmd.get(localPath.getRemote(), source);
402 } catch (Exception e) {
403 e.printStackTrace(listener.error("Failed to get repo from " + source));
404 return false;
405 }
406
407 return createEmptyChangeLog(changeLog, listener, "changelog");
408 }
409
410 @Override
411 public ChangeLogParser createChangeLogParser() {
412 return new DarcsChangeLogParser();
413 }
414
415 @Override
416 public DarcsScmDescriptor getDescriptor() {
417 return (DarcsScmDescriptor) super.getDescriptor();
418 }
419
420
421
422
423
424
425
426
427
428
429 private FilePath createLocalPath(final FilePath base) {
430 if (null != localDir && localDir.length() > 0) {
431 return new FilePath(base, localDir);
432 }
433
434 return base;
435 }
436
437
438
439
440 @Initializer(before = InitMilestone.PLUGINS_STARTED)
441 public static void addAliases() {
442
443 Jenkins.XSTREAM2.addCompatibilityAlias("org.jenkinsci.plugins.darcs.DarcsScm$DescriptorImpl",
444 DarcsScmDescriptor.class);
445 }
446
447 }