changelog

All SCM must be able to write and parse a change log in order to be able to show it per build. The change log file is most of the times a simple XML file. The SCM must implement the method createChangeLogParser that delegates the parsing of the change log file to the SCM which returns a ChangeLogSet derivate. It is the responsibility of the checkout method to write the XML file, which then is parsed after the checkout method returns. To be able to show the changes in a build, two jelly files are used.

Change log architecture

ChangeLogSet

The ChangeLogSet object represents a SCM change list. It contains zero or more change log entries that extends ChangeLogSet.Entry.

ChangeLogSet.Entry

A ChangeLogSet.Entry derivate should contain data about a change in a SCM. What information that should be stored depends on the SCM. There are three abstract methods that must be implemented.

  • Collection<String> getAffectedPaths()
  • User getAuthor()
  • String getMsg()

getAffectedPaths()

Should return a collection of file paths that was affected in the change log entry.

getAuthor()

Should return the author of the change. Hudson automatically creates a User object for all users that is stored in a change log entry.

@Override
public User getAuthor() {
    return User.get(user);
}

getMsg()

Returns the message that was attached to the change log entry. Also known as commit message.

getEditType()

If you would like to display an icon next to the change set or files in the change set that shows what type of action it was, you should implement a method that returns an EditType. The method is then used by the index.jelly to determine which icon to display.

@Exported
public EditType getEditType() {
    if (action.equalsIgnoreCase("delete")) {
        return EditType.DELETE;
    }
    if (action.equalsIgnoreCase("add")) {
        return EditType.ADD;
    }
    return EditType.EDIT;
}

Writing a change log

The change log is saved per build, and most of the times it is an XML file. The change log must be stored by the SCM in the checkout() method, the name of the file is supplied in the checkout() method.

The Team Foundation Server plugin is using the below XML format.

<changelog>
   <changeset version="1212">
       <date></date>
       <user></user>
       <comment></comment>
       <items>
           <item action="add">/file/path/newfile</item>
           ....
       </items>
   </changeset>
   ....
</changelog>

Most of the times a very simple text writer is adequate for creating the XML file.

PrintWriter writer = new PrintWriter(new FileWriter(changelogFile));
writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
writer.println("<changelog>");
for (TeamFoundationChangeSet changeSet : changeSets) {
    writer.println(String.format("\t<changeset version=\"%s\">", changeSet.getVersion()));
    writer.println(String.format("\t\t<date>%s</date>", Util.XS_DATETIME_FORMATTER.format(changeSet.getDate())));
    writer.println(String.format("\t\t<user>%s</user>", changeSet.getUser()));
    writer.println(String.format("\t\t<comment>%s</comment>", changeSet.getComment()));
    writer.println("\t\t<items>");
    for (TeamFoundationChangeSet.Item item : changeSet.getItems()) {
        writer.println(String.format("\t\t\t<item action=\"%s\">%s</item>", item.getAction(), item.getPath()));
    }
    writer.println("\t\t</items>");
    writer.println("\t</changeset>");
}
writer.println("</changelog>");
writer.close();

Parsing a change log

The change log is then parsed by Hudson during a build (after the SCM is done) so the changes in the build can be displayed in the Status and Changes page. The SCM method createChangeLogParser must return an object that can parse the XML file and return a ChangeLogSet derivate. Using digester it is very simple to parse an XML file and create objects from it, the only downside is that the classes that must have a default constructor.

List<TeamFoundationChangeSet> changesetList = new ArrayList<TeamFoundationChangeSet>();
Digester digester = new Digester2();
digester.push(changesetList);

// When digester reads a {{<changeset>}} node it will create a {{TeamFoundationChangeSet}} object
digester.addObjectCreate("*/changeset", TeamFoundationChangeSet.class);
// Reads all attributes in the {{<changeset>}} node and uses setter method in class to set the values
digester.addSetProperties("*/changeset");
// Reads the child node {{<comment>}} and uses {{TeamFoundationChangeSet.setComment()}} to set the value
digester.addBeanPropertySetter("*/changeset/comment");
digester.addBeanPropertySetter("*/changeset/user");
// Reading the {{<date<}} child node will use the {{TeamFoundationChangeSet.setDateStr()}} method
// instead of the default {{TeamFoundationChangeSet.setDate()}}
digester.addBeanPropertySetter("*/changeset/date", "dateStr");
// The digested node/change set is added to the list through {{List.add()}}
digester.addSetNext("*/changeset", "add");


