View Javadoc

1   /*
2    * LICENSE
3    *
4    * "THE BEER-WARE LICENSE" (Revision 42):
5    * "Sven Strittmatter" <ich@weltraumschaf.de> wrote this file.
6    * As long as you retain this notice you can do whatever you want with
7    * this stuff. If we meet some day, and you think this stuff is worth it,
8    * you can buy me a beer in return.
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   * Darcs is a patch based distributed version control system.
46   *
47   * Contains the job configuration options as fields.
48   *
49   * @see http://darcs.net/
50   *
51   * @author Sven Strittmatter <ich@weltraumschaf.de>
52   * @author Ralph Lange <Ralph.Lange@gmx.de>
53   */
54  public class DarcsScm extends SCM implements Serializable {
55  
56      /**
57       * Serial version UID.
58       */
59      private static final long serialVersionUID = 3L;
60      /**
61       * Logging facility.
62       */
63      private static final Logger LOGGER = Logger.getLogger(DarcsScm.class.getName());
64      /**
65       * Source repository URL from which we pull.
66       */
67      private final String source;
68      /**
69       * Local directory with repository.
70       */
71      private final String localDir;
72      /**
73       * Whether to wipe the checked out repository.
74       */
75      private final boolean clean;
76      /**
77       * Used repository browser.
78       */
79      private final DarcsRepositoryBrowser browser;
80  
81      /**
82       * Convenience constructor.
83       *
84       * Sets local directory to {@link #DEFAULT_LOCAL_DIR}, clean to {@code false} and browser to {@code null}.
85       *
86       * @param source repository URL from which we pull
87       */
88      public DarcsScm(final String source) throws SAXException {
89          this(source, "", false, null);
90      }
91  
92      /**
93       * Dedicated constructor.
94       *
95       * @param source repository URL from which we pull
96       * @param localDir Local directory in the workspace
97       * @param clean {@code true} cleans the workspace, {@code false} not
98       * @param browser the browser used to browse the repository
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      * Get the repositories source URL.
111      *
112      * @return URL as string
113      */
114     public String getSource() {
115         return source;
116     }
117 
118     /**
119      * Get the local directory in the workspace.
120      *
121      * @return relative path as string
122      */
123     public String getLocalDir() {
124         return localDir;
125     }
126 
127     /**
128      * Whether to clean the workspace or not.
129      *
130      * @return {@code true} if clean is performed, {@code false} else
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             // If we've never been built before, well, gotta build!
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             // appears that other instances of None occur - its not a singleton.
201             // so do a (fugly) class check.
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      * Calculates the revision state of a repository (local or remote).
232      *
233      * @param launcher
234      * @param listener
235      * @param repo
236      * @return
237      * @throws InterruptedException
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             /* Create a launcher on master
245              * TODO better grab a launcher on 'any slave'
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      * Writes the change log of the last numPatches to the changeLog file.
266      *
267      * @param launcher
268      * @param numPatches
269      * @param workspace
270      * @param changeLog
271      * @throws InterruptedException
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      * Counts the patches in a repository.
320      *
321      * @param build
322      * @param launcher
323      * @param workspace
324      * @param listener
325      * @return
326      * @throws InterruptedException
327      * @throws IOException
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      * Pulls all patches from a remote repository in the workspace repository.
343      *
344      * @param build
345      * @param launcher
346      * @param workspace
347      * @param listener
348      * @param changelogFile
349      * @return boolean
350      * @throws InterruptedException
351      * @throws IOException
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      * Gets a fresh copy of a remote repository.
377      *
378      * @param build
379      * @param launcher
380      * @param workspace
381      * @param listener
382      * @param changelogFile
383      * @return boolean
384      * @throws InterruptedException
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      * Creates a local path relative to the given base.
422      *
423      * If {@link #localDir} is not {@link null} and not empty a relative path to the given base is created, else the
424      * base pat itself is returned. *
425      *
426      * @param base base of the local path
427      * @return local file path.
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      * Add class name aliases for backward compatibility.
439      */
440     @Initializer(before = InitMilestone.PLUGINS_STARTED)
441     public static void addAliases() {
442         // until version 0.3.6 the descriptor was inner class of DarcsScm
443         Jenkins.XSTREAM2.addCompatibilityAlias("org.jenkinsci.plugins.darcs.DarcsScm$DescriptorImpl",
444                 DarcsScmDescriptor.class);
445     }
446 
447 }