• 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

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

26
import java.io.BufferedOutputStream;
27
import java.io.BufferedReader;
28
import java.io.BufferedWriter;
29
import java.io.File;
30
import java.io.FileOutputStream;
31
import java.io.FileReader;
32
import java.io.IOException;
33
import java.io.OutputStream;
34
import java.io.OutputStreamWriter;
35
import java.io.Writer;
36
import java.nio.file.Files;
37
import java.nio.file.Path;
38
import java.util.ArrayList;
39
import java.util.HashMap;
40
import java.util.HashSet;
41
import java.util.Iterator;
42
import java.util.List;
43
import java.util.Map;
44
import java.util.Set;
45
import java.util.concurrent.CountDownLatch;
46
import java.util.concurrent.ExecutorService;
47
import java.util.concurrent.Future;
48
import java.util.concurrent.atomic.AtomicInteger;
49
import java.util.logging.Level;
50
import java.util.logging.Logger;
51
import java.util.stream.Collectors;
52

53
import com.fasterxml.jackson.core.type.TypeReference;
54
import com.fasterxml.jackson.databind.ObjectMapper;
55
import com.fasterxml.jackson.databind.ObjectWriter;
56
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
57
import com.fasterxml.jackson.dataformat.smile.SmileGenerator;
58
import com.fasterxml.jackson.dataformat.smile.SmileParser;
59
import com.fasterxml.jackson.dataformat.smile.databind.SmileMapper;
60
import io.micrometer.core.instrument.Counter;
61
import io.micrometer.core.instrument.MeterRegistry;
62
import org.jetbrains.annotations.Nullable;
63
import org.opengrok.indexer.Metrics;
64
import org.opengrok.indexer.configuration.PathAccepter;
65
import org.opengrok.indexer.configuration.RuntimeEnvironment;
66
import org.opengrok.indexer.logger.LoggerFactory;
67
import org.opengrok.indexer.search.DirectoryEntry;
68
import org.opengrok.indexer.util.Progress;
69
import org.opengrok.indexer.util.Statistics;
70

71

72
/**
73
 * Class representing file based storage of per source file history.
74
 */
