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