• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

oracle / opengrok / #3691

09 Nov 2023 04:15PM UTC coverage: 74.721% (+8.6%) from 66.08%
#3691

push

web-flow
avoid annotations for binary files (#4476)

fixes #4473

6 of 13 new or added lines in 4 files covered. (46.15%)

258 existing lines in 28 files now uncovered.

43797 of 58614 relevant lines covered (74.72%)

0.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.29
/opengrok-indexer/src/main/java/org/opengrok/indexer/history/HistoryGuru.java
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) 2005, 2023, Oracle and/or its affiliates. All rights reserved.
22
 * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
23
 */
24
package org.opengrok.indexer.history;
25

26
import java.io.File;
27
import java.io.IOException;
28
import java.io.InputStream;
29
import java.lang.reflect.InvocationTargetException;
30
import java.nio.file.Path;
31
import java.util.ArrayList;
32
import java.util.Collection;
33
import java.util.Collections;
34
import java.util.HashMap;
35
import java.util.List;
36
import java.util.Map;
37
import java.util.Objects;
38
import java.util.Optional;
39
import java.util.Set;
40
import java.util.concurrent.ConcurrentHashMap;
41
import java.util.concurrent.CountDownLatch;
42
import java.util.concurrent.ExecutionException;
43
import java.util.concurrent.ExecutorService;
44
import java.util.concurrent.Executors;
45
import java.util.concurrent.Future;
46
import java.util.logging.Level;
47
import java.util.logging.Logger;
48
import java.util.stream.Collectors;
49

50
import org.jetbrains.annotations.Nullable;
51
import org.jetbrains.annotations.VisibleForTesting;
52
import org.opengrok.indexer.analysis.AbstractAnalyzer;
53
import org.opengrok.indexer.analysis.AnalyzerGuru;
54
import org.opengrok.indexer.configuration.CommandTimeoutType;
55
import org.opengrok.indexer.configuration.Configuration;
56
import org.opengrok.indexer.configuration.Configuration.RemoteSCM;
57
import org.opengrok.indexer.configuration.OpenGrokThreadFactory;
58
import org.opengrok.indexer.configuration.PathAccepter;
59
import org.opengrok.indexer.configuration.RuntimeEnvironment;
60
import org.opengrok.indexer.logger.LoggerFactory;
61
import org.opengrok.indexer.search.DirectoryEntry;
62
import org.opengrok.indexer.util.ForbiddenSymlinkException;
63
import org.opengrok.indexer.util.PathUtils;
64
import org.opengrok.indexer.util.Progress;
65
import org.opengrok.indexer.util.Statistics;
66

67
/**
68
 * The HistoryGuru is used to implement an transparent layer to the various
69
 * source control systems.
70
 *
71
 * @author Chandan
72
 */
