Skip to content

Commit

Permalink
Merge pull request #4 from tractorcow/pulls/merge-command
Browse files Browse the repository at this point in the history
API Implement merge command
  • Loading branch information
dhensby committed Mar 7, 2016
2 parents 14342cc + 2ccde12 commit f104d94
Show file tree
Hide file tree
Showing 10 changed files with 577 additions and 22 deletions.
24 changes: 24 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,27 @@ don't specify a list of modules then all modules will be translated. Specify 'in
You can use `--push` option to push to origin at the end, or `--exclude` if your list of modules is the list
to exclude. By default all modules are included, unless whitelisted or excluded.

## Branch helper

When a release is done, the laborious task of merging up all changes begins. This is where it
can be handy to use the `branch:merge` command. This command has this syntax:

`branch:merge <from> <to> [<module>, ..] [--interactive] [--push] [--exclude] [-vvv]`

This should be run in the project root, and will automatically merge each core module
from the `<from>` branch into the `<to>` branch. If either branches haven't yet been
pulled from upstream, then this command will automatically pull them down, and will also
refresh any existing branch before the merge.

By default all modules specified in the root composer.json with `self.version` will be merged.
You can specify a single module (or set of modules) by adding additional arguments, which will
instead choose those modules.

If you want the merged changes to be pushed up directly, then use the `--push` command to
trigger a push after the merge is complete.

If a merge fails, or has unresolved conflicts, then a message will be displayed at the end of
execution with the list of directories that should be manually resolved. Once resolved (and
committed), just run the command again and it should continue.

`--interactive` mode will pause before each commit, to allow you to review changes.
3 changes: 3 additions & 0 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ protected function getDefaultCommands()
// Module commands
$commands[] = new Commands\Module\Translate();

// Branch commands
$commands[] = new Commands\Branch\Merge();

return $commands;
}
}
116 changes: 116 additions & 0 deletions src/Commands/Branch/Merge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace SilverStripe\Cow\Commands\Branch;

use SilverStripe\Cow\Commands\Module\Module;
use SilverStripe\Cow\Commands\Release\Release;
use SilverStripe\Cow\Model\ReleaseVersion;
use SilverStripe\Cow\Steps\Branch\MergeBranch;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Logger\ConsoleLogger;


/**
* Description of Create
*
* @author dmooyman
*/
class Merge extends Module
{
/**
* @var string
*/
protected $name = 'branch:merge';

protected $description = 'Merge branches';

protected function configureOptions()
{
$this->addArgument('from', InputArgument::REQUIRED, 'Branch name to merge from');
$this->addArgument('to', InputArgument::REQUIRED, 'Branch name to merge to');
$this->addOption('interactive', 'i', InputOption::VALUE_NONE, 'Use interactive mode');

parent::configureOptions(); // TODO: Change the autogenerated stub
}

protected function fire()
{
$directory = $this->getInputDirectory();
$modules = $this->getInputModules();
$listIsExclusive = $this->getInputExclude();
$push = $this->getInputPush();
$from = $this->getInputFrom();
$to = $this->getInputTo();
$interactive = $this->getInputInteractive();

// Bit of a sanity check on version
if(!$this->canMerge($from, $to)) {
throw new \InvalidArgumentException("{$to} seems like an older version that {$from}. Are you sure that's correct?");
}

$merge = new MergeBranch($this, $directory, $modules, $listIsExclusive, $from, $to, $push, $interactive);
$merge->run($this->input, $this->output);
}

/**
* Using SS workflow, can you merge $from into $to?
*
* @param string $from
* @param string $to
* @return bool
*/
protected function canMerge($from, $to) {
if($from === 'master') {
return false;
}
if($to === 'master') {
return true;
}

// Allow if either $from or $to are non-numeric
if(!preg_match('/^(\d+)(.\d+)*$/', $from) || !preg_match('/^(\d+)(.\d+)*$/', $to)) {
return true;
}

// Apply minor vs major rule (3.3 > 3 but not 3 > 3.3)
if(stripos($from, $to) === 0) {
return true;
}
if(stripos($to, $from) === 0) {
return false;
}

// Otherwise, just make sure the to version is a higher value
return version_compare($to, $from, '>');
}

/**
* Get branch to merge from
*
* @return string
*/
protected function getInputFrom()
{
return $this->input->getArgument('from');
}

/**
* Get branch to merge to
*
* @return string
*/
protected function getInputTo()
{
return $this->input->getArgument('to');
}

/**
* Check if running in interactive mode
*
* @return bool
*/
protected function getInputInteractive() {
return $this->input->getOption('interactive');
}
}
2 changes: 1 addition & 1 deletion src/Model/ChangeLogItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class ChangeLogItem
'/^(ENHANCEMENT|ENHNACEMENT|FEATURE|NEW)\s?:?/i'
),
'Bugfixes' => array(
'/^(BUGFIX|BUGFUX|BUG|FIX|FIXED)\s?:?/',
'/^(BUGFIX|BUGFUX|BUG|FIX|FIXED|FIXING)\s?:?/',
'/^(BUG FIX)\s?:?/'
)
);
Expand Down
140 changes: 129 additions & 11 deletions src/Model/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace SilverStripe\Cow\Model;

use Exception;
use Gitonomy\Git\Reference\Branch;
use Gitonomy\Git\Repository;
use InvalidArgumentException;
use Symfony\Component\Console\Logger\ConsoleLogger;
use Symfony\Component\Console\Output\OutputInterface;

