JBuilder version control system concepts

General description

JBuilder includes a general framework to integrate any version control system (VCS) and manage Java projects with this VCS directly from the IDE. The API has been designed to be small and simple, thus making the job of interfacing the VCS with JBuilder as easy as possible. This document outlines what you must do to add support to JBuilder for a given VCS.

A complete implementation of the VCS API is presented in the SampleVCS OpenTools sample. The code can be found in the samples directory and can be used as a guideline to integrate any VCS into JBuilder taking advantage of the History pane and the VCS Commit Dialog. This document uses snippets of code from the SampleVCS class to illustrate examples of implementation of the VCS API.

Detailed description of feature/subsystem

All the classes involved in Version Management are in the packages included in the com.borland.primetime.teamdev tree. They are: To integrate a VCS into JBuilder, you must implement the interface com.borland.primetime.teamdev.vcs.VCS. This interface defines the API required to register and integrate the VCS into JBuilder. It consists of just 16 methods to make your work as easy as possible. Some of these methods are used to define a series of actions that will be accessible with the Team menu. The scope of these actions is totally under your control and there are no specific constraints or hierarchies to follow. In other words, if the implementer of a VCS back-end must display a dialog box (to administer access rights, for example) the action can define any UI element and execute any action without having to fit into a specific framework. The usual OpenTools and Swing guidelines apply. You can access any feature of the selected VCS without having to adhere to a minimum common denominator.

Configuration of the VCS

To make a VCS available in JBuilder the class must register itself with the VCS Factory (com.borland.primetime.vcs.VCSFactory). For example:
package com.borland.samples.samplevcs;

import com.borland.primetime.teamdev.vcs.*;

public class SampleVCS implements VCS {
    
  public static void initOpenTool( byte major, byte minor ) {
    // VCS support was added starting from JBuilder 4
    if ( major < 4) {
      return;
    }
    VCSFactory.addVCS(new SampleVCS());
  }

  .
  .
  .

  public String getName() {
    return "SampleVCS";
  }

}
This makes the VCS selectable in the Team page in the Project Properties dialog box. Note that the getName() method returns the name that will be associated with the VCS. This is the result in the Team property page.

The SampleVCS is added to the list of registered VCSs The result of calling VCS.getProjectConfigPage()

To provide a configuration panel to the user, define a subclass of com.borland.primetime.properties.PropertyPage (basically a JPanel). In our example, the page has only a text field to enter a directory name and a button to set this value using JBuilder's com.borland.primetime.vfs.ui.UrlChooser. JBuilder obtains this page by calling the VCS.getProjectConfigPage() method.

Save VCS settings together with the project to make them persistent. You can do this using the com.borland.jbuilder.node.JBProject.setAutoProperty() method. The method accepts three parameters. The first is the category for the property. It should always be VCS.CATEGORY. The second parameter is the name of the property. This name should be unique so that it doesn't conflict with other VCSs. It's a good idea to use the name of the VCS to prefix the property name (for example, SampleVCS_path). The last parameter is the property's value. Here is how SampleVCS.ConfigPage saves and restores its properties:

  public void writeProperties() {
    project.setAutoProperty(VCS.CATEGORY,SampleVCS.PROP_PATH,pnlConfig.getPath());
  }

  public HelpTopic getHelpTopic() {
    return null;
  }

  public void readProperties() {
    String path = project.getAutoProperty(VCS.CATEGORY,SampleVCS.PROP_PATH);
    if ( path != null ) {
      pnlConfig.setPath(path);
    }
  }

Context menus

The getVCSContextMenuGroup() method is called by JBuilder to retrieve a list of Actions that can be displayed when the user right-clicks on a file that is under the VCS. The method returns an ActionGroup initialized with all the actions that can be available when right-clicking on files under the given VCS. If the method return null then no menu will be displayed. Here is how the SampleVCS exports its context menu:
  public ActionGroup getVCSContextMenuGroup() {
    ActionGroup aGroup = new ActionGroup("SampleVCS",'S',"Manage file with the Sample VCS",null,true );
    aGroup.add(Actions.ADD);
    aGroup.add(Actions.CHECKIN);
    aGroup.add(Actions.GET_LOG);
    return(aGroup);
  }