75
class FileHistoryCache extends AbstractCache implements HistoryCache {
1✔
76

77
    private static final Logger LOGGER = LoggerFactory.getLogger(FileHistoryCache.class);
1✔
78
    private static final RuntimeEnvironment env = RuntimeEnvironment.getInstance();
1✔
79

80
    private static final String HISTORY_CACHE_DIR_NAME = "historycache";
81
    private static final String LATEST_REV_FILE_NAME = "OpenGroklatestRev";
82

83
    private final PathAccepter pathAccepter = env.getPathAccepter();
1✔
84

85
    private Counter fileHistoryCacheHits;
86
    private Counter fileHistoryCacheMisses;
87

88
    /**
89
     * Generate history cache for single renamed file.
90
     * @param filename file path
91
     * @param repository repository
92
     * @param root root
93
     * @param tillRevision end revision (can be null)
94
     */
95
    public void doRenamedFileHistory(String filename, File file, Repository repository, File root, String tillRevision)
96
            throws HistoryException {
97

98
        History history;
99

100
        if (tillRevision != null) {
1✔
101
            if (!(repository instanceof RepositoryWithPerPartesHistory)) {
1✔
102
                throw new RuntimeException("cannot use non null tillRevision on repository");
×
103
            }
104

105
            RepositoryWithPerPartesHistory repo = (RepositoryWithPerPartesHistory) repository;
1✔
106
            history = repo.getHistory(file, null, tillRevision);
1✔
107
        } else {
1✔
108
            history = repository.getHistory(file);
1✔
109
        }
110

111
        history.strip();
1✔
112
        doFileHistory(filename, history, repository, root, true);
1✔
113
    }
1✔
114

115
    /**
116
     * Generate history cache for single file.
117
     * @param filename name of the file
118
     * @param history history
119
     * @param repository repository object in which the file belongs
120
     * @param root root of the source repository
121
     * @param renamed true if the file was renamed in the past
122
     */
123
    private void doFileHistory(String filename, History history, Repository repository, File root, boolean renamed)
124
            throws HistoryException {
125

126
        File file = new File(root, filename);
1✔
127
        if (file.isDirectory()) {
1✔
128
            return;
1✔
129
        }
130

131
        // Assign tags to changesets they represent.
132
        if (env.isTagsEnabled() && repository.hasFileBasedTags()) {
1✔
UNCOV
133
            repository.assignTagsInHistory(history);
×
134
        }
135

136
        storeFile(history, file, repository, !renamed);
1✔
137
    }
1✔
138

139
    @Override
140
    public void initialize() {
141
        MeterRegistry meterRegistry = Metrics.getRegistry();
1✔
142
        if (meterRegistry != null) {
1✔
143
            fileHistoryCacheHits = Counter.builder("cache.history.file.get").
1✔
144
                    description("file history cache hits").
1✔
145
                    tag("what", "hits").
1✔
146
                    register(meterRegistry);
1✔
147
            fileHistoryCacheMisses = Counter.builder("cache.history.file.get").
1✔
148
                    description("file history cache misses").
1✔
149
                    tag("what", "miss").
1✔
150
                    register(meterRegistry);
1✔
151
        }
152
    }
1✔
153

154
    double getFileHistoryCacheHits() {
155
        return fileHistoryCacheHits.count();
×
156
    }
157

158
    @Override
159
    public void optimize() {
160
        // nothing to do
161
    }
1✔
162

163
    @Override
164
    public boolean supportsRepository(Repository repository) {
165
        // all repositories are supported
166
        return true;
1✔
167
    }
168

169
    /**
170
     * Read complete history from the cache.
171
     */
172
    static History readHistory(File cacheFile, Repository repository) throws IOException {
173
        SmileFactory factory = new SmileFactory();
1✔
174
        ObjectMapper mapper = new SmileMapper();
1✔
175
        List<HistoryEntry> historyEntryList = new ArrayList<>();
1✔
176

177
        try (SmileParser parser = factory.createParser(cacheFile)) {
1✔
178
            parser.setCodec(mapper);
1✔
179
            Iterator<HistoryEntry> historyEntryIterator = parser.readValuesAs(HistoryEntry.class);
1✔
180
            historyEntryIterator.forEachRemaining(historyEntryList::add);
1✔
181
        }
182

183
        History history = new History(historyEntryList);
1✔
184

185
        // Read tags from separate file.
186
        if (env.isTagsEnabled() && repository.hasFileBasedTags()) {
1✔
UNCOV
187
            File tagFile = getTagsFile(cacheFile);
×
UNCOV
188
            try (SmileParser parser = factory.createParser(tagFile)) {
×
UNCOV
189
                parser.setCodec(mapper);
×
UNCOV
190
                Map<String, String> tags = parser.readValueAs(new TypeReference<HashMap<String, String>>() {
×
191
                });
UNCOV
192
                history.setTags(tags);
×
193
            } catch (IOException ioe) {
×
194
                // Handle the exception here gracefully - it impacts the history only partially.
195
                LOGGER.log(Level.WARNING, "failed to read tags from ''{0}''", tagFile);
×
UNCOV
196
            }
×
197
        }
198

199
        return history;
1✔
200
    }
201

202
    static HistoryEntry readLastHistoryEntry(File cacheFile) throws IOException {
203
        SmileFactory factory = new SmileFactory();
1✔
204
        ObjectMapper mapper = new SmileMapper();
1✔
205
        HistoryEntry historyEntry = null;
1✔
206

207
        try (SmileParser parser = factory.createParser(cacheFile)) {
1✔
208
            parser.setCodec(mapper);
1✔
209
            Iterator<HistoryEntry> historyEntryIterator = parser.readValuesAs(HistoryEntry.class);
1✔
210
            if (historyEntryIterator.hasNext()) {
1✔
211
                historyEntry = historyEntryIterator.next();
1✔
212
            }
213
        }
214

215
        return historyEntry;
1✔
216
    }
217

218
    /**
219
     * Write serialized object to file.
220
     * @param history {@link History} instance to be stored
221
     * @param outputFile output file
222
     * @throws IOException on error
223
     */
224
    public static void writeHistoryTo(History history, File outputFile) throws IOException {
225
        ObjectWriter objectWriter = getObjectWriter();
1✔
226

227
        try (OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(outputFile))) {
1✔
228
            for (HistoryEntry historyEntry : history.getHistoryEntries()) {
1✔
229
                byte[] bytes = objectWriter.writeValueAsBytes(historyEntry);
1✔
230
                outputStream.write(bytes);
1✔
231
            }
1✔
232
        }
233
    }
1✔
234