73
public final class HistoryGuru {
74

75
    private static final Logger LOGGER = LoggerFactory.getLogger(HistoryGuru.class);
1✔
76

77
    /**
78
     * The one and only instance of the HistoryGuru.
79
     */
80
    private static final HistoryGuru INSTANCE = new HistoryGuru();
1✔
81

82
    private final RuntimeEnvironment env;
83

84
    /**
85
     * The history cache to use.
86
     */
87
    private final HistoryCache historyCache;
88

89
    /**
90
     * The annotation cache to use.
91
     */
92
    private final AnnotationCache annotationCache;
93

94
    /**
95
     * Map of repositories, with {@code DirectoryName} as key.
96
     */
97
    private final Map<String, Repository> repositories = new ConcurrentHashMap<>();
1✔
98

99
    /**
100
     * Set of repository roots (using ConcurrentHashMap but a throwaway value)
101
     * with parent of {@code DirectoryName} as key.
102
     */
103
    private final Map<String, String> repositoryRoots = new ConcurrentHashMap<>();
1✔
104

105
    /**
106
     * Interface to perform repository lookup for a given file path and HistoryGuru state.
107
     */
108
    private final RepositoryLookup repositoryLookup;
109

110
    private boolean historyIndexDone = false;
1✔
111

112
    public void setHistoryIndexDone() {
113
        historyIndexDone = true;
1✔
114
    }
1✔
115

116
    public boolean isHistoryIndexDone() {
117
        return historyIndexDone;
×
118
    }
119

120
    /**
121
     * Creates a new instance of HistoryGuru. Initialize cache objects.
122
     */
123
    private HistoryGuru() {
1✔
124
        env = RuntimeEnvironment.getInstance();
1✔
125

126
        this.historyCache = initializeHistoryCache();
1✔
127
        this.annotationCache = initializeAnnotationCache();
1✔
128

129
        repositoryLookup = RepositoryLookup.cached();
1✔
130
    }
1✔
131

132
    /**
133
     * Set annotation cache to its default implementation.
134
     * @return {@link AnnotationCache} instance or {@code null} on error
135
     */
136
    @VisibleForTesting
137
    @Nullable
138
    static AnnotationCache initializeAnnotationCache() {
139
        AnnotationCache annotationCacheResult;
140

141
        // The annotation cache is initialized regardless the value of global setting
142
        // RuntimeEnvironment.getInstance().isAnnotationCacheEnabled() to allow for per-project/repository override.
143
        annotationCacheResult = new FileAnnotationCache();
1✔
144

145
        try {
146
            annotationCacheResult.initialize();
1✔
147
        } catch (Exception e) {
×
148
            LOGGER.log(Level.WARNING, "Failed to initialize the annotation cache", e);
×
149
            // Failed to initialize, run without annotation cache.
150
            annotationCacheResult = null;
×
151
        }
1✔
152

153
        return annotationCacheResult;
1✔
154
    }
155

156
    /**
157
     * Set history cache to its default implementation.
158
     * @return {@link HistoryCache} instance
159
     */
160
    private HistoryCache initializeHistoryCache() {
161
        HistoryCache historyCacheResult = null;
1✔
162
        if (env.useHistoryCache()) {
1✔
163
            historyCacheResult = new FileHistoryCache();
1✔
164

165
            try {
166
                historyCacheResult.initialize();
1✔
167
            } catch (CacheException he) {
×
168
                LOGGER.log(Level.WARNING, "Failed to initialize the history cache", he);
×
169
                // Failed to initialize, run without a history cache.
170
                historyCacheResult = null;
×
171
            }
1✔
172
        }
173
        return historyCacheResult;
1✔
174
    }
175

176
    /**
177
     * Get the one and only instance of the HistoryGuru.
178
     *
179
     * @return the one and only HistoryGuru instance
180
     */
181
    public static HistoryGuru getInstance() {
182
        return INSTANCE;
1✔
183
    }
184

185
    /**
186
     * Return whether cache should be used for the history log.
187
     *
188
     * @return {@code true} if the history cache has been enabled and initialized, {@code false} otherwise
189
     */
190
    private boolean useHistoryCache() {
191
        return historyCache != null;
1✔
192
    }
193

194
    /**
195
     * Return whether cache should be used for annotations.
196
     *
197
     * @return {@code true} if the annotation cache has been enabled and
198
     * initialized, {@code false} otherwise
199
     */
200
    private boolean useAnnotationCache() {
201
        return annotationCache != null;
1✔
202
    }
203

204
    /**
205
     * Get a string with information about the history cache.
206
     *
207
     * @return a free form text string describing the history cache instance
208
     */
209
    public String getHistoryCacheInfo() {
210
        return historyCache == null ? "No history cache" : historyCache.getInfo();
1✔
211
    }
212

213
    /**
214
     * Get a string with information about the annotation cache.
215
     *
216
     * @return a free form text string describing the history cache instance
217
     */
218
    public String getAnnotationCacheInfo() {
219
        return annotationCache == null ? "No annotation cache" : annotationCache.getInfo();
1✔
220
    }
221

222
    /**
223
     * Fetch the annotation for given file from the cache or using the repository method.
224
     * @param file file to get the annotation for
225
     * @param rev revision string, specifies revision for which to get the annotation
226
     * @param fallback whether to fall back to repository method
227
     * @return {@link Annotation} instance or <code>null</code>
228
     * @throws IOException on error
229
     */
230
    @Nullable
231
    private Annotation getAnnotation(File file, @Nullable String rev, boolean fallback) throws IOException {
232
        Annotation annotation;
233

234
        Repository repository = getRepository(file);
1✔
235
        if (annotationCache != null && repository != null && repository.isAnnotationCacheEnabled()) {
1✔
236
            try {
237
                annotation = annotationCache.get(file, rev);
1✔
238
                if (annotation != null) {
1✔
239
                    return annotation;
1✔
240
                }
241
            } catch (CacheException e) {
1✔
242
                LOGGER.log(e.getLevel(), e.toString());
1✔
243
            }
1✔
244
        }
245

246
        if (!fallback) {
1✔
247
            LOGGER.log(Level.FINEST, "not falling back to repository to get annotation for ''{0}''", file);
1✔
248
            return null;
1✔
249
        }
250

251
        // Fall back to repository based annotation.
252
        // It might be possible to store the annotation to the annotation cache here, needs further thought.
253
        annotation = getAnnotationFromRepository(file, rev);
1✔
254
        if (annotation != null) {
1✔
255
            annotation.setRevision(LatestRevisionUtil.getLatestRevision(file));
1✔
256
        }
257

258
        return annotation;
1✔
259
    }
260

261
    /**
262
     * Annotate given file using repository method. Makes sure that the resulting annotation has the revision set.
263
     * @param file file object to generate the annotation for
264
     * @param rev revision to get the annotation for or {@code null} for latest revision of given file
265
     * @return annotation object or {@code null}
266
     * @throws IOException on error when getting the annotation
267
     */
268
    @Nullable
269
    private Annotation getAnnotationFromRepository(File file, @Nullable String rev) throws IOException {
270
        if (!env.getPathAccepter().accept(file)) {
1✔
271
            LOGGER.log(Level.FINEST, "file ''{0}'' not accepted for annotation", file);
×
272
            return null;
×
273
        }
274

275
        Repository repository = getRepository(file);
1✔
276
        if (repository != null && hasAnnotation(file)) {
1✔
277
            return repository.annotate(file, rev);
1✔
278
        }
279

280
        return null;
1✔
281
    }
282

283
    /**
284
     * Wrapper for {@link #annotate(File, String, boolean)}.
285
     * @param file the file to annotate
286
     * @param rev the revision to annotate (<code>null</code> means current revision)
287
     * @return file annotation, or <code>null</code>
288
     * @throws IOException on error
289
     */
290
    @Nullable
291
    public Annotation annotate(File file, @Nullable String rev) throws IOException {
292
        return annotate(file, rev, true);
1✔
293
    }
294

295
    /**
296
     * Annotate the specified revision of a file.
297
     *
298
     * @param file the file to annotate
299
     * @param rev the revision to annotate (<code>null</code> means current revision)
300
     * @param fallback whether to fall back to repository method
301
     * @return file annotation, or <code>null</code>
302
     * @throws IOException if I/O exception occurs
303
     */
304
    @Nullable
305
    public Annotation annotate(File file, @Nullable String rev, boolean fallback) throws IOException {
306
        Annotation annotation = getAnnotation(file, rev, fallback);
1✔
307
        if (annotation == null) {
1✔
308
            LOGGER.log(Level.FINEST, "no annotation for ''{0}''", file);
1✔
309
            return null;
1✔
310
        }
311

312
        Repository repo = getRepository(file);
1✔
313
        if (repo == null) {
1✔
314
            LOGGER.log(Level.FINER, "cannot get repository for file ''{0}'' to complete annotation", file);
×
315
            return null;
×
316
        }
317

318
        Statistics statistics = new Statistics();
1✔
319
        completeAnnotationWithHistory(file, annotation, repo);
1✔
320
        statistics.report(LOGGER, Level.FINEST, String.format("completed annotation with history for '%s'", file));
1✔
321

322
        return annotation;
1✔
323
    }
324

325
    private void completeAnnotationWithHistory(File file, Annotation annotation, Repository repo) {
326
        History history = null;
1✔
327
        try {
328
            history = getHistory(file);
1✔
329
        } catch (HistoryException ex) {
×
330
            LOGGER.log(Level.WARNING, "Cannot get messages for tooltip: ", ex);
×
331
        }
1✔
332

333
        if (history != null) {
1✔
334
            Set<String> revs = annotation.getRevisions();
1✔
335
            int revsMatched = 0;
1✔
336
            // !!! cannot do this because of not matching rev ids (keys)
337
            // first is the most recent one, so we need the position of "rev"
338
            // until the end of the list
339
            //if (hent.indexOf(rev)>0) {
340
            //     hent = hent.subList(hent.indexOf(rev), hent.size());
341
            //}
342
            for (HistoryEntry he : history.getHistoryEntries()) {
1✔
343
                String hist_rev = he.getRevision();
1✔
344
                String short_rev = repo.getRevisionForAnnotate(hist_rev);
1✔
345
                if (revs.contains(short_rev)) {
1✔
346
                    annotation.addDesc(short_rev, he.getDescription());
1✔
347
                    // History entries are coming from recent to older,
348
                    // file version should be from oldest to newer.
349
                    annotation.addFileVersion(short_rev, revs.size() - revsMatched);
1✔
350
                    revsMatched++;
1✔
351
                }
352
            }
1✔
353
        }
354
    }
1✔
355

356
    /**
357
     * Get the history for the specified file.
358
     *
359
     * @param file the file to get the history for
360
     * @return history for the file
361
     * @throws HistoryException on error when accessing the history
362
     */
363
    public History getHistory(File file) throws HistoryException {
364
        return getHistory(file, true, false);
1✔
365
    }
366

367
    public History getHistory(File file, boolean withFiles) throws HistoryException {
368
        return getHistory(file, withFiles, false);
1✔
369
    }
370

371
    /**
372
     * Get history for the specified file (called from the web app).
373
     *
374
     * @param file the file to get the history for
375
     * @return history for the file
376
     * @throws HistoryException on error when accessing the history
377
     */
378
    public History getHistoryUI(File file) throws HistoryException {
379
        return getHistory(file, true, true);
×
380
    }
381

382
    /**
383
     * The idea is that some repositories require reaching out to remote server whenever
384
     * a history operation is done. Sometimes this is unwanted and this method decides that.
385
     * This should be consulted before the actual repository operation, i.e. not when fetching
386
     * history from a cache since that is inherently local operation.
387
     * @param repo repository
388
     * @param file file to decide the operation for
389
     * @param ui whether coming from UI
390
     * @return whether to perform the history operation
391
     */
392
    boolean isRepoHistoryEligible(Repository repo, File file, boolean ui) {
393
        RemoteSCM rscm = env.getRemoteScmSupported();
1✔
394
        boolean doRemote = (ui && (rscm == RemoteSCM.UIONLY))
1✔
395
                || (rscm == RemoteSCM.ON)
396
                || (ui || ((rscm == RemoteSCM.DIRBASED) && (repo != null) && repo.hasHistoryForDirectories()));
1✔
397

398
        return (repo != null && repo.isHistoryEnabled() && repo.isWorking() && repo.fileHasHistory(file)
1✔
399
                && (!repo.isRemote() || doRemote));
1✔
400
    }
401

402
    @Nullable
403
    private History getHistoryFromCache(File file, Repository repository, boolean withFiles)
404
            throws CacheException {
405

406
        if (useHistoryCache() && historyCache.supportsRepository(repository)) {
1✔
407
            return historyCache.get(file, repository, withFiles);
1✔
408
        }
409

410
        return null;
×
411
    }
412

413
    @Nullable
414
    private HistoryEntry getLastHistoryEntryFromCache(File file, Repository repository) throws CacheException {
415

416
        if (useHistoryCache() && historyCache.supportsRepository(repository)) {
1✔
417
            return historyCache.getLastHistoryEntry(file);
1✔
418
        }
419

420
        return null;
×
421
    }
422

423
    /**
424
     * Get last {@link HistoryEntry} for a file. First, try to retrieve it from the cache.
425
     * If that fails, fallback to the repository method.
426
     * @param file file to get the history entry for
427
     * @param ui is the request coming from the UI
428
     * @param fallback whether to perform fallback to repository method if cache retrieval fails
429
     * @return last (newest) history entry for given file or {@code null}
430
     * @throws HistoryException if history retrieval failed
431
     */
432
    @Nullable
433
    public HistoryEntry getLastHistoryEntry(File file, boolean ui, boolean fallback) throws HistoryException {
434
        Statistics statistics = new Statistics();
1✔
435
        LOGGER.log(Level.FINEST, "started retrieval of last history entry for ''{0}''", file);
1✔
436
        final File dir = file.isDirectory() ? file : file.getParentFile();
1✔
437
        final Repository repository = getRepository(dir);
1✔
438

439
        try {
440
            HistoryEntry lastHistoryEntry = getLastHistoryEntryFromCache(file, repository);
1✔
441
            if (lastHistoryEntry != null) {
1✔
442
                statistics.report(LOGGER, Level.FINEST,
1✔
443
                        String.format("got latest history entry %s for ''%s'' from history cache",
1✔
444
                        lastHistoryEntry, file), "history.entry.latest");
445
                return lastHistoryEntry;
1✔
446
            }
447
        } catch (CacheException e) {
1✔
448
            LOGGER.log(Level.FINER,
1✔
449
                    String.format("failed to retrieve last history entry for ''%s'' in %s using history cache",
1✔
450
                            file, repository),
451
                            e.getMessage());
1✔
452
        }
1✔
453

454
        if (!fallback) {
1✔
455
            statistics.report(LOGGER, Level.FINEST,
×
456
                    String.format("cannot retrieve the last history entry for ''%s'' in %s because fallback to" +
×
457
                                    "repository method is disabled",
458
                            file, repository), "history.entry.latest");
459
            return null;
×
460
        }
461

462
        if (!isRepoHistoryEligible(repository, file, ui)) {
1✔
463
            statistics.report(LOGGER, Level.FINEST,
1✔
464
                    String.format("cannot retrieve the last history entry for ''%s'' in %s because of settings",
1✔
465
                    file, repository), "history.entry.latest");
466
            return null;
1✔
467
        }
468

469
        // Fallback to the repository method.
470
        HistoryEntry lastHistoryEntry = repository.getLastHistoryEntry(file, ui);
1✔
471
        statistics.report(LOGGER, Level.FINEST,
1✔
472
                String.format("finished retrieval of last history entry for '%s' using repository method (%s)",
1✔
473
                        file, lastHistoryEntry != null ? "success" : "fail"), "history.entry.latest");
1✔
474
        return lastHistoryEntry;
1✔
475
    }
476

477
    public History getHistory(File file, boolean withFiles, boolean ui) throws HistoryException {
478
        return getHistory(file, withFiles, ui, true);
1✔
479
    }
480

481
    /**
482
     * Get the history for the specified file. The history cache is tried first, then the repository.
483
     *
484
     * @param file the file to get the history for
485
     * @param withFiles whether the returned history should contain a
486
     * list of files touched by each changeset (the file list may be skipped if false, but it doesn't have to)
487
     * @param ui called from the webapp
488
     * @param fallback fall back to fetching the history from the repository
489
     *                 if it cannot be retrieved from history cache
490
     * @return history for the file or <code>null</code>
491
     * @throws HistoryException on error when accessing the history
492
     */
493
    @Nullable
494
    public History getHistory(File file, boolean withFiles, boolean ui, boolean fallback) throws HistoryException {
495

496
        final File dir = file.isDirectory() ? file : file.getParentFile();
1✔
497
        final Repository repository = getRepository(dir);
1✔
498

499
        if (repository == null) {
1✔
500
            LOGGER.log(Level.WARNING, "no repository found for ''{0}''", file);
1✔
501
            return null;
1✔
502
        }
503

504
        History history;
505
        try {
506
            history = getHistoryFromCache(file, repository, withFiles);
1✔
507
            if (history != null) {
1✔
508
                return history;
1✔
509
            }
510

511
            if (fallback) {
1✔
512
                return getHistoryFromRepository(file, repository, ui);
1✔
513
            }
514
        } catch (CacheException e) {
×
515
            LOGGER.log(Level.FINER, e.getMessage());
×
516
        }
×
517

518
        return null;
×
519
    }
520

521
    @Nullable
522
    private History getHistoryFromRepository(File file, Repository repository, boolean ui) throws HistoryException {
523

524
        if (!isRepoHistoryEligible(repository, file, ui)) {
1✔
525
            LOGGER.log(Level.FINEST, "''{0}'' in {1} is not eligible for history",
1✔
526
                    new Object[]{file, repository});
527
            return null;
1✔
528
        }
529

530
        /*
531
         * Some mirrors of repositories which are capable of fetching history
532
         * for directories may contain lots of files untracked by given SCM.
533
         * For these it would be waste of time to get their history
534
         * since the history of all files in this repository should have been
535
         * fetched in the first phase of indexing.
536
         */
537
        if (env.isIndexer() && isHistoryIndexDone() &&
1✔
538
                repository.isHistoryEnabled() && repository.hasHistoryForDirectories() &&
×
539
                !env.isFetchHistoryWhenNotInCache()) {
×
540
            LOGGER.log(Level.FINE, "not getting the history for ''{0}'' in repository {1} as the it supports "
×
541
                    + "history for directories",
542
                    new Object[]{file, repository});
543
            return null;
×
544
        }
545

546
        if (!env.getPathAccepter().accept(file)) {
1✔
547
            LOGGER.log(Level.FINEST, "file ''{0}'' not accepted for history", file);
×
548
            return null;
×
549
        }
550

551
        History history;
552
        try {
553
            history = repository.getHistory(file);
1✔
554
        } catch (UnsupportedOperationException e) {
×
555
            // In this case, we've found a file for which the SCM has no history
556
            // An example is a non-SCCS file somewhere in an SCCS-controlled workspace.
557
            LOGGER.log(Level.FINEST, "repository {0} does not have history for ''{1}''",
×
558
                    new Object[]{repository, file});
559
            return null;
×
560
        }
1✔
561

562
        return history;
1✔
563
    }
564

565
    /**
566
     * Gets a named revision of the specified file into the specified target file.
567
     *
568
     * @param target a require target file
569
     * @param parent The directory containing the file
570
     * @param basename The name of the file
571
     * @param rev The revision to get
572
     * @return {@code true} if content was found
573
     * @throws java.io.IOException if an I/O error occurs
574
     */
575
    public boolean getRevision(File target, String parent, String basename, String rev) throws IOException {
576
        Repository repo = getRepository(new File(parent));
×
577
        return repo != null && repo.getHistoryGet(target, parent, basename, rev);
×
578
    }
579

580
    /**
581
     * Get a named revision of the specified file.
582
     *
583
     * @param parent The directory containing the file
584
     * @param basename The name of the file
585
     * @param rev The revision to get
586
     * @return An InputStream containing the named revision of the file.
587
     */
588
    @Nullable
589
    public InputStream getRevision(String parent, String basename, String rev) {
590
        Repository repo = getRepository(new File(parent));
1✔
591
        if (repo == null) {
1✔
592
            LOGGER.log(Level.FINEST, "cannot find repository for ''{0}'' to get revision", parent);
×
593
            return null;
×
594
        }
595

596
        return repo.getHistoryGet(parent, basename, rev);
1✔
597
    }
598

599
    /**
600
     * @param file File object
601
     * @return whether it is possible to retrieve history for the file in any way
602
     */
603
    public boolean hasHistory(File file) {
604
        // If there is a cache entry that is fresh, there is no need to check the repository,
605
        // as the cache entry will be preferred in getHistory(), barring any time sensitive issues (TOUTOC).
606
        if (hasHistoryCacheForFile(file)) {
1✔
607
            try {
608
                if (historyCache.isUpToDate(file)) {
1✔
609
                    return true;
1✔
610
                }
611
            } catch (CacheException e) {
×
612
                LOGGER.log(Level.FINEST, "cannot determine if history cache for ''{0}'' is fresh", file);
×
613
            }
×
614
        }
615

616
        Repository repo = getRepository(file);
1✔
617
        if (repo == null) {
1✔
618
            LOGGER.log(Level.FINEST, "cannot find repository for ''{0}}'' to check history presence", file);
1✔
619
            return false;
1✔
620
        }
621

622
        if (!repositoryHasHistory(file, repo)) {
1✔
623
            return false;
1✔
624
        }
625

626
        // This should return true for Annotate view.
627
        Configuration.RemoteSCM globalRemoteSupport = env.getRemoteScmSupported();
1✔
628
        boolean remoteSupported = ((globalRemoteSupport == RemoteSCM.ON)
1✔
629
                || (globalRemoteSupport == RemoteSCM.UIONLY)
630
                || (globalRemoteSupport == RemoteSCM.DIRBASED)
631
                || !repo.isRemote());
1✔
632

633
        if (!remoteSupported) {
1✔
634
            LOGGER.log(Level.FINEST, "not eligible to display history for ''{0}'' as repository {1} is remote " +
1✔
635
                    "and the global setting is {2}", new Object[]{file, repo, globalRemoteSupport});
636
        }
637

638
        return remoteSupported;
1✔
639
    }
640

641
    private static boolean repositoryHasHistory(File file, Repository repo) {
642
        if (!repo.isWorking()) {
1✔
643
            LOGGER.log(Level.FINEST, "repository {0} for ''{1}'' is not working to check history presence",
1✔
644
                    new Object[]{repo, file});
645
            return false;
1✔
646
        }
647

648
        if (!repo.isHistoryEnabled()) {
1✔
649
            LOGGER.log(Level.FINEST, "repository {0} for ''{1}'' does not have history enabled " +
1✔
650
                            "to check history presence", new Object[]{repo, file});
651
            return false;
1✔
652
        }
653

654
        if (!repo.fileHasHistory(file)) {
1✔
655
            LOGGER.log(Level.FINEST, "''{0}'' in repository {1} does not have history to check history presence",
×
656
                    new Object[]{file, repo});
657
            return false;
×
658
        }
659

660
        return true;
1✔
661
    }
662

663
    /**
664
     * @param file file object
665
     * @return if there is history cache entry for the file
666
     */
667
    public boolean hasHistoryCacheForFile(File file) {
668
        if (!useHistoryCache()) {
1✔
669
            LOGGER.log(Level.FINEST, "history cache is off for ''{0}'' to check history cache presence", file);
×
670
            return false;
×
671
        }
672

673
        try {
674
            return historyCache.hasCacheForFile(file);
1✔
675
        } catch (CacheException ex) {
×
676
            LOGGER.log(Level.FINE,
×
677
                    String.format("failed to get history cache for file '%s' to check history cache presence", file),
×
678
                    ex);
679
            return false;
×
680
        }
681
    }
682

683
    /**
684
     * Check if we can annotate the specified file.
685
     *
686
     * @param file the file to check
687
     * @return whether the file is under version control, can be annotated and the
688
     * version control system supports annotation
689
     */
690
    public boolean hasAnnotation(File file) {
691
        if (file.isDirectory()) {
1✔
692
            LOGGER.log(Level.FINEST, "no annotations for directories (''{0}'') to check annotation presence",
1✔
693
                    file);
694
            return false;
1✔
695
        }
696

697
        AbstractAnalyzer.Genre genre = AnalyzerGuru.getGenre(file.toString());
1✔
698
        if (genre == null) {
1✔
699
            LOGGER.log(Level.INFO, "will not produce annotation for ''{0}'' with unknown genre", file);
1✔
700
            return false;
1✔
701
        }
702
        if (genre.equals(AbstractAnalyzer.Genre.DATA) || genre.equals(AbstractAnalyzer.Genre.IMAGE)) {
1✔
NEW
703
            LOGGER.log(Level.INFO, "no sense to produce annotation for binary file ''{0}''", file);
×
NEW
704
            return false;
×
705
        }
706

707
        Repository repo = getRepository(file);
1✔
708
        if (repo == null) {
1✔
709
            LOGGER.log(Level.FINEST, "cannot find repository for ''{0}'' to check annotation presence", file);
1✔
710
            return false;
1✔
711
        }
712

713
        if (!repo.isWorking()) {
1✔
714
            LOGGER.log(Level.FINEST, "repository {0} for ''{1}'' is not working to check annotation presence",
1✔
715
                    new Object[]{repo, file});
716
            return false;
1✔
717
        }
718

719
        return repo.fileHasAnnotation(file);
1✔
720
    }
721

722
    /**
723
     * @param file file object
724
     * @return if there is annotation cache entry for the file
725
     */
726
    public boolean hasAnnotationCacheForFile(File file) {
727
        if (!useAnnotationCache()) {
1✔
728
            LOGGER.log(Level.FINEST, "annotation cache is off for ''{0}'' to check history cache presence", file);
×
729
            return false;
×
730
        }
731

732
        try {
733
            return annotationCache.hasCacheForFile(file);
1✔
734
        } catch (CacheException ex) {
×
735
            LOGGER.log(Level.FINE,
×
736
                    String.format("failed to get annotation cache for file '%s' to check history cache presence", file),
×
737
                    ex);
738
            return false;
×
739
        }
740
    }
741

742
    /**
743
     * Get the last modified times and descriptions for all files and subdirectories in the specified directory
744
     * and set it into the entries provided.
745
     * @param directory the directory whose files to check
746
     * @param entries list of {@link DirectoryEntry} instances
747
     * @return whether to fall back to file system based time stamps if the date is {@code null}
748
     * @throws org.opengrok.indexer.history.CacheException if history cannot be retrieved
749
     */
750
    public boolean fillLastHistoryEntries(File directory, List<DirectoryEntry> entries) throws CacheException {
751

752
        if (!env.isUseHistoryCacheForDirectoryListing()) {
1✔
753
            LOGGER.log(Level.FINEST, "using history cache to retrieve last modified times for ''{0}}'' is disabled",
×
754
                    directory);
755
            return true;
×
756
        }
757

758
        if (!useHistoryCache()) {
1✔
759
            LOGGER.log(Level.FINEST, "history cache is disabled for ''{0}'' to retrieve last modified times",
×
760
                    directory);
761
            return true;
×
762
        }
763

764
        Repository repository = getRepository(directory);
1✔
765
        if (repository == null) {
1✔
766
            LOGGER.log(Level.FINEST, "cannot find repository for ''{0}}'' to retrieve last modified times",
×
767
                    directory);
768
            return true;
×
769
        }
770

771
        // Do not use history cache for repositories with merge commits disabled as some files in the repository
772
        // could be introduced and changed solely via merge changesets. The call would presumably fall back
773
        // to file system based time stamps, however that might be confusing, so avoid that.
774
        if (repository.isMergeCommitsSupported() && !repository.isMergeCommitsEnabled()) {
1✔
775
            LOGGER.log(Level.FINEST,
1✔
776
                    "will not retrieve last modified times due to merge changesets disabled for ''{0}}''",
777
                    directory);
778
            return true;
1✔
779
        }
780

781
        return !historyCache.fillLastHistoryEntries(entries);
1✔
782
    }
783

784
    /**
785
     * Recursively search for repositories with a depth limit, add those found to the internally used map.
786
     * @see #putRepository(Repository)
787
     *
788
     * @param files list of directories to check if they contain a repository
789
     * @param allowedNesting number of levels of nested repos to allow
790
     * @param depth maximum scanning depth
791
     * @param isNested a value indicating if a parent {@link Repository} was already found above the {@code files}
792
     * @param progress {@link org.opengrok.indexer.util.Progress} instance
793
     * @return collection of added repositories
794
     */
795
    private Collection<RepositoryInfo> addRepositories(File[] files, int allowedNesting, int depth, boolean isNested,
796
                                                       Progress progress) {
797

798
        if (depth < 0) {
1✔
799
            throw new IllegalArgumentException("depth is negative");
×
800
        }
801

802
        List<RepositoryInfo> repoList = new ArrayList<>();
1✔
803
        PathAccepter pathAccepter = env.getPathAccepter();
1✔
804

805
        for (File file : files) {
1✔
806
            if (!file.isDirectory()) {
1✔
807
                continue;
1✔
808
            }
809

810
            try {
811
                Repository repository = null;
1✔
812
                try {
813
                    repository = RepositoryFactory.getRepository(file, CommandTimeoutType.INDEXER, isNested);
1✔
814
                } catch (InstantiationException | NoSuchMethodException | InvocationTargetException e) {
×
815
                    LOGGER.log(Level.WARNING,
×
816
                            String.format("Could not create repository for '%s': could not instantiate the repository.",
×
817
                                    file), e);
818
                } catch (IllegalAccessException iae) {
×
819
                    LOGGER.log(Level.WARNING,
×
820
                            String.format("Could not create repository for '%s': missing access rights.", file), iae);
×
821
                    continue;
822
                } catch (ForbiddenSymlinkException e) {
1✔
823
                    LOGGER.log(Level.WARNING, "Could not create repository for ''{0}'': {1}",
1✔
824
                            new Object[] {file, e.getMessage()});
1✔
825
                    continue;
826
                }
1✔
827

828
                if (repository == null) {
1✔
829
                    if (depth == 0) {
1✔
830
                        // Reached maximum depth, skip looking through the children.
831
                        continue;
832
                    }
833

834
                    // Not a repository, search its sub-dirs.
835
                    if (pathAccepter.accept(file)) {
1✔
836
                        File[] subFiles = file.listFiles();
1✔
837
                        if (subFiles == null) {
1✔
838
                            LOGGER.log(Level.WARNING,
×
839
                                    "Failed to get sub directories for ''{0}'', check access permissions.",
840
                                    file.getAbsolutePath());
×
841
                        } else {
842
                            // Recursive call to scan next depth
843
                            repoList.addAll(addRepositories(subFiles,
1✔
844
                                    allowedNesting, depth - 1, isNested, progress));
845
                        }
846
                    }
1✔
847
                } else {
848
                    LOGGER.log(Level.CONFIG, "Adding repository {0}", repository);
1✔
849

850
                    repoList.add(new RepositoryInfo(repository));
1✔
851
                    putRepository(repository);
1✔
852

853
                    if (allowedNesting > 0 && repository.supportsSubRepositories()) {
1✔
854
                        File[] subFiles = file.listFiles();
1✔
855
                        if (subFiles == null) {
1✔
856
                            LOGGER.log(Level.WARNING,
×
857
                                    "Failed to get sub directories for ''{0}'', check access permissions.",
858
                                    file.getAbsolutePath());
×
859
                        } else if (depth > 0) {
1✔
860
                            repoList.addAll(addRepositories(subFiles,
1✔
861
                                    allowedNesting - 1, depth - 1, true, progress));
862
                        }
863
                    }
864
                }
865
            } catch (IOException exp) {
×
866
                LOGGER.log(Level.WARNING,
×
867
                        "Failed to get canonical path for ''{0}'': {1}",
868
                        new Object[]{file.getAbsolutePath(), exp.getMessage()});
×
869
                LOGGER.log(Level.WARNING, "Repository will be ignored...", exp);
×
870
            } finally {
871
                progress.increment();
1✔
872
            }
873
        }
874

875
        return repoList;
1✔
876
    }
877

878
    /**
879
     * Recursively search for repositories in given directories, add those found to the internally used map.
880
     *
881
     * @param files list of directories to check if they contain a repository
882
     * @return collection of added repositories
883
     */
884
    public Collection<RepositoryInfo> addRepositories(File[] files) {
885
        ExecutorService executor = env.getIndexerParallelizer().getFixedExecutor();
1✔
886
        List<Future<Collection<RepositoryInfo>>> futures = new ArrayList<>();
1✔
887
        List<RepositoryInfo> repoList = new ArrayList<>();
1✔
888

889
        try (Progress progress = new Progress(LOGGER, "directories processed for repository scan")) {
1✔
890
            for (File file : files) {
1✔
891
                /*
892
                 * Adjust scan depth based on source root path. Some directories can be symbolic links pointing
893
                 * outside source root so avoid constructing canonical paths for the computation to work.
894
                 */
895
                int levelsBelowSourceRoot;
896
                try {
897
                    String relativePath = env.getPathRelativeToSourceRoot(file);
1✔
898
                    levelsBelowSourceRoot = Path.of(relativePath).getNameCount();
1✔
899
                } catch (IOException | ForbiddenSymlinkException e) {
×
900
                    LOGGER.log(Level.WARNING, "cannot get path relative to source root for ''{0}'', " +
×
901
                            "skipping repository scan for this directory", file);
902
                    continue;
×
903
                }
1✔
904
                final int scanDepth = env.getScanningDepth() - levelsBelowSourceRoot;
1✔
905

906
                futures.add(executor.submit(() -> addRepositories(new File[]{file},
1✔
907
                        env.getNestingMaximum(), scanDepth, false, progress)));
1✔
908
            }
909

910
            futures.forEach(future -> {
1✔
911
                try {
912
                    repoList.addAll(future.get());
1✔
913
                } catch (Exception e) {
×
914
                    LOGGER.log(Level.WARNING, "failed to get results of repository scan", e);
×
915
                }
1✔
916
            });
1✔
917
        }
918

919
        LOGGER.log(Level.FINER, "Discovered repositories: {0}", repoList);
1✔
920

921
        return repoList;
1✔
922
    }
923

924
    /**
925
     * Recursively search for repositories in given directories, add those found to the internally used map.
926
     *
927
     * @param repos collection of repository paths
928
     * @return collection of {@link RepositoryInfo} objects
929
     */
930
    public Collection<RepositoryInfo> addRepositories(Collection<String> repos) {
931
        return addRepositories(repos.stream().map(File::new).toArray(File[]::new));
1✔
932
    }
933

934
    /**
935
     * Get collection of repositories used internally by HistoryGuru.
936
     * @return collection of {@link RepositoryInfo} objects
937
     */
938
    public Collection<RepositoryInfo> getRepositories() {
939
        return repositories.values().stream().map(RepositoryInfo::new).collect(Collectors.toSet());
1✔
940
    }
941

942
    /**
943
     * Store history for a file into history cache. If the related repository does not support
944
     * getting the history for directories, it will return right away without storing the history.
945
     * @param file file
946
     * @param history {@link History} instance
947
     */
948
    public void storeHistory(File file, History history) {
949
        Repository repository = getRepository(file);
1✔
950
        if (repository.hasHistoryForDirectories()) {
1✔
951
            return;
1✔
952
        }
953

954
        try {
UNCOV
955
            historyCache.storeFile(history, file, repository);
×
956
        } catch (HistoryException e) {
×
957
            LOGGER.log(Level.WARNING,
×
958
                    String.format("cannot create history cache for '%s' in repository %s", file, repository), e);
×
UNCOV
959
        }
×
UNCOV
960
    }
×
961

962
    private void createHistoryCache(Repository repository, String sinceRevision) throws CacheException, HistoryException {
963
        if (!repository.isHistoryEnabled()) {
1✔
964
            LOGGER.log(Level.INFO,
1✔
965
                    "Skipping history cache creation for {0} and its subdirectories", repository);
966
            return;
1✔
967
        }
968

969
        if (repository.isWorking()) {
1✔
970
            Statistics elapsed = new Statistics();
1✔
971

972
            LOGGER.log(Level.INFO, "Creating history cache for {0}", repository);
1✔
973
            repository.createCache(historyCache, sinceRevision);
1✔
974
            elapsed.report(LOGGER, String.format("Done history cache for %s", repository));
1✔
975
        } else {
1✔
976
            LOGGER.log(Level.WARNING,
1✔
977
                    "Skipping creation of history cache for {0}: Missing SCM dependencies?", repository);
978
        }
979
    }
1✔
980

981
    private Map<Repository, Optional<Exception>> createHistoryCacheReal(Collection<Repository> repositories) {
982
        if (repositories.isEmpty()) {
1✔
983
            LOGGER.log(Level.WARNING, "History cache is enabled however the list of repositories is empty. " +
1✔
984
                    "Either specify the repositories in configuration or let the indexer scan them.");
985
            return Collections.emptyMap();
1✔
986
        }
987

988
        Statistics elapsed = new Statistics();
1✔
989
        ExecutorService executor = env.getIndexerParallelizer().getHistoryExecutor();
1✔
990
        // Since we know each repository object from the repositories
991
        // collection is unique, we can abuse HashMap to create a list of
992
        // repository,revision tuples with repository as key (as the revision
993
        // string does not have to be unique - surely it is not unique
994
        // for the initial index case).
995
        HashMap<Repository, String> repos2process = new HashMap<>();
1✔
996

997
        // Collect the list of <latestRev,repo> pairs first so that we
998
        // do not have to deal with latch decrementing in the cycle below.
999
        for (final Repository repo : repositories) {
1✔
1000
            final String latestRev;
1001

1002
            try {
1003
                latestRev = historyCache.getLatestCachedRevision(repo);
1✔
1004
                repos2process.put(repo, latestRev);
1✔
1005
            } catch (CacheException he) {
×
1006
                LOGGER.log(Level.WARNING, String.format("Failed to retrieve latest cached revision for %s", repo), he);
×
1007
            }
1✔
1008
        }
1✔
1009

1010
        LOGGER.log(Level.INFO, "Creating history cache for {0} repositories", repos2process.size());
1✔
1011
        Map<Repository, Future<Optional<Exception>>> futures = new HashMap<>();
1✔
1012
        try (Progress progress = new Progress(LOGGER, "repository invalidation", repos2process.size())) {
1✔
1013
            for (final Map.Entry<Repository, String> entry : repos2process.entrySet()) {
1✔
1014
                futures.put(entry.getKey(), executor.submit(() -> {
1✔
1015
                    try {
1016
                        createHistoryCache(entry.getKey(), entry.getValue());
1✔
1017
                    } catch (Exception ex) {    // We want to catch any exception since we are in thread.
1✔
1018
                        LOGGER.log(Level.WARNING,
1✔
1019
                                String.format("failed to create history cache for %s", entry.getKey()), ex);
1✔
1020
                        return Optional.of(ex);
1✔
1021
                    } finally {
1022
                        progress.increment();
1✔
1023
                    }
1024
                    return Optional.empty();
1✔
1025
                }));
1026
            }
1✔
1027
        }
1028

1029
        /*
1030
         * Wait until the history of all repositories is done. This is necessary
1031
         * since the next phase of generating index will need the history to
1032
         * be ready as it is recorded in Lucene index.
1033
         */
1034
        Map<Repository, Optional<Exception>> results = new HashMap<>();
1✔
1035
        for (Map.Entry<Repository, Future<Optional<Exception>>> entry : futures.entrySet()) {
1✔
1036
            try {
1037
                results.put(entry.getKey(), entry.getValue().get());
1✔
1038
            } catch (InterruptedException | ExecutionException ex) {
×
1039
                results.put(entry.getKey(), Optional.of(ex));
×
1040
            }
1✔
1041
        }
1✔
1042

1043
        // The cache has been populated. Now, optimize how it is stored on
1044
        // disk to enhance performance and save space.
1045
        try {
1046
            historyCache.optimize();
1✔
1047
        } catch (CacheException he) {
×
1048
            LOGGER.log(Level.WARNING, "Failed optimizing the history cache database", he);
×
1049
        }
1✔
1050
        elapsed.report(LOGGER, "Done history cache for all repositories", "indexer.history.cache");
1✔
1051
        setHistoryIndexDone();
1✔
1052

1053
        return results;
1✔
1054
    }
1055

1056
    /**
1057
     * Create history cache for selected repositories.
1058
     * For this to work the repositories have to be already present in the
1059
     * internal map, e.g. via {@code setRepositories()} or {@code addRepositories()}.
1060
     *
1061
     * @param repositories list of repository paths
1062
     * @return map of repository to optional exception
1063
     */
1064
    public Map<Repository, Optional<Exception>> createHistoryCache(Collection<String> repositories) {
1065
        if (!useHistoryCache()) {
1✔
1066
            return Collections.emptyMap();
×
1067
        }
1068
        return createHistoryCacheReal(getReposFromString(repositories));
1✔
1069
    }
1070

1071
    /**
1072
     * Clear entry for single file from history cache.
1073
     * @param path path to the file relative to the source root
1074
     * @param removeHistory whether to remove history cache entry for the path
1075
     */
1076
    public void clearHistoryCacheFile(String path, boolean removeHistory) {
1077
        if (!useHistoryCache()) {
1✔
1078
            return;
×
1079
        }
1080

1081
        Repository repository = getRepository(new File(env.getSourceRootFile(), path));
1✔
1082
        if (repository == null) {
1✔
1083
            return;
1✔
1084
        }
1085

1086
        // Repositories that do not support getting history for directories do not undergo
1087
        // incremental history cache generation, so for these the removeHistory parameter is not honored.
1088
        if (!repository.hasHistoryForDirectories() || removeHistory) {
1✔
1089
            historyCache.clearFile(path);
1✔
1090
        }
1091
    }
1✔
1092

1093
    /**
1094
     * Retrieve and store the annotation cache entry for given file.
1095
     * @param file file object under source root. Needs to have a repository associated for the cache to be created.
1096
     * @param latestRev latest revision of the file
1097
     * @throws CacheException on error, otherwise the cache entry is created
1098
     */
1099
    public void createAnnotationCache(File file, String latestRev) throws CacheException {
1100
        if (!useAnnotationCache()) {
1✔
1101
            throw new CacheException(String.format("annotation cache could not be used to create cache for '%s'",
×
1102
                    file), Level.FINE);
1103
        }
1104

1105
        Repository repository = getRepository(file);
1✔
1106
        if (repository == null) {
1✔
1107
            throw new CacheException(String.format("no repository for '%s'", file), Level.FINE);
×
1108
        }
1109

1110
        if (!repository.isWorking() || !repository.isAnnotationCacheEnabled()) {
1✔
1111
            throw new CacheException(
1✔
1112
                    String.format("repository %s does not allow to create annotation cache for '%s'",
1✔
1113
                            repository, file), Level.FINER);
1114
        }
1115

1116
        LOGGER.log(Level.FINEST, "creating annotation cache for ''{0}''", file);
1✔
1117
        try {
1118
            Statistics statistics = new Statistics();
1✔
1119
            Annotation annotation = getAnnotationFromRepository(file, null);
1✔
1120
            statistics.report(LOGGER, Level.FINEST, String.format("retrieved annotation for ''%s''", file),
1✔
1121
                    "annotation.retrieve.latency");
1122

1123
            if (annotation != null) {
1✔
1124
                annotation.setRevision(latestRev);
1✔
1125

1126
                // Storing annotation has its own statistics.
1127
                annotationCache.store(file, annotation);
1✔
1128
            }
1129
        } catch (IOException e) {
×
1130
            throw new CacheException(e);
×
1131
        }
1✔
1132
    }
1✔
1133

1134
    /**
1135
      * Clear entry for single file from annotation cache.
1136
      * @param path path to the file relative to the source root
1137
      */
1138
    public void clearAnnotationCacheFile(String path) {
1139
        if (!useAnnotationCache()) {
1✔
1140
            return;
×
1141
        }
1142

1143
        annotationCache.clearFile(path);
1✔
1144
    }
1✔
1145

1146
    /**
1147
     * Remove history cache data for a list of repositories. Those that are
1148
     * successfully cleared may be removed from the internal list of repositories,
1149
     * depending on the {@code removeRepositories} parameter.
1150
     *
1151
     * @param repositories list of repository objects relative to source root
1152
     * @return list of repository names
1153
     */
1154
    public List<String> removeHistoryCache(Collection<RepositoryInfo> repositories) {
1155
        if (!useHistoryCache()) {
×
1156
            return List.of();
×
1157
        }
1158

1159
        return historyCache.clearCache(repositories);
×
1160
    }
1161

1162
    /**
1163
     * Remove annotation cache data for a list of repositories. Those that are
1164
     * successfully cleared may be removed from the internal list of repositories,
1165
     * depending on the {@code removeRepositories} parameter.
1166
     *
1167
     * @param repositories list of repository objects relative to source root
1168
     * @return list of repository names
1169
     */
1170
    public List<String> removeAnnotationCache(Collection<RepositoryInfo> repositories) {
1171
        if (!useAnnotationCache()) {
×
1172
            return List.of();
×
1173
        }
1174

1175
        return annotationCache.clearCache(repositories);
×
1176
    }
1177

1178
    /**
1179
     * Create the history cache for all repositories.
1180
     * @return map of repository to optional exception
1181
     */
1182
    public Map<Repository, Optional<Exception>> createHistoryCache() {
1183
        if (!useHistoryCache()) {
1✔
1184
            return Collections.emptyMap();
×
1185
        }
1186

1187
        return createHistoryCacheReal(repositories.values());
1✔
1188
    }
1189

1190
    /**
1191
     * Lookup repositories from list of repository paths.
1192
     * @param repositories paths to repositories relative to source root
1193
     * @return list of repositories
1194
     */
1195
    List<Repository> getReposFromString(Collection<String> repositories) {
1196
        ArrayList<Repository> repos = new ArrayList<>();
1✔
1197
        File srcRoot = env.getSourceRootFile();
1✔
1198

1199
        for (String file : repositories) {
1✔
1200
            File f = new File(srcRoot, file);
1✔
1201
            Repository r = getRepository(f);
1✔
1202
            if (r == null) {
1✔
1203
                LOGGER.log(Level.WARNING, "Could not locate a repository for {0}",
×
1204
                        f.getAbsolutePath());
×
1205
            } else if (!repos.contains(r)) {
1✔
1206
                repos.add(r);
1✔
1207
            }
1208
        }
1✔
1209

1210
        return repos;
1✔
1211
    }
1212

1213
    /**
1214
     * Lookup repository for given file.
1215
     * @param file file object source root
1216
     * @return repository object or {@code null} if not found
1217
     */
1218
    @Nullable
1219
    public Repository getRepository(File file) {
1220
        return repositoryLookup.getRepository(file.toPath(), repositoryRoots.keySet(), repositories,
1✔
1221
                PathUtils::getRelativeToCanonical);
1222
    }
1223

1224
    /**
1225
     * Remove list of repositories from the list maintained in the HistoryGuru.
1226
     * This is much less heavyweight than {@code invalidateRepositories()}
1227
     * since it just removes items from the map.
1228
     * @param repos absolute repository paths
1229
     */
1230
    public void removeRepositories(Collection<String> repos) {
1231
        Set<Repository> removedRepos = repos.stream().map(repositories::remove)
1✔
1232
            .filter(Objects::nonNull).collect(Collectors.toSet());
1✔
1233
        repositoryLookup.repositoriesRemoved(removedRepos);
1✔
1234
        // Re-map the repository roots.
1235
        repositoryRoots.clear();
1✔
1236
        List<Repository> ccopy = new ArrayList<>(repositories.values());
1✔
1237
        ccopy.forEach(this::putRepository);
1✔
1238
    }
1✔
1239

1240
    /**
1241
     * Set list of known repositories which match the list of directories.
1242
     * @param repos list of repositories
1243
     * @param dirs collection of directories that might correspond to the repositories
1244
     * @param cmdType command timeout type
1245
     */
1246
    public void invalidateRepositories(Collection<? extends RepositoryInfo> repos, Collection<String> dirs, CommandTimeoutType cmdType) {
1247
        if (repos != null && !repos.isEmpty() && dirs != null && !dirs.isEmpty()) {
1✔
1248
            List<RepositoryInfo> newrepos = new ArrayList<>();
×
1249
            for (RepositoryInfo i : repos) {
×
1250
                for (String dir : dirs) {
×
1251
                    Path dirPath = new File(dir).toPath();
×
1252
                    Path iPath = new File(i.getDirectoryName()).toPath();
×
1253
                    if (iPath.startsWith(dirPath)) {
×
1254
                        newrepos.add(i);
×
1255
                    }
1256
                }
×
1257
            }
×
1258
            repos = newrepos;
×
1259
        }
1260

1261
        invalidateRepositories(repos, cmdType);
1✔
1262
    }
1✔
1263

1264
    /**
1265
     * Go through the list of specified repositories and determine if they
1266
     * are valid. Those that make it through will form the new HistoryGuru
1267
     * internal map. This means this method should be used only if dealing
1268
     * with whole collection of repositories.
1269
     * <br>
1270
     * The caller is expected to reflect the new list via {@code getRepositories()}.
1271
     * <br>
1272
     * The processing is done via thread pool since the operation
1273
     * is expensive (see {@code RepositoryFactory.getRepository()}).
1274
     *
1275
     * @param repos collection of repositories to invalidate.
1276
     * If null or empty, the internal map of repositories will be cleared.
1277
     * @param cmdType command timeout type
1278
     */
1279
    public void invalidateRepositories(Collection<? extends RepositoryInfo> repos, CommandTimeoutType cmdType) {
1280
        if (repos == null || repos.isEmpty()) {
1✔
1281
            clear();
1✔
1282
            return;
1✔
1283
        }
1284

1285
        Map<String, Repository> repositoryMap = Collections.synchronizedMap(new HashMap<>(repos.size()));
1✔
1286
        Statistics elapsed = new Statistics();
1✔
1287

1288
        LOGGER.log(Level.FINE, "invalidating {0} repositories", repos.size());
1✔
1289

1290
        /*
1291
         * getRepository() below does various checks of the repository
1292
         * which involves executing commands and I/O so make the checks
1293
         * run in parallel to speed up the process.
1294
         */
1295
        final CountDownLatch latch = new CountDownLatch(repos.size());
1✔
1296
        int parallelismLevel;
1297
        // Both indexer and web app startup should be as quick as possible.
1298
        if (cmdType == CommandTimeoutType.INDEXER || cmdType == CommandTimeoutType.WEBAPP_START) {
1✔
1299
            parallelismLevel = env.getIndexingParallelism();
1✔
1300
        } else {
1301
            parallelismLevel = env.getRepositoryInvalidationParallelism();
×
1302
        }
1303
        final ExecutorService executor = Executors.newFixedThreadPool(parallelismLevel,
1✔
1304
                new OpenGrokThreadFactory("invalidate-repos-"));
1305

1306
        try (Progress progress = new Progress(LOGGER, "repository invalidation", repos.size())) {
1✔
1307
            for (RepositoryInfo repositoryInfo : repos) {
1✔
1308
                executor.submit(() -> {
1✔
1309
                    try {
1310
                        Repository r = RepositoryFactory.getRepository(repositoryInfo, cmdType);
1✔
1311
                        if (r == null) {
1✔
1312
                            LOGGER.log(Level.WARNING,
×
1313
                                    "Failed to instantiate internal repository data for {0} in ''{1}''",
1314
                                    new Object[]{repositoryInfo.getType(), repositoryInfo.getDirectoryName()});
×
1315
                        } else {
1316
                            repositoryMap.put(r.getDirectoryName(), r);
1✔
1317
                        }
1318
                    } catch (Exception ex) {
×
1319
                        // We want to catch any exception since we are in thread.
1320
                        LOGGER.log(Level.WARNING, "Could not create " + repositoryInfo.getType()
×
1321
                                + " repository object for '" + repositoryInfo.getDirectoryName() + "'", ex);
×
1322
                    } finally {
1323
                        latch.countDown();
1✔
1324
                        progress.increment();
1✔
1325
                    }
1326
                });
1✔
1327
            }
1✔
1328

1329
            // Wait until all repositories are validated.
1330
            try {
1331
                latch.await();
1✔
1332
            } catch (InterruptedException ex) {
×
1333
                LOGGER.log(Level.SEVERE, "latch exception", ex);
×
1334
            }
1✔
1335
            executor.shutdown();
1✔
1336
        }
1337

1338
        clear();
1✔
1339
        repositoryMap.forEach((key, repo) -> putRepository(repo));
1✔
1340

1341
        elapsed.report(LOGGER, String.format("Done invalidating repositories (%d valid, %d working)",
1✔
1342
                        repositoryMap.size(), repositoryMap.values().stream().
1✔
1343
                                filter(RepositoryInfo::isWorking).collect(Collectors.toSet()).size()),
1✔
1344
                "history.repositories.invalidate");
1345
    }
1✔
1346

1347
    @VisibleForTesting
1348
    public void clear() {
1349
        repositoryRoots.clear();
1✔
1350
        repositories.clear();
1✔
1351
        repositoryLookup.clear();
1✔
1352
    }
1✔
1353

1354
    /**
1355
     * Adds the specified {@code repository} to this instance's repository map
1356
     * and repository-root map (if not already there).
1357
     * @param repository a defined instance
1358
     */
1359
    private void putRepository(Repository repository) {
1360
        String repoDirectoryName = repository.getDirectoryName();
1✔
1361
        File repoDirectoryFile = new File(repoDirectoryName);
1✔
1362
        String repoDirParent = repoDirectoryFile.getParent();
1✔
1363
        repositoryRoots.put(repoDirParent, "");
1✔
1364
        repositories.put(repoDirectoryName, repository);
1✔
1365
    }
1✔
1366
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc