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