235
    public static void writeTagsTo(File outputFile, History history) throws IOException {
UNCOV
236
        SmileFactory smileFactory = new SmileFactory();
×
237
        // need header to enable shared string values
UNCOV
238
        smileFactory.configure(SmileGenerator.Feature.WRITE_HEADER, true);
×
UNCOV
239
        smileFactory.configure(SmileGenerator.Feature.CHECK_SHARED_STRING_VALUES, false);
×
240

UNCOV
241
        ObjectMapper mapper = new SmileMapper(smileFactory);
×
242
        // ObjectMapper mapper = new JsonMapper();
UNCOV
243
        ObjectWriter objectWriter = mapper.writer().forType(HashMap.class);
×
244

UNCOV
245
        try (OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(outputFile))) {
×
UNCOV
246
            byte[] bytes = objectWriter.writeValueAsBytes(history.getTags());
×
UNCOV
247
            outputStream.write(bytes);
×
248
        }
UNCOV
249
    }
×
250

251
    private static ObjectWriter getObjectWriter() {
252
        SmileFactory smileFactory = new SmileFactory();
1✔
253
        // need header to enable shared string values
254
        smileFactory.configure(SmileGenerator.Feature.WRITE_HEADER, true);
1✔
255
        smileFactory.configure(SmileGenerator.Feature.CHECK_SHARED_STRING_VALUES, false);
1✔
256

257
        ObjectMapper mapper = new SmileMapper(smileFactory);
1✔
258
        // ObjectMapper mapper = new JsonMapper();
259
        return mapper.writer().forType(HistoryEntry.class);
1✔
260
    }
261

262
    private void safelyRename(File output, File cacheFile) throws HistoryException {
263
        if (!cacheFile.delete() && cacheFile.exists()) {
1✔
264
            if (!output.delete()) {
×
265
                LOGGER.log(Level.WARNING, "Failed to remove temporary cache file ''{0}''", cacheFile);
×
266
            }
267
            throw new HistoryException(String.format("Cache file '%s' exists, and could not be deleted.",
×
268
                    cacheFile));
269
        }
270
        if (!output.renameTo(cacheFile)) {
1✔
271
            try {
272
                Files.delete(output.toPath());
×
273
            } catch (IOException e) {
×
274
                throw new HistoryException("failed to delete output file", e);
×
275
            }
×
276
            throw new HistoryException(String.format("Failed to rename cache temporary file '%s' to '%s'",
×
277
                    output, cacheFile));
278
        }
279
    }
1✔
280

281
    private static File getTagsFile(File file) {
UNCOV
282
        return new File(file.getAbsolutePath() + ".t");
×
283
    }
284

285
    @Override
286
    public void storeFile(History history, File file, Repository repository) throws HistoryException {
UNCOV
287
        storeFile(history, file, repository, false);
×
UNCOV
288
    }
×
289

290
    /**
291
     * Store {@link History} object in a file.
292
     *
293
     * @param histNew history object to store
294
     * @param file file to store the history object into
295
     * @param repo repository for the file
296
     * @param mergeHistory whether to merge the history with existing or store the histNew as is
297
     * @throws HistoryException if there was any problem with history cache generation
298
     */
299
    private void storeFile(History histNew, File file, Repository repo, boolean mergeHistory) throws HistoryException {
300
        File cacheFile;
301
        try {
302
            cacheFile = getCachedFile(file);
1✔
303
        } catch (CacheException e) {
×
304
            throw new HistoryException(e);
×
305
        }
1✔
306

307
        File dir = cacheFile.getParentFile();
1✔
308
        // calling isDirectory twice to prevent a race condition
309
        if (!dir.isDirectory() && !dir.mkdirs() && !dir.isDirectory()) {
1✔
310
            throw new HistoryException("Unable to create cache directory '" + dir + "'.");
×
311
        }
312

313
        if (LOGGER.isLoggable(Level.FINEST)) {
1✔
314
            LOGGER.log(Level.FINEST, "writing history entries to ''{0}'': {1}",
×
315
                    new Object[]{cacheFile, histNew.getRevisionList()});
×
316
        }
317

318
        final File outputFile;
319
        try {
320
            outputFile = File.createTempFile("ogtmp", null, dir);
1✔
321
            writeHistoryTo(histNew, outputFile);
1✔
322
        } catch (IOException ioe) {
×
323
            throw new HistoryException("Failed to write history", ioe);
×
324
        }
1✔
325

326
        boolean assignTags = env.isTagsEnabled() && repo.hasFileBasedTags();
1✔
327

328
        // Append the contents of the pre-existing cache file to the temporary file.
329
        if (mergeHistory && cacheFile.exists()) {
1✔
330
            try (OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(outputFile, true))) {
1✔
331
                SmileFactory factory = new SmileFactory();
1✔
332
                ObjectMapper mapper = new SmileMapper();
1✔
333
                ObjectWriter objectWriter = getObjectWriter();
1✔
334
                try (SmileParser parser = factory.createParser(cacheFile)) {
1✔
335
                    parser.setCodec(mapper);
1✔
336
                    Iterator<HistoryEntry> historyEntryIterator = parser.readValuesAs(HistoryEntry.class);
1✔
337
                    while (historyEntryIterator.hasNext()) {
1✔
338
                        HistoryEntry historyEntry = historyEntryIterator.next();
1✔
339
                        byte[] bytes = objectWriter.writeValueAsBytes(historyEntry);
1✔
340
                        outputStream.write(bytes);
1✔
341
                        if (assignTags) {
1✔
UNCOV
342
                            histNew.getHistoryEntries().add(historyEntry);
×
343
                        }
344
                    }
1✔
345
                }
346
            } catch (IOException ioe) {
×
347
                throw new HistoryException("Failed to write history", ioe);
×
348
            }
1✔
349

350
            // Re-tag the changesets in case there have been some new
351
            // tags added to the repository. Technically we should just
352
            // re-tag the last revision from the listOld however this
353
            // does not solve the problem when listNew contains new tags
354
            // retroactively tagging changesets from listOld, so we resort
355
            // to this somewhat crude solution of re-tagging from scratch.
356
            if (assignTags) {
1✔
UNCOV
357
                histNew.strip();
×
UNCOV
358
                repo.assignTagsInHistory(histNew);
×
359
            }
360
        }