// When digester reads a {{<items>}} child node of {{<changeset}} it will create a {{TeamFoundationChangeSet.Item}} object
digester.addObjectCreate("*/changeset/items/item", TeamFoundationChangeSet.Item.class);
digester.addSetProperties("*/changeset/items/item");
digester.addBeanPropertySetter("*/changeset/items/item", "path");
// The digested node/item is added to the change set through {{TeamFoundationChangeSet.add()}}
digester.addSetNext("*/changeset/items/item", "add");

// Do the actual parsing
FileReader reader = new FileReader(changelogFile)
digester.parse(reader);
reader.close();

The parsed change log items (change sets) should then be put into a ChangeLogSet derivate object that is returned by ChangeLogParser.parse(). Note that the object's class that is used to parse the change log file is stored in the build.xml, so it is not advisable to change name of the parser class later.

Jelly file

The jelly files are used to display the ChangeLogSet for each build. The files should be stored src/main/resources/hudson/plugins/scm/[pluginname]/ChangeLogSetDerivate folder.

digest.jelly

The digest.jelly file is used to display the change log on the status page. The listing should be brief and most of the time only display the change message.

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <j:choose>
    <j:when test="${it.emptySet}">
      ${%No changes.}
    </j:when>
    <j:otherwise>
      Changes
      <ol>
        <j:forEach var="cs" items="${it.items}" varStatus="loop">
          <li>
            ${cs.msgAnnotated} (<a href="changes#detail${loop.index}">detail</a>)
          </li>
        </j:forEach>
      </ol>
    </j:otherwise>
  </j:choose>
</j:jelly>

The above jelly code, will loop through ChangeLogSet.getLogs() and display the annotated message and a link to the Changes page.

index.jelly

The index.jelly file is used to display a detailed change log on the Changes page. The listing should include all data that is available in each change log entry.

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <h2>${%Summary}</h2>
  <ol>
    <j:forEach var="cs" items="${it.logs}">
      <li><st:out value="${cs.msg}"/></li>
    </j:forEach>
  </ol>
  <table class="pane" style="border:none">
    <j:forEach var="cs" items="${it.items}" varStatus="loop">
      <tr class="pane">
        <td colspan="2" class="changeset">
          <a name="detail${loop.index}"></a>
          <div class="changeset-message">
            <b>
              ${%Version} ${cs.version} by <a href="${rootURL}/${cs.author.url}/">${cs.author}</a>:
            </b><br/>
            ${cs.msgAnnotated}
          </div>
        </td>
      </tr>
      <j:forEach var="item" items="${cs.items}">
        <tr>
          <td><t:editTypeIcon type="${item.editType}" /></td>
          <td>${item.path}</td>
        </tr>
      </j:forEach>
    </j:forEach>
  </table>
</j:jelly>

The above jelly code will first display a listing of all change sets, with each change message. For each change set it will display the version number, a link to the author and annotated message and a list of changed items/files.

Tips

Change log parent

The change log entry must have a reference to the change log set that it is part of. The method ChangeLogEntry.setParent() is used to set the parent log set.

Exported annotation

The change log entries should have the @ExportedBean(defaultVisibility=999) so it is available through the Hudson XML api. Each get method that should be available in the XML api is marked with @Exported, as most of the get methods in the class.

@ExportedBean(defaultVisibility=999)
public class TeamFoundationChangeSet extends ChangeLogSet.Entry {
    private String version;

    @Exported
    public String getVersion() {
        return version;
    }
}

Date handling

The hudson.Util.XS_DATETIME_FORMATTER can help when writing and reading date objects to and from XML.