diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..5c75015
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,269 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+SilverStripe Project Build
+------------------------------------
+
+This build file contains targets to assist in creating new SilverStripe builds and releases.
+
+Important targets
+
+* changelog - Create a changelog.md file with the changes specified in changelog.ini
+* tag - Creates a new git tag in all the nested working copies (optionally pushes the created tag)
+* update-package - Creates a package that excludes several diretories that can be used for extracting over the top of existing installs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Please enter the name of the tag
+
+
+
+
+
+
+
+ Push local tags to origin?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Please enter the name of the tag or branch you wish to checkout
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Please choose archive format
+
+
+
+
+
+
+
+
+
+
+ Please enter a name for the archive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Please enter the module's git or svn URL
+
+
+
+
+
+
+
+
+
+
+ Please enter the module's name (i.e. the folder to module should be created in)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/changelog-definitions.default b/changelog-definitions.default
new file mode 100644
index 0000000..3e7b886
--- /dev/null
+++ b/changelog-definitions.default
@@ -0,0 +1,12 @@
+# Use this file to define which git repositories the "phing changlog" task will include when generating
+# a changelog.md file.
+#
+# Any paths not mentioned here will be excluded from the changelog. The script will ignore any paths that are not git
+# repositories, or are otherwise invalid.
+#
+# Leave the fields blank to include all commits.
+#
+#
+. 2.4.4-rc1 2.4.5
+sapphire 2.4.4-rc1 2.4.5
+cms 2.4.4-rc1 2.4.5
diff --git a/dependent-modules.default b/dependent-modules.default
new file mode 100644
index 0000000..59ec2b9
--- /dev/null
+++ b/dependent-modules.default
@@ -0,0 +1,34 @@
+# File format
+# -----------
+# Each line represents one dependent module.
+#
+# local_module_folder[:git_branch|:svn_revision_number] repository_url [run_dev_build=true] [local]
+#
+# Note that the local_module_folder can contain subfolders delimited via '/' characters
+# A specific git branch or SVN revision can be added in by specifying after the local
+# foldername, separated by a colon. By default, the 'master' branch of a git repository is used.
+#
+# It is recommended to have sqlite3 and cms first with [run_dev_build] set to "false".
+# Having this set to 'false' prevents the execution of the dev/build process, meaning it can be
+# deferred until all dependencies are in place, specifically the sapphire module. List
+# all additional modules after that.
+#
+# Examples
+#
+# frontend-editing:development git://github.com/nyeholt/silverstripe-frontend-editing.git
+# themes/mytheme git://local.server/themes/mytheme.git false
+
+
+cms:master:2.4.5 git://github.com/silverstripe/silverstripe-cms.git
+sapphire:master:2.4.5 git://github.com/silverstripe/sapphire.git
+
+
+
+# The following are the some other modules you might like to import
+
+# sqlite3:master:1.1.0 git://github.com/smindel/silverstripe-sqlite3.git
+# userforms:master:0.3.0 git://github.com/silverstripe/silverstripe-userforms.git
+# securefiles http://svn.polemic.net.nz/silverstripe/modules/SecureFiles/tags/0.30/
+
+
+
diff --git a/tools/CreateChangelog.php b/tools/CreateChangelog.php
new file mode 100644
index 0000000..ab60cc8
--- /dev/null
+++ b/tools/CreateChangelog.php
@@ -0,0 +1,221 @@
+definitions = $definitions;
+ }
+
+ public function setBaseDir($base) {
+ $this->baseDir = $base;
+ }
+
+ public function setSort($sort) {
+ $this->sort = $sort;
+ }
+
+ public function setFilter($filter) {
+ $this->filter = $filter;
+ }
+
+ /**
+ * Checks is a folder is a version control repository
+ */
+ protected function isRepository($dir, $filter) {
+ $dir = realpath($dir);
+
+ // open this directory
+ if ($handle = opendir($dir)) {
+
+ // get each file
+ while (false !== ($file = readdir($handle))) {
+ if ($file == $filter && is_dir($file)) {
+ if ($filter == '.git') { //extra check for git repos
+ if (file_exists($dir.'/'.$file.'/HEAD')) {
+ return true; //$dir is a git repository
+ }
+ } else { //return true for .svn repos
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ protected function isGitRepo($dir) {
+ return $this->isRepository($dir, '.git');
+ }
+
+ protected function isSvnRepo($dir) {
+ return $this->isRepository($dir, '.svn');
+ }
+
+ protected function gitLog($path, $from = null, $to = null) {
+ //set the from -> to range, depending on which os these have been set
+ if ($from && $to) $range = " $from..$to";
+ elseif ($from) $range = " $from..HEAD";
+ else $range = "";
+
+ $log = $this->exec("git log --pretty=tformat:\"%s (%aN) [%h]\"{$range} {$path}", true); //return output of command
+ return $log;
+ }
+
+ /** Sort by the first two letters of the commit string.
+ * Put any commits without BUGFIX, ENHANCEMENT, etc. at the end of the list
+ */
+ static function sortByType($a, $b) {
+ if (strlen($a) >= 2) $a = substr($a,0,2);
+ if (strlen($b) >= 2) $b = substr($b,0,2);
+
+ if (empty($b)) return -1; //put them at the end of the commit list
+ if (is_numeric($b)) return -1;
+ if (self::islower($b)) return -1;
+ if (!self::isupper($b)) return -1;
+
+ if (empty($a)) return +1;
+ if (self::islower($a)) return +1;
+ if (!self::isupper($a)) return +1;
+
+
+ if ($a == $b) {
+ return 0;
+ }
+ return ($a > $b) ? +1 : -1;
+ }
+
+ /** BETTER SORTING FUNCTION: Sort by the first two letters of the commit string.
+ * Put any commits without BUGFIX, ENHANCEMENT, etc. at the end of the list
+ */
+ static function sortByType2($array) {
+ $bugfixes = array();
+ $enhancements = array();
+ $apichanges = array();
+ $features = array();
+ $minors = array();
+ $others = array();
+
+ foreach($array as $ele) {
+ if (strlen($ele) >= 2) $ele1 = substr($ele,0,2); else $ele1 = $ele;
+
+ if ($ele1 == "BU") $bugfixes[] = $ele;
+ elseif ($ele1 == "EN") $enhancements[] = $ele;
+ elseif ($ele1 == "AP") $apichanges[] = $ele;
+ elseif ($ele1 == "FE") $features[] = $ele;
+ elseif ($ele1 == "MI") $minors[] = $ele;
+ elseif ($ele1 == "") ; //discard empty commit messages
+ else $others[] = $ele;
+ }
+
+ return array_merge(array("# API Changes"), $apichanges,
+ array("","# Features & Enhancements"), $features, $enhancements,
+ array("","# Bugfixes"),$bugfixes,
+ array("","# Minors"), $minors,
+ array("","# Others"), $others);
+ }
+
+ static function isupper($i) {
+ return (strtoupper($i) === $i);
+ }
+ static function islower($i) {
+ return (strtolower($i) === $i);
+ }
+
+ public function main() {
+ chdir($this->baseDir); //change current working directory
+
+ //parse the definitions file
+ $items = file($this->definitions);
+ $repos = array(); //git (or svn) repos to scan
+ foreach ($items as $item) {
+ $item = trim($item);
+ if (strpos($item, '#') === 0) {
+ continue;
+ }
+
+ $bits = preg_split('/\s+/', $item);
+
+ if (count($bits) == 1) {
+ $repos[$bits[0]] = "";
+ } elseif (count($bits) == 2) {
+ $repos[$bits[0]] = array($bits[1], null); //sapphire => array(from => HEAD)
+ } elseif (count($bits) == 3) {
+ $repos[$bits[0]] = array($bits[1],$bits[2]); //sapphire => array(from => to)
+ } else {
+ continue;
+ }
+ }
+
+ //check all the paths are valid git repos
+ $gitRepos = array();
+ $svnRepos = array();
+ foreach($repos as $path => $range) {
+ if ($this->isGitRepo($path)) $gitRepos[$path] = $range; //add all git repos to a special array
+ //TODO: for svn support use the isSvnRepo() method to add repos to the svnRepos array
+ }
+
+ //run git log (with author information)
+ $log = array();
+ foreach($gitRepos as $path => $range) {
+ if (!empty($range)) {
+ $from = (isset($range[0])) ? $range[0] : null;
+ $to = (isset($range[1])) ? $range[1] : null;
+ $log[$path] = $this->gitLog($path, $from, $to);
+ } else {
+ $log[$path] = $this->gitLog($path);
+ }
+ }
+
+ //merge all the changelogs together
+ $mergedLog = array();
+ foreach($log as $path => $commitsString) {
+ foreach(explode("\n",$commitsString) as $commit) { //array from newlines
+ $mergedLog[] = $commit;
+ }
+ }
+
+
+ //sort the output (based on params), grouping
+ if ($this->sort == 'type') { //sort by type, i.e. first two letters
+ $mergedLog = $this->sortByType2($mergedLog);
+ } else {
+ //leave as sorted by default order
+ }
+
+ //filter out commits we don't want
+ if ($this->filter) {
+ foreach($mergedLog as $key => $item) {
+ if (preg_match($this->filter, $item)) unset($mergedLog[$key]);
+ }
+ }
+
+ //convert to string
+ //and generate markdown (add list to beginning of each item)
+ $output = "";
+ foreach($mergedLog as $logMessage) {
+ $firstCharacter = substr($logMessage,0,1);
+ if ($firstCharacter != "#" && $firstCharacter != "") $output .= "- $logMessage\n";
+ else $output .= "$logMessage\n";
+ }
+
+
+
+
+ $this->project->setProperty('changelogOutput',$output);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/tools/FindRepositoriesTask.php b/tools/FindRepositoriesTask.php
new file mode 100644
index 0000000..50eaed7
--- /dev/null
+++ b/tools/FindRepositoriesTask.php
@@ -0,0 +1,62 @@
+targetDir = $targetDir;
+ }
+
+ /**
+ * Recursively lists a folder and includes only those directories that have the filter parameter as a sub-item
+ */
+ protected function recursiveListDirFilter($dir, &$result, $filter = '.git') {
+ $dir = realpath($dir);
+
+ // open this directory
+ if ($handle = opendir($dir)) {
+
+ // get each git entry
+ while (false !== ($file = readdir($handle))) {
+ if ($file == "." || $file == "..") continue;
+ //var_dump($file);
+ if ($file == '.git' && is_dir($file)) {
+ if (file_exists($dir.'/'.$file.'/HEAD')) {
+ $result[] = $dir; //$dir is a git repository
+ }
+ } else {
+ $path = $dir.'/'.$file;
+ if (is_dir($path)) {
+ $this->recursiveListDirFilter($path, $result, $filter);
+ }
+ }
+ }
+ }
+
+ // close directory
+ closedir($handle);
+
+ return $result;
+ }
+
+ public function main() {
+ if (!is_dir($this->targetDir)) {
+ throw new BuildException("Invalid target directory: $this->targetDir");
+ }
+
+ $gitDirs = array();
+ $this->recursiveListDirFilter($this->targetDir, $gitDirs, '.git');
+ $this->project->setProperty('GitReposList',implode(',',$gitDirs));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/tools/LoadModulesTask.php b/tools/LoadModulesTask.php
new file mode 100644
index 0000000..1a386ba
--- /dev/null
+++ b/tools/LoadModulesTask.php
@@ -0,0 +1,312 @@
+
+ *
+ */
+class LoadModulesTask extends SilverStripeBuildTask {
+ /**
+ * Character used to separate the module/revision name from the output path
+ */
+ const MODULE_SEPARATOR = ':';
+
+ /**
+ * The file that defines the dependency
+ *
+ * @var String
+ */
+ private $file = '';
+ /**
+ * Optionally specify a module name
+ *
+ * @var String
+ */
+ private $name = '';
+ /**
+ * And a module url
+ * @var String
+ */
+ private $url = '';
+
+ /**
+ * Is this a non-interactive build session?
+ * @var boolean
+ */
+ private $nonInteractive = false;
+
+ public function setNoninteractive($v) {
+ if (!strpos($v, '${') && $v == 'true' || $v == 1) {
+ $this->nonInteractive = true;
+ }
+ }
+
+ public function setFile($v) {
+ $this->file = $v;
+ }
+
+ public function setName($v) {
+ $this->name = $v;
+ }
+
+ public function setUrl($v) {
+ $this->url = $v;
+ }
+
+ public function main() {
+ $this->configureEnvFile();
+
+ if ($this->name) {
+ $this->loadModule($this->name, $this->url);
+ } else {
+ // load the items from the dependencies file
+ if (!file_exists($this->file)) {
+ throw new BuildException("Modules file " . $this->modulesFile . " cannot be read");
+ }
+
+ $items = file($this->file);
+ foreach ($items as $item) {
+ $item = trim($item);
+ if (strpos($item, '#') === 0) {
+ continue;
+ }
+
+ $bits = preg_split('/\s+/', $item);
+ // skip malformed lines
+ if (count($bits) < 2) {
+ continue;
+ }
+
+ $moduleName = trim($bits[0], '/');
+ $svnUrl = trim($bits[1], '/');
+ $storeLocally = false;
+
+ if (isset($bits[2])) {
+ $devBuild = $bits[2] == 'true';
+ $storeLocally = $bits[2] == 'local';
+ if (isset($bits[3])) {
+ $storeLocally = $bits[3] == 'local';
+ }
+ }
+
+ $this->loadModule($moduleName, $svnUrl, $devBuild, $storeLocally);
+ }
+ }
+ }
+
+ /**
+ * Actually load the module!
+ *
+ * @param String $moduleName
+ * @param String $svnUrl
+ * @param boolean $devBuild
+ * Do we run a dev/build?
+ * @param boolean $storeLocally
+ * Should we store the module locally, for it to be included in
+ * the local project's repository?
+ */
+ protected function loadModule($moduleName, $svnUrl, $devBuild = false, $storeLocally=false) {
+ $git = strrpos($svnUrl, '.git') == (strlen($svnUrl) - 4);
+ $branch = 'master';
+ $cmd = '';
+
+ $originalName = $moduleName;
+
+ if (strpos($moduleName, self::MODULE_SEPARATOR) > 0) {
+ $branch = substr($moduleName, strpos($moduleName, self::MODULE_SEPARATOR) + 1);
+ $moduleName = substr($moduleName, 0, strpos($moduleName, self::MODULE_SEPARATOR));
+ }
+
+ $md = $this->loadMetadata();
+ if (!isset($md['store'])) {
+ // backwards compatibility
+ $md['store'] = false;
+ }
+
+ // check the module out if it doesn't exist
+ $currentDir = trim(`pwd`," \n");
+ if (!file_exists($moduleName)) {
+ echo "Check out $moduleName from $svnUrl\n";
+ // check whether it's git or svn
+ if ($git) {
+ $this->exec("git clone $svnUrl $moduleName");
+ if ($branch != 'master') {
+ // check if we're also hooking onto a revision
+ $commitId = null;
+ if (strpos($branch, self::MODULE_SEPARATOR) > 0) {
+ $commitId = substr($branch, strpos($branch, self::MODULE_SEPARATOR) + 1);
+ $branch = substr($branch, 0, strpos($branch, self::MODULE_SEPARATOR));
+ }
+ // need to make sure we've pulled from the correct branch also
+ $currentDir = trim(`pwd`," \n");
+ if ($branch != 'master') {
+ $this->exec("cd $moduleName && git checkout -f -b $branch --track origin/$branch && cd \"$currentDir\"");
+ }
+
+ if ($commitId) {
+ $this->exec("cd $moduleName && git checkout $commitId && cd \"$currentDir\"");
+ }
+ }
+
+ if ($storeLocally) {
+ rrmdir("$moduleName/.git");
+ }
+ } else {
+ $revision = '';
+ if ($branch != 'master') {
+ $revision = " --revision $branch ";
+ }
+
+ $cmd = 'co';
+ if ($storeLocally) {
+ $cmd = 'export';
+ }
+
+ $this->exec("svn $cmd $revision $svnUrl $moduleName");
+ }
+
+ // make sure to append it to the .gitignore file
+ if (!$storeLocally && file_exists('.gitignore')) {
+ $gitIgnore = file_get_contents('.gitignore');
+ if (strpos($gitIgnore, $moduleName) === false) {
+ $this->exec("echo $moduleName >> .gitignore");
+ }
+ }
+ } else {
+ echo "Updating $moduleName $branch from $svnUrl\n";
+
+ $overwrite = true;
+ if (!$storeLocally) {
+ $statCmd = $git ? "git diff --name-status" : "svn status";
+ $mods = trim($this->exec("cd $moduleName && $statCmd && cd \"$currentDir\"", true));
+ if (strlen($mods) && !$storeLocally) {
+ $this->log("The following files are locally modified");
+ echo "\n $mods\n\n";
+ if (!$this->nonInteractive) {
+ $overwrite = strtolower(trim($this->getInput("Overwrite local changes? [y/N]")));
+ $overwrite = $overwrite == 'y';
+ }
+ }
+ }
+
+ // get the metadata and make sure it's not the same
+ if ($md && isset($md[$moduleName]) && isset($md[$moduleName]['url'])) {
+ if ($md[$moduleName]['url'] != $svnUrl || $md[$moduleName]['store'] != $storeLocally) {
+ if ($overwrite) {
+ // delete the directory and reload the module
+ echo "Deleting $moduleName and reloading\n";
+ unset($md[$moduleName]);
+ $this->writeMetadata($md);
+ rrmdir($moduleName, true);
+ $this->loadModule($originalName, $svnUrl, $devBuild, $storeLocally);
+ return;
+ } else {
+ throw new Exception("You have chosen not to overwrite changes, but also want to change your " .
+ "SCM settings. Please resolve changes and try again");
+ }
+ }
+ }
+
+ if (!$storeLocally) {
+ if ($git) {
+ $commitId = null;
+ if (strpos($branch, self::MODULE_SEPARATOR) > 0) {
+ $commitId = substr($branch, strpos($branch, self::MODULE_SEPARATOR) + 1);
+ $branch = substr($branch, 0, strpos($branch, self::MODULE_SEPARATOR));
+ }
+
+ $currentDir = trim(`pwd`," \n");
+
+ $currentBranch = trim($this->exec("cd $moduleName && git branch && cd \"$currentDir\"", true));
+
+ $overwriteOpt = $overwrite ? '-f' : '';
+
+ $this->exec("cd $moduleName && git checkout $overwriteOpt $branch && git pull origin $branch && cd \"$currentDir\"");
+
+ if ($commitId) {
+ $this->exec("cd $moduleName && git pull && git checkout $commitId && cd \"$currentDir\"");
+ }
+ } else {
+ $revision = '';
+ if ($branch != 'master') {
+ $revision = " --revision $branch ";
+ }
+
+ echo $this->exec("svn up $revision $moduleName");
+ }
+ }
+ }
+
+ $metadata = array(
+ 'url' => $svnUrl,
+ 'store' => $storeLocally,
+ 'branch' => str_replace($moduleName, '', $originalName),
+ );
+
+ $md[$moduleName] = $metadata;
+ $this->writeMetadata($md);
+
+
+
+ // make sure to remove from the .gitignore file - don't need to do it EVERY
+ // run, but it's better than munging code up above
+ if ($storeLocally && file_exists('.gitignore')) {
+ $gitIgnore = file('.gitignore');
+ $newIgnore = array();
+ foreach ($gitIgnore as $line) {
+ $line = trim($line);
+ if (!$line || $line == $moduleName || $line == "$moduleName/") {
+ continue;
+ }
+ $newIgnore[] = $line;
+ }
+
+ file_put_contents('.gitignore', implode("\n", $newIgnore));
+ }
+
+ if ($devBuild) {
+ $this->devBuild();
+ }
+ }
+
+ protected function loadMetadata() {
+ $metadataFile = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'phing-metadata';
+
+ $md = array();
+ if (file_exists($metadataFile)) {
+ $md = unserialize(file_get_contents($metadataFile));
+ }
+
+ return $md;
+ }
+
+ protected function writeMetadata($md) {
+ $metadataFile = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'phing-metadata';
+ file_put_contents($metadataFile, serialize($md));
+ }
+
+}
+
+if (!function_exists('rrmdir')) {
+ function rrmdir($dir) {
+ if (is_dir($dir)) {
+ $objects = scandir($dir);
+ foreach ($objects as $object) {
+ if ($object != "." && $object != "..") {
+ if (filetype($dir . "/" . $object) == "dir")
+ rrmdir($dir . "/" . $object); else
+ unlink($dir . "/" . $object);
+ }
+ }
+ reset($objects);
+ rmdir($dir);
+ }
+ }
+}
diff --git a/tools/SilverStripeBuildTask.php b/tools/SilverStripeBuildTask.php
new file mode 100644
index 0000000..da21abb
--- /dev/null
+++ b/tools/SilverStripeBuildTask.php
@@ -0,0 +1,80 @@
+cleanupEnv = false;
+ if (!file_exists($envFile)) {
+ file_put_contents($envFile, $ssEnv);
+ $this->cleanupEnv = true;
+ }
+ }
+
+ protected function cleanEnv() {
+ if ($this->cleanupEnv) {
+ $envFile = dirname(dirname(__FILE__)).'/_ss_environment.php';
+ if (file_exists($envFile)) {
+ unlink($envFile);
+ }
+ }
+ }
+
+ protected function devBuild() {
+ if (file_exists('sapphire/cli-script.php')) {
+ $this->log("Running dev/build");
+ $this->exec('php sapphire/cli-script.php dev/build');
+ }
+ }
+
+
+ /**
+ * Get some input from the user
+ *
+ * @param string $prompt
+ * @return string
+ */
+ protected function getInput($prompt) {
+ require_once 'phing/input/InputRequest.php';
+ $request = new InputRequest($prompt);
+ $request->setPromptChar(':');
+
+ $this->project->getInputHandler()->handleInput($request);
+ $value = $request->getInput();
+ return $value;
+ }
+
+ protected function exec($cmd, $returnContent = false, $ignoreError = false) {
+ $ret = null;
+ $return = null;
+ if ($returnContent) {
+ $ret = shell_exec($cmd);
+ } else {
+ passthru($cmd, $return);
+ }
+
+ if ($return != 0 && !$ignoreError) {
+ throw new BuildException("Command '$cmd' failed");
+ }
+
+ return $ret;
+ }
+}
\ No newline at end of file
diff --git a/tools/_manifest_exclude b/tools/_manifest_exclude
new file mode 100644
index 0000000..e69de29