The Actions class defines the static Action objects used by several methods of the SampleVCS class:
public class Actions {

  .
  .
  .

  public static UpdateAction ADD = new UpdateAction("Add",'A',"Add the selected file(s) to the VCS") {
    public void update( Object source ) {
      // compute the enabled status based on the type of nodes selected
    }

    public void actionPerformed( ActionEvent e ) {
      Node[] nodes = Browser.getActiveBrowser().getProjectView().getSelectedNodes();
      SampleVCS vcs = (SampleVCS)VCSFactory.getVCS("SampleVCS");
      int len = nodes.length;
      for( int i=0; i <len; i++ ) {
        File workFile = ((FileNode)nodes[i]).getUrl().getFileObject();
        vcs.addFile(workFile);
      }
    }
  };


  public static UpdateAction CHECKIN = new UpdateAction("Checkin",'C',"Commit changes to the repository") {
    public void update( Object source ) {
      setEnabled(getEnabledState());
    }

    public void actionPerformed( ActionEvent e ) {
       .
       .
       .
    }
  };

  public static UpdateAction GET_LOG = new UpdateAction("See log",'L',"See the comments for changes checked in") {
    public void update( Object source ) {
      setEnabled(getEnabledState());
    }

    public void actionPerformed( ActionEvent e ) {
      .
      .
      .
    }
  };

};

Integration in the History pane

JBuilder's users expects to see all the available revisions in the History pane. Fortunately this is one feature that both very useful yet easy to implement. JBuilder calls the VCS.getRevisions() method to retrieve a Vector of RevisionInfo for a specified file. The method receives an Url (the JBuilder version, not the one in the java.net package) and fills the Vector with all the revisions found for the file. The RevisionInfo class is designed to report revision information in a VCS-neutral way (at least that was the goal). Here is an example:
  public Vector getRevisions(Url url) {
    Vector revs = new Vector();
    File repo = getFileInRepository(url.getFileObject()).getParentFile();
    final String fname = url.getName();
    String[] list = repo.list( new FilenameFilter() {
      public boolean accept( File dir, String name ) {
        return name.startsWith(fname) && (! name.endsWith(".comment"));
      }
    });
    int len = list.length;
    String revNumber;
    for( int i=0; i < len; i++ ) {
      revNumber = list[i].substring(list[i].lastIndexOf(',')+1);
      try {
        BufferedReader reader = new BufferedReader(new FileReader(new File(repo,list[i]+".comment")));
        String desc = reader.readLine();
        String who = reader.readLine();
        String time = reader.readLine();
        String label = reader.readLine();
        GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
        cal.setTime(new Date(Long.parseLong(time)));
        cal.setTimeZone(TimeZone.getDefault());
        RevisionInfo r = new RevisionInfo(revNumber,cal.getTime().getTime(),who,desc, label);
        if ( getCurrentRevision(url.getFileObject()).equals(revNumber) ) {
          r.setWorkingRevision(true);
        }
        revs.add(r);
      }
      catch (Exception ex) {
        ex.printStackTrace();
      }
    }
    // The VCSCommitBrowser assumes that the first element has the newest revision
    Collections.reverse(revs);
    return revs;
  }
As the example demonstrates, the parameters passed to RevisionInfo are the revision number, the time of the revision (in this case time is expressed using the GMT time zone), the name of the user that checked in the revision, the description of the changes, and the version label. The revision number is expressed here as a String, but it's actually more complex than that. There's a class designed to represent version numbers in a way that is more suitable for sorting than using Strings. See com.borland.primetime.teamdev.vcs.AbstractRevisionNumber, com.borland.primetime.teamdev.vcs.NumericRevisionNumber, and com.borland.primetime.teamdev.vcs.StringRevisionNumber.

