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