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