/**
* A module installed in a project
Expand Down Expand Up @@ -150,17 +153,27 @@ public function getLink()
$name = $this->getName();
return "https://github.com/{$team}/silverstripe-{$name}/";
}

/**
* Get git repo for this module
*
*
* @param OutputInterface $output Optional output to log to
* @return Repository
*/
public function getRepository()
public function getRepository(OutputInterface $output = null)
{
return new Repository($this->directory, array(
'environment_variables' => array('HOME' => getenv('HOME'))
$repo = new Repository($this->directory, array(
'environment_variables' => array(
'HOME' => getenv('HOME'),
'SSH_AUTH_SOCK' => getenv('SSH_AUTH_SOCK')
)
));
// Include logger if requested
if($output) {
$logger = new ConsoleLogger($output);
$repo->setLogger($logger);
}
return $repo;
}

/**
Expand All @@ -177,14 +190,38 @@ public function getBranch()
}

/**
* Change to another branch, creating it if it doesn't exist
* Gets local branches
*
* @param string $branch
* @param string $remote If specified, select from remote instead. If ignored, select local
* @return array
*/
public function changeBranch($branch)
{
$repo = $this->getRepository();
$repo->run('checkout', array('-B', $branch));
public function getBranches($remote = null) {
// Query remotes
$result = $this->getRepository()->run('branch', $remote ? ['-r'] : []);

// Filter output
$branches = [];
foreach(preg_split('/\R/u', $result) as $line) {
$line = trim($line);
// Skip empty lines, or anything with whitespace in it
if(empty($line) || preg_match('#\s#', $line)) {
continue;
}
// Check remote prefix
if($remote) {
$prefix = "{$remote}/";
if(stripos($line, $prefix) === 0) {
$line = substr($line, strlen($prefix));
} else {
// Skip if not a branch on this remote
continue;
}
}

// Save branch
$branches[] = $line;
}
return $branches;
}

/**
Expand Down Expand Up @@ -216,6 +253,7 @@ public function addTag($tag)
*
* @param string $remote
* @param bool $tags Push tags?
* @throws Exception
*/
public function pushTo($remote = 'origin', $tags = false)
{
Expand All @@ -235,4 +273,84 @@ public function pushTo($remote = 'origin', $tags = false)
// Push
$repo->run('push', $args);
}

/**
* Fetch all upstream changes
*
* @param OutputInterface $output
* @param string $remote
*/
public function fetch(OutputInterface $output, $remote = 'origin') {
$this->getRepository($output)
->run('fetch', array($remote));
}

/**
* Checkout given branch name.
*
* Note that this method respects ambiguous branch names (e.g. 3.1.0 branch which
* may have just been tagged as 3.1.0, and is about to get deleted).
*
* @param OutputInterface $output
* @param string $branch
* @param string $remote
*/
public function checkout(OutputInterface $output, $branch, $remote = 'origin') {
// Check if local branch exists
$localBranches = $this->getBranches();
$remoteBranches = $this->getBranches($remote);
$repository = $this->getRepository($output);

// Make sure branch exists somewhere
if(!in_array($branch, $localBranches) && !in_array($branch, $remoteBranches)) {
throw new InvalidArgumentException("Branch {$branch} is not a local or remote branch");
}

// Check if we need to switch branch
if($this->getBranch() !== $branch) {
// Find source for branch to checkout from (must disambiguate from tags)
if (!in_array($branch, $localBranches)) {
$sourceRef = "{$remote}/{$branch}";
} else {
$sourceRef = "refs/heads/{$branch}";
}

// Checkout branch
$repository->run('checkout', [
'-B',
$branch,
$sourceRef,
]);
}

// If branch is on live and local, we need to synchronise changes on local
// (but don't push!)
if(in_array($branch, $localBranches) && in_array($branch, $remoteBranches)) {
$repository->run('pull', [$remote, $branch]);
}
}

/**
* Get composer.json as array format
*
* @return array
* @throws Exception
*/
public function getComposerData() {
$path = $this->getDirectory() . '/composer.json';
if(!file_exists($path)) {
throw new Exception("No composer.json found in module " . $this->getName());
}
return json_decode(file_get_contents($path), true);
}

/**
* Get composer name
*
* @return string
*/
public function getComposerName() {
$data = $this->getComposerData();
return $data['name'];
}
}
25 changes: 21 additions & 4 deletions src/Model/Project.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ public static function exists_in($directory)
}

/**
* Gets the list of modules in this installer
* Gets the list of self.version modules in this installer
*
* @param array $filter Optional list of modules to filter
* @param bool $listIsExclusive Set to true if this list is exclusive
* @return Module[]
*/
public function getModules($filter = array(), $listIsExclusive = false)
{
$composer = $this->getComposerData();

// Include self as head module
$modules = array();
if (empty($filter) || in_array($this->getName(), $filter) != $listIsExclusive) {
Expand All @@ -53,11 +55,26 @@ public function getModules($filter = array(), $listIsExclusive = false)
continue;
}

// Filter
// Skip if filtered
$name = basename($dir);
if (empty($filter) || in_array($name, $filter) != $listIsExclusive) {
$modules[] = new Module($dir, $name, $this);
if (!empty($filter) && (in_array($name, $filter) == $listIsExclusive)) {
continue;
}
$module = new Module($dir, $name, $this);

// Filter by self.version module,
// but let whitelisted queries to override this
if(empty($filter) || $listIsExclusive) {
$composerName = $module->getComposerName();
if (!isset($composer['require'][$composerName]) ||
($composer['require'][$composerName] !== 'self.version')
) {
continue;
}
}

// Save
$modules[] = $module;
}
return $modules;
}
Expand Down
Loading

0 comments on commit f104d94

Please sign in to comment.