361

362
        // Generate the file with a temporary name and move it into place when
363
        // done, so it is not necessary to protect the readers for partially updated
364
        // files.
365
        if (assignTags) {
1✔
366
            // Store tags in separate file.
367
            // Ideally that should be done using the cycle above to avoid dealing with complete History instance.
UNCOV
368
            File outputTagsFile = getTagsFile(outputFile);
×
369
            try {
UNCOV
370
                writeTagsTo(outputTagsFile, histNew);
×
371
            } catch (IOException ioe) {
×
372
                throw new HistoryException("Failed to write tags", ioe);
×
UNCOV
373
            }
×
374

UNCOV
375
            safelyRename(outputTagsFile, getTagsFile(cacheFile));
×
376
        }
377

378
        safelyRename(outputFile, cacheFile);
1✔
379
    }
1✔
380

381
    private void finishStore(Repository repository, String latestRev) throws CacheException {
382
        String histDir = CacheUtil.getRepositoryCacheDataDirname(repository, this);
1✔
383
        if (histDir == null || !(new File(histDir)).isDirectory()) {
1✔
384
            // If the history was not created for some reason (e.g. temporary
385
            // failure), do not create the CachedRevision file as this would
386
            // create confusion (once it starts working again).
387
            throw new CacheException(String.format("Could not store history for repository %s: '%s' is not a directory",
×
388
                repository, histDir));
389
        } else {
390
            storeLatestCachedRevision(repository, latestRev);
1✔
391
        }
392
    }
1✔
393

394
    @Override
395
    public void store(History history, Repository repository) throws CacheException {
396
        store(history, repository, null);
1✔
397
    }
1✔
398

399
    /**
400
     * Go through history entries for this repository acquired through
401
     * history/log command executed for top-level directory of the repo
402
     * and parsed into {@link HistoryEntry} structures and create hash map which
403
     * maps file names into list of HistoryEntry structures corresponding
404
     * to changesets in which the file was modified.
405
     * @return latest revision
406
     */
407
    private String createFileMap(History history, HashMap<String, List<HistoryEntry>> map) {
408
        String latestRev = null;
1✔
409
        HashMap<String, Boolean> acceptanceCache = new HashMap<>();
1✔
410

411
        for (HistoryEntry e : history.getHistoryEntries()) {
1✔
412
            // The history entries are sorted from newest to oldest.
413
            if (latestRev == null) {
1✔
414
                latestRev = e.getRevision();
1✔
415
            }
416
            for (String s : e.getFiles()) {
1✔
417
                /*
418
                 * We do not want to generate history cache for files which
419
                 * do not currently exist in the repository.
420
                 *
421
                 * Also, we cache the result of this evaluation to boost
422
                 * performance, since a particular file can appear in many
423
                 * repository revisions.
424
                 */
425
                File test = new File(env.getSourceRootPath() + s);
1✔
426
                String testKey = test.getAbsolutePath();
1✔
427
                Boolean cachedAcceptance = acceptanceCache.get(testKey);
1✔
428
                if (cachedAcceptance != null) {
1✔
429
                    if (!cachedAcceptance) {
1✔
430
                        continue;
1✔
431
                    }
432
                } else {
433
                    boolean testResult = test.exists() && pathAccepter.accept(test);
1✔
434
                    acceptanceCache.put(testKey, testResult);
1✔
435
                    if (!testResult) {
1✔
436
                        continue;
1✔
437
                    }
438
                }
439

440
                List<HistoryEntry> list = map.computeIfAbsent(s, k -> new ArrayList<>());
1✔
441

442
                list.add(e);
1✔
443
            }
1✔
444
        }
1✔
445
        return latestRev;
1✔
446
    }
