• 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

86.3
/opengrok-indexer/src/main/java/org/opengrok/indexer/history/GitRepository.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) 2017, 2020, Chris Fraire <cfraire@me.com>.
23
 * Portions Copyright (c) 2019, Krystof Tulinger <k.tulinger@seznam.cz>.
24
 * Portions Copyright (c) 2023, Ric Harris <harrisric@users.noreply.github.com>.
25
 */
26
package org.opengrok.indexer.history;
27

28
import java.io.File;
29
import java.io.IOException;
30
import java.io.OutputStream;
31
import java.nio.charset.StandardCharsets;
32
import java.nio.file.Path;
33
import java.nio.file.Paths;
34
import java.util.Date;
35
import java.util.HashSet;
36
import java.util.List;
37
import java.util.HashMap;
38
import java.util.Map;
39
import java.util.Scanner;
40
import java.util.Set;
41
import java.util.SortedSet;
42
import java.util.TreeSet;
43
import java.util.concurrent.ExecutionException;
44
import java.util.concurrent.ExecutorService;
45
import java.util.concurrent.Executors;
46
import java.util.concurrent.Future;
47
import java.util.concurrent.TimeUnit;
48
import java.util.concurrent.TimeoutException;
49
import java.util.function.Consumer;
50
import java.util.logging.Level;
51
import java.util.logging.Logger;
52

53
import org.eclipse.jgit.api.BlameCommand;
54
import org.eclipse.jgit.api.Git;
55
import org.eclipse.jgit.api.errors.GitAPIException;
56
import org.eclipse.jgit.blame.BlameResult;
57
import org.eclipse.jgit.diff.DiffEntry;
58
import org.eclipse.jgit.diff.DiffFormatter;
59
import org.eclipse.jgit.diff.RawText;
60
import org.eclipse.jgit.diff.RawTextComparator;
61
import org.eclipse.jgit.lib.Config;
62
import org.eclipse.jgit.lib.Constants;
63
import org.eclipse.jgit.lib.ObjectId;
64
import org.eclipse.jgit.lib.ObjectLoader;
65
import org.eclipse.jgit.lib.ObjectReader;
66
import org.eclipse.jgit.lib.PersonIdent;
67
import org.eclipse.jgit.lib.Ref;
68
import org.eclipse.jgit.lib.Repository;
69
import org.eclipse.jgit.revwalk.FollowFilter;
70
import org.eclipse.jgit.revwalk.RevCommit;
71
import org.eclipse.jgit.revwalk.RevTree;
72
import org.eclipse.jgit.revwalk.RevWalk;
73
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
74
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
75
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
76
import org.eclipse.jgit.treewalk.TreeWalk;
77
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
78
import org.eclipse.jgit.treewalk.filter.PathFilter;
79
import org.eclipse.jgit.treewalk.filter.TreeFilter;
80
import org.eclipse.jgit.util.io.CountingOutputStream;
81
import org.eclipse.jgit.util.io.NullOutputStream;
82
import org.jetbrains.annotations.NotNull;
83
import org.jetbrains.annotations.Nullable;
84
import org.opengrok.indexer.configuration.CommandTimeoutType;
85
import org.opengrok.indexer.configuration.OpenGrokThreadFactory;
86
import org.opengrok.indexer.configuration.RuntimeEnvironment;
87
import org.opengrok.indexer.logger.LoggerFactory;
88
import org.opengrok.indexer.util.ForbiddenSymlinkException;
89
import org.opengrok.indexer.util.Progress;
90

91
import static org.opengrok.indexer.history.History.TAGS_SEPARATOR;
92

93
/**
94
 * Access to a Git repository.
95
 *
96
 */
