xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/history/SSCMRepository.java (revision 82105c7daea2ce6ea1053c2c4b934e54bf71b9e7)
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) 2013, 2021, Oracle and/or its affiliates. All rights reserved.
22  * Portions Copyright (c) 2018, Chris Fraire <cfraire@me.com>.
23  */
24 package org.opengrok.indexer.history;
25 
26 import java.io.BufferedReader;
27 import java.io.File;
28 import java.io.FileInputStream;
29 import java.io.FileReader;
30 import java.io.IOException;
31 import java.io.OutputStream;
32 import java.io.Reader;
33 import java.nio.file.Files;
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.Properties;
37 import java.util.Scanner;
38 import java.util.logging.Level;
39 import java.util.logging.Logger;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
42 
43 import org.opengrok.indexer.configuration.CommandTimeoutType;
44 import org.opengrok.indexer.configuration.RuntimeEnvironment;
45 import org.opengrok.indexer.logger.LoggerFactory;
46 import org.opengrok.indexer.util.Executor;
47 
48 /**
49  * Access to Surround SCM repository.
50  *
51  */
52 public class SSCMRepository extends Repository {
53 
54     private static final Logger LOGGER = LoggerFactory.getLogger(SSCMRepository.class);
55 
56     private static final long serialVersionUID = 1L;
57 
58     /**
59      * The property name used to obtain the client command for this repository.
60      */
61     public static final String CMD_PROPERTY_KEY = "org.opengrok.indexer.history.sscm";
62     /**
63      * The command to use to access the repository if none was given explicitly.
64      */
65     public static final String CMD_FALLBACK = "sscm";
66 
67     private static final Pattern ANNOTATE_PATTERN = Pattern.compile("^(\\w+)\\s+(\\d+)\\s+.*$");
68 
69     private static final String MYSCMSERVERINFO_FILE = ".MySCMServerInfo";
70     private static final String BRANCH_PROPERTY = "SCMBranch";
71     private static final String REPOSITORY_PROPERTY = "SCMRepository";
72 
SSCMRepository()73     public SSCMRepository() {
74         setType("SSCM");
75         setRemote(true);
76         datePatterns = new String[]{
77             "M/d/yyyy h:mm a"
78         };
79     }
80 
81     @Override
fileHasHistory(File file)82     boolean fileHasHistory(File file) {
83         return true;
84     }
85 
86     @Override
hasHistoryForDirectories()87     boolean hasHistoryForDirectories() {
88         return false;
89     }
90 
91     @Override
isWorking()92     public boolean isWorking() {
93         if (working == null) {
94             ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
95             working = checkCmd(RepoCommand, "version");
96         }
97         return working;
98     }
99 
getProperties(File file)100     private Properties getProperties(File file) {
101         Properties props = new Properties();
102         File propFile;
103         if (file.isDirectory()) {
104             propFile = new File(file, MYSCMSERVERINFO_FILE);
105         } else {
106             propFile = new File(file.getParent(), MYSCMSERVERINFO_FILE);
107         }
108 
109         if (propFile.isFile()) {
110             try (BufferedReader br = new BufferedReader(new FileReader(propFile))) {
111                 props.load(br);
112             } catch (IOException ex) {
113                 LOGGER.log(Level.WARNING,
114                         "Failed to work with {0} file of {1}: {2}", new Object[]{
115                             MYSCMSERVERINFO_FILE,
116                             getDirectoryName(), ex.getClass()});
117             }
118         }
119 
120         return props;
121     }
122 
123     /**
124      * Get an executor to be used for retrieving the history log for the named
125      * file or directory.
126      *
127      * @param file The file or directory to retrieve history for
128      * @param sinceRevision  the oldest changeset to return from the executor, or
129      *                  {@code null} if all changesets should be returned
130      * @return An Executor ready to be started
131      */
getHistoryLogExecutor(final File file, String sinceRevision)132     Executor getHistoryLogExecutor(final File file, String sinceRevision) throws IOException {
133 
134         List<String> argv = new ArrayList<>();
135         ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
136         argv.add(RepoCommand);
137         argv.add("history");
138         if (file.isDirectory()) {
139             argv.add("/");
140         } else {
141             argv.add(file.getName());
142         }
143         if (sinceRevision != null) {
144             try (Scanner scanner = new Scanner(sinceRevision)) {
145                 if (scanner.hasNextInt()) {
146                     argv.add("-v" + (Integer.parseInt(sinceRevision) + 1) + ":" + Integer.MAX_VALUE);
147                 }
148             }
149         }
150         argv.add("-w-");
151 
152         Properties props = getProperties(file);
153         String branch = props.getProperty(BRANCH_PROPERTY);
154         if (branch != null && !branch.isEmpty()) {
155             argv.add("-b" + branch);
156         }
157         String repo = props.getProperty(REPOSITORY_PROPERTY);
158         if (repo != null && !repo.isEmpty()) {
159             argv.add("-p" + repo);
160         }
161 
162         return new Executor(argv, new File(getDirectoryName()), sinceRevision != null);
163     }
164 
165     @Override
getHistory(File file)166     History getHistory(File file) throws HistoryException {
167         return getHistory(file, null);
168     }
169 
170     @Override
getHistory(File file, String sinceRevision)171     History getHistory(File file, String sinceRevision)
172             throws HistoryException {
173         return new SSCMHistoryParser(this).parse(file, sinceRevision);
174     }
175 
176     @Override
getHistoryGet(OutputStream out, String parent, String basename, String rev)177     boolean getHistoryGet(OutputStream out, String parent, String basename, String rev) {
178 
179         File directory = new File(parent);
180 
181         try {
182             final File tmp = Files.createTempDirectory("opengrokSSCMtmp").toFile();
183             String tmpName = tmp.getCanonicalPath();
184 
185             List<String> argv = new ArrayList<>();
186             ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
187             argv.add(RepoCommand);
188             argv.add("get");
189             argv.add(basename);
190             argv.add("-d" + tmpName);
191             Properties props = getProperties(directory);
192             String branch = props.getProperty(BRANCH_PROPERTY);
193             if (branch != null && !branch.isEmpty()) {
194                 argv.add("-b" + branch);
195             }
196             String repo = props.getProperty(REPOSITORY_PROPERTY);
197             if (repo != null && !repo.isEmpty()) {
198                 argv.add("-p" + repo);
199             }
200             if (rev != null) {
201                 argv.add("-v" + rev);
202             }
203             argv.add("-q");
204             argv.add("-tmodify");
205             argv.add("-wreplace");
206             Executor exec = new Executor(argv, directory);
207             int status = exec.exec();
208 
209             if (status != 0) {
210                 LOGGER.log(Level.WARNING,
211                         "Failed get revision {2} for: \"{0}\" Exit code: {1}",
212                         new Object[]{new File(parent, basename).getAbsolutePath(), String.valueOf(status), rev});
213                 return false;
214             }
215 
216             File tmpFile = new File(tmp, basename);
217             try (FileInputStream in = new FileInputStream(tmpFile)) {
218                 copyBytes(out::write, in);
219             } finally {
220                 boolean deleteOnExit = false;
221                 // delete the temporary file on close
222                 if (!tmpFile.delete()) {
223                     // try on JVM exit
224                     deleteOnExit = true;
225                     tmpFile.deleteOnExit();
226                 }
227                 // delete the temporary directory on close
228                 if (deleteOnExit || !tmp.delete()) {
229                     // try on JVM exit
230                     tmp.deleteOnExit();
231                 }
232             }
233             return true;
234         } catch (IOException exception) {
235             LOGGER.log(Level.SEVERE, "Failed to get file", exception);
236         }
237 
238         return false;
239     }
240 
241     @Override
fileHasAnnotation(File file)242     boolean fileHasAnnotation(File file) {
243         File propFile = new File(file.getParent(), MYSCMSERVERINFO_FILE);
244         if (propFile.isFile()) {
245             try (BufferedReader br = new BufferedReader(new FileReader(propFile))) {
246                 // The bottom part is formatted:
247                 //  file name.ext;date;version;crc;
248                 String line;
249                 while ((line = br.readLine()) != null) {
250                     String[] parts = line.split(";");
251                     // Check if the filename matches
252                     if (parts[0].equals(file.getName())) {
253                         // Check if the version field is greater than 1
254                         //  which indicates that annotate will work
255                         if (parts.length > 2) {
256                             try (Scanner scanner = new Scanner(parts[2])) {
257                                 if (scanner.hasNextInt()) {
258                                     return Integer.parseInt(parts[2]) > 1;
259                                 }
260                             }
261                         }
262                         break;
263                     }
264                 }
265             } catch (IOException ex) {
266                 LOGGER.log(Level.WARNING,
267                         "Failed to work with {0} file of {1}: {2}", new Object[]{
268                             MYSCMSERVERINFO_FILE,
269                             getDirectoryName(), ex.getClass()});
270             }
271         }
272         return false;
273     }
274 
275     /**
276      * Annotate the specified file/revision.
277      *
278      * @param file file to annotate
279      * @param revision revision to annotate
280      * @return file annotation
281      */
282     @Override
annotate(File file, String revision)283     Annotation annotate(File file, String revision) throws IOException {
284         ArrayList<String> argv = new ArrayList<>();
285 
286         ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
287         argv.add(RepoCommand);
288         argv.add("annotate");
289         argv.add(file.getName());
290         Properties props = getProperties(file);
291         String branch = props.getProperty(BRANCH_PROPERTY);
292         if (branch != null && !branch.isEmpty()) {
293             argv.add("-b" + branch);
294         }
295         String repo = props.getProperty(REPOSITORY_PROPERTY);
296         if (repo != null && !repo.isEmpty()) {
297             argv.add("-p" + repo);
298         }
299         if (revision != null) {
300             argv.add("-aV:" + revision);
301         }
302         Executor exec = new Executor(argv, file.getParentFile(),
303                 RuntimeEnvironment.getInstance().getInteractiveCommandTimeout());
304         int status = exec.exec();
305 
306         if (status != 0) {
307             LOGGER.log(Level.WARNING,
308                     "Failed annotate for: {2} \"{0}\" Exit code: {1}",
309                     new Object[]{file.getAbsolutePath(), String.valueOf(status), revision});
310         }
311 
312         return parseAnnotation(exec.getOutputReader(), file.getName());
313     }
314 
parseAnnotation(Reader input, String fileName)315     protected Annotation parseAnnotation(Reader input, String fileName)
316             throws IOException {
317         BufferedReader in = new BufferedReader(input);
318         Annotation ret = new Annotation(fileName);
319         String line = "";
320         int lineno = 0;
321         boolean hasStarted = false;
322         Matcher matcher = ANNOTATE_PATTERN.matcher(line);
323         while ((line = in.readLine()) != null) {
324             // For some reason there are empty lines.  Line ends may not be applied correctly.
325             if (line.isEmpty()) {
326                 continue;
327             }
328             ++lineno;
329             matcher.reset(line);
330             if (matcher.find()) {
331                 hasStarted = true;
332                 String rev = matcher.group(2);
333                 String author = matcher.group(1);
334                 ret.addLine(rev, author, true);
335             } else if (hasStarted) {
336                 LOGGER.log(Level.SEVERE,
337                         "Error: did not find annotation in line {0}: [{1}]",
338                         new Object[]{String.valueOf(lineno), line});
339             }
340         }
341         return ret;
342     }
343 
344     @Override
isRepositoryFor(File file, CommandTimeoutType cmdType)345     boolean isRepositoryFor(File file, CommandTimeoutType cmdType) {
346         if (file.isDirectory()) {
347             File f = new File(file, MYSCMSERVERINFO_FILE);
348             return f.exists() && f.isFile();
349         }
350         return false;
351     }
352 
353     @Override
determineParent(CommandTimeoutType cmdType)354     String determineParent(CommandTimeoutType cmdType) throws IOException {
355         return null;
356     }
357 
358     @Override
determineBranch(CommandTimeoutType cmdType)359     String determineBranch(CommandTimeoutType cmdType) {
360         return null;
361     }
362 
363     @Override
determineCurrentVersion(CommandTimeoutType cmdType)364     String determineCurrentVersion(CommandTimeoutType cmdType) throws IOException {
365         return null;
366     }
367 }
368