xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/history/AccuRevRepository.java (revision 0e4c55544f8ea0a68e8bae37b0e502097e008ec1)
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 &lt;user&gt;)
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