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, 2021, Oracle and/or its affiliates. All rights reserved. 22 * Portions Copyright (c) 2017, Chris Fraire <cfraire@me.com>. 23 */ 24 package org.opengrok.indexer.history; 25 26 import java.io.BufferedReader; 27 import java.io.ByteArrayInputStream; 28 import java.io.File; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.InputStreamReader; 32 import java.nio.charset.StandardCharsets; 33 import java.nio.file.InvalidPathException; 34 import java.text.ParseException; 35 import java.util.ArrayList; 36 import java.util.Date; 37 import java.util.List; 38 import java.util.logging.Level; 39 import java.util.logging.Logger; 40 import org.opengrok.indexer.configuration.RuntimeEnvironment; 41 import org.opengrok.indexer.logger.LoggerFactory; 42 import org.opengrok.indexer.util.Executor; 43 import org.opengrok.indexer.util.ForbiddenSymlinkException; 44 45 /** 46 * Parse a stream of Bazaar log comments. 47 */ 48 class BazaarHistoryParser implements Executor.StreamHandler { 49 50 private static final Logger LOGGER = LoggerFactory.getLogger(BazaarHistoryParser.class); 51 52 private String myDir; 53 private final List<HistoryEntry> entries = new ArrayList<>(); 54 private final BazaarRepository repository; 55 BazaarHistoryParser(BazaarRepository repository)56 BazaarHistoryParser(BazaarRepository repository) { 57 this.repository = repository; 58 myDir = repository.getDirectoryName() + File.separator; 59 } 60 parse(File file, String sinceRevision)61 History parse(File file, String sinceRevision) throws HistoryException { 62 try { 63 Executor executor = repository.getHistoryLogExecutor(file, sinceRevision); 64 int status = executor.exec(true, this); 65 66 if (status != 0) { 67 throw new HistoryException("Failed to get history for: \"" + 68 file.getAbsolutePath() + "\" Exit code: " + status); 69 } 70 } catch (IOException e) { 71 throw new HistoryException("Failed to get history for: \"" + 72 file.getAbsolutePath() + "\"", e); 73 } 74 75 // If a changeset to start from is specified, remove that changeset 76 // from the list, since only the ones following it should be returned. 77 // Also check that the specified changeset was found, otherwise throw 78 // an exception. 79 if (sinceRevision != null) { 80 repository.removeAndVerifyOldestChangeset(entries, sinceRevision); 81 } 82 83 return new History(entries); 84 } 85 86 /** 87 * Process the output from the log command and insert the HistoryEntries 88 * into the history field. 89 * 90 * @param input The output from the process 91 * @throws java.io.IOException If an error occurs while reading the stream 92 */ 93 @Override processStream(InputStream input)94 public void processStream(InputStream input) throws IOException { 95 RuntimeEnvironment env = RuntimeEnvironment.getInstance(); 96 97 BufferedReader in = new BufferedReader(new InputStreamReader(input)); 98 String s; 99 100 HistoryEntry entry = null; 101 int state = 0; 102 while ((s = in.readLine()) != null) { 103 if ("------------------------------------------------------------".equals(s)) { 104 if (entry != null && state > 2) { 105 entries.add(entry); 106 } 107 entry = new HistoryEntry(); 108 entry.setActive(true); 109 state = 0; 110 continue; 111 } 112 113 switch (state) { 114 case 0: 115 // First, go on until revno is found. 116 if (s.startsWith("revno:")) { 117 String[] rev = s.substring("revno:".length()).trim().split(" "); 118 entry.setRevision(rev[0]); 119 ++state; 120 } 121 break; 122 case 1: 123 // Then, look for committer. 124 if (s.startsWith("committer:")) { 125 entry.setAuthor(s.substring("committer:".length()).trim()); 126 ++state; 127 } 128 break; 129 case 2: 130 // And then, look for timestamp. 131 if (s.startsWith("timestamp:")) { 132 try { 133 Date date = repository.parse(s.substring("timestamp:".length()).trim()); 134 entry.setDate(date); 135 } catch (ParseException e) { 136 // 137 // Overriding processStream() thus need to comply with the 138 // set of exceptions it can throw. 139 // 140 throw new IOException("Failed to parse history timestamp:" + s, e); 141 } 142 ++state; 143 } 144 break; 145 case 3: 146 // Expect the commit message to follow immediately after 147 // the timestamp, and that everything up to the list of 148 // modified, added and removed files is part of the commit 149 // message. 150 if (s.startsWith("modified:") || s.startsWith("added:") || s.startsWith("removed:")) { 151 ++state; 152 } else if (s.startsWith(" ")) { 153 // Commit messages returned by bzr log -v are prefixed 154 // with two blanks. 155 entry.appendMessage(s.substring(2)); 156 } 157 break; 158 case 4: 159 // Finally, store the list of modified, added and removed 160 // files. (Except the labels.) 161 if (!(s.startsWith("modified:") || s.startsWith("added:") || s.startsWith("removed:"))) { 162 // The list of files is prefixed with blanks. 163 s = s.trim(); 164 165 int idx = s.indexOf(" => "); 166 if (idx != -1) { 167 s = s.substring(idx + 4); 168 } 169 170 File f = new File(myDir, s); 171 try { 172 String name = env.getPathRelativeToSourceRoot(f); 173 entry.addFile(name.intern()); 174 } catch (ForbiddenSymlinkException e) { 175 LOGGER.log(Level.FINER, e.getMessage()); 176 // ignored 177 } catch (InvalidPathException e) { 178 LOGGER.log(Level.WARNING, e.getMessage()); 179 } 180 } 181 break; 182 default: 183 LOGGER.log(Level.WARNING, "Unknown parser state: {0}", state); 184 break; 185 } 186 } 187 188 if (entry != null && state > 2) { 189 entries.add(entry); 190 } 191 } 192 193 /** 194 * Parse the given string. 195 * 196 * @param buffer The string to be parsed 197 * @return The parsed history 198 * @throws IOException if we fail to parse the buffer 199 */ parse(String buffer)200 History parse(String buffer) throws IOException { 201 myDir = File.separator; 202 processStream(new ByteArrayInputStream(buffer.getBytes(StandardCharsets.UTF_8))); 203 return new History(entries); 204 } 205 } 206