447

448
    private static String getRevisionString(String revision) {
449
        if (revision == null) {
1✔
450
            return "end of history";
1✔
451
        } else {
452
            return "revision " + revision;
1✔
453
        }
454
    }
455

456
    /**
457
     * Store history for the whole repository in directory hierarchy resembling
458
     * the original repository structure. History of individual files will be
459
     * stored under this hierarchy, each file containing history of
460
     * corresponding source file.
461
     *
462
     * <p>
463
     * <b>Note that the history object will be changed in the process of storing the history into cache.
464
     * Namely the list of files from the history entries will be stripped.</b>
465
     * </p>
466
     *
467
     * @param history history object to process into per-file histories
468
     * @param repository repository object
469
     * @param tillRevision end revision (can be null)
470
     */
471
    @Override
472
    public void store(History history, Repository repository, String tillRevision) throws CacheException {
473

474
        final boolean handleRenamedFiles = repository.isHandleRenamedFiles();
1✔
475

476
        String latestRev = null;
1✔
477

478
        // Return immediately when there is nothing to do.
479
        List<HistoryEntry> entries = history.getHistoryEntries();
1✔
480
        if (entries.isEmpty()) {
1✔
481
            return;
1✔
482
        }
483

484
        HashMap<String, List<HistoryEntry>> map = new HashMap<>();
1✔
485
        String fileMapLatestRev = createFileMap(history, map);
1✔
486
        if (history.getLatestRev() != null) {
1✔
487
            latestRev = history.getLatestRev();
1✔
488
        } else {
489
            latestRev = fileMapLatestRev;
1✔
490
        }
491

492
        // File based history cache does not store files for individual changesets so strip them.
493
        history.strip();
1✔
494

495
        String repoCachePath = CacheUtil.getRepositoryCacheDataDirname(repository, this);
1✔
496
        if (repoCachePath == null) {
1✔
497
            throw new CacheException(String.format("failed to get cache directory path for %s", repository));
1✔
498
        }
499
        File histDataDir = new File(repoCachePath);
1✔
500
        // Check the directory again in case of races (might happen in the presence of sub-repositories).
501
        if (!histDataDir.isDirectory() && !histDataDir.mkdirs() && !histDataDir.isDirectory()) {
1✔
502
            throw new CacheException(String.format("cannot create history cache directory for '%s'", histDataDir));
×
503
        }
504

505
        Set<String> regularFiles = map.keySet().stream().
1✔
506
                filter(e -> !history.isRenamed(e)).collect(Collectors.toSet());
1✔
507
        createDirectoriesForFiles(regularFiles, repository, "regular files for history till " +
1✔
508
                getRevisionString(tillRevision));
1✔
509

510
        /*
511
         * Now traverse the list of files from the hash map built above and for each file store its history
512
         * (saved in the value of the hash map entry for the file) in a file.
513
         * The renamed files will be handled separately.
514
         */
515
        Level logLevel = Level.FINE;
1✔
516
        LOGGER.log(logLevel, "Storing history for {0} regular files in repository {1} till {2}",
1✔
517
                new Object[]{regularFiles.size(), repository, getRevisionString(tillRevision)});
1✔
518
        final File root = env.getSourceRootFile();
1✔
519

520
        final CountDownLatch latch = new CountDownLatch(regularFiles.size());
1✔
521
        AtomicInteger fileHistoryCount = new AtomicInteger();
1✔
522
        try (Progress progress = new Progress(LOGGER,
1✔
523
                String.format("history cache for regular files of %s till %s", repository,
1✔
524
                        getRevisionString(tillRevision)),
1✔
525
                regularFiles.size(), logLevel)) {
1✔
526
            for (String file : regularFiles) {
1✔
527
                env.getIndexerParallelizer().getHistoryFileExecutor().submit(() -> {
1✔
528
                    try {
529
                        doFileHistory(file, new History(map.get(file)), repository, root, false);
1✔
530
                        fileHistoryCount.getAndIncrement();
1✔
531
                    } catch (Exception ex) {
×
532
                        // We want to catch any exception since we are in a thread.
533
                        LOGGER.log(Level.WARNING, "doFileHistory() got exception ", ex);
×
534
                    } finally {
535
                        latch.countDown();
1✔
536
                        progress.increment();
1✔
537
                    }
538
                });
1✔
539
            }
1✔
540

541
            // Wait for the executors to finish.
542
            try {
543
                latch.await();
1✔
544
            } catch (InterruptedException ex) {
×
545
                LOGGER.log(Level.SEVERE, "latch exception", ex);
×
546
            }
1✔
547
            LOGGER.log(logLevel, "Stored history for {0} regular files in repository {1}",
1✔
548
                    new Object[]{fileHistoryCount, repository});
549
        }
550

551
        if (!handleRenamedFiles) {
1✔
552
            finishStore(repository, latestRev);
1✔
553
            return;
1✔
554
        }
555

556
        storeRenamed(history.getRenamedFiles(), repository, tillRevision);
1✔
557

558
        finishStore(repository, latestRev);
1✔
559
    }
