Testing VCS commit & update hooks

Hello all,

I was wondering what's the best way to test a plugin that registers a custom CheckinHandlerFactory as well as a custom UpdatedFilesListener like so:

CheckinHandlersManager.getInstance().registerCheckinHandlerFactory(new CheckinHandlerFactory {...})

project.getMessageBus.connect().subscribe(UpdatedFilesListener.UPDATED_FILES, new UpdatedFilesListener {...})

The purpose being to hook into the commit/update operations against a VCS. Also, please feel free to suggest alternative ways to achieve this if I'm doing it wrong.

I guess I want something similar to AbstractVcsTestCase but with simulating a VCS commit (i.e checkin) and update commands, which I haven't been able to find there, or in any of the fellow helpers (MockVcsHelper, MockAbstractVcs, AbstractVcsHelper).

As usual, your assistance and words of wisdom would be greatly appreciated.

Thank you in advance,

Comment actions Permalink

Yeah, that didn't go so well and failed to meet some other requirements I had (for instance, revision info is not published on the bus along with the filenames).

I'm now trying to employ VcsEventsListenerManager and pass it my own CheckinEnvironment, UpdateEnvironment and RollbackEnvironment instances to decorate the original ones.
The problem seems to be that my custom environments objects get into play only if VcsActiveEnvironmentsProxy.proxyVcs gets called after my plugin has had the chance to call addCheckin/addUpdate/addRollback on the VcsEventsListenerManager instance, which I can't really guarantee.
It seems like a race condition between my plugin and all the VCS plugins - Git, Subversion, Hg and Mercurial, which trigger a call to VcsActiveEnvironmentsProxy.proxyVcs.

Is there any way I can make sure my plugin gets loaded before all the VCS plugins?
(I saw the "depends" tag, but it only gives me the opposite direction, i.e., guarantee I'm loaded after some plugin, not before)

Thanks a lot for your time,

Comment actions Permalink

So, let's summarize: what is the original problem you're trying to solve?

The purpose being to hook into the commit/update operations against a VCS.

This one?

Then CheckinHandlerFactory is a correct way to do it for commit operation. Does it work for you?

And I'm afraid there is no way to do it for update if the UpdatedFilesListener doesn't suit you. However, in some VCS plugins there are other ways to do it. Do you want to hook into update operation generally for all VCSs or only for specific ones (e.g. only for Git)?
Note that unlike the commit operation which is unified for all VCSs, there are usually ways to update the code from certain plugins, other than Update Project (e.g. Git | Pull for Git and other).

Btw VcsEventsListenerManager seems a bit obsolete, I'm not sure if it works correctly at all.

Comment actions Permalink

Hello Kirill,

Thanks for your quick reply.

That's right, my goal is to hook into the commit/update/rollback phases of any given VCS without having to do it per VCS type. My hypothesis here is that every VSS has these stages, and they are in fact, implemented for all VCS types in the form of a corresponding {Checkin | Update | Rollback} Environment class.
I'm basically interested in getting notified whenever VCS managed files change their revision, which must be as a result of one of the following: {commit, update, rollback), given all changes are done within IntelliJ.

The information I'm interested in upon such a change is:

  1. The file whose revision has changed
  2. The file's new revision
  3. The file's old revision

From what I gather after researching things for a good few days, the only mechanism meeting all these requirements is the VCS environments: CheckinEnvironment, UpdateEnvironment and RollbackEnvironment, and the only way to customize them is via the ProjectLevelVcsManager#getVcsEventsListenerManager which coincidentally, or not, is the same instance given back by ProjectLevelVcsManager#getProxyCreator which decorates default VCS environments upon instantiating a VCS. This environment decoration only takes place once during a given VCS lifecycle, and happens during the init phase. For my customizations to apply I must add them before the first init of each VCS.
The problem is that the decoration time it not under my control, thus, if anyone calls VcsEP#getVcs before I've had the chance to add my customizations, VcsActiveEnvironmentsProxy.proxyVcs(myVcs) gets called and and that's it, it creates the VCS with the environment it has at that point in time and any environments added after this point will never make it to the VCS instance used by the system. For now I can already see Hg and Git get created before I get the chance to tweak them, but my solution seems to work for SVN (almost done implementing) which is great since that would be my first usage pilot for the whole thing anyway.

My question is basically if there's any way to control the order in which VCS are loaded / instantiated, or, alternatively, get my customization in place before any VCS gets init-ed.
Or, of course, it you belive the above mentioned requirements can be met in some other way, I'm all open to suggestions :)

Sorry for the lengthy message, I hope I was able to make myself clear.
Please let me know if I can provide further info.

Thank you in advance,

Comment actions Permalink

Personally I never tried this thing, but your approach seems to be correct, at least from what I see from the code (although, it is not used in IDEA platform for now).

To make sure that your plugin gets initialized before any VCS you may try to define an ApplicationComponent - they are initialized before any Project component is.
Alternatively and if you need an instance of the Project, you may define a pre-startup activity: StartupManager.register...

Please check if that works for you.

Comment actions Permalink
  • Tried a few more angles at the ApplicationComponent#initComponent level:
  1. ApplicationManager.getApplication.getMessageBus.connect().subscribe(ProjectLifecycleListener.TOPIC, new ProjectLifecycleListener.Adapter {

          override def projectComponentsInitialized(project: Project): Unit = {}

          override def beforeProjectLoaded(project: Project): Unit = { ... }

          override def afterProjectClosed(project: Project): Unit = {}

Namely, beforeProjectLoaded. This didn't work because it looks like at this stage I still don't have a valid ProjectLevelVcsManager instance (an exception is thrown when I try to getInstance it), same goes for StartupManager.

  • Also tried using StartupManager at various points in my ProjectComponent, both initComponent and projectOpened, but by the time my code gets executed it's alrady too late and the VCSs I'd like to tweak have already been loaded.

Stil no luck.
Comment actions Permalink

A short update, listening to UpdateEnvironment doesn't work because things are done asynchroniously, and the proxyed UpdateEnvironment's UpdateSession instance is hidden by the original one's, and so the proxyed UpdateSession#onRefreshFilesCompleted is never called and the listener has no way of knowing when the update operation is complete and receive the actual update result - the updated files.


    final T proxy = (T) Proxy.newProxyInstance(myClazz.getClassLoader(),
                                        new Class[]{myClazz},
                                        new InvocationHandler() {
                                          public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
                                            synchronized (myLock) {
                                                new Pair<VcsKey, Consumer<T>>(key, new Consumer<T>() {
                                                  public void consume(T t) {
                                                    try {
                                                      // in case of an UpdateEnvironment#updateDirectories, it returns an UpdateSession instance, which is never stored anywhere 
                                                      method.invoke(t, args);
                                                    catch (IllegalAccessException e) {
                                                    catch (InvocationTargetException e) {
                                            // in case of an UpdateEnvironment#updateDirectories, the original's UpdateEnvironment#updateDirectories return value is returned
                                            // hiding the UpdateSession retunred earlier
                                            return method.invoke(environment, args);

Guess I'll have to make do with UpdatedFilesListener for now.

Can't help but feeling the the VCS listener model is somewhat non tirivial at the moment :)
It would be great if we could have a consistent way of listening to commit/update/rollback operations in a generic manner, applicable to any VCS.


Comment actions Permalink

Let me check/debug it with some sample plugin in a while. I'll write back when I'll perform the test and will be ready to answer.


Please sign in to leave a comment.