void fetch(
      @NonNull TaskListener listener,
      @NonNull final SVNRepositoryView repository,
      long rev,
      @NonNull final String repoPath,
      @NonNull SortedSet<List<String>> paths,
      @NonNull List<String> prefix,
      @NonNull List<String> realPath,
      @NonNull SortedSet<List<String>> excludedPaths,
      @CheckForNull SCMSourceCriteria branchCriteria,
      @NonNull SCMHeadObserver observer)
      throws IOException, SVNException {
    String svnPath = SVNPathUtil.append(repoPath, StringUtils.join(realPath, '/'));
    assert prefix.size() == realPath.size();
    assert wildcardStartsWith(realPath, prefix);
    SortedMap<List<String>, SortedSet<List<String>>> includePaths = groupPaths(paths, prefix);
    listener
        .getLogger()
        .println("Checking directory " + svnPath + (rev > -1 ? "@" + rev : "@HEAD"));
    SVNRepositoryView.NodeEntry node = repository.getNode(svnPath, rev);
    if (!SVNNodeKind.DIR.equals(node.getType()) || node.getChildren() == null) {
      return;
    }
    for (Map.Entry<List<String>, SortedSet<List<String>>> entry : includePaths.entrySet()) {
      for (List<String> path : entry.getValue()) {
        String name = path.get(prefix.size());
        SVNRepositoryView.ChildEntry[] children = node.getChildren().clone();
        Arrays.sort(
            children,
            new Comparator<SVNRepositoryView.ChildEntry>() {
              public int compare(SVNRepositoryView.ChildEntry o1, SVNRepositoryView.ChildEntry o2) {
                long diff = o2.getRevision() - o1.getRevision();
                return diff < 0 ? -1 : diff > 0 ? 1 : 0;
              }
            });
        for (final SVNRepositoryView.ChildEntry svnEntry : children) {
          if (svnEntry.getType() == SVNNodeKind.DIR && isMatch(svnEntry.getName(), name)) {
            List<String> childPrefix = copyAndAppend(prefix, name);
            List<String> childRealPath = copyAndAppend(realPath, svnEntry.getName());
            if (wildcardStartsWith(childRealPath, excludedPaths)) {
              continue;
            }
            if (path.equals(childPrefix)) {
              final String childPath = StringUtils.join(childRealPath, '/');
              final String candidateRootPath = SVNPathUtil.append(repoPath, childPath);
              final long candidateRevision = svnEntry.getRevision();
              final long lastModified = svnEntry.getLastModified();
              listener
                  .getLogger()
                  .println(
                      "Checking candidate branch " + candidateRootPath + "@" + candidateRevision);
              if (branchCriteria == null
                  || branchCriteria.isHead(
                      new SCMSourceCriteria.Probe() {
                        @Override
                        public String name() {
                          return childPath;
                        }

                        @Override
                        public long lastModified() {
                          return lastModified;
                        }

                        @Override
                        public boolean exists(@NonNull String path) throws IOException {
                          try {
                            return repository.checkPath(
                                    SVNPathUtil.append(candidateRootPath, path), candidateRevision)
                                != SVNNodeKind.NONE;
                          } catch (SVNException e) {
                            throw new IOException(e);
                          }
                        }
                      },
                      listener)) {
                listener.getLogger().println("Met criteria");
                SCMHead head = new SCMHead(childPath);
                observer.observe(head, new SCMRevisionImpl(head, svnEntry.getRevision()));
                if (!observer.isObserving()) {
                  return;
                }
              } else {
                listener.getLogger().println("Does not meet criteria");
              }
            } else {
              fetch(
                  listener,
                  repository,
                  svnEntry.getRevision(),
                  repoPath,
                  paths,
                  childPrefix,
                  childRealPath,
                  excludedPaths,
                  branchCriteria,
                  observer);
            }
          }
        }
      }
    }
  }
  @NonNull
  @Override
  protected void retrieve(@NonNull final SCMHeadObserver observer, @NonNull TaskListener listener)
      throws IOException, InterruptedException {
    String cacheEntry = getCacheEntry();
    Lock cacheLock = getCacheLock(cacheEntry);
    cacheLock.lock();
    try {
      File cacheDir = getCacheDir(cacheEntry);
      Git git =
          Git.with(listener, new EnvVars(System.getenv()))
              .in(cacheDir)
              .using(GitTool.getDefaultInstallation().getGitExe());
      GitClient client = git.getClient();
      client.addDefaultCredentials(getCredentials());
      if (!client.hasGitRepo()) {
        listener.getLogger().println("Creating git repository in " + cacheDir);
        client.init();
      }
      String remoteName = getRemoteName();
      listener.getLogger().println("Setting " + remoteName + " to " + getRemote());
      client.setRemoteUrl(remoteName, getRemote());
      listener.getLogger().println("Fetching " + remoteName + "...");
      List<RefSpec> refSpecs = getRefSpecs();
      client.fetch(remoteName, refSpecs.toArray(new RefSpec[refSpecs.size()]));
      listener.getLogger().println("Pruning stale remotes...");
      final Repository repository = client.getRepository();
      try {
        client.prune(new RemoteConfig(repository.getConfig(), remoteName));
      } catch (UnsupportedOperationException e) {
        e.printStackTrace(listener.error("Could not prune stale remotes"));
      } catch (URISyntaxException e) {
        e.printStackTrace(listener.error("Could not prune stale remotes"));
      }
      listener.getLogger().println("Getting remote branches...");
      SCMSourceCriteria branchCriteria = getCriteria();
      RevWalk walk = new RevWalk(repository);
      try {
        walk.setRetainBody(false);
        for (Branch b : client.getRemoteBranches()) {
          if (!b.getName().startsWith(remoteName + "/")) {
            continue;
          }
          final String branchName = StringUtils.removeStart(b.getName(), remoteName + "/");
          listener.getLogger().println("Checking branch " + branchName);
          if (isExcluded(branchName)) {
            continue;
          }
          if (branchCriteria != null) {
            RevCommit commit = walk.parseCommit(b.getSHA1());
            final long lastModified = TimeUnit.SECONDS.toMillis(commit.getCommitTime());
            final RevTree tree = commit.getTree();
            SCMSourceCriteria.Probe probe =
                new SCMSourceCriteria.Probe() {
                  @Override
                  public String name() {
                    return branchName;
                  }

                  @Override
                  public long lastModified() {
                    return lastModified;
                  }

                  @Override
                  public boolean exists(@NonNull String path) throws IOException {
                    TreeWalk tw = TreeWalk.forPath(repository, path, tree);
                    try {
                      return tw != null;
                    } finally {
                      if (tw != null) {
                        tw.release();
                      }
                    }
                  }
                };
            if (branchCriteria.isHead(probe, listener)) {
              listener.getLogger().println("Met criteria");
            } else {
              listener.getLogger().println("Does not meet criteria");
              continue;
            }
          }
          SCMHead head = new SCMHead(branchName);
          SCMRevision hash = new SCMRevisionImpl(head, b.getSHA1String());
          observer.observe(head, hash);
          if (!observer.isObserving()) {
            return;
          }
        }
      } finally {
        walk.dispose();
      }

      listener.getLogger().println("Done.");
    } finally {
      cacheLock.unlock();
    }
  }