1✔
560

561
    /**
562
     * handle renamed files (in parallel).
563
     * @param renamedFiles set of renamed file paths
564
     * @param repository repository
565
     * @param tillRevision end revision (can be null)
566
     */
567
    public void storeRenamed(Set<String> renamedFiles, Repository repository, String tillRevision) throws CacheException {
568
        final File root = env.getSourceRootFile();
1✔
569
        if (renamedFiles.isEmpty()) {
1✔
570
            return;
1✔
571
        }
572

573
        renamedFiles = renamedFiles.stream().filter(f -> new File(env.getSourceRootPath() + f).exists()).
1✔
574
                collect(Collectors.toSet());
1✔
575
        Level logLevel = Level.FINE;
1✔
576
        LOGGER.log(logLevel, "Storing history for {0} renamed files in repository {1} till {2}",
1✔
577
                new Object[]{renamedFiles.size(), repository, getRevisionString(tillRevision)});
1✔
578

579
        createDirectoriesForFiles(renamedFiles, repository, "renamed files for history " +
1✔
580
                getRevisionString(tillRevision));
1✔
581

582
        final Repository repositoryF = repository;
1✔
583
        final CountDownLatch latch = new CountDownLatch(renamedFiles.size());
1✔
584
        AtomicInteger renamedFileHistoryCount = new AtomicInteger();
1✔
585
        try (Progress progress = new Progress(LOGGER,
1✔
586
                String.format("history cache for renamed files of %s till %s", repository,
1✔
587
                        getRevisionString(tillRevision)),
1✔
588
                renamedFiles.size(), logLevel)) {
1✔
589
            for (final String file : renamedFiles) {
1✔
590
                env.getIndexerParallelizer().getHistoryFileExecutor().submit(() -> {
1✔
591
                    try {
592
                        doRenamedFileHistory(file,
1✔
593
                                new File(env.getSourceRootPath() + file),
1✔
594
                                repositoryF, root, tillRevision);
595
                        renamedFileHistoryCount.getAndIncrement();
1✔
596
                    } catch (Exception ex) {
×
597
                        // We want to catch any exception since we are in thread.
598
                        LOGGER.log(Level.WARNING, "doFileHistory() got exception ", ex);
×
599
                    } finally {
600
                        latch.countDown();
1✔
601
                        progress.increment();
1✔
602
                    }
603
                });
1✔
604
            }
1✔
605

606
            // Wait for the executors to finish.
607
            try {
608
                // Wait for the executors to finish.
609
                latch.await();
1✔
610
            } catch (InterruptedException ex) {
×
611
                LOGGER.log(Level.SEVERE, "latch exception", ex);
×
612
            }
1✔
613
        }
614
        LOGGER.log(logLevel, "Stored history for {0} renamed files in repository {1}",
1✔
615
                new Object[]{renamedFileHistoryCount.intValue(), repository});
1✔
616
    }
1✔
617

