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