xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/history/SSCMHistoryParser.java (revision 8ae5e26240a7eaee82e6c91c783733247f45601a)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Copyright (c) 2013, 2020, Oracle and/or its affiliates. All rights reserved.
22  */
23 package org.opengrok.indexer.history;
24 
25 import java.io.BufferedReader;
26 import java.io.ByteArrayInputStream;
27 import java.io.File;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.InputStreamReader;
31 import java.nio.charset.StandardCharsets;
32 import java.text.ParseException;
33 import java.util.ArrayList;
34 import java.util.logging.Level;
35 import java.util.logging.Logger;
36 import java.util.regex.Matcher;
37 import java.util.regex.Pattern;
38 import org.opengrok.indexer.logger.LoggerFactory;
39 import org.opengrok.indexer.util.Executor;
40 
41 /**
42  *
43  * @author michailf
44  */
45 public class SSCMHistoryParser implements Executor.StreamHandler {
46 
47     private static final Logger LOGGER = LoggerFactory.getLogger(SSCMHistoryParser.class);
48 
49     private final SSCMRepository repository;
50 
SSCMHistoryParser(SSCMRepository repository)51     SSCMHistoryParser(SSCMRepository repository) {
52         this.repository = repository;
53     }
54 
55     private static final String ACTION_PATTERN = "[a-z][a-z ]+";
56     private static final String USER_PATTERN = "\\w+";
57     private static final String VERSION_PATTERN = "\\d+";
58     private static final String TIME_PATTERN = "\\d{1,2}/\\d{1,2}/\\d{4} \\d{1,2}:\\d{2} [AP]M";
59     private static final String COMMENT_START_PATTERN = "Comments - ";
60     // ^([a-z][a-z ]+)(?:\[(.*?)\])?\s+(\w+)\s+(\d+)\s+(\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{2} [AP]M)$\s*(?:Comments - )?
61     private static final Pattern HISTORY_PATTERN = Pattern.compile("^(" + ACTION_PATTERN +
62                     ")(?:\\[(.*?)\\])?\\s+(" + USER_PATTERN + ")\\s+(" + VERSION_PATTERN + ")\\s+(" + TIME_PATTERN +
63                     ")$\\s*(?:" + COMMENT_START_PATTERN + ")?",
64             Pattern.MULTILINE);
65 
66     private static final String NEWLINE = System.getProperty("line.separator");
67 
68     private History history;
69 
70     /**
71      * Process the output from the history command and insert the HistoryEntries
72      * into the history field.
73      *
74      * @param input The output from the process
75      * @throws java.io.IOException If an error occurs while reading the stream
76      */
77     @Override
processStream(InputStream input)78     public void processStream(InputStream input) throws IOException {
79         history = new History();
80 
81         BufferedReader in = new BufferedReader(new InputStreamReader(input));
82         StringBuilder total = new StringBuilder(input.available());
83         String line;
84         while ((line = in.readLine()) != null) {
85             total.append(line).append(NEWLINE);
86         }
87 
88         ArrayList<HistoryEntry> entries = new ArrayList<>();
89         HistoryEntry entry = null;
90         int prevEntryEnd = 0;
91 
92         long revisionCounter = 0;
93         Matcher matcher = HISTORY_PATTERN.matcher(total);
94         while (matcher.find()) {
95             if (entry != null) {
96                 if (matcher.start() != prevEntryEnd) {
97                     // Get the comment and reduce all double new lines to single
98                     //  add a space as well for better formatting in RSS feeds.
99                     entry.appendMessage(total.substring(prevEntryEnd, matcher.start()).replaceAll("(\\r?\\n){2}", " $1").trim());
100                 }
101                 entries.add(0, entry);
102                 entry = null;
103             }
104             String revision = matcher.group(4);
105             String author = matcher.group(3);
106             String context = matcher.group(2);
107             String date = matcher.group(5);
108 
109             long currentRevision = 0;
110             try {
111                 currentRevision = Long.parseLong(revision);
112             } catch (NumberFormatException ex) {
113                 LOGGER.log(Level.WARNING, "Failed to parse revision: '" + revision + "'", ex);
114             }
115             // We're only interested in history entries that change file content
116             if (revisionCounter < currentRevision) {
117                 revisionCounter = currentRevision;
118 
119                 entry = new HistoryEntry();
120                 // Add context of action to message.  Helps when branch name is used
121                 //   as indicator of why promote was made.
122                 if (context != null) {
123                     entry.appendMessage("[" + context + "] ");
124                 }
125                 entry.setAuthor(author);
126                 entry.setRevision(revision);
127                 try {
128                     entry.setDate(repository.parse(date));
129                 } catch (ParseException ex) {
130                     LOGGER.log(Level.WARNING, "Failed to parse date: '" + date + "'", ex);
131                 }
132                 entry.setActive(true);
133             }
134             prevEntryEnd = matcher.end();
135         }
136 
137         if (entry != null) {
138             if (total.length() != prevEntryEnd) {
139                 // Get the comment and reduce all double new lines to single
140                 //  add a space as well for better formatting in RSS feeds.
141                 entry.appendMessage(total.substring(prevEntryEnd).replaceAll("(\\r?\\n){2}", " $1").trim());
142             }
143             entries.add(0, entry);
144         }
145         history.setHistoryEntries(entries);
146     }
147 
parse(File file, String sinceRevision)148     History parse(File file, String sinceRevision) throws HistoryException {
149         try {
150             Executor executor = repository.getHistoryLogExecutor(file, sinceRevision);
151             int status = executor.exec(true, this);
152 
153             if (status != 0) {
154                 throw new HistoryException("Failed to get history for: \""
155                         + file.getAbsolutePath() + "\" Exit code: " + status);
156             }
157         } catch (IOException e) {
158             throw new HistoryException("Failed to get history for: \""
159                     + file.getAbsolutePath() + "\"", e);
160         }
161 
162         return history;
163     }
164 
165     /**
166      * Parse the given string.
167      *
168      * @param buffer The string to be parsed
169      * @return The parsed history
170      * @throws IOException if we fail to parse the buffer
171      */
parse(String buffer)172     History parse(String buffer) throws IOException {
173         processStream(new ByteArrayInputStream(buffer.getBytes(StandardCharsets.UTF_8)));
174         return history;
175     }
176 }
177