618
    private void createDirectoriesForFiles(Set<String> files, Repository repository, String label) {
619

620
        // The directories for the files have to be created before
621
        // the actual files otherwise storeFile() might be racing for
622
        // mkdirs() if there are multiple files from single directory
623
        // handled in parallel.
624
        Statistics elapsed = new Statistics();
1✔
625
        LOGGER.log(Level.FINE, "Starting directory creation for {0} ({1}): {2} directories",
1✔
626
                new Object[]{repository, label, files.size()});
1✔
627
        for (final String file : files) {
1✔
628
            File cache;
629
            try {
630
                cache = getCachedFile(new File(env.getSourceRootPath() + file));
1✔
631
            } catch (CacheException ex) {
×
632
                LOGGER.log(Level.FINER, ex.getMessage());
×
633
                continue;
×
634
            }
1✔
635
            File dir = cache.getParentFile();
1✔
636

637
            if (!dir.isDirectory() && !dir.mkdirs()) {
1✔
638
                LOGGER.log(Level.WARNING, "Unable to create cache directory ''{0}''", dir);
×
639
            }
640
        }
1✔
641
        elapsed.report(LOGGER, Level.FINE, String.format("Done creating directories for %s (%s)", repository, label));
1✔
642
    }
1✔
643

644
    @Override
645
    public History get(File file, Repository repository, boolean withFiles) throws CacheException {
646

647
        if (file.isDirectory()) {
1✔
648
            return null;
×
649
        }
650

651
        if (isUpToDate(file)) {
1✔
652
            File cacheFile = getCachedFile(file);
1✔
653
            try {
654
                if (fileHistoryCacheHits != null) {
1✔
655
                    fileHistoryCacheHits.increment();
1✔
656
                }
657
                return readHistory(cacheFile, repository);
1✔
658
            } catch (IOException e) {
×
659
                LOGGER.log(Level.WARNING, String.format("Error when reading cache file '%s'", cacheFile), e);
×
660
            }
661
        }
662

663
        if (fileHistoryCacheMisses != null) {
1✔
664
            fileHistoryCacheMisses.increment();
1✔
665
        }
666

667
        return null;
1✔
668
    }
669

670
    @Override
671
    @Nullable
672
    public HistoryEntry getLastHistoryEntry(File file) throws CacheException {
673
        if (isUpToDate(file)) {
1✔
674
            File cacheFile = getCachedFile(file);
1✔
675
            try {
676
                if (fileHistoryCacheHits != null) {
1✔
677
                    fileHistoryCacheHits.increment();
1✔
678
                }
679
                return readLastHistoryEntry(cacheFile);
1✔
680
            } catch (IOException e) {
×
681
                LOGGER.log(Level.WARNING, String.format("Error when reading cache file '%s'", cacheFile), e);
×
682
            }
683
        }
684

685
        if (fileHistoryCacheMisses != null) {
1✔
686
            fileHistoryCacheMisses.increment();
1✔
687
        }
688

689
        return null;
1✔
690
    }
691

692
    /**
693
     * @param file the file to check
694
     * @return {@code true} if the cache is up-to-date for the file, {@code false} otherwise
695
     */
696
    public boolean isUpToDate(File file) throws CacheException {
697
        File cachedFile = getCachedFile(file);
1✔
698
        return cachedFile != null && cachedFile.exists() && file.lastModified() <= cachedFile.lastModified();
1✔
699
    }
700

701
    private String getRepositoryCachedRevPath(RepositoryInfo repository) {
702
        String histDir = CacheUtil.getRepositoryCacheDataDirname(repository, this);
1✔
703
        if (histDir == null) {
1✔
704
            return null;
1✔
705
        }
706
        return histDir + File.separatorChar + LATEST_REV_FILE_NAME;
1✔
707
    }
708

709
    /**
710
     * Store latest indexed revision for the repository under data directory.
711
     * @param repository repository
712
     * @param rev latest revision which has been just indexed
713
     */
714
    private void storeLatestCachedRevision(Repository repository, String rev) {
715
        Path newPath = Path.of(getRepositoryCachedRevPath(repository));
1✔
716
        try (Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newPath.toFile())))) {
1✔
717
            writer.write(rev);
1✔
718
        } catch (IOException ex) {
×
719
            LOGGER.log(Level.WARNING,
×
720
                    String.format("Cannot write latest cached revision to file for repository %s", repository), ex);
×
721
        }
1✔
722
    }
1✔
723

724
    @Override
725
    @Nullable
726
    public String getLatestCachedRevision(Repository repository) {
727
        return getCachedRevision(repository, getRepositoryCachedRevPath(repository));
1✔
728
    }
729

730
    @Nullable