Note that care is taken to identify the revision that was used to check out the file in consideration. Depending on the VCS, you can obtain this value in a variety of ways. You can notify JBuilder of this conditions using the RevisionInfo.setWorkingRevision(boolean) method. From the previous example:

  if ( getCurrentRevision(url.getFileObject()).equals(revNumber) ) {
    r.setWorkingRevision(true);
  }

Providing project-wide status: the VCSCommitBrowser

There is a point in the development cycle when single-file management isn't sufficient. After modifying, adding or removing several files, a developer wants to know how many files must be committed to the repository and what type of changes occurred. The VSCCommitBrowser is designed to provide a project-wide view of the changes and to provide one-click access to the Visual Diff Engine, both for changes done to the workspace, and to see what happened in the repository if the VCS, like CVS, allows concurrent updates.

The VCSCommitBrowser class is part of the com.borland.primetime.teamdev.frontend package and can be called using the vcs.VCSUtils.showProjectStatus(CommitAction checkinAction) method.

This method calls in turn the vcs.VCS.getProjectStatus(Project project) method. The return value from getProjectStatus is a java.util.List filled with instances of vcs.VCS.VCSFileInfo. VCSFileInfo is a simple storage class used to return status information about a file managed by a VCS. The purpose of VCSFileInfo is to simply hold some file information since the VCSCommitBrowser can display data about files that are not directly under control of JBuilder's projects. The class keeps a flag that can be used by the VCS implementation to check if the file has to be committed. This flag is set or cleared by clicking the Exec check box in the VCSCommitBrowser. Perhaps the most important component of the VCSFileInfo class is the status field. It is of type vcs.VCSFileStatus, an abstract class that must be subclassed by any VCS implementation that needs to use the VCSCommitBrowser.

Subclassing VCSFileStatus provides these advantages:

Here is an example from SampleVCS:

public class RevisionStatus extends VCSFileStatus
{

  public static final int STATUS_NEW         = 0;
  public static final int STATUS_WSP_CHANGE  = 1;
  public static final int STATUS_REPO_CHANGE = 2;
  public static final int STATUS_REMOVED     = 3;

  private class FileDesc {
    public Icon icon;
    public String desc;

    public FileDesc( Icon icon, String desc ) {
      this.icon = icon; this.desc = desc;
    }
  }

  private FileDesc[] descs = {
                               new FileDesc(VCSIcons.FILE_NEW,         "New !" ),
                               new FileDesc(VCSIcons.WORKSPACE_CHANGE, "Changed in workspace"),
                               new FileDesc(VCSIcons.REPOSITORY_CHANGE,"Changed in repository"),
                               new FileDesc(VCSIcons.FILE_REMOVED,     "Removed !")
                             };

  public RevisionStatus( int status ) {
    this.status = status;
  }


  public ActionGroup getActions() {
    // We don't need no stinkin' actions...
    return null;
  }

  /**
   * Returns the icon that will be displayed in the VCSCommitBrowser
   */

  public Icon getStatusIcon() {
    return descs[status].icon;
  }

  /**
   * called by VCSCommitBrowser to determine if the file has repository changes
   */
  public boolean isModifiedInVCS() {
    return status == STATUS_REPO_CHANGE;
  }

  public String getDescription() {
    return descs[status].desc;
  }

  public boolean isModifiedLocally() {
    return status == STATUS_WSP_CHANGE;
  }
}
Note the definition of the methods isModifiedInVCS() and isModifiedLocally(). The VCSCommitBrowser uses these to determine what kind of Diff tab must displayed and how to retrieve the revisions for the Visual Diff. Refer to the SampleVCS code (the Actions class ) to see how this plugs into the JBuilder menu system.

The vcs.CommitAction class is a subclass of Swing's Action, with two methods to report error conditions. The action is called when the user chooses OK in the VCSCommitBrowserDialog. The idea behind it is that the action will scan all the VCSFileInfos that are set to be committed and it will perform the appropriate action based on the specific VCS rules.

In conclusion, the VCSCommitBrowser and related classes provides a consistent look-and-feel to analyze what changes need to be committed to the VCS repository. You have a minimal amount of work to do to provide this effective UI element. The most complicated bit of work is the implementation of the CommitAction.