xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/history/SubversionRepository.java (revision 25e5d9bb7222938228ecb6e57eb56cabc9932f2f)
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) 2007, 2021, Oracle and/or its affiliates. All rights reserved.
22  * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
23  */
24 package org.opengrok.indexer.history;
25 
26 import java.io.File;
27 import java.io.IOException;
28 import java.io.OutputStream;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.logging.Level;
32 import java.util.logging.Logger;
33 import javax.xml.XMLConstants;
34 import javax.xml.parsers.DocumentBuilder;
35 import javax.xml.parsers.DocumentBuilderFactory;
36 import javax.xml.parsers.ParserConfigurationException;
37 
38 import org.opengrok.indexer.configuration.CommandTimeoutType;
39 import org.opengrok.indexer.configuration.RuntimeEnvironment;
40 import org.opengrok.indexer.logger.LoggerFactory;
41 import org.opengrok.indexer.util.Executor;
42 import org.w3c.dom.Document;
43 import org.w3c.dom.Node;
44 import org.xml.sax.SAXException;
45 
46 /**
47  * Access to a Subversion repository.
48  *
49  * <b>TODO</b> The current implementation does <b>not</b> support nested
50  * repositories as described in http://svnbook.red-bean.com/en/1.0/ch07s03.html
51  *
52  * @author Trond Norbye
53  */
54 public class SubversionRepository extends Repository {
55 
56     private static final Logger LOGGER = LoggerFactory.getLogger(SubversionRepository.class);
57 
58     private static final long serialVersionUID = 1L;
59 
60     private static final String ENV_SVN_USERNAME = "OPENGROK_SUBVERSION_USERNAME";
61     private static final String ENV_SVN_PASSWORD = "OPENGROK_SUBVERSION_PASSWORD";
62 
63     /**
64      * The property name used to obtain the client command for this repository.
65      */
66     public static final String CMD_PROPERTY_KEY
67             = "org.opengrok.indexer.history.Subversion";
68     /**
69      * The command to use to access the repository if none was given explicitly.
70      */
71     public static final String CMD_FALLBACK = "svn";
72 
73     private static final String XML_OPTION = "--xml";
74     private static final String NON_INTERACT_OPTION = "--non-interactive";
75 
76     private static final String URLattr = "url";
77 
78     protected String reposPath;
79 
SubversionRepository()80     public SubversionRepository() {
81         type = "Subversion";
82         datePatterns = new String[]{
83             "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
84             "yyyy-MM-dd'T'HH:mm:ss.'Z'",
85             "yyyy-MM-dd'T'HH:mm:ss'Z'"
86         };
87 
88         ignoredDirs.add(".svn");
89     }
90 
getValue(Node node)91     private String getValue(Node node) {
92         if (node == null) {
93             return null;
94         }
95         StringBuilder sb = new StringBuilder();
96         Node n = node.getFirstChild();
97         while (n != null) {
98             if (n.getNodeType() == Node.TEXT_NODE) {
99                 sb.append(n.getNodeValue());
100             }
101 
102             n = n.getNextSibling();
103         }
104         return sb.toString();
105     }
106 
107     /**
108      * Get {@code Document} corresponding to the parsed XML output from
109      * {@code svn info} command.
110      * @return document with data from {@code info} or null if the {@code svn}
111      * command failed
112      */
getInfoDocument()113     private Document getInfoDocument() {
114         Document document = null;
115         List<String> cmd = new ArrayList<>();
116 
117         cmd.add(RepoCommand);
118         cmd.add("info");
119         cmd.add(XML_OPTION);
120         File directory = new File(getDirectoryName());
121 
122         Executor executor = new Executor(cmd, directory,
123                 RuntimeEnvironment.getInstance().getInteractiveCommandTimeout());
124         if (executor.exec() == 0) {
125             try {
126                 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
127                 // Prohibit the use of all protocols by external entities:
128                 factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
129                 factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
130 
131                 DocumentBuilder builder = factory.newDocumentBuilder();
132                 document = builder.parse(executor.getOutputStream());
133             } catch (SAXException saxe) {
134                 LOGGER.log(Level.WARNING,
135                         "Parser error parsing svn output", saxe);
136             } catch (ParserConfigurationException pce) {
137                 LOGGER.log(Level.WARNING,
138                         "Parser configuration error parsing svn output", pce);
139             } catch (IOException ioe) {
140                 LOGGER.log(Level.WARNING,
141                         "IOException reading from svn process", ioe);
142             }
143         } else {
144             LOGGER.log(Level.WARNING,
145                             "Failed to execute svn info for [{0}]. Repository disabled.",
146                             getDirectoryName());
147         }
148 
149         return document;
150     }
151 
152     /**
153      * Get value of given tag in 'svn info' document.
154      * @param document document object containing {@code info} contents
155      * @param tagName name of the tag to return value for
156      * @return value string
157      */
getInfoPart(Document document, String tagName)158     private String getInfoPart(Document document, String tagName) {
159         return getValue(document.getElementsByTagName(tagName).item(0));
160     }
161 
162     @Override
setDirectoryName(File directory)163     public void setDirectoryName(File directory) {
164         super.setDirectoryName(directory);
165 
166         if (isWorking()) {
167             // set to true if we manage to find the root directory
168             Boolean rootFound = Boolean.FALSE;
169 
170             Document document = getInfoDocument();
171             if (document != null) {
172                 String url = getInfoPart(document, URLattr);
173                 if (url == null) {
174                     LOGGER.log(Level.WARNING,
175                             "svn info did not contain an URL for [{0}]. Assuming remote repository.",
176                             getDirectoryName());
177                     setRemote(true);
178                 } else {
179                     if (!url.startsWith("file")) {
180                         setRemote(true);
181                     }
182                 }
183 
184                 String root
185                         = getValue(document.getElementsByTagName("root").item(0));
186                 if (url != null && root != null) {
187                     reposPath = url.substring(root.length());
188                     rootFound = Boolean.TRUE;
189                 }
190             }
191             setWorking(rootFound);
192         }
193     }
194 
195     /**
196      * Get an executor to be used for retrieving the history log for the named
197      * file.
198      *
199      * @param file The file to retrieve history for
200      * @param sinceRevision the revision number immediately preceding the first
201      *                      revision we want, or {@code null} to fetch the entire
202      *                      history
203      * @param numEntries number of entries to return. If 0, return all.
204      * @param cmdType command timeout type
205      * @return An Executor ready to be started
206      */
getHistoryLogExecutor(final File file, String sinceRevision, int numEntries, CommandTimeoutType cmdType)207     Executor getHistoryLogExecutor(final File file, String sinceRevision,
208             int numEntries, CommandTimeoutType cmdType) throws IOException {
209 
210         String filename = getRepoRelativePath(file);
211 
212         List<String> cmd = new ArrayList<>();
213         ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
214         cmd.add(RepoCommand);
215         cmd.add("log");
216         cmd.add(NON_INTERACT_OPTION);
217         cmd.addAll(getAuthCommandLineParams());
218         cmd.add(XML_OPTION);
219         cmd.add("-v");
220         if (numEntries > 0) {
221             cmd.add("-l" + numEntries);
222         }
223         if (sinceRevision != null) {
224             cmd.add("-r");
225             // We would like to use sinceRevision+1 here, but if no new
226             // revisions have been added after sinceRevision, it would fail
227             // because there is no such revision as sinceRevision+1. Instead,
228             // fetch the unneeded revision and remove it later.
229             cmd.add("BASE:" + sinceRevision);
230         }
231         if (filename.length() > 0) {
232             cmd.add(escapeFileName(filename));
233         }
234 
235         return new Executor(cmd, new File(getDirectoryName()),
236                     RuntimeEnvironment.getInstance().getCommandTimeout(cmdType));
237     }
238 
239     @Override
getHistoryGet(OutputStream out, String parent, String basename, String rev)240     boolean getHistoryGet(OutputStream out, String parent, String basename, String rev) {
241 
242         File directory = new File(getDirectoryName());
243 
244         String filepath;
245         try {
246             filepath = (new File(parent, basename)).getCanonicalPath();
247         } catch (IOException exp) {
248             LOGGER.log(Level.SEVERE,
249                     "Failed to get canonical path: {0}", exp.getClass().toString());
250             return false;
251         }
252         String filename = filepath.substring(getDirectoryName().length() + 1);
253 
254         List<String> cmd = new ArrayList<>();
255         ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
256         cmd.add(RepoCommand);
257         cmd.add("cat");
258         cmd.add(NON_INTERACT_OPTION);
259         cmd.addAll(getAuthCommandLineParams());
260         cmd.add("-r");
261         cmd.add(rev);
262         cmd.add(escapeFileName(filename));
263 
264         Executor executor = new Executor(cmd, directory,
265                 RuntimeEnvironment.getInstance().getInteractiveCommandTimeout());
266         if (executor.exec() == 0) {
267             try {
268                 copyBytes(out::write, executor.getOutputStream());
269                 return true;
270             } catch (IOException e) {
271                 LOGGER.log(Level.SEVERE, "Failed to get content for {0}",
272                         basename);
273             }
274         }
275 
276         return false;
277     }
278 
279     @Override
hasHistoryForDirectories()280     boolean hasHistoryForDirectories() {
281         return true;
282     }
283 
284     @Override
getHistory(File file)285     History getHistory(File file) throws HistoryException {
286         return getHistory(file, null, 0, CommandTimeoutType.INDEXER);
287     }
288 
289     @Override
getHistory(File file, String sinceRevision)290     History getHistory(File file, String sinceRevision) throws HistoryException {
291         return getHistory(file, sinceRevision, 0, CommandTimeoutType.INDEXER);
292     }
293 
getHistory(File file, String sinceRevision, int numEntries, CommandTimeoutType cmdType)294     private History getHistory(File file, String sinceRevision, int numEntries,
295             CommandTimeoutType cmdType)
296             throws HistoryException {
297         return new SubversionHistoryParser().parse(file, this, sinceRevision,
298                 numEntries, cmdType);
299     }
300 
escapeFileName(String name)301     private String escapeFileName(String name) {
302         if (name.length() == 0) {
303             return name;
304         }
305         return name + "@";
306     }
307 
308     @Override
annotate(File file, String revision)309     public Annotation annotate(File file, String revision) throws IOException {
310         ArrayList<String> argv = new ArrayList<>();
311         ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
312         argv.add(RepoCommand);
313         argv.add("annotate");
314         argv.addAll(getAuthCommandLineParams());
315         argv.add(NON_INTERACT_OPTION);
316         argv.add(XML_OPTION);
317         if (revision != null) {
318             argv.add("-r");
319             argv.add(revision);
320         }
321         argv.add(escapeFileName(file.getName()));
322 
323         Executor executor = new Executor(argv, file.getParentFile(),
324                 RuntimeEnvironment.getInstance().getInteractiveCommandTimeout());
325         SubversionAnnotationParser parser = new SubversionAnnotationParser(file.getName());
326         int status = executor.exec(true, parser);
327         if (status != 0) {
328             LOGGER.log(Level.WARNING,
329                     "Failed to get annotations for: \"{0}\" Exit code: {1}",
330                     new Object[]{file.getAbsolutePath(), String.valueOf(status)});
331             throw new IOException(executor.getErrorString());
332         } else {
333             return parser.getAnnotation();
334         }
335     }
336 
337     @Override
fileHasAnnotation(File file)338     public boolean fileHasAnnotation(File file) {
339         return true;
340     }
341 
342     @Override
fileHasHistory(File file)343     public boolean fileHasHistory(File file) {
344         // @TODO: Research how to cheaply test if a file in a given
345         // SVN repo has history.  If there is a cheap test, then this
346         // code can be refined, boosting performance.
347         return true;
348     }
349 
350     @Override
isRepositoryFor(File file, CommandTimeoutType cmdType)351     boolean isRepositoryFor(File file, CommandTimeoutType cmdType) {
352         if (file.isDirectory()) {
353             File f = new File(file, ".svn");
354             return f.exists() && f.isDirectory();
355         }
356         return false;
357     }
358 
359     @Override
isWorking()360     public boolean isWorking() {
361         if (working == null) {
362             ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
363             working = checkCmd(RepoCommand, "--help");
364         }
365         return working;
366     }
367 
getAuthCommandLineParams()368     private List<String> getAuthCommandLineParams() {
369         List<String> result = new ArrayList<>();
370         String userName = System.getenv(ENV_SVN_USERNAME);
371         String password = System.getenv(ENV_SVN_PASSWORD);
372         if (userName != null && !userName.isEmpty() && password != null
373                 && !password.isEmpty()) {
374             result.add("--username");
375             result.add(userName);
376             result.add("--password");
377             result.add(password);
378         }
379 
380         return result;
381     }
382 
383     @Override
determineParent(CommandTimeoutType cmdType)384     String determineParent(CommandTimeoutType cmdType) {
385         String part = null;
386         Document document = getInfoDocument();
387 
388         if (document != null) {
389             part = getInfoPart(document, URLattr);
390         }
391 
392         return part;
393     }
394 
395     @Override
determineBranch(CommandTimeoutType cmdType)396     String determineBranch(CommandTimeoutType cmdType) throws IOException {
397         String branch = null;
398         Document document = getInfoDocument();
399 
400         if (document != null) {
401             String url = getInfoPart(document, URLattr);
402             int idx;
403             final String branchesStr = "branches/";
404             if (url != null && (idx = url.indexOf(branchesStr)) > 0) {
405                 branch = url.substring(idx + branchesStr.length());
406             }
407         }
408 
409         return branch;
410     }
411 
412     @Override
determineCurrentVersion(CommandTimeoutType cmdType)413     public String determineCurrentVersion(CommandTimeoutType cmdType) throws IOException {
414         String curVersion = null;
415 
416         try {
417             History hist = getHistory(new File(getDirectoryName()), null, 1, cmdType);
418             if (hist != null) {
419                 List<HistoryEntry> hlist = hist.getHistoryEntries();
420                 if (hlist != null && !hlist.isEmpty()) {
421                     HistoryEntry he = hlist.get(0);
422                     curVersion = format(he.getDate()) + " " +
423                             he.getRevision() + " " + he.getAuthor() + " " +
424                             he.getMessage();
425                 }
426             }
427         } catch (HistoryException ex) {
428             LOGGER.log(Level.WARNING, "cannot get current version info for {0}",
429                     getDirectoryName());
430         }
431 
432         return curVersion;
433     }
434 }
435