001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.scm.provider.git.gitexe.command.blame;
020
021import java.text.DateFormat;
022import java.text.SimpleDateFormat;
023import java.util.ArrayList;
024import java.util.Date;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028
029import org.apache.maven.scm.command.blame.BlameLine;
030import org.apache.maven.scm.util.AbstractConsumer;
031
032/**
033 * Parses the --porcelain format of git-blame
034 *
035 * For more information about the porcelain format, please read the official
036 * <a href="http://www.kernel.org/pub/software/scm/git/docs/git-blame.html#_the_porcelain_format">
037 * GIT blame porcelain format</a> description.
038 *
039 * @author Evgeny Mandrikov
040 * @author Olivier Lamy
041 * @author Mark Struberg
042 * @since 1.4
043 */
044public class GitBlameConsumer extends AbstractConsumer {
045    private static final String GIT_COMMITTER_PREFIX = "committer";
046    private static final String GIT_COMMITTER = GIT_COMMITTER_PREFIX + " ";
047    private static final String GIT_COMMITTER_TIME = GIT_COMMITTER_PREFIX + "-time ";
048    private static final String GIT_AUTHOR = "author ";
049
050    private final List<BlameLine> lines = new ArrayList<>();
051
052    /**
053     * Since the porcelain format only contains the commit information
054     * the first time a specific sha-1 commit appears, we need to store
055     * this information somwehere.
056     *
057     * key: the sha-1 of the commit
058     * value: the {@link BlameLine} containing the full committer/author info
059     */
060    private final Map<String, BlameLine> commitInfo = new HashMap<>();
061
062    private boolean expectRevisionLine = true;
063
064    private String revision = null;
065    private String author = null;
066    private String committer = null;
067    private Date time = null;
068
069    public void consumeLine(String line) {
070        if (line == null) {
071            return;
072        }
073
074        if (expectRevisionLine) {
075            // this is the revision line
076            String parts[] = line.split("\\s", 4);
077
078            if (parts.length >= 1) {
079                revision = parts[0];
080
081                BlameLine oldLine = commitInfo.get(revision);
082
083                if (oldLine != null) {
084                    // restore the commit info
085                    author = oldLine.getAuthor();
086                    committer = oldLine.getCommitter();
087                    time = oldLine.getDate();
088                }
089
090                expectRevisionLine = false;
091            }
092        } else {
093            if (line.startsWith(GIT_AUTHOR)) {
094                author = line.substring(GIT_AUTHOR.length());
095                return;
096            }
097
098            if (line.startsWith(GIT_COMMITTER)) {
099                committer = line.substring(GIT_COMMITTER.length());
100                return;
101            }
102
103            if (line.startsWith(GIT_COMMITTER_TIME)) {
104                String timeStr = line.substring(GIT_COMMITTER_TIME.length());
105                time = new Date(Long.parseLong(timeStr) * 1000L);
106                return;
107            }
108
109            if (line.startsWith("\t")) {
110                // this is the content line.
111                // we actually don't need the content, but this is the right time to add the blame line
112                BlameLine blameLine = new BlameLine(time, revision, author, committer);
113                getLines().add(blameLine);
114
115                // keep commitinfo for this sha-1
116                commitInfo.put(revision, blameLine);
117
118                if (logger.isDebugEnabled()) {
119                    DateFormat df = SimpleDateFormat.getDateTimeInstance();
120                    logger.debug(author + " " + df.format(time));
121                }
122
123                expectRevisionLine = true;
124            }
125        }
126    }
127
128    public List<BlameLine> getLines() {
129        return lines;
130    }
131}