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) 2008, 2022, 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.HashMap; 35 import java.util.List; 36 37 import io.github.g00fy2.versioncompare.Version; 38 import org.jetbrains.annotations.VisibleForTesting; 39 import org.opengrok.indexer.util.Executor; 40 41 /** 42 * Parse a stream of CVS log comments. 43 */ 44 class CVSHistoryParser implements Executor.StreamHandler { 45 46 private enum ParseState { 47 NAMES, TAG, REVISION, METADATA, COMMENT 48 } 49 50 private History history; 51 private CVSRepository cvsRepository = new CVSRepository(); 52 53 /** 54 * Process the output from the log command and insert the {@link HistoryEntry} objects created therein 55 * into the {@link #history} field. 56 * 57 * @param input The output from the process 58 * @throws java.io.IOException If an error occurs while reading the stream 59 */ 60 @Override processStream(InputStream input)61 public void processStream(InputStream input) throws IOException { 62 ArrayList<HistoryEntry> entries = new ArrayList<>(); 63 64 BufferedReader in = new BufferedReader(new InputStreamReader(input)); 65 66 history = new History(); 67 HistoryEntry entry = null; 68 HashMap<String, String> tags = null; 69 ParseState state = ParseState.NAMES; 70 String s = in.readLine(); 71 while (s != null) { 72 if (state == ParseState.NAMES && s.startsWith("symbolic names:")) { 73 tags = new HashMap<>(); 74 state = ParseState.TAG; 75 s = in.readLine(); 76 } 77 if (state == ParseState.TAG) { 78 if (s.startsWith("\t")) { 79 parseTag(tags, s); 80 } else { 81 state = ParseState.REVISION; 82 s = in.readLine(); 83 } 84 } 85 if (state == ParseState.REVISION && s.startsWith("revision ")) { 86 if (entry != null) { 87 entries.add(entry); 88 } 89 entry = new HistoryEntry(); 90 entry.setActive(true); 91 String commit = s.substring("revision".length()).trim(); 92 entry.setRevision(commit); 93 if (tags.containsKey(commit)) { 94 history.addTags(entry, tags.get(commit)); 95 } 96 state = ParseState.METADATA; 97 s = in.readLine(); 98 } 99 if (state == ParseState.METADATA && s.startsWith("date: ")) { 100 parseDateAuthor(entry, s); 101 102 state = ParseState.COMMENT; 103 s = in.readLine(); 104 } 105 if (state == ParseState.COMMENT) { 106 if (s.equals("----------------------------")) { 107 state = ParseState.REVISION; 108 } else if (s.equals("=============================================================================")) { 109 state = ParseState.NAMES; 110 } else { 111 if (entry != null) { 112 entry.appendMessage(s); 113 } 114 } 115 } 116 s = in.readLine(); 117 } 118 119 if (entry != null) { 120 entries.add(entry); 121 } 122 123 history.setHistoryEntries(entries); 124 } 125 parseDateAuthor(HistoryEntry entry, String s)126 private void parseDateAuthor(HistoryEntry entry, String s) throws IOException { 127 for (String pair : s.split(";")) { 128 String[] keyVal = pair.split(":", 2); 129 String key = keyVal[0].trim(); 130 String val = keyVal[1].trim(); 131 132 if ("date".equals(key)) { 133 try { 134 val = val.replace('/', '-'); 135 entry.setDate(cvsRepository.parse(val)); 136 } catch (ParseException pe) { 137 // 138 // Overriding processStream() thus need to comply with the 139 // set of exceptions it can throw. 140 // 141 throw new IOException("Failed to parse date: '" + val + "'", pe); 142 } 143 } else if ("author".equals(key)) { 144 entry.setAuthor(val); 145 } 146 } 147 } 148 parseTag(HashMap<String, String> tags, String s)149 private void parseTag(HashMap<String, String> tags, String s) throws IOException { 150 String[] pair = s.trim().split(": "); 151 if (pair.length != 2) { 152 // 153 // Overriding processStream() thus need to comply with the 154 // set of exceptions it can throw. 155 // 156 throw new IOException("Failed to parse tag: '" + s + "'"); 157 } else { 158 if (tags.containsKey(pair[1])) { 159 // Join multiple tags for one revision 160 String oldTag = tags.get(pair[1]); 161 tags.remove(pair[1]); 162 tags.put(pair[1], oldTag + " " + pair[0]); 163 } else { 164 tags.put(pair[1], pair[0]); 165 } 166 } 167 } 168 169 /** 170 * Sort history entries in the object according to semantic ordering of the revision string. 171 * @param history {@link History} object 172 */ 173 @VisibleForTesting sortHistoryEntries(History history)174 static void sortHistoryEntries(History history) { 175 List<HistoryEntry> entries = history.getHistoryEntries(); 176 entries.sort((h1, h2) -> new Version(h2.getRevision()).compareTo(new Version(h1.getRevision()))); 177 history.setHistoryEntries(entries); 178 } 179 180 /** 181 * Parse the history for the specified file. 182 * 183 * @param file the file to parse history for 184 * @param repository Pointer to the SubversionRepository 185 * @return object representing the file's history 186 */ parse(File file, Repository repository)187 History parse(File file, Repository repository) throws HistoryException { 188 cvsRepository = (CVSRepository) repository; 189 try { 190 Executor executor = cvsRepository.getHistoryLogExecutor(file); 191 int status = executor.exec(true, this); 192 193 if (status != 0) { 194 throw new HistoryException("Failed to get history for: \"" + 195 file.getAbsolutePath() + "\" Exit code: " + status); 196 } 197 } catch (IOException e) { 198 throw new HistoryException("Failed to get history for: \"" + 199 file.getAbsolutePath() + "\"", e); 200 } 201 202 // In case there is a branch, the log entries can be returned in 203 // unsorted order (as a result of using '-r1.1:branch' for 'cvs log') 204 // so they need to be sorted according to revision. 205 if (cvsRepository.getBranch() != null && !cvsRepository.getBranch().isEmpty()) { 206 sortHistoryEntries(history); 207 } 208 209 return history; 210 } 211 212 /** 213 * Parse the given string. Used for testing. 214 * 215 * @param buffer The string to be parsed 216 * @return The parsed history 217 * @throws IOException if we fail to parse the buffer 218 */ 219 @VisibleForTesting parse(String buffer)220 History parse(String buffer) throws IOException { 221 processStream(new ByteArrayInputStream(buffer.getBytes(StandardCharsets.UTF_8))); 222 return history; 223 } 224 } 225