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