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