/* * The MIT License * * Copyright (c) 2004-2011, Oracle Corporation, Andrew Bayer, Anton Kozak, Nikita Levyankov, rogerhu * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.plugins.git; import hudson.MarkupText; import hudson.model.User; import hudson.scm.ChangeLogAnnotator; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.AffectedFile; import hudson.scm.EditType; import hudson.tasks.Mailer; import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import static hudson.Util.fixEmpty; /** * Represents a change set. * * @author Nigel Magnay * @author Nikita Levyankov */ public class GitChangeSet extends ChangeLogSet.Entry { private static final Logger LOGGER = Logger.getLogger(GitSCM.class.getName()); private static final Pattern FILE_LOG_ENTRY = Pattern.compile( "^:[0-9]{6} [0-9]{6} ([0-9a-f]{40}) ([0-9a-f]{40}) ([ACDMRTUX])(?>[0-9]+)?\t(.*)$"); private static final Pattern AUTHOR_ENTRY = Pattern.compile("^author (.*) <(.*)> (.*) (.*)$"); private static final Pattern COMMITTER_ENTRY = Pattern.compile("^committer (.*) <(.*)> (.*) (.*)$"); private static final Pattern RENAME_SPLIT = Pattern.compile("^(.*?)\t(.*)$"); private static final String NULL_HASH = "0000000000000000000000000000000000000000"; private String committer; private String committerEmail; private String committerTime; private String committerTz; private String author; private String authorEmail; private String authorTime; private String authorTz; private String comment; private String title; private String id; private String parentCommit; private Collection<Path> paths = new HashSet<Path>(); private boolean authorOrCommitter; public GitChangeSet(List<String> lines, boolean authorOrCommitter) { this.authorOrCommitter = authorOrCommitter; if (lines.size() > 0) { parseCommit(lines); } } private void parseCommit(List<String> lines) { String message = ""; for (String line : lines) { if (line.length() > 0) { if (line.startsWith("commit ")) { this.id = line.split(" ")[1]; } else if (line.startsWith("parent ")) { this.parentCommit = line.split(" ")[1]; } else if (line.startsWith("committer ")) { Matcher committerMatcher = COMMITTER_ENTRY.matcher(line); if (committerMatcher.matches() && committerMatcher.groupCount() >= 4) { this.committer = committerMatcher.group(1); this.committerEmail = committerMatcher.group(2); this.committerTime = committerMatcher.group(3); this.committerTz = committerMatcher.group(4); } } else if (line.startsWith("author ")) { Matcher authorMatcher = AUTHOR_ENTRY.matcher(line); if (authorMatcher.matches() && authorMatcher.groupCount() >= 4) { this.author = authorMatcher.group(1); this.authorEmail = authorMatcher.group(2); this.authorTime = authorMatcher.group(3); this.authorTz = authorMatcher.group(4); } } else if (line.startsWith(" ")) { message += line.substring(4) + "\n"; } else if (':' == line.charAt(0)) { Matcher fileMatcher = FILE_LOG_ENTRY.matcher(line); if (fileMatcher.matches() && fileMatcher.groupCount() >= 4) { String mode = fileMatcher.group(3); if (mode.length() == 1) { String src = null; String dst = null; String path = fileMatcher.group(4); char editMode = mode.charAt(0); if (editMode == 'M' || editMode == 'A' || editMode == 'D' || editMode == 'R' || editMode == 'C') { src = parseHash(fileMatcher.group(1)); dst = parseHash(fileMatcher.group(2)); } // Handle rename as two operations - a delete and an add if (editMode == 'R') { Matcher renameSplitMatcher = RENAME_SPLIT.matcher(path); if (renameSplitMatcher.matches() && renameSplitMatcher.groupCount() >= 2) { String oldPath = renameSplitMatcher.group(1); String newPath = renameSplitMatcher.group(2); this.paths.add(new Path(src, dst, 'D', oldPath, this)); this.paths.add(new Path(src, dst, 'A', newPath, this)); } } // Handle copy as an add else if (editMode == 'C') { Matcher copySplitMatcher = RENAME_SPLIT.matcher(path); if (copySplitMatcher.matches() && copySplitMatcher.groupCount() >= 2) { String newPath = copySplitMatcher.group(2); this.paths.add(new Path(src, dst, 'A', newPath, this)); } } else { this.paths.add(new Path(src, dst, editMode, path, this)); } } } } } } this.comment = message; int endOfFirstLine = this.comment.indexOf('\n'); if (endOfFirstLine == -1) { this.title = this.comment; } else { this.title = this.comment.substring(0, endOfFirstLine); } } private String parseHash(String hash) { return NULL_HASH.equals(hash) ? null : hash; } @Exported public String getDate() { DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String dateStr; String csTime; String csTz; Date csDate; if (authorOrCommitter) { csTime = this.authorTime; csTz = this.authorTz; } else { csTime = this.committerTime; csTz = this.committerTz; } try { csDate = new Date(Long.parseLong(csTime) * 1000L); } catch (NumberFormatException e) { csDate = new Date(); } dateStr = fmt.format(csDate) + " " + csTz; return dateStr; } @Override public void setParent(ChangeLogSet parent) { LOGGER.log(Level.FINEST, "Set parent " + parent); super.setParent(parent); } public String getParentCommit() { return parentCommit; } /** * {@inheritDoc} */ public Collection<String> getAffectedPaths() { Collection<String> affectedPaths = new HashSet<String>(this.paths.size()); for (Path file : this.paths) { affectedPaths.add(file.getPath()); } return affectedPaths; } /** * Gets the files that are changed in this commit. * * @return can be empty but never null. */ @Exported public Collection<Path> getPaths() { return paths; } /** * {@inheritDoc} */ @Override public Collection<Path> getAffectedFiles() { return this.paths; } @Exported public User getAuthor() { String csAuthor; String csAuthorEmail; // If true, use the author field from git log rather than the committer. if (authorOrCommitter) { csAuthor = this.author; csAuthorEmail = this.authorEmail; } else { csAuthor = this.committer; csAuthorEmail = this.committerEmail; } if (csAuthor == null) { throw new RuntimeException("No author in this changeset!"); } return findOrCreateUser(csAuthor, csAuthorEmail, isCreateAccountBaseOnCommitterEmail()); } /** * Returns user of the change set. * * @param csAuthor user name. * @param csAuthorEmail user email. * @param createAccountBaseOnCommitterEmail true if create new user based on committer's email. * @return {@link User} */ User findOrCreateUser(String csAuthor, String csAuthorEmail, boolean createAccountBaseOnCommitterEmail) { User user; if (createAccountBaseOnCommitterEmail) { user = User.get(csAuthorEmail, true); try { user.setFullName(csAuthor); user.save(); } catch (IOException e) { LOGGER.log(Level.FINEST, "Could not set author name to user properties.", e); } } else { user = User.get(csAuthor, true); } // set email address for user if needed if (fixEmpty(csAuthorEmail) != null) { try { user.addProperty(new Mailer.UserProperty(csAuthorEmail)); } catch (IOException e) { LOGGER.log(Level.FINEST, "Failed to add email to user properties.", e); } } return user; } private boolean isCreateAccountBaseOnCommitterEmail() { ChangeLogSet parent = getParent(); boolean createAccountBaseOnCommitterEmail = false; if (parent != null) { createAccountBaseOnCommitterEmail = ((GitSCM) parent.getBuild().getProject().getScm()). isCreateAccountBaseOnCommitterEmail(); } return createAccountBaseOnCommitterEmail; } /** * Gets the author name for this changeset - note that this is mainly here * so that we can test authorOrCommitter without needing a fully instantiated * Hudson (which is needed for User.get in getAuthor()). * * @return author name. */ public String getAuthorName() { String csAuthor; // If true, use the author field from git log rather than the committer. if (authorOrCommitter) { csAuthor = this.author; } else { csAuthor = this.committer; } if (csAuthor == null) { throw new RuntimeException("No author in this changeset!"); } return csAuthor; } /** * {@inheritDoc} */ public String getUser() { return getAuthorName(); } @Exported public String getMsg() { return this.title; } @Exported public String getId() { return this.id; } /** * @return revision id * @deprecated * @since 2.0.1 * @see #getCurrentRevision() */ public String getRevision() { return this.id; } /** * {@inheritDoc} */ @Override public String getCurrentRevision() { return getRevision(); } @Exported public String getComment() { return this.comment; } /** * Gets {@linkplain #getComment() the comment} fully marked up by {@link ChangeLogAnnotator}. */ public String getCommentAnnotated() { MarkupText markup = new MarkupText(getComment()); for (ChangeLogAnnotator a : ChangeLogAnnotator.all()) { a.annotate(getParent().build, this, markup); } return markup.toString(false); } @ExportedBean(defaultVisibility = 999) public static class Path implements AffectedFile { private String src; private String dst; private char action; private String path; private GitChangeSet changeSet; private Path(String source, String destination, char action, String filePath, GitChangeSet changeSet) { this.src = source; this.dst = destination; this.action = action; this.path = filePath; this.changeSet = changeSet; } public String getSrc() { return src; } public String getDst() { return dst; } @Exported(name = "file") public String getPath() { return path; } public GitChangeSet getChangeSet() { return changeSet; } @Exported public EditType getEditType() { switch (action) { case 'A': return EditType.ADD; case 'D': return EditType.DELETE; default: return EditType.EDIT; } } } }