docs: @Override
      public CompletableFuture<Map<String, Change<?>>> diff(Revision from, Revision to, String pathPattern) {
          final ServiceRequestContext ctx = context();
          return CompletableFuture.supplyAsync(() -> {
              requireNonNull(from, "from");
              requireNonNull(to, "to");
              requireNonNull(pathPattern, "pathPattern");
  
              failFastIfTimedOut(this, logger, ctx, "diff", from, to, pathPattern);
  
              final RevisionRange range = normalizeNow(from, to).toAscending();
              readLock();
              try (RevWalk rw = new RevWalk(jGitRepository)) {
                  final RevTree treeA = rw.parseTree(commitIdDatabase.get(range.from()));
                  final RevTree treeB = rw.parseTree(commitIdDatabase.get(range.to()));
  
                  // Compare the two Git trees.
                  // Note that we do not cache here because CachingRepository caches the final result already.
                  return toChangeMap(blockingCompareTreesUncached(treeA, treeB,
                                                                  pathPatternFilterOrTreeFilter(pathPattern)));
              } catch (StorageException e) {
                  throw e;
              } catch (Exception e) {
                  throw new StorageException("failed to parse two trees: range=" + range, e);
              } finally {
                  readUnlock();
              }
          }, repositoryWorker);
      }