Ensure initialization of GitRepositoryManager before doing plugin operation

In my plugin, I query for all Git repositories at plugin initialization time:

GitUtil.getRepositoryManager(project).getRepositories()

Sometimes this operation returns an empty list even when there are repositories available. I assume that it is some timing issue. Executing the same line a bit later returns the expected results.

Is there a way to enforce proper initialization of GitRepositoryManager?

13 comments
Comment actions Permalink
Official comment

GitRepository objects indeed are initialized not immediately on startup, and in a very asynchronous way. Here is the sequence:

1. ProjectLevelVcsManager reads all VCS root mappings in a post-startup activity, on a background thread (see `vcsManager.addInitializationRequest(VcsInitObject.MAPPINGS` in `NewMappings()`).

2. It sends the `directoryMappingsChanged()` event.

3. The event is handled by the `VcsRepositoryManager` which asynchronously constructs `GitRepository` objects for each Git root.

Currently there is no direct API to get notified when a GitRepository() object is created. But you can do one of two following things:

1. Subscribe to `GitRepository.GIT_REPO_CHANGE`: this event is sent on every change in `.git`, but also when the GitRepository object is initialized.

2. Subscribe to the same `directoryMappingsChanged()`, go to a background thread (to free the EDT and let other subscribers, including the VcsRepositoryManager, handle the event) and call `GitRepositoryManager.getRepositoryForRoot`: the latter will wait for the GitRepository initialization if it hasn't been initialized yet.

Comment actions Permalink

Thanks for your detailed explanation.

In my usecase I just need a list of all available Git repos, I do not need to react on changes at runtime.

Is there an event when the full initialization is done (probably also a more global one)? A very ugly workaround would also be to delay accessing GitRepoManager by a hardcoded number of seconds...

0
Comment actions Permalink

And when/where do you need this list? Is it a toolwindow that need to show some information about them, or something like that?

Because if it is (just for example) something like AnAction which shows a dialog when called, then you don't need the list of repositories on startup, and can get the list when the action is called.

At any case, maybe it will be enough for you to get the list of Git roots (as VirtualFiles), and not GitRepository objects? Then you can subscribe to directoryMappingsChanged() and update the list of every mapping change. But, as you saw above, it still is not available on the startup immediately.

0
Comment actions Permalink

I need it for loading a list in a tool window (I load all open reviews from Gerrit for repositories which are available in current project).

0
Comment actions Permalink

And plain roots as VirtualFiles are not enough, because you need to access GitRemotes, right?

0
Comment actions Permalink

I think the right thing to do is to subscribe to GitRepository.GIT_REPO_CHANGE events. After all, the toolwindow should be updated, if the user adds a new Git root to the project, or removes one, shouldn't it? 

(It should also update if a user removes a Git root from the project, and GIT_REPO_CHANGE won't be fired in this case: you'll need directoryMappingsChange() to handle removal then).

0
Comment actions Permalink

I have implemented something which basically does the work based on your input. 

List<GitRepository> repositories = GitUtil.getRepositoryManager(project).getRepositories();
if (!repositories.isEmpty()) {
reloadChanges(project, false); // calls async method
}
GitRepositoryChangeListener gitRepositoryChangeListener = new GitRepositoryChangeListener() {
@Override
public void repositoryChanged(@NotNull GitRepository gitRepository) {
reloadChanges(project, false); // calls async method
}
};
project.getMessageBus().connect().subscribe(GitRepository.GIT_REPO_CHANGE, gitRepositoryChangeListener);

 

 

The 'if' is required because it is possible that the event got fired before my listener is subscribed.

Is there an easy way to debounce the 'reloadChanges' call in your API? It is an expensive REST call which should not be executed many times when many repos are in one project.

0
Comment actions Permalink

What do you mean by "debounce"? The call will happen on almost every change in .git in all repositories. If you need to call it only for new repositories, just store a list of GitRepositories and check if gitRepository in the method parameter is new.

0
Comment actions Permalink

With "debounce" I mean something like that: https://lodash.com/docs/#debounce

I.e. when a project contains 4 Git repos, my listener is called 4 times (which is fine of course). But I do not want to execute 4 REST HTTP requests in row. I would do it after the last of these 4 events in row has been fired. If there is no easy way to do that with your API, I could try to do something like that: https://stackoverflow.com/a/20978973

0
Comment actions Permalink

Then maybe you should try the second approach that I've mentioned above: listen to directoryMappingsChanged(), transfer execution to another thread and call getRepositoryForRoot() for every repository, which will force their initialization right away.

This debouncing looks like a hack for this task, since it relies only on time, and not on the number of roots in the project.

0
Comment actions Permalink

Thank you very much! This is the solution I've implemented based on your proposals: https://github.com/uwolfer/gerrit-intellij-plugin/commit/ef256e28aea0412cb7bc8f42965e09549ec60bcd

0
Comment actions Permalink

Hi! 

I think it's better to use VCS_REPOSITORY_MAPPING_UPDATED - a special topic for VcsRepositoryMappingListener.

Subscription to this event means that Repositories has been changed (mappings changes and Repository instances already created if needed).

0
Comment actions Permalink

Thanks Nadya, that works fine.

0

Please sign in to leave a comment.