diff --git a/tools/changes2xml.pike b/tools/changes2xml.pike
new file mode 100755
index 0000000000000000000000000000000000000000..0fb98f8b28c024fbe24b66fb07e4762cdb933fd8
--- /dev/null
+++ b/tools/changes2xml.pike
@@ -0,0 +1,155 @@
+#!/usr/bin/env pike
+
+// Converts CHANGES to an XML form used to port change notes on pike.ida.liu.se
+// Usage: tools/changes2html.pike CHANGES | xclip   (And then paste into CMS)
+
+constant sub = "---"; //If a line begins with this the line above is a header.
+
+void main(int argc, array argv)
+{
+    string changes = Stdio.read_file(argv[1]);
+    string state = "INIT";
+    string out, tmp;
+    
+    out = "<column charwidth='60'>\n\n";
+    
+    foreach(changes/"\n", string line)
+    {
+	line -= "\r";
+	//	werror("state: %O\nout: %O\ntmp: %O\nline: %O\n-------\n", state, out, tmp, line);
+	switch(state) {
+	case "INIT":
+	    if(!text(line))
+		continue;
+	    if(line[0..sizeof(sub)-1] == sub)
+	    {
+		out += make_header(1, tmp);
+		state = "NONE";
+		continue;
+	    }
+	    if(sizeof(line)>=sizeof(sub))
+		tmp = line;
+	    continue;
+	case "NONE":
+	    if(!text(line))
+		continue;
+	    if(line[0..1] == "o ")
+	    {
+		tmp = line[2..]+"\n";
+		out += make_list_start();
+		state = "BULLET";
+		continue;
+	    }
+	    tmp = line;
+	    state = "HEADER"; //FIXME: large assumption, really should be TEXT.
+	    continue;
+	case "TEXT":
+	    exit(1, "FATAL: unhandled state 'TEXT'\n");
+	case "HEADER":
+	    //We only want the latest batch of changes.
+	    if(has_prefix(tmp, "Changes since Pike"))
+	    {
+		out += "</column>\n";
+
+		//Tweaks
+
+		//Remove 2nd empty <p>. Should really be done in make_bullet()
+		out = replace(out,
+			      ({ "\n<p>\n</p><p>\n</p>" }),
+			      ({ "\n<p></p>" }));
+
+		//Remove ":" at end of header line
+		out = replace(out,
+			      ({ ":</b>" }),
+			      ({ "</b>" }));
+
+		//Things that still has to be fixed manually:
+		// o Sub-bullets (could be automated easily)
+		// o One sentence bullets spread over several lines (trivial a)
+		// o Run-on descriptions with no clear bullet header (resonable method for some: Let "(Fixed bug .*) ((that|where) .*)" be header: /1, body: /2)
+		// o Putting emphasis on "You are encouraged to ugrade just for this fix .*" sentences. (trivial a)
+
+		write(out);
+		exit(0);
+	    }
+	    if(line[0..sizeof(sub)-1] == sub)
+	    {
+		out += make_header(2, tmp);
+		state = "NONE";
+		continue;
+	    } else
+		exit(1, "FATAL: Header state without underline.\n");
+	    continue;
+	case "BULLET": //broken by header or new bullet (or unindented text?)
+	    if(line[0..1] == "o ")
+	    {
+		out += make_bullet(tmp);
+		tmp = line[2..]+"\n";
+		state = "BULLET";
+		continue;
+	    }
+	    if(!indented(line))
+	    {
+		out += make_bullet(tmp);
+		out += make_list_end();
+		tmp = line;
+		state = "HEADER";
+		continue;
+	    }
+	    //not interrupted? add to tmp.
+	    tmp += line;
+	}
+    }
+}
+
+string make_bullet(string lines)
+{
+    string outtmp = "";
+
+    foreach(lines/"\n"; int nr; string line)
+    {
+	if(nr == 0)
+	    outtmp += "<li><b>"+line+"</b>\n<p>\n";
+	else
+	{
+	    if(!text(line))
+		outtmp += "</p><p>\n";
+	    else
+		outtmp += line+"\n";
+	}
+    }
+    outtmp += "</p></li>\n\n";
+
+    return outtmp;
+}
+
+string make_list_start()
+{
+    return "<p><ul>\n\n";
+}
+
+string make_list_end()
+{
+    return "</ul></p>\n\n";
+}
+
+int indented(string line)
+{
+    if(!text(line) || line[0..0] == " ")
+	return 1;
+    else
+	return 0;
+}
+
+int text(string line)
+{
+    if(sizeof(line-" "))
+	return 1;
+    else
+	return 0;
+}
+
+string make_header(int level, string text)
+{
+    return sprintf("<h%d>%s</h%d>\n\n", level, text, level);
+}
\ No newline at end of file