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.