731
    private String getCachedRevision(Repository repository, String revPath) {
732
        String rev;
733
        BufferedReader input;
734

735
        if (revPath == null) {
1✔
736
            LOGGER.log(Level.WARNING, "no rev path for repository {0}", repository);
1✔
737
            return null;
1✔
738
        }
739

740
        try {
741
            input = new BufferedReader(new FileReader(revPath));
1✔
742
            try {
743
                rev = input.readLine();
1✔
744
            } catch (java.io.IOException e) {
×
745
                LOGGER.log(Level.WARNING, "failed to load ", e);
×
746
                return null;
×
747
            } finally {
748
                try {
749
                    input.close();
1✔
750
                } catch (java.io.IOException e) {
×
751
                    LOGGER.log(Level.WARNING, "failed to close", e);
×
752
                }
1✔
753
            }
754
        } catch (java.io.FileNotFoundException e) {
1✔
755
            LOGGER.log(Level.FINE,
1✔
756
                "not loading latest cached revision file from {0}", revPath);
757
            return null;
1✔
758
        }
1✔
759

760
        return rev;
1✔
761
    }
762

763
    /**
764
     * Attempt to fill the date and description for the input instances from pertaining last history entries.
765
     * @param entries list of {@link DirectoryEntry} instances
766
     * @return true if all of them were filled, false otherwise in which case the date/description field
767
     * for the entries will be zeroed.
768
     */
769
    @Override
770
    public boolean fillLastHistoryEntries(List<DirectoryEntry> entries) {
771
        if (entries == null) {
1✔
772
            return false;
×
773
        }
774

775
        boolean ret = true;
1✔
776

777
        Statistics statistics = new Statistics();
1✔
778

779
        final ExecutorService executor = env.getDirectoryListingExecutor();
1✔
780
        Set<Future<Boolean>> futures = new HashSet<>();
1✔
781
        for (DirectoryEntry directoryEntry : entries) {
1✔
782
            futures.add(executor.submit(() -> {
1✔
783
                try {
784
                    File file = directoryEntry.getFile();
1✔
785
                    if (file.isDirectory()) {
1✔
786
                        directoryEntry.setDescription("-");
1✔
787
                        directoryEntry.setDate(null);
1✔
788
                        return true;
1✔
789
                    }
790

791
                    HistoryEntry historyEntry = getLastHistoryEntry(file);
1✔
792
                    if (historyEntry != null && historyEntry.getDate() != null) {
1✔
793
                        directoryEntry.setDescription(historyEntry.getDescription());
1✔
794
                        directoryEntry.setDate(historyEntry.getDate());
1✔
795
                    } else {
796
                        LOGGER.log(Level.FINE, "cannot get last history entry for ''{0}''",
1✔
797
                                directoryEntry.getFile());
1✔
798
                        return false;
1✔
799
                    }
800
                } catch (CacheException e) {
×
801
                    LOGGER.log(Level.FINER, "cannot get last history entry for ''{0}''", directoryEntry.getFile());
×
802
                    return false;
×
803
                }
1✔
804
                return true;
1✔
805
            }));
806
        }
1✔
807

808
        // Wait for all the futures to complete. This is important as they are modifying the input parameter.
809
        for (Future<Boolean> future : futures) {
1✔
810
            try {
811
                if (Boolean.FALSE.equals(future.get())) {
1✔
812
                    ret = false;
1✔
813
                }
814
            } catch (Exception e) {
×
815
                ret = false;
×
816
            }
1✔
817
        }
1✔
818

819
        statistics.report(LOGGER, Level.FINER, "done filling directory entries");
1✔
820

821
        // Enforce the all-or-nothing semantics.
822
        if (!ret) {
1✔
823
            entries.forEach(e -> e.setDate(null));
1✔
824
            entries.forEach(e -> e.setDescription(null));
1✔
825
        }
826

827
        return ret;
1✔
828
    }
829

830
    @Override
831
    public void clear(RepositoryInfo repository) {
832
        String revPath = getRepositoryCachedRevPath(repository);
1✔
833
        if (revPath != null) {
1✔
834
            // remove the file cached last revision (done separately in case
835
            // it gets ever moved outside the hierarchy)
836
            File cachedRevFile = new File(revPath);
1✔
837
            try {
838
                Files.delete(cachedRevFile.toPath());
1✔
839
            } catch (IOException e) {
1✔
840
                LOGGER.log(Level.WARNING, String.format("failed to delete '%s'", cachedRevFile), e);
1✔
841
            }
1✔
842
        }
843

844
        CacheUtil.clearCacheDir(repository, this);
1✔
845
    }
1✔
846

847
    @Override
848
    public String getInfo() {
849
        return getClass().getSimpleName();
1✔
850
    }
851

852
    @Override
853
    public String getCacheDirName() {
854
        return HISTORY_CACHE_DIR_NAME;
1✔
855
    }
856
}
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