xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/history/BitKeeperRepository.java (revision c6f0939b1c668e9f8e1e276424439c3106b3a029)
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) 2017, James Service <jas2701@googlemail.com>
22  * Portions Copyright (c) 2017, 2021, Oracle and/or its affiliates.
23  * Portions Copyright (c) 2018, Chris Fraire <cfraire@me.com>.
24  */
25 package org.opengrok.indexer.history;
26 
27 import java.io.File;
28 import java.io.IOException;
29 import java.io.OutputStream;
30 import java.util.ArrayList;
31 import java.util.List;
32 import java.util.logging.Level;
33 import java.util.logging.Logger;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 
37 import org.opengrok.indexer.configuration.CommandTimeoutType;
38 import org.suigeneris.jrcs.rcs.InvalidVersionNumberException;
39 import org.suigeneris.jrcs.rcs.Version;
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 BitKeeper repository.
46  *
47  * @author James Service  {@literal <jas2701@googlemail.com>}
48  */
49 public class BitKeeperRepository extends Repository {
50 
51     private static final Logger LOGGER = LoggerFactory.getLogger(BitKeeperRepository.class);
52 
53     private static final long serialVersionUID = 1L;
54     /**
55      * The property name used to obtain the client command for this repository.
56      */
57     public static final String CMD_PROPERTY_KEY = "org.opengrok.indexer.history.BitKeeper";
58     /**
59      * The command to use to access the repository if none was given explicitly.
60      */
61     public static final String CMD_FALLBACK = "bk";
62     /**
63      * The output format specification for log commands.
64      */
65     private static final String LOG_DSPEC =
66             "D :DPN:\\t:REV:\\t:D_: :T: GMT:TZ:\\t:USER:$if(:RENAME:){\\t:DPN|PARENT:}\\n$each(:C:){C (:C:)\\n}";
67     /**
68      * The output format specification for tags commands. Versions 7.3 and greater.
69      */
70     private static final String TAG_DSPEC = "D :REV:\\t:D_: :T: GMT:TZ:\\n$each(:TAGS:){T (:TAGS:)\\n}";
71     /**
72      * The output format specification for tags commands. Versions 7.2 and less.
73      */
74     private static final String TAG_DSPEC_OLD = "D :REV:\\t:D_: :T: GMT:TZ:\\n$each(:TAG:){T (:TAG:)\\n}";
75     /**
76      * The output format specification for tags commands. Versions 7.2 and less.
77      */
78     private static final Version NEW_DSPEC_VERSION = new Version(7, 3);
79     /*
80      * Using a dspec not only makes it easier to parse, but also means we don't get tripped up by any system-wide
81      * non-default dspecs on the box we are running on.
82      */
83     /**
84      * Pattern to parse a version number from output of {@code bk --version}.
85      */
86     private static final Pattern VERSION_PATTERN = Pattern.compile("BitKeeper version is .*-(\\d(\\.\\d)*)");
87 
88     /**
89      * The version of the BitKeeper executable. This affects the correct dspec to use for tags.
90      */
91     private Version version = null;
92 
93     /**
94      * Constructor to construct the thing to be constructed.
95      */
BitKeeperRepository()96     public BitKeeperRepository() {
97         type = "BitKeeper";
98         datePatterns = new String[] {"yyyy-MM-dd HH:mm:ss z"};
99 
100         ignoredDirs.add(".bk");
101     }
102 
103     /**
104      * Updates working and version member variables by running {@code bk --version}.
105      */
ensureVersion()106     private void ensureVersion() {
107         if (working == null) {
108             ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
109             final Executor exec = new Executor(new String[] {RepoCommand, "--version" });
110             if (exec.exec(false) == 0) {
111                 working = Boolean.TRUE;
112                 final Matcher matcher = VERSION_PATTERN.matcher(exec.getOutputString());
113                 if (matcher.find()) {
114                     try {
115                         version = new Version(matcher.group(1));
116                     } catch (final InvalidVersionNumberException e) {
117                         assert false : "Failed to parse a version number.";
118                     }
119                 }
120             } else {
121                 working = Boolean.FALSE;
122             }
123             if (version == null) {
124                 version = new Version(0, 0);
125             }
126         }
127     }
128 
129     /**
130      * Returns whether file represents a BitKeeper repository. A BitKeeper repository has a folder named .bk at its
131      * source root.
132      *
133      * @return ret a boolean denoting whether it is or not
134      */
135     @Override
isRepositoryFor(File file, CommandTimeoutType cmdType)136     boolean isRepositoryFor(File file, CommandTimeoutType cmdType) {
137         if (file.isDirectory()) {
138             final File f = new File(file, ".bk");
139             return f.exists() && f.isDirectory();
140         }
141         return false;
142     }
143 
144     /**
145      * Returns whether the BitKeeper command is working.
146      *
147      * @return working a boolean denoting whether it is or not
148      */
149     @Override
isWorking()150     public boolean isWorking() {
151         ensureVersion();
152         return working;
153     }
154 
155     /**
156      * Returns the version of the BitKeeper executable.
157      *
158      * @return version a Version object
159      */
getVersion()160     public Version getVersion() {
161         ensureVersion();
162         return version;
163     }
164 
165     /**
166      * Implementation of abstract method determineBranch. BitKeeper doesn't really have branches as such.
167      *
168      * @return null
169      */
170     @Override
determineBranch(CommandTimeoutType cmdType)171     String determineBranch(CommandTimeoutType cmdType) throws IOException {
172         return null;
173     }
174 
175     /**
176      * Return the first listed pull parent of this repository BitKeeper can have multiple push parents and pul parents.
177      *
178      * @return parent a string denoting the parent, or null.
179      */
180     @Override
determineParent(CommandTimeoutType cmdType)181     String determineParent(CommandTimeoutType cmdType) throws IOException {
182         final File directory = new File(getDirectoryName());
183 
184         final ArrayList<String> argv = new ArrayList<>();
185         ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
186         argv.add(RepoCommand);
187         argv.add("parent");
188         argv.add("-1il");
189 
190         final Executor executor = new Executor(argv, directory,
191                 RuntimeEnvironment.getInstance().getCommandTimeout(cmdType));
192         final int rc = executor.exec(false);
193         final String parent = executor.getOutputString().trim();
194         if (rc == 0) {
195             return parent;
196         } else if (parent.equals("This repository has no pull parent.")) {
197             return null;
198         } else {
199             throw new IOException(executor.getErrorString());
200         }
201     }
202 
203     /* History Stuff */
204     /*
205      * BitKeeper has independent revisions for its individual files like CVS, but also provides changesets, which is an
206      * atomic commit of a group of deltas to files. Changesets have their own revision numbers.
207      *
208      * When constructing a history then, we therefore have a choice of whether to go by file revisions, or changeset
209      * revisions. It seemed like doing it by changeset revisions would be both a) more difficult, and b) not in tune
210      * with how BitKeeper is actually used (although, in the interest of full disclosure, I have only been using it for
211      * a month).
212      */
213 
214     /**
215      * Returns whether BitKeeper has history for its directories.
216      *
217      * @return false
218      */
219     @Override
hasHistoryForDirectories()220     boolean hasHistoryForDirectories() {
221         return false;
222     }
223 
224     /**
225      * Returns whether BitKeeper has history for a file.
226      *
227      * @return ret a boolean denoting whether it does or not
228      */
229     @Override
fileHasHistory(File file)230     public boolean fileHasHistory(File file) {
231         final File absolute = file.getAbsoluteFile();
232         final File directory = absolute.getParentFile();
233         final String basename = absolute.getName();
234 
235         final ArrayList<String> argv = new ArrayList<>();
236         ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
237         argv.add(RepoCommand);
238         argv.add("files");
239         argv.add(basename);
240 
241         final Executor executor = new Executor(argv, directory);
242         if (executor.exec(true) != 0) {
243             LOGGER.log(Level.SEVERE, "Failed to check file: {0}", executor.getErrorString());
244             return false;
245         }
246 
247         return executor.getOutputString().trim().equals(basename);
248     }
249 
250     /**
251      * Construct a History for a file in this repository.
252      *
253      * @param file a file in the repository
254      * @return history a history object
255      */
256     @Override
getHistory(File file)257     History getHistory(File file) throws HistoryException {
258         return getHistory(file, null);
259     }
260 
261     /**
262      * Construct a History for a file in this repository.
263      *
264      * @param file a file in the repository
265      * @param sinceRevision omit history from before, and including, this revision
266      * @return history a history object
267      */
268     @Override
getHistory(File file, String sinceRevision)269     History getHistory(File file, String sinceRevision) throws HistoryException {
270         final File absolute = file.getAbsoluteFile();
271         final File directory = absolute.getParentFile();
272         final String basename = absolute.getName();
273 
274         final ArrayList<String> argv = new ArrayList<>();
275         ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
276         argv.add(RepoCommand);
277         argv.add("log");
278         if (sinceRevision != null) {
279             argv.add("-r" + sinceRevision + "..");
280         }
281         argv.add("-d" + LOG_DSPEC);
282         argv.add(basename);
283 
284         final Executor executor = new Executor(argv, directory);
285         final BitKeeperHistoryParser parser = new BitKeeperHistoryParser(datePatterns[0]);
286         if (executor.exec(true, parser) != 0) {
287             throw new HistoryException(executor.getErrorString());
288         }
289 
290         final RuntimeEnvironment env = RuntimeEnvironment.getInstance();
291         final History history = parser.getHistory();
292 
293         // Assign tags to changesets they represent
294         // We don't need to check if this repository supports tags,
295         // because we know it :-)
296         if (env.isTagsEnabled()) {
297             assignTagsInHistory(history);
298         }
299 
300         return history;
301     }
302 
303     @Override
getHistoryGet(OutputStream out, String parent, String basename, String revision)304     boolean getHistoryGet(OutputStream out, String parent, String basename, String revision) {
305 
306         final File directory = new File(parent).getAbsoluteFile();
307         final List<String> argv = new ArrayList<>();
308         ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
309         argv.add(RepoCommand);
310         argv.add("get");
311         argv.add("-p");
312         if (revision != null) {
313             argv.add("-r" + revision);
314         }
315         argv.add(basename);
316 
317         final Executor executor = new Executor(argv, directory,
318                 RuntimeEnvironment.getInstance().getInteractiveCommandTimeout());
319         if (executor.exec(true) != 0) {
320             LOGGER.log(Level.SEVERE, "Failed to get history: {0}", executor.getErrorString());
321             return false;
322         }
323 
324         try {
325             copyBytes(out::write, executor.getOutputStream());
326             return true;
327         } catch (IOException e) {
328             LOGGER.log(Level.SEVERE, "Failed to get content for {0}",
329                     basename);
330         }
331 
332         return false;
333     }
334 
335     /* Annotation Stuff */
336 
337     /**
338      * Returns whether BitKeeper has annotation for a file. It does if it has history for the file.
339      *
340      * @return ret a boolean denoting whether it does or not
341      */
342     @Override
fileHasAnnotation(File file)343     public boolean fileHasAnnotation(File file) {
344         return fileHasHistory(file);
345     }
346 
347     /**
348      * Annotate the specified file/revision. The options {@code -aur} to @{code bk annotate} specify that Bitkeeper will output the
349      * last user to edit the line, the last revision the line was edited, and then the line itself, each separated by a
350      * hard tab.
351      *
352      * @param file file to annotate
353      * @param revision revision to annotate, or null for latest
354      * @return annotation file annotation
355      */
356     @Override
annotate(File file, String revision)357     public Annotation annotate(File file, String revision) throws IOException {
358         final File absolute = file.getCanonicalFile();
359         final File directory = absolute.getParentFile();
360         final String basename = absolute.getName();
361 
362         final ArrayList<String> argv = new ArrayList<>();
363         ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
364         argv.add(RepoCommand);
365         argv.add("annotate");
366         argv.add("-aur");
367         if (revision != null) {
368             argv.add("-r" + revision);
369         }
370         argv.add(basename);
371 
372         final Executor executor = new Executor(argv, directory,
373                 RuntimeEnvironment.getInstance().getInteractiveCommandTimeout());
374         final BitKeeperAnnotationParser parser = new BitKeeperAnnotationParser(basename);
375         int status = executor.exec(true, parser);
376         if (status != 0) {
377             LOGGER.log(Level.WARNING,
378                     "Failed to get annotations for: \"{0}\" Exit code: {1}",
379                     new Object[]{file.getAbsolutePath(), String.valueOf(status)});
380             throw new IOException(executor.getErrorString());
381         } else {
382             return parser.getAnnotation();
383         }
384     }
385 
386     /* Tag Stuff */
387 
388     /**
389      * Returns whether a set of tags should be constructed up front. BitKeeper tags changesets, not files, so yes.
390      *
391      * @return true
392      */
393     @Override
hasFileBasedTags()394     boolean hasFileBasedTags() {
395         return true;
396     }
397 
398     /**
399      * Returns the version of the BitKeeper executable.
400      *
401      * @return version a Version object
402      */
getTagDspec()403     private String getTagDspec() {
404         if (NEW_DSPEC_VERSION.compareVersions(getVersion()) <= 0) {
405             return TAG_DSPEC;
406         } else {
407             return TAG_DSPEC_OLD;
408         }
409     }
410 
411     /**
412      * Constructs a set of tags up front.
413      *
414      * @param directory the repository directory
415      * @param cmdType command timeout type
416      */
417     @Override
buildTagList(File directory, CommandTimeoutType cmdType)418     public void buildTagList(File directory, CommandTimeoutType cmdType) {
419         final ArrayList<String> argv = new ArrayList<>();
420         argv.add("bk");
421         argv.add("tags");
422         argv.add("-d" + getTagDspec());
423 
424         RuntimeEnvironment env = RuntimeEnvironment.getInstance();
425         final Executor executor = new Executor(argv, directory, env.getCommandTimeout(cmdType));
426         final BitKeeperTagParser parser = new BitKeeperTagParser(datePatterns[0]);
427         int status = executor.exec(true, parser);
428         if (status != 0) {
429             LOGGER.log(Level.WARNING,
430                     "Failed to get tags for: \"{0}\" Exit code: {1}",
431                     new Object[]{directory.getAbsolutePath(), String.valueOf(status)});
432         } else {
433             tagList = parser.getEntries();
434         }
435     }
436 
437     @Override
determineCurrentVersion(CommandTimeoutType cmdType)438     String determineCurrentVersion(CommandTimeoutType cmdType) throws IOException {
439         return null;
440     }
441 }
442