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) 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.IOException; 29 import java.io.OutputStream; 30 import java.nio.file.Files; 31 import java.nio.file.Paths; 32 import java.nio.file.Path; 33 import java.util.ArrayList; 34 import java.util.logging.Level; 35 import java.util.logging.Logger; 36 import java.util.regex.Matcher; 37 import java.util.regex.Pattern; 38 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 an AccuRev repository (here an actual user workspace) 46 * 47 * AccuRev requires that a user logs into their system before it can be used. So 48 * on the machine acting as the OpenGrok server, some valid user has to be 49 * permanently logged in. (accurev login -n <user>) 50 * 51 * It appears that the file path that is given to all these methods is the 52 * complete path to the file which includes the path to the root of the source 53 * location. This means that when using the -P option of OpenGrok to make all 54 * the directories pointed to by the source root to be seen as separate projects 55 * is not all as it would seem. The History GURU always starts building the 56 * history cache using the source root. Well there is NO HISTORY for anything at 57 * the source root because it is not part of an actual AccuRev depot. The 58 * directories within the source root directory represent the work areas of 59 * AccuRev and it is those areas where history can be obtained. This 60 * implementation allows those directories to be symbolic links to the actual 61 * workspaces. 62 * 63 * Other assumptions: 64 * 65 * There is only one associated AccuRev depot associated with workspaces. 66 * 67 * @author Steven Haehn 68 */ 69 public class AccuRevRepository extends Repository { 70 71 private static final Logger LOGGER = LoggerFactory.getLogger(AccuRevRepository.class); 72 73 private static final long serialVersionUID = 1L; 74 /** 75 * The property name used to obtain the client command for this repository. 76 */ 77 public static final String CMD_PROPERTY_KEY = "org.opengrok.indexer.history.AccuRev"; 78 /** 79 * The command to use to access the repository if none was given explicitly. 80 */ 81 public static final String CMD_FALLBACK = "accurev"; 82 83 private static final Pattern DEPOT_PATTERN = Pattern.compile("^Depot:\\s+(\\w+)"); 84 private static final Pattern PARENT_PATTERN = Pattern.compile("^Basis:\\s+(\\w+)"); 85 private static final Pattern WORKSPACE_ROOT_PATTERN = Pattern.compile("Top:\\s+(.+)$"); 86 87 private static final RuntimeEnvironment env = RuntimeEnvironment.getInstance(); 88 89 private String depotName = null; 90 private String parentInfo = null; 91 private String wsRoot = null; 92 private String relRoot = ""; 93 94 /** 95 * This will be /./ on Unix and \.\ on Windows . 96 */ 97 private static final String depotRoot = String.format("%s.%s", File.separator, File.separator); 98 AccuRevRepository()99 public AccuRevRepository() { 100 type = "AccuRev"; 101 datePatterns = new String[]{ 102 "yyyy/MM/dd hh:mm:ss" 103 }; 104 ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK); 105 } 106 107 @Override annotate(File file, String rev)108 public Annotation annotate(File file, String rev) throws IOException { 109 110 ArrayList<String> cmd = new ArrayList<>(); 111 112 // Do not use absolute paths because symbolic links will cause havoc. 113 String path = getDepotRelativePath( file ); 114 115 cmd.add(RepoCommand); 116 cmd.add("annotate"); 117 cmd.add("-fvu"); // version & user 118 119 if (rev != null) { 120 cmd.add("-v"); 121 cmd.add(rev.trim()); 122 } 123 124 cmd.add(path); 125 126 Executor executor = new Executor(cmd, file.getParentFile(), 127 RuntimeEnvironment.getInstance().getInteractiveCommandTimeout()); 128 AccuRevAnnotationParser parser = new AccuRevAnnotationParser(file.getName()); 129 executor.exec(true, parser); 130 131 return parser.getAnnotation(); 132 } 133 134 /** 135 * Get an executor to be used for retrieving the history log for the given 136 * file. (used by AccuRevHistoryParser). 137 * 138 * @param file file for which history is to be retrieved. 139 * @return An Executor ready to be started 140 */ getHistoryLogExecutor(File file)141 Executor getHistoryLogExecutor(File file) throws IOException { 142 143 // Do not use absolute paths because symbolic links will cause havoc. 144 String path = getDepotRelativePath( file ); 145 146 ArrayList<String> cmd = new ArrayList<>(); 147 148 cmd.add(RepoCommand); 149 cmd.add("hist"); 150 151 if (!file.isDirectory()) { 152 cmd.add("-k"); 153 cmd.add("keep"); // get a list of all 'real' file versions 154 } 155 156 cmd.add(path); 157 158 File workingDirectory = file.isDirectory() ? file : file.getParentFile(); 159 160 return new Executor(cmd, workingDirectory); 161 } 162 163 @Override getHistoryGet(OutputStream out, String parent, String basename, String rev)164 boolean getHistoryGet(OutputStream out, String parent, String basename, String rev) { 165 166 ArrayList<String> cmd = new ArrayList<>(); 167 File directory = new File(parent); 168 169 /* 170 * Only way to guarantee getting the contents of a file is to fire off 171 * an AccuRev 'stat'us command to get the element ID number for the 172 * subsequent 'cat' command. (Element ID's are unique for a file, unless 173 * evil twins are present) This is because it is possible that the file 174 * may have been moved to a different place in the depot. The 'stat' 175 * command will produce a line with the format: 176 * 177 * <filePath> <elementID> <virtualVersion> (<realVersion>) (<status>) 178 * 179 * /./myFile e:17715 CP.73_Depot/2 (3220/2) (backed) 180 */ 181 cmd.add(RepoCommand); 182 cmd.add("stat"); 183 cmd.add("-fe"); 184 cmd.add(basename); 185 Executor executor = new Executor(cmd, directory); 186 executor.exec(); 187 188 String elementID = null; 189 190 try (BufferedReader info = new BufferedReader(executor.getOutputReader())) { 191 String line = info.readLine(); 192 String[] statInfo = line.split("\\s+"); 193 elementID = statInfo[1].substring(2); // skip over 'e:' 194 195 } catch (IOException e) { 196 LOGGER.log(Level.SEVERE, 197 "Could not obtain status for {0}", basename); 198 } 199 200 if (elementID != null) { 201 /* 202 * This really gets the contents of the file. 203 */ 204 cmd.clear(); 205 cmd.add(RepoCommand); 206 cmd.add("cat"); 207 cmd.add("-v"); 208 cmd.add(rev.trim()); 209 cmd.add("-e"); 210 cmd.add(elementID); 211 212 executor = new Executor(cmd, directory); 213 executor.exec(); 214 try { 215 copyBytes(out::write, executor.getOutputStream()); 216 return true; 217 } catch (IOException e) { 218 LOGGER.log(Level.SEVERE, "Failed to obtain content for {0}", 219 basename); 220 } 221 } 222 223 return false; 224 } 225 226 @Override fileHasHistory(File file)227 boolean fileHasHistory(File file) { 228 return true; 229 } 230 231 @Override fileHasAnnotation(File file)232 public boolean fileHasAnnotation(File file) { 233 return true; 234 } 235 236 /** 237 * Expecting data of the form: 238 * 239 * Principal: shaehn 240 * Host: waskly 241 * Server name: lean.machine.com 242 * Port: 5050 243 * DB Encoding: Unicode 244 * ACCUREV_BIN: C:\Program Files (x86)\AccuRev\bin 245 * Client time: 2017/08/02 13:30:31 Eastern Daylight Time (1501695031) 246 * Server time: 2017/08/02 13:30:54 Eastern Daylight Time (1501695054) 247 * Depot: bread_and_butter 248 * Workspace/ref: BABS_2_shaehn 249 * Basis: BABS2 250 * Top: C:\Users\shaehn\workspaces\BABS_2 251 * 252 * Output would be similar on Unix boxes, but with '/' appearing 253 * in path names instead of '\'. The 'Basis' (BABS2) is the parent 254 * stream of the user workspace (BABS_2_shaehn). The 'Top' is the 255 * path to the root of the user workspace/repository. The elements 256 * below 'Server time' will be missing when current working directory 257 * is not within a known AccuRev workspace/repository. 258 */ getAccuRevInfo(File wsPath, CommandTimeoutType cmdType)259 private boolean getAccuRevInfo(File wsPath, CommandTimeoutType cmdType) { 260 261 ArrayList<String> cmd = new ArrayList<>(); 262 boolean status = false; 263 Path given = Paths.get(wsPath.toString()); 264 Path realWsPath = null; 265 266 try { 267 // This helps overcome symbolic link issues so that 268 // Accurev will report the desired information. 269 // Otherwise it claims: 270 // "You are not in a directory associated with a workspace" 271 realWsPath = given.toRealPath(); 272 } catch (IOException e) { 273 LOGGER.log(Level.SEVERE, 274 "Could not determine real path for {0}", wsPath); 275 } 276 277 cmd.add(RepoCommand); 278 cmd.add("info"); 279 280 Executor executor = new Executor(cmd, realWsPath.toFile(), env.getCommandTimeout(cmdType)); 281 executor.exec(); 282 283 try (BufferedReader info = new BufferedReader(executor.getOutputReader())) { 284 String line; 285 while ((line = info.readLine()) != null) { 286 287 if (line.contains("not logged in")) { 288 LOGGER.log(Level.SEVERE, "Not logged into AccuRev server"); 289 break; 290 } 291 292 if (line.startsWith("Depot")) { 293 Matcher depotMatch = DEPOT_PATTERN.matcher(line); 294 if (depotMatch.find()) { 295 depotName = depotMatch.group(1); 296 status = true; 297 } 298 } else if (line.startsWith("Basis")) { 299 Matcher parentMatch = PARENT_PATTERN.matcher(line); 300 if (parentMatch.find()) { 301 parentInfo = parentMatch.group(1); 302 } 303 } else if (line.startsWith("Top")) { 304 Matcher workspaceRoot = WORKSPACE_ROOT_PATTERN.matcher(line); 305 if (workspaceRoot.find()) { 306 wsRoot = workspaceRoot.group(1); 307 // Normally, the source root path and the workspace root 308 // are the same, but if the source root has been extended 309 // into the actual AccuRev workspace, there is going to 310 // be a residual relative path needed to construct 311 // depot relative names. 312 // 313 // Rare but possible: 314 // srcRoot: C:\Users\shaehn\workspaces\BABS_2\tools - 315 // wsRoot: C:\Users\shaehn\workspaces\BABS_2 316 // 317 // Gives: \tools for relRoot 318 // 319 // Assuming that the given name is to the root of the 320 // AccuRev workspace, check to see if it happens to be 321 // a symbolic link (which means its path name will differ 322 // from the path known by Accurev) 323 324 if (Files.isSymbolicLink(given)) { 325 LOGGER.log(Level.INFO, "{0} is symbolic link.", wsPath); 326 327 // When we know that the two paths DO NOT point to the 328 // same place (that is, the given path is deeper into 329 // the repository workspace), then need to get the 330 // real path pointed to by the symbolic link so that 331 // the relative root fragment can be extracted. 332 if (!Files.isSameFile(given, Paths.get(wsRoot))) { 333 String linkedTo = Files.readSymbolicLink(given).toRealPath().toString(); 334 if (linkedTo.regionMatches(0, wsRoot, 0, wsRoot.length())) { 335 relRoot = linkedTo.substring(wsRoot.length()); 336 } 337 } 338 } else { 339 // The source root and the workspace root will both 340 // be canonical paths. There will be a non-empty 341 // relative root whenever the source root is longer 342 // than the workspace root known to AccuRev. 343 String srcRoot = env.getSourceRootPath(); 344 if (srcRoot.length() > wsRoot.length()) { 345 relRoot = srcRoot.substring(wsRoot.length()); 346 } 347 } 348 349 if (relRoot.length() > 0) { 350 LOGGER.log(Level.INFO, "Source root relative to workspace root by: {0}", relRoot); 351 } 352 } 353 } 354 } 355 } catch (IOException e) { 356 LOGGER.log(Level.SEVERE, 357 "Could not find AccuRev repository for {0}", wsPath); 358 } 359 360 return status; 361 } 362 363 /** 364 * Check if a given path is associated with an AccuRev workspace 365 * 366 * The AccuRev 'info' command provides a Depot name when in a known 367 * workspace. Otherwise, the Depot name will be missing. 368 * 369 * @param wsPath The presumed path to an AccuRev workspace directory. 370 * @return true if the given path is in the depot, false otherwise 371 */ isInAccuRevDepot(File wsPath, CommandTimeoutType cmdType)372 private boolean isInAccuRevDepot(File wsPath, CommandTimeoutType cmdType) { 373 374 // Once depot name is determined, always assume inside depot. 375 boolean status = (depotName != null); 376 377 if (!status && isWorking()) { 378 status = getAccuRevInfo(wsPath, cmdType); 379 } 380 381 return status; 382 } 383 384 /** 385 * Obtain a depot relative name 386 * for a given repository element path. For example, 387 * when the repository root is "/home/shaehn/workspaces/BABS_2" then 388 * 389 * given file path: /home/shaehn/workspaces/BABS_2/tools 390 * depot relative: /./tools 391 * 392 * Using depot relative names instead of absolute file paths solves 393 * the problems encountered when symbolic links are made for repository 394 * root paths. For example, when the following path 395 * 396 * /home/shaehn/active/src/BABS is a symbolic link to 397 * /home/shaehn/workspaces/BABS_2 then 398 * 399 * given file path: /home/shaehn/active/src/BABS/tools 400 * depot relative: /./tools 401 * 402 * @param file path to repository element 403 * @return a depot relative file element path 404 */ getDepotRelativePath(File file)405 public String getDepotRelativePath(File file) { 406 407 String path = depotRoot; 408 try { 409 // This should turn any symbolically linked paths into the real thing... 410 Path realPath = Paths.get(file.toString()).toRealPath(); 411 // ... so that removing the workspace root will give the depot relative path 412 // (Note realPath should always be starting with wsRoot.) 413 String relativePath = realPath.toString().substring(wsRoot.length()); 414 415 if (relativePath.length() > 0) { 416 path = Paths.get(depotRoot, relativePath).toString(); 417 } 418 419 } catch (IOException e) { 420 LOGGER.log(Level.WARNING, 421 "Unable to determine depot relative path for {0}", 422 file.getPath()); 423 } 424 425 return path; 426 } 427 428 @Override isRepositoryFor(File sourceHome, CommandTimeoutType cmdType)429 boolean isRepositoryFor(File sourceHome, CommandTimeoutType cmdType) { 430 431 if (sourceHome.isDirectory()) { 432 return isInAccuRevDepot(sourceHome, cmdType); 433 } 434 435 return false; 436 } 437 438 @Override isWorking()439 public boolean isWorking() { 440 441 if (working == null) { 442 working = checkCmd(RepoCommand, "info"); 443 } 444 445 return working; 446 } 447 448 @Override hasHistoryForDirectories()449 boolean hasHistoryForDirectories() { 450 return true; 451 } 452 453 @Override getHistory(File file)454 History getHistory(File file) throws HistoryException { 455 return new AccuRevHistoryParser().parse(file, this); 456 } 457 458 @Override determineParent(CommandTimeoutType cmdType)459 String determineParent(CommandTimeoutType cmdType) throws IOException { 460 getAccuRevInfo(new File(getDirectoryName()), cmdType); 461 return parentInfo; 462 } 463 464 @Override determineBranch(CommandTimeoutType cmdType)465 String determineBranch(CommandTimeoutType cmdType) { 466 return null; 467 } 468 469 @Override determineCurrentVersion(CommandTimeoutType cmdType)470 String determineCurrentVersion(CommandTimeoutType cmdType) throws IOException { 471 return null; 472 } 473 } 474