| /* |
| * Copyright (C) 2010, Google Inc. |
| * and other copyright owners as documented in the project's IP log. |
| * |
| * This program and the accompanying materials are made available |
| * under the terms of the Eclipse Distribution License v1.0 which |
| * accompanies this distribution, is reproduced below, and is |
| * available at http://www.eclipse.org/org/documents/edl-v10.php |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or |
| * without modification, are permitted provided that the following |
| * conditions are met: |
| * |
| * - Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * |
| * - Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following |
| * disclaimer in the documentation and/or other materials provided |
| * with the distribution. |
| * |
| * - Neither the name of the Eclipse Foundation, Inc. nor the |
| * names of its contributors may be used to endorse or promote |
| * products derived from this software without specific prior |
| * written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
| * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
| * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
| * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
| * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| package org.eclipse.jgit.iplog; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileReader; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.text.MessageFormat; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| import javax.xml.transform.OutputKeys; |
| import javax.xml.transform.Transformer; |
| import javax.xml.transform.TransformerConfigurationException; |
| import javax.xml.transform.TransformerException; |
| import javax.xml.transform.TransformerFactory; |
| import javax.xml.transform.dom.DOMSource; |
| import javax.xml.transform.stream.StreamResult; |
| |
| import org.eclipse.jgit.diff.Edit; |
| import org.eclipse.jgit.diff.EditList; |
| import org.eclipse.jgit.diff.MyersDiff; |
| import org.eclipse.jgit.diff.RawText; |
| import org.eclipse.jgit.diff.RawTextComparator; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.iplog.Committer.ActiveRange; |
| import org.eclipse.jgit.lib.BlobBasedConfig; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.MutableObjectId; |
| import org.eclipse.jgit.lib.ObjectReader; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevTree; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.treewalk.NameConflictTreeWalk; |
| import org.eclipse.jgit.treewalk.TreeWalk; |
| import org.eclipse.jgit.treewalk.filter.TreeFilter; |
| import org.eclipse.jgit.util.RawParseUtils; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| |
| /** |
| * Creates an Eclipse IP log in XML format. |
| * |
| * @see <a href="http://www.eclipse.org/projects/xml/iplog.xsd">IP log XSD</a> |
| */ |
| public class IpLogGenerator { |
| private static final String IPLOG_NS = "http://www.eclipse.org/projects/xml/iplog"; |
| |
| private static final String IPLOG_PFX = "iplog:"; |
| |
| private static final String INDENT = "{http://xml.apache.org/xslt}indent-amount"; |
| |
| /** Projects indexed by their ID string, e.g. {@code technology.jgit}. */ |
| private final Map<String, Project> projects = new TreeMap<String, Project>(); |
| |
| /** Projects indexed by their ID string, e.g. {@code technology.jgit}. */ |
| private final Map<String, Project> consumedProjects = new TreeMap<String, Project>(); |
| |
| /** Known committers, indexed by their foundation ID. */ |
| private final Map<String, Committer> committersById = new HashMap<String, Committer>(); |
| |
| /** Known committers, indexed by their email address. */ |
| private final Map<String, Committer> committersByEmail = new HashMap<String, Committer>(); |
| |
| /** Discovered contributors. */ |
| private final Map<String, Contributor> contributorsByName = new HashMap<String, Contributor>(); |
| |
| /** All known CQs matching the projects we care about. */ |
| private final Set<CQ> cqs = new HashSet<CQ>(); |
| |
| /** Root commits which were scanned to gather project data. */ |
| private final Set<RevCommit> commits = new HashSet<RevCommit>(); |
| |
| /** The meta file we loaded to bootstrap our definitions. */ |
| private IpLogMeta meta; |
| |
| /** URL to obtain review information about a specific contribution. */ |
| private String reviewUrl; |
| |
| private String characterEncoding = "UTF-8"; |
| |
| private Repository db; |
| |
| private RevWalk rw; |
| |
| private NameConflictTreeWalk tw; |
| |
| private ObjectReader curs; |
| |
| private final MutableObjectId idbuf = new MutableObjectId(); |
| |
| private Document doc; |
| |
| /** Create an empty generator. */ |
| public IpLogGenerator() { |
| // Do nothing. |
| } |
| |
| /** |
| * Set the character encoding used to write the output file. |
| * |
| * @param encodingName |
| * the character set encoding name. |
| */ |
| public void setCharacterEncoding(String encodingName) { |
| characterEncoding = encodingName; |
| } |
| |
| /** |
| * Scan a Git repository's history to compute the changes within it. |
| * |
| * @param repo |
| * the repository to scan. |
| * @param startCommit |
| * commit the IP log is needed for. |
| * @param version |
| * symbolic label for the version. |
| * @throws IOException |
| * the repository cannot be read. |
| * @throws ConfigInvalidException |
| * the {@code .eclipse_iplog} file present at the top level of |
| * {@code startId} is not a valid configuration file. |
| */ |
| public void scan(Repository repo, RevCommit startCommit, String version) |
| throws IOException, ConfigInvalidException { |
| try { |
| db = repo; |
| curs = db.newObjectReader(); |
| rw = new RevWalk(curs); |
| tw = new NameConflictTreeWalk(curs); |
| |
| RevCommit c = rw.parseCommit(startCommit); |
| |
| loadEclipseIpLog(version, c); |
| loadCommitters(repo); |
| scanProjectCommits(meta.getProjects().get(0), c); |
| commits.add(c); |
| } finally { |
| curs.release(); |
| db = null; |
| rw = null; |
| tw = null; |
| } |
| } |
| |
| private void loadEclipseIpLog(String version, RevCommit commit) |
| throws IOException, ConfigInvalidException { |
| TreeWalk log = TreeWalk.forPath(db, IpLogMeta.IPLOG_CONFIG_FILE, commit |
| .getTree()); |
| if (log == null) |
| return; |
| |
| meta = new IpLogMeta(); |
| try { |
| meta.loadFrom(new BlobBasedConfig(null, db, log.getObjectId(0))); |
| } catch (ConfigInvalidException e) { |
| throw new ConfigInvalidException(MessageFormat.format(IpLogText.get().configurationFileInCommitIsInvalid |
| , log.getPathString(), commit.name()), e); |
| } |
| |
| if (meta.getProjects().isEmpty()) { |
| throw new ConfigInvalidException(MessageFormat.format(IpLogText.get().configurationFileInCommitHasNoProjectsDeclared |
| , log.getPathString(), commit.name())); |
| } |
| |
| for (Project p : meta.getProjects()) { |
| p.setVersion(version); |
| projects.put(p.getName(), p); |
| } |
| for (Project p : meta.getConsumedProjects()) { |
| consumedProjects.put(p.getName(), p); |
| } |
| cqs.addAll(meta.getCQs()); |
| reviewUrl = meta.getReviewUrl(); |
| } |
| |
| private void loadCommitters(Repository repo) throws IOException { |
| SimpleDateFormat dt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| File list = new File(repo.getDirectory(), "gerrit_committers"); |
| BufferedReader br = new BufferedReader(new FileReader(list)); |
| try { |
| String line; |
| |
| while ((line = br.readLine()) != null) { |
| String[] field = line.trim().split(" *\\| *"); |
| String user = field[1]; |
| String name = field[2]; |
| String email = field[3]; |
| Date begin = parseDate(dt, field[4]); |
| Date end = parseDate(dt, field[5]); |
| |
| if (user.startsWith("username:")) |
| user = user.substring("username:".length()); |
| |
| Committer who = committersById.get(user); |
| if (who == null) { |
| who = new Committer(user); |
| int sp = name.indexOf(' '); |
| if (0 < sp) { |
| who.setFirstName(name.substring(0, sp).trim()); |
| who.setLastName(name.substring(sp + 1).trim()); |
| } else { |
| who.setFirstName(name); |
| who.setLastName(null); |
| } |
| committersById.put(who.getID(), who); |
| } |
| |
| who.addEmailAddress(email); |
| who.addActiveRange(new ActiveRange(begin, end)); |
| committersByEmail.put(email, who); |
| } |
| } finally { |
| br.close(); |
| } |
| } |
| |
| private Date parseDate(SimpleDateFormat dt, String value) |
| throws IOException { |
| if ("NULL".equals(value) || "".equals(value) || value == null) |
| return null; |
| int dot = value.indexOf('.'); |
| if (0 < dot) |
| value = value.substring(0, dot); |
| try { |
| return dt.parse(value); |
| } catch (ParseException e) { |
| IOException err = new IOException(MessageFormat.format(IpLogText.get().invalidDate, value)); |
| err.initCause(e); |
| throw err; |
| } |
| } |
| |
| private void scanProjectCommits(Project proj, RevCommit start) |
| throws IOException { |
| rw.reset(); |
| rw.markStart(start); |
| |
| RevCommit commit; |
| while ((commit = rw.next()) != null) { |
| if (proj.isSkippedCommit(commit)) { |
| continue; |
| } |
| |
| final PersonIdent author = commit.getAuthorIdent(); |
| final Date when = author.getWhen(); |
| |
| Committer who = committersByEmail.get(author.getEmailAddress()); |
| if (who != null && who.inRange(when)) { |
| // Commit was written by the committer while they were |
| // an active committer on the project. |
| // |
| who.setHasCommits(true); |
| continue; |
| } |
| |
| // Commit from a non-committer contributor. |
| // |
| final int cnt = commit.getParentCount(); |
| if (2 <= cnt) { |
| // Avoid a pointless merge attributed to a non-committer. |
| // Skip this commit if every file matches at least one |
| // of the parent commits exactly, if so then the blame |
| // for code in that file can be fully passed onto that |
| // parent and this non-committer isn't responsible. |
| // |
| tw.setFilter(TreeFilter.ANY_DIFF); |
| tw.setRecursive(true); |
| |
| RevTree[] trees = new RevTree[1 + cnt]; |
| trees[0] = commit.getTree(); |
| for (int i = 0; i < cnt; i++) |
| trees[i + 1] = commit.getParent(i).getTree(); |
| tw.reset(trees); |
| |
| boolean matchAll = true; |
| while (tw.next()) { |
| boolean matchOne = false; |
| for (int i = 1; i <= cnt; i++) { |
| if (tw.getRawMode(0) == tw.getRawMode(i) |
| && tw.idEqual(0, i)) { |
| matchOne = true; |
| break; |
| } |
| } |
| if (!matchOne) { |
| matchAll = false; |
| break; |
| } |
| } |
| if (matchAll) |
| continue; |
| } |
| |
| Contributor contributor = contributorsByName.get(author.getName()); |
| if (contributor == null) { |
| String id = author.getEmailAddress(); |
| String name = author.getName(); |
| contributor = new Contributor(id, name); |
| contributorsByName.put(name, contributor); |
| } |
| |
| String id = commit.name(); |
| String subj = commit.getShortMessage(); |
| SingleContribution item = new SingleContribution(id, when, subj); |
| |
| if (2 <= cnt) { |
| item.setSize("(merge)"); |
| contributor.add(item); |
| continue; |
| } |
| |
| int addedLines = 0; |
| if (1 == cnt) { |
| final RevCommit parent = commit.getParent(0); |
| tw.setFilter(TreeFilter.ANY_DIFF); |
| tw.setRecursive(true); |
| tw.reset(new RevTree[] { parent.getTree(), commit.getTree() }); |
| while (tw.next()) { |
| if (tw.getFileMode(1).getObjectType() != Constants.OBJ_BLOB) |
| continue; |
| |
| byte[] oldImage; |
| if (tw.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) |
| oldImage = openBlob(0); |
| else |
| oldImage = new byte[0]; |
| |
| EditList edits = MyersDiff.INSTANCE.diff( |
| RawTextComparator.DEFAULT, new RawText(oldImage), |
| new RawText(openBlob(1))); |
| for (Edit e : edits) |
| addedLines += e.getEndB() - e.getBeginB(); |
| } |
| |
| } else { // no parents, everything is an addition |
| tw.setFilter(TreeFilter.ALL); |
| tw.setRecursive(true); |
| tw.reset(commit.getTree()); |
| while (tw.next()) { |
| if (tw.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) { |
| byte[] buf = openBlob(0); |
| for (int ptr = 0; ptr < buf.length;) { |
| ptr = RawParseUtils.nextLF(buf, ptr); |
| addedLines++; |
| } |
| } |
| } |
| } |
| |
| if (addedLines < 0) |
| throw new IOException(MessageFormat.format(IpLogText.get().incorrectlyScanned, commit.name())); |
| if (1 == addedLines) |
| item.setSize("+1 line"); |
| else |
| item.setSize("+" + addedLines + " lines"); |
| contributor.add(item); |
| } |
| } |
| |
| private byte[] openBlob(int side) throws IOException { |
| tw.getObjectId(idbuf, side); |
| return curs.open(idbuf, Constants.OBJ_BLOB).getCachedBytes(); |
| } |
| |
| /** |
| * Dump the scanned information into an XML file. |
| * |
| * @param out |
| * the file stream to write to. The caller is responsible for |
| * closing the stream upon completion. |
| * @throws IOException |
| * the stream cannot be written. |
| */ |
| public void writeTo(OutputStream out) throws IOException { |
| try { |
| TransformerFactory factory = TransformerFactory.newInstance(); |
| Transformer s = factory.newTransformer(); |
| s.setOutputProperty(OutputKeys.ENCODING, characterEncoding); |
| s.setOutputProperty(OutputKeys.METHOD, "xml"); |
| s.setOutputProperty(OutputKeys.INDENT, "yes"); |
| s.setOutputProperty(INDENT, "2"); |
| s.transform(new DOMSource(toXML()), new StreamResult(out)); |
| } catch (ParserConfigurationException e) { |
| IOException err = new IOException(IpLogText.get().cannotSerializeXML); |
| err.initCause(e); |
| throw err; |
| |
| } catch (TransformerConfigurationException e) { |
| IOException err = new IOException(IpLogText.get().cannotSerializeXML); |
| err.initCause(e); |
| throw err; |
| |
| } catch (TransformerException e) { |
| IOException err = new IOException(IpLogText.get().cannotSerializeXML); |
| err.initCause(e); |
| throw err; |
| } |
| } |
| |
| private Document toXML() throws ParserConfigurationException { |
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| factory.setNamespaceAware(true); |
| doc = factory.newDocumentBuilder().newDocument(); |
| |
| Element root = createElement("iplog"); |
| doc.appendChild(root); |
| |
| if (projects.size() == 1) { |
| Project soleProject = projects.values().iterator().next(); |
| root.setAttribute("name", soleProject.getID()); |
| } |
| |
| Set<String> licenses = new TreeSet<String>(); |
| for (Project project : sort(projects, Project.COMPARATOR)) { |
| root.appendChild(createProject(project)); |
| licenses.addAll(project.getLicenses()); |
| } |
| |
| if (!consumedProjects.isEmpty()) |
| appendBlankLine(root); |
| for (Project project : sort(consumedProjects, Project.COMPARATOR)) { |
| root.appendChild(createConsumes(project)); |
| licenses.addAll(project.getLicenses()); |
| } |
| |
| for (RevCommit c : sort(commits)) |
| root.appendChild(createCommitMeta(c)); |
| |
| if (licenses.size() > 1) |
| appendBlankLine(root); |
| for (String name : sort(licenses)) |
| root.appendChild(createLicense(name)); |
| |
| if (!cqs.isEmpty()) |
| appendBlankLine(root); |
| for (CQ cq : sort(cqs, CQ.COMPARATOR)) |
| root.appendChild(createCQ(cq)); |
| |
| if (!committersByEmail.isEmpty()) |
| appendBlankLine(root); |
| for (Committer committer : sort(committersById, Committer.COMPARATOR)) |
| root.appendChild(createCommitter(committer)); |
| |
| for (Contributor c : sort(contributorsByName, Contributor.COMPARATOR)) { |
| appendBlankLine(root); |
| root.appendChild(createContributor(c)); |
| } |
| |
| return doc; |
| } |
| |
| private void appendBlankLine(Element root) { |
| root.appendChild(doc.createTextNode("\n\n ")); |
| } |
| |
| private Element createProject(Project p) { |
| Element project = createElement("project"); |
| populateProjectType(p, project); |
| return project; |
| } |
| |
| private Element createConsumes(Project p) { |
| Element project = createElement("consumes"); |
| populateProjectType(p, project); |
| return project; |
| } |
| |
| private void populateProjectType(Project p, Element project) { |
| required(project, "id", p.getID()); |
| required(project, "name", p.getName()); |
| optional(project, "comments", p.getComments()); |
| optional(project, "version", p.getVersion()); |
| } |
| |
| private Element createCommitMeta(RevCommit c) { |
| Element meta = createElement("meta"); |
| required(meta, "key", "git-commit"); |
| required(meta, "value", c.name()); |
| return meta; |
| } |
| |
| private Element createLicense(String name) { |
| Element license = createElement("license"); |
| required(license, "id", name); |
| optional(license, "description", null); |
| optional(license, "comments", null); |
| return license; |
| } |
| |
| private Element createCQ(CQ cq) { |
| Element r = createElement("cq"); |
| required(r, "id", Long.toString(cq.getID())); |
| required(r, "description", cq.getDescription()); |
| optional(r, "license", cq.getLicense()); |
| optional(r, "use", cq.getUse()); |
| optional(r, "state", mapCQState(cq.getState())); |
| optional(r, "comments", cq.getComments()); |
| return r; |
| } |
| |
| private String mapCQState(String state) { |
| // "approved" CQs shall be listed as "active" in the iplog |
| if (state.equals("approved")) |
| return "active"; |
| return state; |
| } |
| |
| private Element createCommitter(Committer who) { |
| Element r = createElement("committer"); |
| required(r, "id", who.getID()); |
| required(r, "firstName", who.getFirstName()); |
| required(r, "lastName", who.getLastName()); |
| optional(r, "affiliation", who.getAffiliation()); |
| required(r, "active", Boolean.toString(who.isActive())); |
| required(r, "hasCommits", Boolean.toString(who.hasCommits())); |
| optional(r, "comments", who.getComments()); |
| return r; |
| } |
| |
| private Element createContributor(Contributor c) { |
| Element r = createElement("contributor"); |
| required(r, "id", c.getID()); |
| required(r, "name", c.getName()); |
| |
| for (SingleContribution s : sort(c.getContributions(), |
| SingleContribution.COMPARATOR)) |
| r.appendChild(createContribution(s)); |
| |
| return r; |
| } |
| |
| private Element createContribution(SingleContribution s) { |
| Element r = createElement("contribution"); |
| required(r, "id", s.getID()); |
| required(r, "description", s.getSummary()); |
| required(r, "size", s.getSize()); |
| if (reviewUrl != null) |
| optional(r, "url", reviewUrl + s.getID()); |
| return r; |
| } |
| |
| private Element createElement(String name) { |
| return doc.createElementNS(IPLOG_NS, IPLOG_PFX + name); |
| } |
| |
| private void required(Element r, String name, String value) { |
| if (value == null) |
| value = ""; |
| r.setAttribute(name, value); |
| } |
| |
| private void optional(Element r, String name, String value) { |
| if (value != null && value.length() > 0) |
| r.setAttribute(name, value); |
| } |
| |
| private static <T, Q extends Comparator<T>> Iterable<T> sort( |
| Collection<T> objs, Q cmp) { |
| ArrayList<T> sorted = new ArrayList<T>(objs); |
| Collections.sort(sorted, cmp); |
| return sorted; |
| } |
| |
| private static <T, Q extends Comparator<T>> Iterable<T> sort( |
| Map<?, T> objs, Q cmp) { |
| return sort(objs.values(), cmp); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static <T extends Comparable> Iterable<T> sort(Collection<T> objs) { |
| ArrayList<T> sorted = new ArrayList<T>(objs); |
| Collections.sort(sorted); |
| return sorted; |
| } |
| } |