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