97
public class GitRepository extends RepositoryWithHistoryTraversal {
98

99
    private static final Logger LOGGER = LoggerFactory.getLogger(GitRepository.class);
1✔
100

101
    private static final long serialVersionUID = -6126297612958508386L;
102

103
    public static final int GIT_ABBREV_LEN = 8;
104
    public static final int MAX_CHANGESETS = 65536;
105

106
    public GitRepository() {
1✔
107
        type = "git";
1✔
108

109
        ignoredDirs.add(".git");
1✔
110
        ignoredFiles.add(".git");
1✔
111
    }
1✔
112

113
    @Override
114
    public boolean isMergeCommitsSupported() {
115
        return true;
1✔
116
    }
117

118
    /**
119
     * Be careful, git uses only forward slashes in its command and output (not in file path).
120
     * Using backslashes together with git show will get empty output and 0 status code.
121
     * @return string with separator characters replaced with forward slash
122
     */
123
    private static String getGitFilePath(String filePath) {
124
        return filePath.replace(File.separatorChar, '/');
1✔
125
    }
126

127
    /**
128
     * Try to get file contents for given revision.
129
     *
130
     * @param out a required OutputStream
131
     * @param fullpath full pathname of the file
132
     * @param rev revision string
133
     * @return a defined instance with {@code success} == {@code true} if no
134
     * error occurred and with non-zero {@code iterations} if some data was transferred
135
     */
136
    private HistoryRevResult getHistoryRev(OutputStream out, String fullpath, String rev) {
137

138
        HistoryRevResult result = new HistoryRevResult();
1✔
139
        File directory = new File(getDirectoryName());
1✔
140

141
        String filename;
142
        result.success = false;
1✔
143
        try {
144
            filename = getGitFilePath(Paths.get(getCanonicalDirectoryName()).relativize(Paths.get(fullpath)).toString());
1✔
145
        } catch (IOException e) {
×
146
            LOGGER.log(Level.WARNING, String.format("Failed to relativize '%s' for '%s'",
×
147
                    fullpath, directory), e);
148
            return result;
×
149
        }
1✔
150

151
        try (org.eclipse.jgit.lib.Repository repository = getJGitRepository(directory.getAbsolutePath())) {
1✔
152
            ObjectId commitId = repository.resolve(rev);
1✔
153

154
            // A RevWalk allows walking over commits based on some filtering that is defined.
155
            try (RevWalk revWalk = new RevWalk(repository)) {
1✔
156
                RevCommit commit = revWalk.parseCommit(commitId);
1✔
157
                // and using commit's tree find the path
158
                RevTree tree = commit.getTree();
1✔
159

160
                // Now try to find a specific file.
161
                try (TreeWalk treeWalk = new TreeWalk(repository)) {
1✔
162
                    treeWalk.addTree(tree);
1✔
163
                    treeWalk.setRecursive(true);
1✔
164
                    treeWalk.setFilter(PathFilter.create(filename));
1✔
165
                    if (!treeWalk.next()) {
1✔
166
                        LOGGER.log(Level.FINEST, "Did not find expected file ''{0}'' in revision {1} " +
1✔
167
                                "for ''{2}''", new Object[] {filename, rev, directory});
168
                        return result;
1✔
169
                    }
170

171
                    ObjectId objectId = treeWalk.getObjectId(0);
1✔
172
                    ObjectLoader loader = repository.open(objectId);
1✔
173

174
                    CountingOutputStream countingOutputStream = new CountingOutputStream(out);
1✔
175
                    loader.copyTo(countingOutputStream);
1✔
176
                    result.iterations = countingOutputStream.getCount();
1✔
177
                    result.success = true;
1✔
178
                }
1✔
179

180
                revWalk.dispose();
1✔
181
            }
1✔
182
        } catch (IOException e) {
1✔
183
            LOGGER.log(Level.WARNING, String.format("Failed to get file '%s' in revision %s for '%s'",
×
184
                    filename, rev, directory), e);
185
        }
1✔
186

187
        return result;
1✔
188
    }
189

190
    @Override
191
    boolean getHistoryGet(OutputStream out, String parent, String basename, String rev) {
192

193
        String fullPath;
194
        try {
195
            fullPath = new File(parent, basename).getCanonicalPath();
1✔
196
        } catch (IOException e) {
×
197
            LOGGER.log(Level.WARNING, e, () -> String.format(
×
198
                    "Failed to get canonical path: %s/%s", parent, basename));
199
            return false;
×
200
        }
1✔
201

202
        HistoryRevResult result = getHistoryRev(out, fullPath, rev);
1✔
203
        if (!result.success && result.iterations < 1) {
1✔
204
            /*
205
             * If we failed to get the contents it might be that the file was
206
             * renamed, so we need to find its original name in that revision
207
             * and retry with the original name.
208
             */
209
            String origPath;
210
            try {
211
                origPath = findOriginalName(fullPath, rev);
1✔
212
            } catch (IOException exp) {
×
213
                LOGGER.log(Level.SEVERE, exp, () -> String.format(
×
214
                        "Failed to get original revision: %s/%s (revision %s)",
215
                        parent, basename, rev));
216
                return false;
×
217
            }
1✔
218

219
            if (origPath != null) {
1✔
220
                String fullRenamedPath;
221
                try {
222
                    fullRenamedPath = Paths.get(getCanonicalDirectoryName(), origPath).toString();
1✔
223
                } catch (IOException e) {
×
224
                    LOGGER.log(Level.WARNING, e, () -> String.format(
×
225
                            "Failed to get canonical path: .../%s", origPath));
226
                    return false;
×
227
                }
1✔
228
                if (!fullRenamedPath.equals(fullPath)) {
1✔
229
                    result = getHistoryRev(out, fullRenamedPath, rev);
1✔
230
                }
231
            }
232
        }
233

234
        return result.success;
1✔
235
    }
236

237
    private String getPathRelativeToCanonicalRepositoryRoot(String fullPath) throws IOException {
238
        String repoPath = getCanonicalDirectoryName() + File.separator;
1✔
239
        if (fullPath.startsWith(repoPath)) {
1✔
240
            return fullPath.substring(repoPath.length());
1✔
241
        }
242
        return fullPath;
×
243
    }
244

245
    /**
246
     * Get the name of file in given revision. The returned file name is relative to the repository root.
247
     * Assumes renamed file handling is on.
248
     *
249
     * @param fullpath full file path
250
     * @param changeset revision ID
251
     * @return original filename relative to the repository root
252
     * @throws java.io.IOException if I/O exception occurred
253
     * @see #getPathRelativeToCanonicalRepositoryRoot(String)
254
     */
255
    String findOriginalName(String fullpath, String changeset) throws IOException {
256

257
        if (fullpath == null || fullpath.isEmpty()) {
1✔
258
            throw new IOException(String.format("Invalid file path string: '%s'", fullpath));
1✔
259
        }
260

261
        if (changeset == null || changeset.isEmpty()) {
1✔
262
            throw new IOException(String.format("Invalid changeset string for '%s': %s",
×
263
                    fullpath, changeset));
264
        }
265

266
        String fileInRepo = getGitFilePath(getPathRelativeToCanonicalRepositoryRoot(fullpath));
1✔
267

268
        String originalFile = fileInRepo;
1✔
269
        try (org.eclipse.jgit.lib.Repository repository = getJGitRepository(getDirectoryName());
1✔
270
             RevWalk walk = new RevWalk(repository)) {
1✔
271

272
            walk.markStart(walk.parseCommit(repository.resolve(Constants.HEAD)));
1✔
273
            walk.markUninteresting(walk.lookupCommit(repository.resolve(changeset)));
1✔
274

275
            Config config = repository.getConfig();
1✔
276
            config.setBoolean("diff", null, "renames", true);
1✔
277
            org.eclipse.jgit.diff.DiffConfig dc = config.get(org.eclipse.jgit.diff.DiffConfig.KEY);
1✔
278
            FollowFilter followFilter = FollowFilter.create(getGitFilePath(fileInRepo), dc);
1✔
279
            walk.setTreeFilter(followFilter);
1✔
280

281
            for (RevCommit commit : walk) {
1✔
282
                if (commit.getParentCount() > 1 && !isMergeCommitsEnabled()) {
1✔
283
                    continue;
×
284
                }
285

286
                if (commit.getId().getName().equals(changeset)) {
1✔
287
                    break;
×
288
                }
289

290
                if (commit.getParentCount() >= 1) {
1✔
291
                    OutputStream outputStream = NullOutputStream.INSTANCE;
1✔
292
                    try (DiffFormatter formatter = new DiffFormatter(outputStream)) {
1✔
293
                        formatter.setRepository(repository);
1✔
294
                        formatter.setDetectRenames(true);
1✔
295

296
                        List<DiffEntry> diffs = formatter.scan(prepareTreeParser(repository, commit.getParent(0)),
1✔
297
                                prepareTreeParser(repository, commit));
1✔
298

299
                        for (DiffEntry diff : diffs) {
1✔
300
                            if (diff.getChangeType() == DiffEntry.ChangeType.RENAME &&
1✔
301
                                    originalFile.equals(diff.getNewPath())) {
1✔
302
                                originalFile = diff.getOldPath();
1✔
303
                            }
304
                        }
1✔
305
                    }
306
                }
307
            }
1✔
308
        }
309

310
        if (originalFile == null) {
1✔
311
            LOGGER.log(Level.WARNING, "Failed to get original name in revision {0} for: \"{1}\"",
×
312
                    new Object[]{changeset, fullpath});
313
            return null;
×
314
        }
315

316
        return getNativePath(originalFile);
1✔
317
    }
318

319
    /**
320
     * Annotate the specified file/revision.
321
     *
322
     * @param file file to annotate
323
     * @param revision revision to annotate
324
     * @return file annotation or {@code null}
325
     * @throws java.io.IOException if I/O exception occurred
326
     */
327
    @Override
328
    public Annotation annotate(File file, String revision) throws IOException {
329
        String filePath = getPathRelativeToCanonicalRepositoryRoot(file.getCanonicalPath());
1✔
330

331
        if (revision == null) {
1✔
332
            revision = getFirstRevision(filePath);
1✔
333
        }
334
        String fileName = Path.of(filePath).getFileName().toString();
1✔
335
        Annotation annotation = getAnnotation(revision, filePath, fileName);
1✔
336

337
        if (annotation.getRevisions().isEmpty() && isHandleRenamedFiles()) {
1✔
338
            // The file might have changed its location if it was renamed.
339
            // Try looking up its original name and get the annotation again.
340
            String origName = findOriginalName(file.getCanonicalPath(), revision);
1✔
341
            if (origName != null) {
1✔
342
                annotation = getAnnotation(revision, origName, fileName);
1✔
343
            }
344
        }
345

346
        return annotation;
1✔
347
    }
348

349
    private String getFirstRevision(String filePath) {
350
        String revision = null;
1✔
351
        try (org.eclipse.jgit.lib.Repository repository = getJGitRepository(getDirectoryName()); Git gitRepo = new Git(repository)) {
1✔
352
            Iterable<RevCommit> commits = gitRepo.log().
1✔
353
                    addPath(getGitFilePath(filePath)).
1✔
354
                    setMaxCount(1).
1✔
355
                    call();
1✔
356
            RevCommit commit = commits.iterator().next();
1✔
357
            if (commit != null) {
1✔
358
                revision = commit.getId().getName();
1✔
359
            } else {
360
                LOGGER.log(Level.WARNING, "cannot get first revision of ''{0}'' in repository ''{1}''",
×
361
                        new Object[] {filePath, getDirectoryName()});
×
362
            }
363
        } catch (IOException | GitAPIException e) {
×
364
            LOGGER.log(Level.WARNING,
×
365
                    String.format("cannot get first revision of '%s' in repository '%s'",
×
366
                            filePath, getDirectoryName()), e);
×
367
        }
1✔
368
        return revision;
1✔
369
    }
370

371
    @NotNull
372
    private Annotation getAnnotation(String revision, String filePath, String fileName) throws IOException {
373
        Annotation annotation = new Annotation(fileName);
1✔
374

375
        try (org.eclipse.jgit.lib.Repository repository = getJGitRepository(getDirectoryName()); Git gitRepo = new Git(repository)) {
1✔
376
            BlameCommand blameCommand = gitRepo.blame().setFilePath(getGitFilePath(filePath));
1✔
377
            ObjectId commitId = repository.resolve(revision);
1✔
378
            blameCommand.setStartCommit(commitId);
1✔
379
            blameCommand.setFollowFileRenames(isHandleRenamedFiles());
1✔
380
            final BlameResult result = blameCommand.setTextComparator(RawTextComparator.WS_IGNORE_ALL).call();
1✔
381
            if (result != null) {
1✔
382
                final RawText rawText = result.getResultContents();
1✔
383
                for (int i = 0; i < rawText.size(); i++) {
1✔
384
                    final PersonIdent sourceAuthor = result.getSourceAuthor(i);
1✔
385
                    final RevCommit sourceCommit = result.getSourceCommit(i);
1✔
386
                    annotation.addLine(
1✔
387
                      sourceCommit.getId().name(),
1✔
388
                      sourceAuthor.getName(), true,
1✔
389
                      sourceCommit.getId().abbreviate(GIT_ABBREV_LEN).name());
1✔
390
                }
391
            }
392
        } catch (GitAPIException e) {
×
393
            LOGGER.log(Level.FINER,
×
394
                    String.format("failed to get annotation for file '%s' in repository '%s' in revision '%s'",
×
395
                            filePath, getDirectoryName(), revision));
×
396
        }
1✔
397
        return annotation;
1✔
398
    }
399

400
    @Override
401
    public boolean fileHasAnnotation(File file) {
402
        return true;
1✔
403
    }
404

405
    @Override
406
    public boolean fileHasHistory(File file) {
407
        return true;
1✔
408
    }
409

410
    @Override
411
    boolean isRepositoryFor(File file, CommandTimeoutType cmdType) {
412
        if (file.isDirectory()) {
1✔
413
            File f = new File(file, Constants.DOT_GIT);
1✔
414
            // No check for directory or file as submodules contain '.git' file.
415
            return f.exists();
1✔
416
        }
417
        return false;
×
418
    }
419

420
    @Override
421
    boolean supportsSubRepositories() {
422
        return true;
1✔
423
    }
424

425
    /**
426
     * Gets a value indicating the instance is nestable.
427
     * @return {@code true}
428
     */
429
    @Override
430
    boolean isNestable() {
431
        return true;
1✔
432
    }
433

434
    @Override
435
    public boolean isWorking() {
436
        // TODO: check isBare() in JGit ?
437
        return true;
1✔
438
    }
439

440
    @Override
441
    boolean hasHistoryForDirectories() {
442
        return true;
1✔
443
    }
444

445
    @Override
446
    History getHistory(File file) throws HistoryException {
447
        return getHistory(file, null);
1✔
448
    }
449

450
    @Override
451
    History getHistory(File file, String sinceRevision) throws HistoryException {
452
        return getHistory(file, sinceRevision, null);
1✔
453
    }
454

455
    @Override
456
    public int getPerPartesCount() {
457
        return MAX_CHANGESETS;
1✔
458
    }
459

460
    @Override
461
    public void accept(String sinceRevision, Consumer<BoundaryChangesets.IdWithProgress> visitor, Progress progress)
462
            throws HistoryException {
463

464
        try (org.eclipse.jgit.lib.Repository repository = getJGitRepository(getDirectoryName());
1✔
465
             RevWalk walk = new RevWalk(repository)) {
1✔
466

467
            if (sinceRevision != null) {
1✔
468
                ObjectId objId = repository.resolve(sinceRevision);
1✔
469
                if (objId == null) {
1✔
470
                    throw new HistoryException("cannot resolve " + sinceRevision);
×
471
                }
472
                walk.markUninteresting(walk.lookupCommit(objId));
1✔
473
            }
474
            ObjectId objId = repository.resolve(Constants.HEAD);
1✔
475
            if (objId == null) {
1✔
476
                throw new HistoryException("cannot resolve HEAD");
1✔
477
            }
478
            walk.markStart(walk.parseCommit(objId));
1✔
479

480
            for (RevCommit commit : walk) {
1✔
481
                // Do not abbreviate the Id as this could cause AmbiguousObjectException in getHistory().
482
                visitor.accept(new BoundaryChangesets.IdWithProgress(commit.getId().name(), progress));
1✔
483
            }
1✔
484
        } catch (IOException e) {
1✔
485
            throw new HistoryException(e);
1✔
486
        }
1✔
487
    }
1✔
488

489
    @Nullable
490
    @Override
491
    public HistoryEntry getLastHistoryEntry(File file, boolean ui) throws HistoryException {
492
        History hist = getHistory(file, null, null, 1);
1✔
493
        return hist.getLastHistoryEntry();
1✔
494
    }
495

496
    @Override
497
    public History getHistory(File file, String sinceRevision, String tillRevision) throws HistoryException {
498
        return getHistory(file, sinceRevision, tillRevision, null);
1✔
499
    }
500

501
    @Override
502
    public void traverseHistory(File file, String sinceRevision, String tillRevision,
503
                              Integer numCommits, List<ChangesetVisitor> visitors) throws HistoryException {
504

505
        if (numCommits != null && numCommits <= 0) {
1✔
506
            throw new HistoryException("invalid number of commits to retrieve");
×
507
        }
508

509
        boolean isDirectory = file.isDirectory();
1✔
510

511
        try (org.eclipse.jgit.lib.Repository repository = getJGitRepository(getDirectoryName());
1✔
512
             RevWalk walk = new RevWalk(repository)) {
1✔
513

514
            setupWalk(file, sinceRevision, tillRevision, repository, walk);
1✔
515

516
            int num = 0;
1✔
517
            for (RevCommit commit : walk) {
1✔
518
                CommitInfo commitInfo = new CommitInfo(commit.getId().name(),
1✔
519
                        commit.getId().abbreviate(GIT_ABBREV_LEN).name(),
1✔
520
                        commit.getAuthorIdent().getWhen(), commit.getAuthorIdent().getName(),
1✔
521
                        commit.getAuthorIdent().getEmailAddress(), commit.getFullMessage());
1✔
522

523
                for (ChangesetVisitor visitor : visitors) {
1✔
524
                    if (isDirectory) {
1✔
525
                        SortedSet<String> files = new TreeSet<>();
1✔
526
                        final Set<String> renamedFiles = new HashSet<>();
1✔
527
                        final Set<String> deletedFiles = new HashSet<>();
1✔
528
                        getFilesForCommit(renamedFiles, files, deletedFiles, commit, repository);
1✔
529
                        visitor.accept(new ChangesetInfo(commitInfo, files, renamedFiles, deletedFiles,
1✔
530
                                commit.getParentCount() > 1));
1✔
531
                    } else {
1✔
532
                        visitor.accept(new ChangesetInfo(commitInfo));
1✔
533
                    }
534
                }
1✔
535

536
                if (numCommits != null && ++num >= numCommits) {
1✔
537
                    break;
1✔
538
                }
539
            }
1✔
540
        } catch (IOException | ForbiddenSymlinkException e) {
×
541
            throw new HistoryException(String.format("failed to get history for '%s'", file), e);
×
542
        }
1✔
543
    }
1✔
544

545
    private void setupWalk(File file, String sinceRevision, String tillRevision, Repository repository, RevWalk walk)
546
            throws IOException, ForbiddenSymlinkException {
547

548
        if (sinceRevision != null) {
1✔
549
            walk.markUninteresting(walk.lookupCommit(repository.resolve(sinceRevision)));
1✔
550
        }
551

552
        if (tillRevision != null) {
1✔
553
            walk.markStart(walk.lookupCommit(repository.resolve(tillRevision)));
1✔
554
        } else {
555
            walk.markStart(walk.parseCommit(repository.resolve(Constants.HEAD)));
1✔
556
        }
557

558
        String relativePath = RuntimeEnvironment.getInstance().getPathRelativeToSourceRoot(file);
1✔
559
        if (!getDirectoryNameRelative().equals(relativePath)) {
1✔
560
            if (isHandleRenamedFiles()) {
1✔
561
                Config config = repository.getConfig();
1✔
562
                config.setBoolean("diff", null, "renames", true);
1✔
563
                org.eclipse.jgit.diff.DiffConfig dc = config.get(org.eclipse.jgit.diff.DiffConfig.KEY);
1✔
564
                FollowFilter followFilter = FollowFilter.create(getGitFilePath(getRepoRelativePath(file)), dc);
1✔
565
                walk.setTreeFilter(followFilter);
1✔
566
            } else {
1✔
567
                walk.setTreeFilter(AndTreeFilter.create(
1✔
568
                        PathFilter.create(getGitFilePath(getRepoRelativePath(file))),
1✔
569
                        TreeFilter.ANY_DIFF));
570
            }
571
        }
572
    }
1✔
573

574
    /**
575
     * Accumulate list of changed/deleted/renamed files for given commit.
576
     * @param renamedFiles output: renamed files in this commit (if renamed file handling is enabled)
577
     * @param changedFiles output: changed files in this commit
578
     * @param deletedFiles output: deleted files in this commit
579
     * @param commit RevCommit object
580
     * @param repository repository object
581
     * @throws IOException on error traversing the commit tree
582
     */
583
    private void getFilesForCommit(Set<String> renamedFiles, SortedSet<String> changedFiles, Set<String> deletedFiles,
584
                                   RevCommit commit,
585
                                   Repository repository) throws IOException {
586

587
        if (commit.getParentCount() == 0) { // first commit - add all files
1✔
588
            try (TreeWalk treeWalk = new TreeWalk(repository)) {
1✔
589
                treeWalk.addTree(commit.getTree());
1✔
590
                treeWalk.setRecursive(true);
1✔
591

592
                while (treeWalk.next()) {
1✔
593
                    changedFiles.add(getNativePath(getDirectoryNameRelative()) + File.separator +
1✔
594
                            getNativePath(treeWalk.getPathString()));
1✔
595
                }
596
            }
597
        } else {
598
            getFilesBetweenCommits(repository, commit.getParent(0), commit, changedFiles, renamedFiles, deletedFiles);
1✔
599
        }
600
    }
1✔
601

602
    private static String getNativePath(String path) {
603
        if (!File.separator.equals("/")) {
1✔
UNCOV
604
            return path.replace("/", File.separator);
×
605
        }
606

607
        return path;
1✔
608
    }
609

610
    /**
611
     * Assemble list of changed/deleted/renamed files between a commit and its parent.
612
     * @param repository repository object
613
     * @param oldCommit parent commit
614
     * @param newCommit new commit (the method assumes oldCommit is its parent)
615
     * @param changedFiles output: set of changedFiles that changed (excludes renamed changedFiles)
616
     * @param renamedFiles output: set of renamed files (if renamed handling is enabled)
617
     * @param deletedFiles output: set of deleted files
618
     * @throws IOException on I/O problem
619
     */
620
    private void getFilesBetweenCommits(org.eclipse.jgit.lib.Repository repository,
621
                                        RevCommit oldCommit, RevCommit newCommit,
622
                                        Set<String> changedFiles, Set<String> renamedFiles, Set<String> deletedFiles)
623
            throws IOException {
624

625
        OutputStream outputStream = NullOutputStream.INSTANCE;
1✔
626
        try (DiffFormatter formatter = new DiffFormatter(outputStream)) {
1✔
627
            formatter.setRepository(repository);
1✔
628
            if (isHandleRenamedFiles()) {
1✔
629
                formatter.setDetectRenames(true);
1✔
630
            }
631

632
            List<DiffEntry> diffs = formatter.scan(prepareTreeParser(repository, oldCommit),
1✔
633
                    prepareTreeParser(repository, newCommit));
1✔
634

635
            for (DiffEntry diff : diffs) {
1✔
636
                String newPath = getNativePath(getDirectoryNameRelative()) + File.separator +
1✔
637
                        getNativePath(diff.getNewPath());
1✔
638

639
                handleDiff(changedFiles, renamedFiles, deletedFiles, diff, newPath);
1✔
640
            }
1✔
641
        }
642
    }
1✔
643

644
    private void handleDiff(Set<String> changedFiles, Set<String> renamedFiles, Set<String> deletedFiles,
645
                            DiffEntry diff, String newPath) {
646

647
        switch (diff.getChangeType()) {
1✔
648
            case DELETE:
649
                if (deletedFiles != null) {
1✔
650
                    // newPath would be "/dev/null"
651
                    String oldPath = getNativePath(getDirectoryNameRelative()) + File.separator +
1✔
652
                            getNativePath(diff.getOldPath());
1✔
653
                    deletedFiles.add(oldPath);
1✔
654
                }
1✔
655
                break;
656
            case RENAME:
657
                if (isHandleRenamedFiles()) {
1✔
658
                    renamedFiles.add(newPath);
1✔
659
                    if (deletedFiles != null) {
1✔
660
                        String oldPath = getNativePath(getDirectoryNameRelative()) + File.separator +
1✔
661
                                getNativePath(diff.getOldPath());
1✔
662
                        deletedFiles.add(oldPath);
1✔
663
                    }
1✔
664
                }
665
                break;
666
            default:
667
                if (changedFiles != null) {
1✔
668
                    // Added files (ChangeType.ADD) are treated as changed.
669
                    changedFiles.add(newPath);
1✔
670
                }
671
                break;
672
        }
673
    }
1✔
674

675
    private static AbstractTreeIterator prepareTreeParser(org.eclipse.jgit.lib.Repository repository,
676
                                                          RevCommit commit) throws IOException {
677
        // from the commit we can build the tree which allows us to construct the TreeParser
678
        try (RevWalk walk = new RevWalk(repository)) {
1✔
679
            RevTree tree = walk.parseTree(commit.getTree().getId());
1✔
680

681
            CanonicalTreeParser treeParser = new CanonicalTreeParser();
1✔
682
            try (ObjectReader reader = repository.newObjectReader()) {
1✔
683
                treeParser.reset(reader, tree.getId());
1✔
684
            }
685

686
            walk.dispose();
1✔
687

688
            return treeParser;
1✔
689
        }
690
    }
691

692
    @Override
693
    boolean hasFileBasedTags() {
694
        return true;
1✔
695
    }
696

697
    /**
698
     * @param dotGit {@code .git} file
699
     * @return value of the {@code gitdir} property from the file
700
     */
701
    private String getGitDirValue(File dotGit) {
702
        try (Scanner scanner = new Scanner(dotGit, StandardCharsets.UTF_8)) {
1✔
703
            while (scanner.hasNextLine()) {
1✔
704
                String line = scanner.nextLine();
1✔
705
                if (line.startsWith(Constants.GITDIR)) {
1✔
706
                    return line.substring(Constants.GITDIR.length());
1✔
707
                }
708
            }
×
709
        } catch (IOException e) {
1✔
710
            LOGGER.log(Level.WARNING, "failed to scan the contents of file ''{0}''", dotGit);
1✔
711
        }
×
712

713
        return null;
1✔
714
    }
715

716
    private org.eclipse.jgit.lib.Repository getJGitRepository(String directory) throws IOException {
717
        File dotGitFile = Paths.get(directory, Constants.DOT_GIT).toFile();
1✔
718
        if (dotGitFile.isDirectory()) {
1✔
719
            return FileRepositoryBuilder.create(dotGitFile);
1✔
720
        }
721

722
        // Assume this is a sub-module so dotGitFile is a file.
723
        String gitDirValue = getGitDirValue(dotGitFile);
1✔
724
        if (gitDirValue == null) {
1✔
725
            throw new IOException("cannot get gitDir value from " + dotGitFile);
1✔
726
        }
727

728
        // If the gitDir value is relative path, make it absolute.
729
        // This is necessary for the JGit Repository construction.
730
        File gitDirFile = new File(gitDirValue);
1✔
731
        if (!gitDirFile.isAbsolute()) {
1✔
732
            gitDirFile = new File(directory, gitDirValue);
1✔
733
        }
734

735
        return new FileRepositoryBuilder().setWorkTree(new File(directory)).setGitDir(gitDirFile).build();
1✔
736
    }
737

738
    private void rebuildTagList(File directory) {
739
        this.tagList = new TreeSet<>();
1✔
740
        try (org.eclipse.jgit.lib.Repository repository = getJGitRepository(directory.getAbsolutePath())) {
1✔
741
            try (Git git = new Git(repository)) {
1✔
742
                List<Ref> refList = git.tagList().call(); // refs sorted according to tag names
1✔
743
                Map<RevCommit, String> commit2Tags = new HashMap<>();
1✔
744
                for (Ref ref : refList) {
1✔
745
                    try {
746
                        RevCommit commit = getCommit(repository, ref);
1✔
747
                        String tagName = ref.getName().replace("refs/tags/", "");
1✔
748
                        commit2Tags.merge(commit, tagName, (oldValue, newValue) -> oldValue + TAGS_SEPARATOR + newValue);
1✔
749
                    } catch (IOException e) {
×
750
                        LOGGER.log(Level.FINEST,
×
751
                                String.format("cannot get tags for '%s'", directory.getAbsolutePath()), e);
×
752
                    }
1✔
753
                }
1✔
754

755
                for (Map.Entry<RevCommit, String> entry : commit2Tags.entrySet()) {
1✔
756
                    int commitTime = entry.getKey().getCommitTime();
1✔
757
                    Date date = new Date((long) commitTime * 1000);
1✔
758
                    GitTagEntry tagEntry = new GitTagEntry(entry.getKey().getName(),
1✔
759
                            date, entry.getValue());
1✔
760
                    this.tagList.add(tagEntry);
1✔
761
                }
1✔
762
            }
763
        } catch (IOException | GitAPIException e) {
×
764
            LOGGER.log(Level.WARNING, String.format("cannot get tags for '%s'", directory.getAbsolutePath()), e);
×
765
            // In case of partial success, do not null-out tagList here.
766
        }
1✔
767

768
        if (LOGGER.isLoggable(Level.FINER)) {
1✔
769
            LOGGER.log(Level.FINER, "Read tags count={0} for ''{1}''",
×
770
                    new Object[] {tagList.size(), directory});
×
771
        }
772
    }
1✔
773

774
    /**
775
     * Builds a Git tag list by querying Git commit hash, commit time, and tag
776
     * names.
777
     * <p>Repository technically relies on the tag list to be ancestor ordered.
778
     * <p>For a version control system that uses "linear revision numbering"
779
     * (e.g. Subversion or Mercurial), the natural ordering in the
780
     * {@link TreeSet} is by ancestor order and so
781
     * {@link TagEntry#compareTo(HistoryEntry)} always determines the correct
782
     * tag.
783
     * <p>For {@link GitTagEntry} that does not use linear revision numbering,
784
     * the {@link TreeSet} will be ordered by date. That does not necessarily
785
     * align with ancestor order. In that case,
786
     * {@link GitTagEntry#compareTo(HistoryEntry)} that compares by date can
787
     * find the wrong tag.
788
     * <p>Linus Torvalds: [When asking] "'can commit X be an ancestor of commit
789
     * Y' (as a way to basically limit certain algorithms from having to walk
790
     * all the way down). We've used commit dates for it, and realistically it
791
     * really has worked very well. But it was always a broken heuristic."
792
     * <p>"I think the lack of [generation numbers] is literally the only real
793
     * design mistake we have [in Git]."
794
     * <p>"We discussed adding generation numbers about 6 years ago [in 2005].
795
     * We clearly *should* have done it. Instead, we went with the hacky `let's
796
     * use commit time', that everybody really knew was technically wrong, and
797
     * was a hack, but avoided the need."
798
     * <p>If Git ever gets standard generation numbers,
799
     * {@link GitTagEntry#compareTo(HistoryEntry)} should be revised to work
800
     * reliably in all cases akin to a version control system that uses "linear
801
     * revision numbering."
802
     * @param directory a defined directory of the repository
803
     * @param cmdType command timeout type
804
     */
805
    @Override
806
    protected void buildTagList(File directory, CommandTimeoutType cmdType) {
807
        final ExecutorService executor = Executors.newSingleThreadExecutor(new OpenGrokThreadFactory("git-tags"));
1✔
808
        final Future<?> future = executor.submit(() -> rebuildTagList(directory));
1✔
809
        executor.shutdown();
1✔
810

811
        try {
812
            future.get(RuntimeEnvironment.getInstance().getCommandTimeout(cmdType), TimeUnit.SECONDS);
1✔
813
        } catch (InterruptedException | ExecutionException e) {
×
814
            LOGGER.log(Level.WARNING, String.format("failed tag rebuild for directory '%s'", directory), e);
×
815
        } catch (TimeoutException e) {
×
816
            LOGGER.log(Level.WARNING, String.format("timed out tag rebuild for directory '%s'", directory), e);
×
817
        }
1✔
818

819
        if (!executor.isTerminated()) {
1✔
UNCOV
820
            executor.shutdownNow();
×
821
        }
822
    }
1✔
823

824
    @NotNull
825
    private RevCommit getCommit(org.eclipse.jgit.lib.Repository repository, Ref ref) throws IOException {
826
        try (RevWalk walk = new RevWalk(repository)) {
1✔
827
            return walk.parseCommit(ref.getObjectId());
1✔
828
        }
829
    }
830

831
    @Override
832
    @Nullable
833
    String determineParent(CommandTimeoutType cmdType) throws IOException {
834
        try (org.eclipse.jgit.lib.Repository repository = getJGitRepository(getDirectoryName())) {
1✔
835
            if (repository.getConfig() != null) {
1✔
836
                return repository.getConfig().getString("remote", Constants.DEFAULT_REMOTE_NAME, "url");
1✔
837
            } else {
838
                return null;
×
839
            }
840
        }
1✔
841
    }
842

843
    @Override
844
    String determineBranch(CommandTimeoutType cmdType) throws IOException {
845
        try (org.eclipse.jgit.lib.Repository repository = getJGitRepository(getDirectoryName())) {
1✔
846
            return repository.getBranch();
1✔
847
        }
848
    }
849

850
    @Override
851
    public String determineCurrentVersion(CommandTimeoutType cmdType) throws IOException {
852
        try (org.eclipse.jgit.lib.Repository repository = getJGitRepository(getDirectoryName())) {
1✔
853
            Ref head = repository.exactRef(Constants.HEAD);
1✔
854
            if (head != null && head.getObjectId() != null) {
1✔
855
                try (RevWalk walk = new RevWalk(repository); ObjectReader reader = repository.newObjectReader()) {
1✔
856
                    RevCommit commit = walk.parseCommit(head.getObjectId());
1✔
857
                    int commitTime = commit.getCommitTime();
1✔
858
                    Date date = new Date((long) commitTime * 1000);
1✔
859
                    return String.format("%s %s %s %s",
1✔
860
                            format(date),
1✔
861
                            reader.abbreviate(head.getObjectId()).name(),
1✔
862
                            commit.getAuthorIdent().getName(),
1✔
863
                            commit.getShortMessage());
1✔
864
                }
865
            }
866
        }
1✔
867

868
        return null;
1✔
869
    }
870
}
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