xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/history/BazaarHistoryParser.java (revision c6f0939b1c668e9f8e1e276424439c3106b3a029)
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