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, 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.BufferedReader; 27 import java.io.File; 28 import java.io.FileReader; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.OutputStream; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.List; 35 import java.util.logging.Level; 36 import java.util.logging.Logger; 37 38 import org.jetbrains.annotations.Nullable; 39 import org.opengrok.indexer.configuration.CommandTimeoutType; 40 import org.opengrok.indexer.configuration.RuntimeEnvironment; 41 import org.opengrok.indexer.logger.LoggerFactory; 42 import org.opengrok.indexer.util.Executor; 43 44 /** 45 * Access to a CVS repository. 46 */ 47 public class CVSRepository extends RCSRepository { 48 49 private static final Logger LOGGER = LoggerFactory.getLogger(CVSRepository.class); 50 51 private static final long serialVersionUID = 1L; 52 /** 53 * The property name used to obtain the client command for repository. 54 */ 55 public static final String CMD_PROPERTY_KEY = "org.opengrok.indexer.history.cvs"; 56 /** 57 * The command to use to access the repository if none was given explicitly. 58 */ 59 public static final String CMD_FALLBACK = "cvs"; 60 CVSRepository()61 public CVSRepository() { 62 /* 63 * This variable is set in the ancestor to TRUE which has a side effect 64 * that this repository is always marked as working even though it does 65 * not have the binary available on the system. 66 * 67 * Setting this to null restores the default behavior (as java 68 * default for reference is null) for this repository - detecting the 69 * binary and act upon that. 70 * 71 * @see #isWorking 72 */ 73 working = null; 74 setType("CVS"); 75 datePatterns = new String[]{ 76 "yyyy-MM-dd hh:mm:ss" 77 }; 78 79 ignoredFiles.add(".cvsignore"); 80 ignoredDirs.add("CVS"); 81 ignoredDirs.add("CVSROOT"); 82 } 83 84 @Override isWorking()85 public boolean isWorking() { 86 if (working == null) { 87 ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); 88 working = checkCmd(RepoCommand, "--version"); 89 } 90 return working; 91 } 92 93 @Override setDirectoryName(File directory)94 public void setDirectoryName(File directory) { 95 super.setDirectoryName(directory); 96 97 if (isWorking()) { 98 File rootFile = new File(getDirectoryName() + File.separatorChar 99 + "CVS" + File.separatorChar + "Root"); 100 String root; 101 102 if (!rootFile.exists()) { 103 LOGGER.log(Level.FINE, "CVS Root file {0} does not exist", rootFile); 104 return; 105 } 106 107 try (BufferedReader input = new BufferedReader(new FileReader(rootFile))) { 108 root = input.readLine(); 109 } catch (IOException e) { 110 LOGGER.log(Level.WARNING, String.format("failed to read %s", rootFile), e); 111 return; 112 } 113 114 if (!root.startsWith("/")) { 115 setRemote(true); 116 } 117 } 118 } 119 120 @Override getRCSFile(File file)121 File getRCSFile(File file) { 122 File cvsFile 123 = RCSHistoryParser.getCVSFile(file.getParent(), file.getName()); 124 if (cvsFile != null && cvsFile.exists()) { 125 return cvsFile; 126 } 127 return null; 128 } 129 130 @Override isRepositoryFor(File file, CommandTimeoutType cmdType)131 public boolean isRepositoryFor(File file, CommandTimeoutType cmdType) { 132 if (file.isDirectory()) { 133 File cvsDir = new File(file, "CVS"); 134 return cvsDir.isDirectory(); 135 } 136 return false; 137 } 138 139 @Override determineBranch(CommandTimeoutType cmdType)140 String determineBranch(CommandTimeoutType cmdType) throws IOException { 141 String branch = null; 142 143 File tagFile = new File(getDirectoryName(), "CVS/Tag"); 144 if (tagFile.isFile()) { 145 try (BufferedReader br = new BufferedReader(new FileReader(tagFile))) { 146 String line = br.readLine(); 147 if (line != null) { 148 branch = line.substring(1); 149 } 150 } catch (IOException ex) { 151 LOGGER.log(Level.WARNING, 152 "Failed to work with CVS/Tag file of {0}", 153 getDirectoryName() + ": " + ex.getClass().toString()); 154 } catch (Exception exp) { 155 LOGGER.log(Level.WARNING, 156 "Failed to get revision tag of {0}", 157 getDirectoryName() + ": " + exp.getClass().toString()); 158 } 159 } 160 161 return branch; 162 } 163 164 /** 165 * Get an executor to be used for retrieving the history log for the named 166 * file. 167 * 168 * @param file The file to retrieve history for 169 * @return An Executor ready to be started 170 */ getHistoryLogExecutor(final File file)171 Executor getHistoryLogExecutor(final File file) throws IOException { 172 String filename = getRepoRelativePath(file); 173 174 List<String> cmd = new ArrayList<>(); 175 ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); 176 cmd.add(RepoCommand); 177 cmd.add("log"); 178 179 if (getBranch() != null && !getBranch().isEmpty()) { 180 // Generate history on this branch and follow up to the origin. 181 cmd.add("-r1.1:" + branch); 182 } else { 183 // Get revisions on this branch only (otherwise the revisions 184 // list produced by the cvs log command would be unsorted). 185 cmd.add("-b"); 186 } 187 188 if (filename.length() > 0) { 189 cmd.add(filename); 190 } 191 192 return new Executor(cmd, new File(getDirectoryName())); 193 } 194 195 @Override getHistoryGet(OutputStream out, String parent, String basename, String rev)196 boolean getHistoryGet(OutputStream out, String parent, String basename, String rev) { 197 String revision = rev; 198 if (rev.indexOf(':') != -1) { 199 revision = rev.substring(0, rev.indexOf(':')); 200 } 201 202 try { 203 ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); 204 String[] argv = {RepoCommand, "up", "-p", "-r", revision, basename}; 205 Executor executor = new Executor(Arrays.asList(argv), new File(parent), 206 RuntimeEnvironment.getInstance().getInteractiveCommandTimeout()); 207 executor.exec(); 208 209 byte[] buffer = new byte[32 * 1024]; 210 try (InputStream in = executor.getOutputStream()) { 211 int len; 212 213 while ((len = in.read(buffer)) != -1) { 214 if (len > 0) { 215 out.write(buffer, 0, len); 216 } 217 } 218 } 219 } catch (Exception exp) { 220 LOGGER.log(Level.WARNING, "Failed to get history", exp); 221 return false; 222 } 223 224 return true; 225 } 226 227 @Override fileHasAnnotation(File file)228 public boolean fileHasAnnotation(File file) { 229 return true; 230 } 231 232 @Override fileHasHistory(File file)233 public boolean fileHasHistory(File file) { 234 // @TODO: Research how to cheaply test if a file in a given 235 // CVS repo has history. If there is a cheap test, then this 236 // code can be refined, boosting performance. 237 return true; 238 } 239 240 @Override getHistory(File file)241 History getHistory(File file) throws HistoryException { 242 return new CVSHistoryParser().parse(file, this); 243 } 244 245 @Override annotate(File file, String revision)246 Annotation annotate(File file, String revision) throws IOException { 247 ArrayList<String> cmd = new ArrayList<>(); 248 ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); 249 cmd.add(RepoCommand); 250 cmd.add("annotate"); 251 if (revision != null) { 252 cmd.add("-r"); 253 cmd.add(revision); 254 } else if (getBranch() != null && !getBranch().isEmpty()) { 255 cmd.add("-r"); 256 cmd.add(getBranch()); 257 } 258 cmd.add(file.getName()); 259 260 Executor exec = new Executor(cmd, file.getParentFile(), 261 RuntimeEnvironment.getInstance().getInteractiveCommandTimeout()); 262 CVSAnnotationParser parser = new CVSAnnotationParser(file.getName()); 263 int status = exec.exec(true, parser); 264 265 if (status != 0) { 266 LOGGER.log(Level.WARNING, 267 "Failed to get annotations for: \"{0}\" Exit code: {1}", 268 new Object[]{file.getAbsolutePath(), String.valueOf(status)}); 269 } 270 271 return parser.getAnnotation(); 272 } 273 274 @Override 275 @Nullable determineParent(CommandTimeoutType cmdType)276 String determineParent(CommandTimeoutType cmdType) throws IOException { 277 File rootFile = new File(getDirectoryName() + File.separator + "CVS" 278 + File.separator + "Root"); 279 String parent = null; 280 281 if (rootFile.isFile()) { 282 try (BufferedReader br = new BufferedReader(new FileReader(rootFile))) { 283 parent = br.readLine(); 284 } catch (IOException ex) { 285 LOGGER.log(Level.WARNING, String.format("Failed to read CVS/Root file %s", rootFile), ex); 286 } 287 } 288 289 return parent; 290 } 291 } 292