| /******************************************************************************* |
| * Copyright (c) 2012 Sonatype Inc. and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Sonatype Inc. - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.tycho.extras.buildtimestamp.jgit; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.apache.maven.execution.MavenSession; |
| import org.apache.maven.plugin.MojoExecution; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.project.MavenProject; |
| import org.codehaus.plexus.component.annotations.Component; |
| import org.codehaus.plexus.component.annotations.Requirement; |
| import org.codehaus.plexus.logging.Logger; |
| import org.codehaus.plexus.util.xml.Xpp3Dom; |
| import org.eclipse.jgit.api.Status; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.IndexDiff; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.storage.file.FileRepositoryBuilder; |
| import org.eclipse.jgit.submodule.SubmoduleWalk.IgnoreSubmoduleMode; |
| import org.eclipse.jgit.treewalk.FileTreeIterator; |
| import org.eclipse.jgit.treewalk.filter.AndTreeFilter; |
| import org.eclipse.jgit.treewalk.filter.TreeFilter; |
| import org.eclipse.tycho.buildversion.BuildTimestampProvider; |
| |
| /** |
| * Build timestamp provider that returns date of the most recent git commit that touches any file |
| * under project basedir. Because this timestamp provider is meant to be used for reproducible |
| * builds, by default an exception is thrown if <code>git status</code> is not clean (i.e. |
| * uncommitted changes are detected). |
| * <p/> |
| * If uncommitted changes should be tolerated with a warning, configure |
| * |
| * <pre> |
| * <jgit.dirtyWorkingTree>warning</jgit.dirtyWorkingTree> |
| * </pre> |
| * |
| * In this case, this timestamp provider will delegate to the default timestamp provider which uses |
| * the current build timestamp. |
| * <p/> |
| * |
| * For additional flexibility, some files can be ignored using gitignore patterns specified in |
| * <jgit.ignore> element of tycho-packaging-plugin configuration block. |
| * |
| * <p> |
| * Typical usage |
| * |
| * <pre> |
| * ... |
| * <plugin> |
| * <groupId>org.eclipse.tycho</groupId> |
| * <artifactId>tycho-packaging-plugin</artifactId> |
| * <version>${tycho-version}</version> |
| * <dependencies> |
| * <dependency> |
| * <groupId>org.eclipse.tycho.extras</groupId> |
| * <artifactId>tycho-buildtimestamp-jgit</artifactId> |
| * <version>${tycho-version}</version> |
| * </dependency> |
| * </dependencies> |
| * <configuration> |
| * <timestampProvider>jgit</timestampProvider> |
| * <jgit.ignore>pom.xml</jgit.ignore> |
| * </configuration> |
| * </plugin> |
| * ... |
| * </pre> |
| */ |
| @Component(role = BuildTimestampProvider.class, hint = "jgit") |
| public class JGitBuildTimestampProvider implements BuildTimestampProvider { |
| |
| @Requirement(hint = "default") |
| private BuildTimestampProvider defaultTimestampProvider; |
| |
| @Requirement |
| private Logger logger; |
| |
| private static enum DirtyBehavior { |
| |
| ERROR, WARNING, IGNORE; |
| |
| public static DirtyBehavior getDirtyWorkingTreeBehaviour(MojoExecution execution) { |
| final DirtyBehavior defaultBehaviour = ERROR; |
| Xpp3Dom pluginConfiguration = (Xpp3Dom) execution.getPlugin().getConfiguration(); |
| if (pluginConfiguration == null) { |
| return defaultBehaviour; |
| } |
| Xpp3Dom dirtyWorkingTreeDom = pluginConfiguration.getChild("jgit.dirtyWorkingTree"); |
| if (dirtyWorkingTreeDom == null) { |
| return defaultBehaviour; |
| } |
| String value = dirtyWorkingTreeDom.getValue(); |
| if (value == null) { |
| return defaultBehaviour; |
| } |
| value = value.trim(); |
| if ("warning".equals(value)) { |
| return WARNING; |
| } else if ("ignore".equals(value)) { |
| return IGNORE; |
| } |
| return defaultBehaviour; |
| } |
| } |
| |
| @Override |
| public Date getTimestamp(MavenSession session, MavenProject project, MojoExecution execution) |
| throws MojoExecutionException { |
| FileRepositoryBuilder builder = new FileRepositoryBuilder() // |
| .readEnvironment() // |
| .findGitDir(project.getBasedir()) // |
| .setMustExist(true); |
| if (builder.getGitDir() == null) { |
| throw new MojoExecutionException("No git repository found searching upwards from " + project.getBasedir()); |
| } |
| try { |
| try (Repository repository = builder.build()) { |
| String relPath = getRelPath(repository, project); |
| TreeFilter pathFilter = createPathFilter(relPath, execution); |
| ObjectId headId = repository.resolve(Constants.HEAD); |
| if (headId == null) { |
| String message = "Git repository without HEAD on " + project.getBasedir() |
| + ". You can make a first commit to solve that."; |
| logger.warn(message); |
| logger.warn("Fallback to default timestamp provider"); |
| return defaultTimestampProvider.getTimestamp(session, project, execution); |
| } |
| DirtyBehavior dirtyBehaviour = DirtyBehavior.getDirtyWorkingTreeBehaviour(execution); |
| if (dirtyBehaviour != DirtyBehavior.IGNORE) { |
| // 1. check if 'git status' is clean for relPath |
| IndexDiff diff = new IndexDiff(repository, headId, new FileTreeIterator(repository)); |
| // Ignore all the submodules (together with the pathFilter this will ignore changes done in not related submodules #480951) |
| diff.setIgnoreSubmoduleMode(IgnoreSubmoduleMode.ALL); |
| if (pathFilter != null) { |
| diff.setFilter(pathFilter); |
| } |
| diff.diff(); |
| Status status = new Status(diff); |
| if (!status.isClean()) { |
| String message = "Working tree is dirty.\ngit status " + (relPath != null ? relPath : "") |
| + ":\n" + toGitStatusStyleOutput(diff); |
| if (dirtyBehaviour == DirtyBehavior.WARNING) { |
| logger.warn(message); |
| logger.warn("Fallback to default timestamp provider"); |
| return defaultTimestampProvider.getTimestamp(session, project, execution); |
| } else { |
| throw new MojoExecutionException(message + "\n" |
| + "You are trying to use tycho-buildtimestamp-jgit on a directory that has uncommitted changes (see details above)." |
| + "\nEither commit all changes/add files to .gitignore, or enable fallback to default timestamp provider by configuring " |
| + "\njgit.dirtyWorkingTree=warning for tycho-packaging-plugin"); |
| } |
| } |
| } |
| // 2. get latest commit for relPath |
| try (RevWalk walk = new RevWalk(repository)) { |
| if (pathFilter != null) { |
| walk.setTreeFilter(AndTreeFilter.create(pathFilter, TreeFilter.ANY_DIFF)); |
| } |
| walk.markStart(walk.parseCommit(headId)); |
| walk.setRewriteParents(false); |
| RevCommit commit = walk.next(); |
| // When dirtyBehaviour==ignore and no commit was ever done, |
| // the commit is null, so we fallback to the defaultTimestampProvider |
| if (commit == null) { |
| logger.info( |
| "Fallback to default timestamp provider, because no commit could be found for that project (Shared but not committed yet)."); |
| return defaultTimestampProvider.getTimestamp(session, project, execution); |
| } |
| return new Date(commit.getCommitTime() * 1000L); |
| } |
| } |
| } catch (IOException e) { |
| throw new MojoExecutionException("Could not determine git commit timestamp", e); |
| } |
| } |
| |
| private static TreeFilter createPathFilter(String relPath, MojoExecution execution) throws IOException { |
| if (relPath != null && relPath.length() > 0) { |
| return new PathFilter(relPath, getIgnoreFilter(execution)); |
| } |
| return null; |
| } |
| |
| private static String getIgnoreFilter(MojoExecution execution) { |
| Xpp3Dom pluginConfiguration = (Xpp3Dom) execution.getPlugin().getConfiguration(); |
| if (pluginConfiguration == null) { |
| return null; |
| } |
| Xpp3Dom ignoreDom = pluginConfiguration.getChild("jgit.ignore"); |
| if (ignoreDom == null) { |
| return null; |
| } |
| return ignoreDom.getValue().trim(); |
| } |
| |
| private String getRelPath(Repository repository, MavenProject project) throws IOException { |
| String workTree = repository.getWorkTree().getCanonicalPath(); |
| |
| String path = project.getBasedir().getCanonicalPath(); |
| |
| if (!path.startsWith(workTree)) { |
| throw new IOException(project + " is not in git repository working tree " + repository.getWorkTree()); |
| } |
| |
| path = path.substring(workTree.length()); |
| |
| if (path.startsWith(File.separator)) { |
| path = path.substring(File.separator.length()); |
| } |
| |
| // git stores paths unix-style |
| path = path.replace(File.separatorChar, '/'); |
| |
| return path; |
| } |
| |
| private static String toGitStatusStyleOutput(IndexDiff diff) { |
| StringWriter sw = new StringWriter(); |
| PrintWriter pw = new PrintWriter(sw); |
| List<String> toBeCommitted = new ArrayList<>(diff.getAdded()); |
| toBeCommitted.addAll(diff.getChanged()); |
| toBeCommitted.addAll(diff.getRemoved()); |
| if (toBeCommitted.size() > 0) { |
| pw.println("Changes to be committed:"); |
| printList(pw, "\tnew file: ", diff.getAdded()); |
| printList(pw, "\tmodified: ", diff.getChanged()); |
| printList(pw, "\tdeleted: ", diff.getRemoved()); |
| } |
| List<String> notStaged = new ArrayList<>(diff.getModified()); |
| notStaged.addAll(diff.getMissing()); |
| if (notStaged.size() > 0) { |
| pw.println(); |
| pw.println("Changes not staged for commit:"); |
| printList(pw, "\tmodified: ", diff.getModified()); |
| printList(pw, "\tdeleted: ", diff.getMissing()); |
| } |
| if (diff.getConflicting().size() > 0) { |
| pw.println(); |
| pw.println("Conflicting files:"); |
| printList(pw, "\tconflict: ", diff.getConflicting()); |
| } |
| if (diff.getUntracked().size() > 0) { |
| pw.println(); |
| pw.println("Untracked files:"); |
| printList(pw, "\t", diff.getUntracked()); |
| } |
| return sw.toString(); |
| } |
| |
| private static void printList(PrintWriter witer, String prefix, Set<String> files) { |
| for (String file : files) { |
| witer.println(prefix + file); |
| } |
| } |
| |
| } |