View Javadoc

1   /*
2    * Copyright (c) 2011 GitHub Inc.
3    *
4    * Permission is hereby granted, free of charge, to any person obtaining a copy
5    * of this software and associated documentation files (the "Software"), to
6    * deal in the Software without restriction, including without limitation the
7    * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8    * sell copies of the Software, and to permit persons to whom the Software is
9    * furnished to do so, subject to the following conditions:
10   *
11   * The above copyright notice and this permission notice shall be included in
12   * all copies or substantial portions of the Software.
13   *
14   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20   * IN THE SOFTWARE.
21   */
22  package com.github.maven.plugins.site;
23  
24  import static java.lang.Integer.MAX_VALUE;
25  import static org.eclipse.egit.github.core.Blob.ENCODING_BASE64;
26  import static org.eclipse.egit.github.core.TreeEntry.MODE_BLOB;
27  import static org.eclipse.egit.github.core.TreeEntry.TYPE_BLOB;
28  import static org.eclipse.egit.github.core.TypedResource.TYPE_COMMIT;
29  
30  import com.github.maven.plugins.core.GitHubProjectMojo;
31  import com.github.maven.plugins.core.PathUtils;
32  import com.github.maven.plugins.core.StringUtils;
33  
34  import java.io.ByteArrayOutputStream;
35  import java.io.File;
36  import java.io.FileInputStream;
37  import java.io.IOException;
38  import java.text.MessageFormat;
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.Collections;
42  import java.util.List;
43  
44  import org.apache.maven.execution.MavenSession;
45  import org.apache.maven.plugin.MojoExecutionException;
46  import org.apache.maven.project.MavenProject;
47  import org.apache.maven.settings.Settings;
48  import org.eclipse.egit.github.core.Blob;
49  import org.eclipse.egit.github.core.Commit;
50  import org.eclipse.egit.github.core.Reference;
51  import org.eclipse.egit.github.core.RepositoryId;
52  import org.eclipse.egit.github.core.Tree;
53  import org.eclipse.egit.github.core.TreeEntry;
54  import org.eclipse.egit.github.core.TypedResource;
55  import org.eclipse.egit.github.core.client.RequestException;
56  import org.eclipse.egit.github.core.service.DataService;
57  import org.eclipse.egit.github.core.util.EncodingUtils;
58  
59  /**
60   * Mojo which copies files to a GitHub repository branch. This directly uses the
61   * GitHub data API to upload blobs, make commits, and update references and so a
62   * local Git repository is not used.
63   *
64   * @author Kevin Sawicki (kevin@github.com)
65   * @goal site
66   */
67  public class SiteMojo extends GitHubProjectMojo {
68  
69  	/**
70  	 * BRANCH_DEFAULT
71  	 */
72  	public static final String BRANCH_DEFAULT = "refs/heads/gh-pages";
73  
74  	/**
75  	 * NO_JEKYLL_FILE
76  	 */
77  	public static final String NO_JEKYLL_FILE = ".nojekyll";
78  
79  	/**
80  	 * Branch to update
81  	 *
82  	 * @parameter default-value="refs/heads/gh-pages"
83  	 */
84  	private String branch = BRANCH_DEFAULT;
85  
86  	/**
87  	 * Path of tree
88  	 *
89  	 * @parameter
90  	 */
91  	private String path;
92  
93  	/**
94  	 * The commit message used when committing the site.
95  	 *
96  	 * @parameter
97  	 * @required
98  	 */
99  	private String message;
100 
101 	/**
102 	 * The name of the repository. This setting must be set if the project's url and scm metadata are not set.
103 	 *
104 	 * @parameter expression="${github.site.repositoryName}"
105 	 */
106 	private String repositoryName;
107 
108 	/**
109 	 * The owner of repository. This setting must be set if the project's url and scm metadata are not set.
110 	 *
111 	 * @parameter expression="${github.site.repositoryOwner}"
112 	 */
113 	private String repositoryOwner;
114 
115 	/**
116 	 * The user name for authentication
117 	 *
118 	 * @parameter expression="${github.site.userName}"
119 	 *            default-value="${github.global.userName}"
120 	 */
121 	private String userName;
122 
123 	/**
124 	 * The password for authentication
125 	 *
126 	 * @parameter expression="${github.site.password}"
127 	 *            default-value="${github.global.password}"
128 	 */
129 	private String password;
130 
131 	/**
132 	 * The oauth2 token for authentication
133 	 *
134 	 * @parameter expression="${github.site.oauth2Token}"
135 	 *            default-value="${github.global.oauth2Token}"
136 	 */
137 	private String oauth2Token;
138 
139 	/**
140 	 * The Host for API calls.
141 	 *
142 	 * @parameter expression="${github.site.host}"
143 	 *            default-value="${github.global.host}"
144 	 */
145 	private String host;
146 
147 	/**
148 	 * The <em>id</em> of the server to use to retrieve the Github credentials. This id must identify a
149      * <em>server</em> from your <em>setting.xml</em> file.
150 	 *
151 	 * @parameter expression="${github.site.server}"
152 	 *            default-value="${github.global.server}"
153 	 */
154 	private String server;
155 
156 	/**
157 	 * Paths and patterns to include
158 	 *
159 	 * @parameter
160 	 */
161 	private String[] includes;
162 
163 	/**
164 	 * Paths and patterns to exclude
165 	 *
166 	 * @parameter
167 	 */
168 	private String[] excludes;
169 
170 	/**
171 	 * The base directory to commit files from. <em>target/site</em> by default.
172 	 *
173 	 * @parameter expression="${siteOutputDirectory}"
174 	 *            default-value="${project.reporting.outputDirectory}"
175 	 * @required
176 	 */
177 	private File outputDirectory;
178 
179 	/**
180 	 * The project being built
181 	 *
182 	 * @parameter expression="${project}
183 	 * @required
184 	 */
185 	private MavenProject project;
186 
187 	/**
188 	 * The Maven session
189 	 *
190 	 * @parameter expression="${session}
191 	 */
192 	private MavenSession session;
193 
194 	/**
195 	 * The Maven settings
196 	 *
197 	 * @parameter expression="${settings}
198 	 */
199 	private Settings settings;
200 
201 	/**
202 	 * Force reference update
203 	 *
204 	 * @parameter expression="${github.site.force}"
205 	 */
206 	private boolean force;
207 
208 	/**
209 	 * Set it to {@code true} to always create a '.nojekyll' file at the root of the site if one
210 	 * doesn't already exist.
211 	 *
212 	 * @parameter expression="${github.site.noJekyll}"
213 	 */
214 	private boolean noJekyll;
215 
216 	/**
217 	 * Set it to {@code true} to merge with existing the existing tree that is referenced by the commit
218 	 * that the ref currently points to
219 	 *
220 	 * @parameter expression="${github.site.merge}"
221 	 */
222 	private boolean merge;
223 
224 	/**
225 	 * Show what blob, trees, commits, and references would be created/updated
226 	 * but don't actually perform any operations on the target GitHub
227 	 * repository.
228 	 *
229 	 * @parameter expression="${github.site.dryRun}"
230 	 */
231 	private boolean dryRun;
232 
233     /**
234      * Skip the site upload.
235      *
236      * @parameter expression="${github.site.skip}"
237      *            default-value="false"
238      * @since 0.9
239      */
240     private boolean skip;
241 
242 	/**
243 	 * Create blob
244 	 *
245 	 * @param service
246 	 * @param repository
247 	 * @param path
248 	 * @return blob SHA-1
249 	 * @throws MojoExecutionException
250 	 */
251 	protected String createBlob(DataService service, RepositoryId repository,
252 			String path) throws MojoExecutionException {
253 		File file = new File(outputDirectory, path);
254 		final long length = file.length();
255 		final int size = length > MAX_VALUE ? MAX_VALUE : (int) length;
256 		ByteArrayOutputStream output = new ByteArrayOutputStream(size);
257 		FileInputStream stream = null;
258 		try {
259 			stream = new FileInputStream(file);
260 			final byte[] buffer = new byte[8192];
261 			int read;
262 			while ((read = stream.read(buffer)) != -1)
263 				output.write(buffer, 0, read);
264 		} catch (IOException e) {
265 			throw new MojoExecutionException("Error reading file: "
266 					+ getExceptionMessage(e), e);
267 		} finally {
268 			if (stream != null)
269 				try {
270 					stream.close();
271 				} catch (IOException e) {
272 					debug("Exception closing stream", e);
273 				}
274 		}
275 
276 		Blob blob = new Blob().setEncoding(ENCODING_BASE64);
277 		String encoded = EncodingUtils.toBase64(output.toByteArray());
278 		blob.setContent(encoded);
279 
280 		try {
281 			if (isDebug())
282 				debug(MessageFormat.format("Creating blob from {0}",
283 						file.getAbsolutePath()));
284 			if (!dryRun)
285 				return service.createBlob(repository, blob);
286 			else
287 				return null;
288 		} catch (IOException e) {
289 			throw new MojoExecutionException("Error creating blob: "
290 					+ getExceptionMessage(e), e);
291 		}
292 	}
293 
294 	public void execute() throws MojoExecutionException {
295         if (skip) {
296             info("Github Site Plugin execution skipped");
297             return;
298         }
299 
300 		RepositoryId repository = getRepository(project, repositoryOwner,
301 				repositoryName);
302 
303 		if (dryRun)
304 			info("Dry run mode, repository will not be modified");
305 
306 		// Find files to include
307 		String baseDir = outputDirectory.getAbsolutePath();
308 		String[] includePaths = StringUtils.removeEmpties(includes);
309 		String[] excludePaths = StringUtils.removeEmpties(excludes);
310 		if (isDebug())
311 			debug(MessageFormat.format(
312 					"Scanning {0} and including {1} and exluding {2}", baseDir,
313 					Arrays.toString(includePaths),
314 					Arrays.toString(excludePaths)));
315 		String[] paths = PathUtils.getMatchingPaths(includePaths, excludePaths,
316 				baseDir);
317 
318 		if (paths.length != 1)
319 			info(MessageFormat.format("Creating {0} blobs", paths.length));
320 		else
321 			info("Creating 1 blob");
322 		if (isDebug())
323 			debug(MessageFormat.format("Scanned files to include: {0}",
324 					Arrays.toString(paths)));
325 
326 		DataService service = new DataService(createClient(host, userName,
327 				password, oauth2Token, server, settings, session));
328 
329 		// Write blobs and build tree entries
330 		List<TreeEntry> entries = new ArrayList<TreeEntry>(paths.length);
331 		String prefix = path;
332 		if (prefix == null)
333 			prefix = "";
334 		if (prefix.length() > 0 && !prefix.endsWith("/"))
335 			prefix += "/";
336 
337 		// Convert separator to forward slash '/'
338 		if ('\\' == File.separatorChar)
339 			for (int i = 0; i < paths.length; i++)
340 				paths[i] = paths[i].replace('\\', '/');
341 
342 		boolean createNoJekyll = noJekyll;
343 
344 		for (String path : paths) {
345 			TreeEntry entry = new TreeEntry();
346 			entry.setPath(prefix + path);
347 			// Only create a .nojekyll file if it doesn't already exist
348 			if (createNoJekyll && NO_JEKYLL_FILE.equals(entry.getPath()))
349 				createNoJekyll = false;
350 			entry.setType(TYPE_BLOB);
351 			entry.setMode(MODE_BLOB);
352 			entry.setSha(createBlob(service, repository, path));
353 			entries.add(entry);
354 		}
355 
356 		if (createNoJekyll) {
357 			TreeEntry entry = new TreeEntry();
358 			entry.setPath(NO_JEKYLL_FILE);
359 			entry.setType(TYPE_BLOB);
360 			entry.setMode(MODE_BLOB);
361 
362 			if (isDebug())
363 				debug("Creating empty .nojekyll blob at root of tree");
364 			if (!dryRun)
365 				try {
366 					entry.setSha(service.createBlob(repository, new Blob()
367 							.setEncoding(ENCODING_BASE64).setContent("")));
368 				} catch (IOException e) {
369 					throw new MojoExecutionException(
370 							"Error creating .nojekyll empty blob: "
371 									+ getExceptionMessage(e), e);
372 				}
373 			entries.add(entry);
374 		}
375 
376 		Reference ref = null;
377 		try {
378 			ref = service.getReference(repository, branch);
379 		} catch (RequestException e) {
380 			if (404 != e.getStatus())
381 				throw new MojoExecutionException("Error getting reference: "
382 						+ getExceptionMessage(e), e);
383 		} catch (IOException e) {
384 			throw new MojoExecutionException("Error getting reference: "
385 					+ getExceptionMessage(e), e);
386 		}
387 
388 		if (ref != null && !TYPE_COMMIT.equals(ref.getObject().getType()))
389 			throw new MojoExecutionException(
390 					MessageFormat
391 							.format("Existing ref {0} points to a {1} ({2}) instead of a commmit",
392 									ref.getRef(), ref.getObject().getType(),
393 									ref.getObject().getSha()));
394 
395 		// Write tree
396 		Tree tree;
397 		try {
398 			int size = entries.size();
399 			if (size != 1)
400 				info(MessageFormat.format(
401 						"Creating tree with {0} blob entries", size));
402 			else
403 				info("Creating tree with 1 blob entry");
404 			String baseTree = null;
405 			if (merge && ref != null) {
406 				Tree currentTree = service.getCommit(repository,
407 						ref.getObject().getSha()).getTree();
408 				if (currentTree != null)
409 					baseTree = currentTree.getSha();
410 				info(MessageFormat.format("Merging with tree {0}", baseTree));
411 			}
412 			if (!dryRun)
413 				tree = service.createTree(repository, entries, baseTree);
414 			else
415 				tree = new Tree();
416 		} catch (IOException e) {
417 			throw new MojoExecutionException("Error creating tree: "
418 					+ getExceptionMessage(e), e);
419 		}
420 
421 		// Build commit
422 		Commit commit = new Commit();
423 		commit.setMessage(message);
424 		commit.setTree(tree);
425 
426 		// Set parent commit SHA-1 if reference exists
427 		if (ref != null)
428 			commit.setParents(Collections.singletonList(new Commit().setSha(ref
429 					.getObject().getSha())));
430 
431 		Commit created;
432 		try {
433 			if (!dryRun)
434 				created = service.createCommit(repository, commit);
435 			else
436 				created = new Commit();
437 			info(MessageFormat.format("Creating commit with SHA-1: {0}",
438 					created.getSha()));
439 		} catch (IOException e) {
440 			throw new MojoExecutionException("Error creating commit: "
441 					+ getExceptionMessage(e), e);
442 		}
443 
444 		TypedResource object = new TypedResource();
445 		object.setType(TYPE_COMMIT).setSha(created.getSha());
446 		if (ref != null) {
447 			// Update existing reference
448 			ref.setObject(object);
449 			try {
450 				info(MessageFormat.format(
451 						"Updating reference {0} from {1} to {2}", branch,
452 						commit.getParents().get(0).getSha(), created.getSha()));
453 				if (!dryRun)
454 					service.editReference(repository, ref, force);
455 			} catch (IOException e) {
456 				throw new MojoExecutionException("Error editing reference: "
457 						+ getExceptionMessage(e), e);
458 			}
459 		} else {
460 			// Create new reference
461 			ref = new Reference().setObject(object).setRef(branch);
462 			try {
463 				info(MessageFormat.format(
464 						"Creating reference {0} starting at commit {1}",
465 						branch, created.getSha()));
466 				if (!dryRun)
467 					service.createReference(repository, ref);
468 			} catch (IOException e) {
469 				throw new MojoExecutionException("Error creating reference: "
470 						+ getExceptionMessage(e), e);
471 			}
472 		}
473 	}
474 }