Merge branch 'develop' of projects.ceondo.com:indefero into develop

This commit is contained in:
Loïc d'Anterroches 2010-10-22 16:25:37 +02:00
commit 8b2363fd6f
28 changed files with 813 additions and 590 deletions

View File

@ -1,62 +0,0 @@
# monotone implementation notes
## general
This version of indefero contains an implementation of the monotone
automation interface. It needs at least monotone version 0.99
(interface version 13.0) or newer.
To set up a new IDF project with monotone quickly, all you need to do
is to create a new monotone database with
$ mtn db init -d project.mtn
in the configured repository path `$cfg['mtn_repositories']` and
configure `$cfg['mtn_db_access']` to "local".
To have a really workable setup, this database needs an initial commit
on the configured master branch of the project. This can be done easily
with
$ mkdir tmp && touch tmp/remove_me
$ mtn import -d project.mtn -b master.branch.name \
-m "initial commit" tmp
$ rm -rf tmp
Its expected that more scripts arrive soon to automate this and other
tasks in the future for (multi)forge setups.
## current state / internals
The implementation should be fairly stable and fast, though some
information, such as individual file sizes or last change information,
won't scale well with the tree size. Its expected that the mtn
automation interface improves in this area in the future and that
these parts can then be rewritten with speed in mind.
As the idf.conf-dist explains more in detail, different access patterns
are possible to retrieve changeset data from monotone. Please refer
to the documentation there for more information.
## indefero critique:
It was not always 100% clear what some of the abstract SCM API method
wanted in return. While it helped a lot to have prior art in form of the
SVN and git implementation, the documentation of the abstract IDF_Scm
should probably still be improved.
Since branch and tag names can be of arbitrary size, it was not possible
to display them completely in the default layout. This might be a problem
in other SCMs as well, in particular for the monotone implementation I
introduced a special filter, called "IDF_Views_Source_ShortenString".
The API methods getPathInfo() and getTree() return similar VCS "objects"
which unfortunately do not have a well-defined structure - this should
probably addressed in future indefero releases.
While the returned objects from getTree() contain all the needed
information, indefero doesn't seem to use them to sort the output
f.e. alphabetically or in such a way that directories are outputted
before files. It was unclear if the SCM implementor should do this
task or not and what the admired default sorting should be.

175
doc/syncmonotone.mdtext Normal file
View File

@ -0,0 +1,175 @@
# Plugin SyncMonotone by Thomas Keller (me@thomaskeller.biz)
The SyncMonotone plugin allow the direct creation and synchronisation of
monotone repositories with the InDefero database. It has been built to
work together with monotone's "super server" usher, which is used to control
several repositories at once, acts as proxy and single entrance.
## Prerequisites
* a unixoid operating system
* monotone >= 0.99
* for a proxy setup with usher:
* boost headers (for usher compilation)
* a current version of usher
* a daemonizer, like supervise
## Installation of monotone
If you install monotone from a distribution package, ensure you do not
install and / or activate the server component. We just need a plain
client installation which usually consists only of the `mtn` binary and
a few docs.
If you install monotone from source (<http://monotone.ca/downloads.php>),
please follow the `INSTALL` document which comes with the software.
It contains detailed instructions, including all needed dependencies.
## Choose your indefero setup
The monotone plugin can be used in several different ways:
1. One database for everything. This is the easiest setup and of possible
use in case you do not want indefero to manage the access to your project.
Your `idf.php` should look like this:
$ cat idf.php
...
$cfg['mtn_path'] = 'mtn';
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles');
$cfg['mtn_repositories'] = '/home/monotone/all_projects.mtn';
$cfg['mtn_remote_url'] = 'ssh://monotone@my.server.com:~all_projects.mtn';
$cfg['mtn_db_access'] = 'local';
...
Pro:
* easy to setup and to manage
Con:
* you need to give committers SSH access to your machine
* database lock problem: the database from which
indefero reads its data might be locked in case a user
syncs at the very moment via SSH
2. One database for every project. Similar to the above setup, but this
time you use the '%s' placeholder which is replaced with the short name
of the indefero project:
$ cat idf.php
...
$cfg['mtn_path'] = 'mtn';
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles');
$cfg['mtn_repositories'] = '/home/monotone/%s.mtn';
$cfg['mtn_remote_url'] = 'ssh://monotone@my.server.com:~%s.mtn';
$cfg['mtn_db_access'] = 'local';
...
The same pro's and con's apply. Additionally you have to be careful about
not giving people physical read/write access of another project's database.
Furthermore, if you do not want to use `ssh`, but `netsync` transport,
each project's database must be served over a separate port.
3. One database for every project, all managed with usher. This is the
recommended setup for a mid-size forge setup. The remaining part of this
document will describe the process to set this up in detail.
Pro:
* access rights can be granted per project and are automatically
managed by indefero, just like the user's public monotone keys
* no database locking issues
* one public server running on the one well-known port
Con:
* harder to setup
## Installation and configuration of usher
1. Clone usher's monotone repository:
$ mtn clone "mtn://monotone.ca?net.venge.monotone.contrib.usher"
2. Compile usher:
$ autoreconf -i
$ ./configure && make
$ sudo make install
This installs the usher binary in $prefix/bin.
3. Create a new usher user:
$ adduser --system --disabled-login --home /var/lib/usher usher
4. Create the basic usher setup:
$ cd /var/lib/usher
$ mkdir projects logs
$ cat > usher.conf
userpass "admin" "<secret-password>"
adminaddr "127.0.0.1:12345"
logdir "log"
^D
$ chmod 600 usher.conf
Your indefero www user needs later write access to `usher.conf` and
`projects/`. There are two ways of setting this up:
* Make the usher user the web user, for example via Apache's `suexec`
* Use acls, like this:
$ setfacl -m u:www:rw usher.conf
$ setfacl -m d:u:www:rwx projects/
5. Wrap a daemonizer around usher, for example supervise from daemontools
(<http://cr.yp.to/damontools.html>):
$ cat > run
#!/bin/sh
cd /var/lib/usher
exec 2>&1
exec \
setuidgid usher \
usher usher.conf
^D
The service can now be started through supervise:
$ supervise /var/lib/usher
## Configuration of indefero
Based on the above setup, the configuration in `src/IDF/conf/idf.php` should
look like this:
$ cat idf.php
...
$cfg['mtn_path'] = 'mtn';
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles');
$cfg['mtn_repositories'] = '/var/lib/usher/projects/%s/';
$cfg['mtn_remote_url'] = 'mtn://my.server.com/%s';
$cfg['mtn_db_access'] = 'remote';
$cfg['mtn_remote_auth'] = true;
$cfg['mtn_usher_conf'] = '/var/lib/usher/usher.conf';
...
The `%s` placeholders are automatically replaced by the name of the
indefero project. The plugin assumes that every project is separated
by a distinct server name in the monotone URL (hence the use of `/%s`),
so if a user calls
$ mtn sync mtn://my.server.com/project1
then the database / repository of the indefero `project1` is used.
Note that 'mtn_remote_url' is also used as internal URI to query the data
for indefero's source view, so it *must* be a valid host!
Usher also allows the identification of a project repository by hostname,
which would allow an URL template like `mtn://%s.my.server.com`, however
the plugin does not write out the configuration which is needed for this
yet.
For even more advanced setups, usher can also be used to forward sync
requests to other remote servers for load balancing, please consult the
README file for more information.

View File

@ -15,8 +15,7 @@ res=$(cd "$dir" && /bin/pwd || "$dir")
SCRIPTDIR="$res/$(readlink $0)" SCRIPTDIR="$res/$(readlink $0)"
PHP_POST_PUSH=$SCRIPTDIR/mtnpostpush.php PHP_POST_PUSH=$SCRIPTDIR/mtnpostpush.php
base=$(basename "$0") TMPFILE=$(mktemp /tmp/mtn-post-push.XXXXXX) || exit 1
TMPFILE=$(mktemp /tmp/${tempfoo}.XXXXXX) || exit 1
while read rev; do echo $rev >> $TMPFILE; done while read rev; do echo $rev >> $TMPFILE; done
echo php $PHP_POST_PUSH "$1" \< $TMPFILE \&\& rm -f $TMPFILE |\ echo php $PHP_POST_PUSH "$1" \< $TMPFILE \&\& rm -f $TMPFILE |\

View File

@ -72,6 +72,9 @@ class IDF_Diff
$indiff = true; $indiff = true;
continue; continue;
} else if (0 === strpos($line, '=========')) { } else if (0 === strpos($line, '=========')) {
// ignore pseudo stanzas with a hint of a binary file
if (preg_match("/^# (.+) is binary/", $this->lines[$i]))
continue;
// by default always use the new name of a possibly renamed file // by default always use the new name of a possibly renamed file
$current_file = self::getMtnFile($this->lines[$i+1]); $current_file = self::getMtnFile($this->lines[$i+1]);
// mtn 0.48 and newer set /dev/null as file path for dropped files // mtn 0.48 and newer set /dev/null as file path for dropped files

View File

@ -313,6 +313,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
'labels_download_one_max' => IDF_Form_UploadConf::init_one_max, 'labels_download_one_max' => IDF_Form_UploadConf::init_one_max,
'labels_wiki_predefined' => IDF_Form_WikiConf::init_predefined, 'labels_wiki_predefined' => IDF_Form_WikiConf::init_predefined,
'labels_wiki_one_max' => IDF_Form_WikiConf::init_one_max, 'labels_wiki_one_max' => IDF_Form_WikiConf::init_one_max,
'labels_issue_template' => IDF_Form_IssueTrackingConf::init_template,
'labels_issue_open' => IDF_Form_IssueTrackingConf::init_open, 'labels_issue_open' => IDF_Form_IssueTrackingConf::init_open,
'labels_issue_closed' => IDF_Form_IssueTrackingConf::init_closed, 'labels_issue_closed' => IDF_Form_IssueTrackingConf::init_closed,
'labels_issue_predefined' => IDF_Form_IssueTrackingConf::init_predefined, 'labels_issue_predefined' => IDF_Form_IssueTrackingConf::init_predefined,

View File

@ -45,6 +45,9 @@ class IDF_Form_IssueCreate extends Pluf_Form
or $this->user->hasPerm('IDF.project-member', $this->project)) { or $this->user->hasPerm('IDF.project-member', $this->project)) {
$this->show_full = true; $this->show_full = true;
} }
$contentTemplate = $this->project->getConf()->getVal(
'labels_issue_template', IDF_Form_IssueTrackingConf::init_template
);
$this->fields['summary'] = new Pluf_Form_Field_Varchar( $this->fields['summary'] = new Pluf_Form_Field_Varchar(
array('required' => true, array('required' => true,
'label' => __('Summary'), 'label' => __('Summary'),
@ -57,7 +60,7 @@ class IDF_Form_IssueCreate extends Pluf_Form
$this->fields['content'] = new Pluf_Form_Field_Varchar( $this->fields['content'] = new Pluf_Form_Field_Varchar(
array('required' => true, array('required' => true,
'label' => __('Description'), 'label' => __('Description'),
'initial' => '', 'initial' => $contentTemplate,
'widget' => 'Pluf_Form_Widget_TextareaInput', 'widget' => 'Pluf_Form_Widget_TextareaInput',
'widget_attrs' => array( 'widget_attrs' => array(
'cols' => 58, 'cols' => 58,

View File

@ -31,6 +31,15 @@ class IDF_Form_IssueTrackingConf extends Pluf_Form
* Defined as constants to easily access the value in the * Defined as constants to easily access the value in the
* IssueUpdate/Create form in the case nothing is in the db yet. * IssueUpdate/Create form in the case nothing is in the db yet.
*/ */
const init_template = 'Steps to reproduce the problem:
1.
2.
3.
Expected result:
Actual result:
';
const init_open = 'New = Issue has not had initial review yet const init_open = 'New = Issue has not had initial review yet
Accepted = Problem reproduced / Need acknowledged Accepted = Problem reproduced / Need acknowledged
Started = Work on this issue has begun'; Started = Work on this issue has begun';
@ -66,6 +75,15 @@ Maintainability = Hinders future changes';
public function initFields($extra=array()) public function initFields($extra=array())
{ {
$this->fields['labels_issue_template'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Define an issue template to hint the reporter to provide certain information'),
'initial' => self::init_template,
'widget_attrs' => array('rows' => 7,
'cols' => 75),
'widget' => 'Pluf_Form_Widget_TextareaInput',
));
$this->fields['labels_issue_open'] = new Pluf_Form_Field_Varchar( $this->fields['labels_issue_open'] = new Pluf_Form_Field_Varchar(
array('required' => true, array('required' => true,
'label' => __('Open issue status values'), 'label' => __('Open issue status values'),
@ -99,8 +117,6 @@ Maintainability = Hinders future changes';
'widget_attrs' => array('size' => 60), 'widget_attrs' => array('size' => 60),
)); ));
} }
} }

View File

@ -80,7 +80,7 @@ class IDF_Key extends Pluf_Model
if (preg_match('#^\[pubkey ([^\]]+)\]\s*(\S+)\s*\[end\]$#', $this->content, $m)) { if (preg_match('#^\[pubkey ([^\]]+)\]\s*(\S+)\s*\[end\]$#', $this->content, $m)) {
return array('mtn', $m[1], $m[2]); return array('mtn', $m[1], $m[2]);
} }
else if (preg_match('#^ssh\-[a-z]{3}\s(\S+)\s(\S+)$#', $this->content, $m)) { else if (preg_match('#^ssh\-[a-z]{3}\s(\S+)(?:\s(\S+))?$#', $this->content, $m)) {
return array('ssh', $m[2], $m[1]); return array('ssh', $m[2], $m[1]);
} }

View File

@ -112,6 +112,7 @@ function IDF_Middleware_ContextPreProcessor($request)
$c = array_merge($c, $request->rights); $c = array_merge($c, $request->rights);
} }
$c['usherConfigured'] = Pluf::f("mtn_usher_conf", null) !== null; $c['usherConfigured'] = Pluf::f("mtn_usher_conf", null) !== null;
$c['allProjects'] = IDF_Views::getProjects($request->user);
return $c; return $c;
} }

View File

@ -76,6 +76,10 @@ class IDF_Plugin_SyncMonotone
return; return;
} }
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
$projecttempl = Pluf::f('mtn_repositories', false); $projecttempl = Pluf::f('mtn_repositories', false);
if ($projecttempl === false) { if ($projecttempl === false) {
throw new IDF_Scm_Exception( throw new IDF_Scm_Exception(
@ -246,7 +250,9 @@ class IDF_Plugin_SyncMonotone
array('key' => 'server', 'values' => array($shortname)), array('key' => 'server', 'values' => array($shortname)),
array('key' => 'local', 'values' => array( array('key' => 'local', 'values' => array(
'--confdir', $projectpath, '--confdir', $projectpath,
'-d', $dbfile '-d', $dbfile,
'--timestamps',
'--ticker=dot'
)), )),
); );
@ -278,6 +284,10 @@ class IDF_Plugin_SyncMonotone
return; return;
} }
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
$mtn = IDF_Scm_Monotone::factory($project); $mtn = IDF_Scm_Monotone::factory($project);
$stdio = $mtn->getStdio(); $stdio = $mtn->getStdio();
@ -338,6 +348,10 @@ class IDF_Plugin_SyncMonotone
return; return;
} }
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
$usher_config = Pluf::f('mtn_usher_conf', false); $usher_config = Pluf::f('mtn_usher_conf', false);
if (!$usher_config || !is_writable($usher_config)) { if (!$usher_config || !is_writable($usher_config)) {
throw new IDF_Scm_Exception( throw new IDF_Scm_Exception(
@ -419,8 +433,13 @@ class IDF_Plugin_SyncMonotone
*/ */
public function processKeyCreate($key) public function processKeyCreate($key)
{ {
if ($key->getType() != 'mtn') if ($key->getType() != 'mtn') {
return; return;
}
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
foreach (Pluf::factory('IDF_Project')->getList() as $project) { foreach (Pluf::factory('IDF_Project')->getList() as $project) {
$conf = new IDF_Conf(); $conf = new IDF_Conf();
@ -525,8 +544,13 @@ class IDF_Plugin_SyncMonotone
*/ */
public function processKeyDelete($key) public function processKeyDelete($key)
{ {
if ($key->getType() != 'mtn') if ($key->getType() != 'mtn') {
return; return;
}
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
foreach (Pluf::factory('IDF_Project')->getList() as $project) { foreach (Pluf::factory('IDF_Project')->getList() as $project) {
$conf = new IDF_Conf(); $conf = new IDF_Conf();

View File

@ -157,9 +157,11 @@ class IDF_Scm_Monotone extends IDF_Scm
*/ */
private function _getCerts($rev) private function _getCerts($rev)
{ {
static $certCache = array(); $cache = Pluf_Cache::factory();
$cachekey = 'mtn-plugin-certs-for-rev-' . $rev;
$certs = $cache->get($cachekey);
if (!array_key_exists($rev, $certCache)) { if ($certs === null) {
$out = $this->stdio->exec(array('certs', $rev)); $out = $this->stdio->exec(array('certs', $rev));
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out); $stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
@ -183,10 +185,10 @@ class IDF_Scm_Monotone extends IDF_Scm
} }
} }
} }
$certCache[$rev] = $certs; $cache->set($cachekey, $certs);
} }
return $certCache[$rev]; return $certs;
} }
/** /**
@ -212,34 +214,6 @@ class IDF_Scm_Monotone extends IDF_Scm
return array_unique($certValues); return array_unique($certValues);
} }
/**
* Returns the revision in which the file has been last changed,
* starting from the start rev
*
* @param string
* @param string
* @return string
*/
private function _getLastChangeFor($file, $startrev)
{
$out = $this->stdio->exec(array(
'get_content_changed', $startrev, $file
));
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
// FIXME: we only care about the first returned content mark
// everything else seem to be very, very rare cases
foreach ($stanzas as $stanza) {
foreach ($stanza as $stanzaline) {
if ($stanzaline['key'] == 'content_mark') {
return $stanzaline['hash'];
}
}
}
return null;
}
/** /**
* @see IDF_Scm::inBranches() * @see IDF_Scm::inBranches()
*/ */
@ -297,51 +271,62 @@ class IDF_Scm_Monotone extends IDF_Scm
} }
/** /**
* @see IDF_Scm::getTree() * Takes a single stanza coming from an extended manifest output
* and converts it into a file structure used by IDF
*
* @param string $forceBasedir If given then the element's path is checked
* to be directly beneath the given directory.
* If not, null is returned and the parsing is
* aborted.
* @return array | null
*/ */
public function getTree($commit, $folder='/', $branch=null) private function _fillFileEntry(array $manifestEntry, $forceBasedir = null)
{ {
$revs = $this->_resolveSelector($commit); $fullpath = $manifestEntry[0]['values'][0];
if (count($revs) == 0) { $filename = basename($fullpath);
return array(); $dirname = dirname($fullpath);
$dirname = $dirname == '.' ? '' : $dirname;
if ($forceBasedir !== null && $forceBasedir != $dirname) {
return null;
} }
$out = $this->stdio->exec(array(
'get_manifest_of', $revs[0]
));
$files = array();
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
$folder = $folder == '/' || empty($folder) ? '' : $folder.'/';
foreach ($stanzas as $stanza) {
if ($stanza[0]['key'] == 'format_version')
continue;
$path = $stanza[0]['values'][0];
if (!preg_match('#^'.$folder.'([^/]+)$#', $path, $m))
continue;
$file = array(); $file = array();
$file['file'] = $m[1]; $file['file'] = $filename;
$file['fullpath'] = $path; $file['fullpath'] = $fullpath;
$file['efullpath'] = self::smartEncode($path); $file['efullpath'] = self::smartEncode($fullpath);
if ($stanza[0]['key'] == 'dir') { $wanted_mark = '';
if ($manifestEntry[0]['key'] == 'dir') {
$file['type'] = 'tree'; $file['type'] = 'tree';
$file['size'] = 0; $file['size'] = 0;
$wanted_mark = 'path_mark';
} }
else else {
{
$file['type'] = 'blob'; $file['type'] = 'blob';
$file['hash'] = $stanza[1]['hash']; $file['hash'] = $manifestEntry[1]['hash'];
$file['size'] = strlen($this->getFile((object)$file)); $size = 0;
foreach ($manifestEntry as $line) {
if ($line['key'] == 'size') {
$size = $line['values'][0];
break;
}
}
$file['size'] = $size;
$wanted_mark = 'content_mark';
} }
$rev = $this->_getLastChangeFor($file['fullpath'], $revs[0]); $rev_mark = null;
if ($rev !== null) { foreach ($manifestEntry as $line) {
$file['rev'] = $rev; if ($line['key'] == $wanted_mark) {
$certs = $this->_getCerts($rev); $rev_mark = $line['hash'];
break;
}
}
if ($rev_mark !== null) {
$file['rev'] = $rev_mark;
$certs = $this->_getCerts($rev_mark);
// FIXME: this assumes that author, date and changelog are always given // FIXME: this assumes that author, date and changelog are always given
$file['author'] = implode(", ", $certs['author']); $file['author'] = implode(", ", $certs['author']);
@ -360,6 +345,35 @@ class IDF_Scm_Monotone extends IDF_Scm
$file['log'] = $split[0]; $file['log'] = $split[0];
} }
return $file;
}
/**
* @see IDF_Scm::getTree()
*/
public function getTree($commit, $folder='/', $branch=null)
{
$revs = $this->_resolveSelector($commit);
if (count($revs) == 0) {
return array();
}
$out = $this->stdio->exec(array(
'get_extended_manifest_of', $revs[0]
));
$files = array();
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
$folder = $folder == '/' || empty($folder) ? '' : $folder;
foreach ($stanzas as $stanza) {
if ($stanza[0]['key'] == 'format_version')
continue;
$file = $this->_fillFileEntry($stanza, $folder);
if ($file === null)
continue;
$files[] = (object) $file; $files[] = (object) $file;
} }
return $files; return $files;
@ -399,7 +413,7 @@ class IDF_Scm_Monotone extends IDF_Scm
$certs = $scm->_getCerts($revs[0]); $certs = $scm->_getCerts($revs[0]);
// for the very seldom case that a revision // for the very seldom case that a revision
// has no branch certificate // has no branch certificate
if (count($certs['branch']) == 0) { if (!array_key_exists('branch', $certs)) {
$branch = '*'; $branch = '*';
} }
else else
@ -505,7 +519,7 @@ class IDF_Scm_Monotone extends IDF_Scm
return false; return false;
$out = $this->stdio->exec(array( $out = $this->stdio->exec(array(
'get_manifest_of', $revs[0] 'get_extended_manifest_of', $revs[0]
)); ));
$files = array(); $files = array();
@ -515,43 +529,10 @@ class IDF_Scm_Monotone extends IDF_Scm
if ($stanza[0]['key'] == 'format_version') if ($stanza[0]['key'] == 'format_version')
continue; continue;
$path = $stanza[0]['values'][0]; if ($stanza[0]['values'][0] != $file)
if (!preg_match('#^'.$file.'$#', $path, $m))
continue; continue;
$file = array(); $file = $this->_fillFileEntry($stanza);
$file['fullpath'] = $path;
if ($stanza[0]['key'] == "dir") {
$file['type'] = "tree";
$file['hash'] = null;
$file['size'] = 0;
}
else
{
$file['type'] = 'blob';
$file['hash'] = $stanza[1]['hash'];
$file['size'] = strlen($this->getFile((object)$file));
}
$pathinfo = pathinfo($file['fullpath']);
$file['file'] = $pathinfo['basename'];
$rev = $this->_getLastChangeFor($file['fullpath'], $revs[0]);
if ($rev !== null) {
$file['rev'] = $rev;
$certs = $this->_getCerts($rev);
// FIXME: this assumes that author, date and changelog are always given
$file['author'] = implode(", ", $certs['author']);
$dates = array();
foreach ($certs['date'] as $date)
$dates[] = date('Y-m-d H:i:s', strtotime($date));
$file['date'] = implode(', ', $dates);
$file['log'] = implode("\n---\n", $certs['changelog']);
}
return (object) $file; return (object) $file;
} }
return false; return false;
@ -623,10 +604,12 @@ class IDF_Scm_Monotone extends IDF_Scm
$dates[] = date('Y-m-d H:i:s', strtotime($date)); $dates[] = date('Y-m-d H:i:s', strtotime($date));
$res['date'] = implode(', ', $dates); $res['date'] = implode(', ', $dates);
$res['title'] = implode("\n---\n", $certs['changelog']); $combinedChangelog = implode("\n---\n", $certs['changelog']);
$split = preg_split("/[\n\r]/", $combinedChangelog, 2);
$res['title'] = $split[0];
$res['full_message'] = (isset($split[1])) ? trim($split[1]) : '';
$res['commit'] = $revs[0]; $res['commit'] = $revs[0];
$res['changes'] = ($getdiff) ? $this->_getDiff($revs[0]) : ''; $res['changes'] = ($getdiff) ? $this->_getDiff($revs[0]) : '';
return (object) $res; return (object) $res;
@ -680,11 +663,18 @@ class IDF_Scm_Monotone extends IDF_Scm
// read in the initial branches we should follow // read in the initial branches we should follow
if (count($initialBranches) == 0) { if (count($initialBranches) == 0) {
if (!isset($certs['branch'])) {
throw new IDF_Scm_Exception(sprintf(
__("revision %s has no branch cert - cannot start ".
"logging from this revision"), $rev
));
}
$initialBranches = $certs['branch']; $initialBranches = $certs['branch'];
} }
// only add it to our log if it is on one of the initial branches // only add it to our log if it is on one of the initial branches
if (count(array_intersect($initialBranches, $certs['branch'])) > 0) { // ignore revisions without any branch certificate
if (count(array_intersect($initialBranches, (array)@$certs['branch'])) > 0) {
--$n; --$n;
$log = array(); $log = array();

View File

@ -38,14 +38,15 @@ class IDF_Scm_Monotone_BasicIO
{ {
$pos = 0; $pos = 0;
$stanzas = array(); $stanzas = array();
$length = strlen($in);
while ($pos < strlen($in)) { while ($pos < $length) {
$stanza = array(); $stanza = array();
while ($pos < strlen($in)) { while ($pos < $length) {
if ($in[$pos] == "\n") break; if ($in[$pos] == "\n") break;
$stanzaLine = array('key' => '', 'values' => array(), 'hash' => null); $stanzaLine = array('key' => '', 'values' => array(), 'hash' => null);
while ($pos < strlen($in)) { while ($pos < $length) {
$ch = $in[$pos]; $ch = $in[$pos];
if ($ch == '"' || $ch == '[') break; if ($ch == '"' || $ch == '[') break;
++$pos; ++$pos;
@ -53,6 +54,9 @@ class IDF_Scm_Monotone_BasicIO
$stanzaLine['key'] .= $ch; $stanzaLine['key'] .= $ch;
} }
// symbol w/o a value list
if ($pos >= $length || $in[$pos] == "\n") break;
if ($in[$pos] == '[') { if ($in[$pos] == '[') {
unset($stanzaLine['values']); unset($stanzaLine['values']);
++$pos; // opening square bracket ++$pos; // opening square bracket
@ -64,18 +68,31 @@ class IDF_Scm_Monotone_BasicIO
{ {
unset($stanzaLine['hash']); unset($stanzaLine['hash']);
$valCount = 0; $valCount = 0;
while ($in[$pos] == '"') { // if hashs and plain values are encountered in the same
++$pos; // opening quote // value list, we add the hash values as simple values as well
while ($in[$pos] == '"' || $in[$pos] == '[') {
$isHashValue = $in[$pos] == '[';
++$pos; // opening quote / bracket
$stanzaLine['values'][$valCount] = ''; $stanzaLine['values'][$valCount] = '';
while ($pos < strlen($in)) { while ($pos < $length) {
$ch = $in[$pos]; $pr = $in[$pos-1]; $ch = $in[$pos]; $pr = $in[$pos-1];
if ($ch == '"' && $pr != '\\') break; if (($isHashValue && $ch == ']')
||(!$isHashValue && $ch == '"' && $pr != '\\'))
break;
++$pos; ++$pos;
$stanzaLine['values'][$valCount] .= $ch; $stanzaLine['values'][$valCount] .= $ch;
} }
++$pos; // closing quote ++$pos; // closing quote
if ($pos >= strlen($in)) if (!$isHashValue) {
$stanzaLine['values'][$valCount] = str_replace(
array("\\\\", "\\\""),
array("\\", "\""),
$stanzaLine['values'][$valCount]
);
}
if ($pos >= $length)
break; break;
if ($in[$pos] == ' ') { if ($in[$pos] == ' ') {
@ -83,14 +100,6 @@ class IDF_Scm_Monotone_BasicIO
++$valCount; ++$valCount;
} }
} }
for ($i = 0; $i <= $valCount; $i++) {
$stanzaLine['values'][$i] = str_replace(
array("\\\\", "\\\""),
array("\\", "\""),
$stanzaLine['values'][$i]
);
}
} }
$stanza[] = $stanzaLine; $stanza[] = $stanzaLine;

View File

@ -76,7 +76,7 @@ class IDF_Scm_Monotone_Usher
$single_conns = preg_split('/[ ]/', $conn); $single_conns = preg_split('/[ ]/', $conn);
$ret = array(); $ret = array();
foreach ($single_conns as $conn) { foreach ($single_conns as $conn) {
preg_match('/\(\w+\)([^:]):(\d+)/', $conn, $matches); preg_match('/\((\w+)\)([^:]+):(\d+)/', $conn, $matches);
$ret[$matches[1]][] = (object)array( $ret[$matches[1]][] = (object)array(
'server' => $matches[1], 'server' => $matches[1],
'address' => $matches[2], 'address' => $matches[2],
@ -84,6 +84,12 @@ class IDF_Scm_Monotone_Usher
); );
} }
if ($server !== null) {
if (array_key_exists($server, $ret))
return $ret[$server];
return array();
}
return $ret; return $ret;
} }

View File

@ -61,6 +61,66 @@ class IDF_Views_Project
$request); $request);
} }
/**
* Returns an associative array with available model filters
*
* @return array
*/
private static function getAvailableModelFilters()
{
return array(
'all' => __('All Updates'),
'commits' => __('Commits'),
'issues' => __('Issues and Comments'),
'downloads' => __('Downloads'),
'documents' => __('Documents'),
'reviews' => __('Reviews and Patches'),
);
}
/**
* Returns an array of model classes for which the current user
* has rights and which should be used according to his filter
*
* @param object $request
* @param string $model_filter
* @return array
*/
private static function determineModelClasses($request, $model_filter = 'all')
{
$classes = array();
if (true === IDF_Precondition::accessSource($request) &&
($model_filter == 'all' || $model_filter == 'commits')) {
$classes[] = '\'IDF_Commit\'';
// FIXME: this looks like a hack...
IDF_Scm::syncTimeline($request->project);
}
if (true === IDF_Precondition::accessIssues($request) &&
($model_filter == 'all' || $model_filter == 'issues')) {
$classes[] = '\'IDF_Issue\'';
$classes[] = '\'IDF_IssueComment\'';
}
if (true === IDF_Precondition::accessDownloads($request) &&
($model_filter == 'all' || $model_filter == 'downloads')) {
$classes[] = '\'IDF_Upload\'';
}
if (true === IDF_Precondition::accessWiki($request) &&
($model_filter == 'all' || $model_filter == 'documents')) {
$classes[] = '\'IDF_WikiPage\'';
$classes[] = '\'IDF_WikiRevision\'';
}
if (true === IDF_Precondition::accessReview($request) &&
($model_filter == 'all' || $model_filter == 'reviews')) {
$classes[] = '\'IDF_Review_Comment\'';
$classes[] = '\'IDF_Review_Patch\'';
}
if (count($classes) == 0) {
$classes[] = '\'IDF_Dummy\'';
}
return $classes;
}
/** /**
* Timeline of the project. * Timeline of the project.
*/ */
@ -68,38 +128,21 @@ class IDF_Views_Project
public function timeline($request, $match) public function timeline($request, $match)
{ {
$prj = $request->project; $prj = $request->project;
$title = sprintf(__('%s Updates'), (string) $prj);
$team = $prj->getMembershipData(); $model_filter = @$match[2];
$all_model_filters = self::getAvailableModelFilters();
if (!array_key_exists($model_filter, $all_model_filters)) {
$model_filter = 'all';
}
$title = (string)$prj . ' ' . $all_model_filters[$model_filter];
$pag = new IDF_Timeline_Paginator(new IDF_Timeline()); $pag = new IDF_Timeline_Paginator(new IDF_Timeline());
$pag->class = 'recent-issues'; $pag->class = 'recent-issues';
$pag->item_extra_props = array('request' => $request); $pag->item_extra_props = array('request' => $request);
$pag->summary = __('This table shows the project updates.'); $pag->summary = __('This table shows the project updates.');
// Need to check the rights
$rights = array(); $classes = self::determineModelClasses($request, $model_filter);
if (true === IDF_Precondition::accessSource($request)) { $sql = sprintf('model_class IN (%s)', implode(', ', $classes));
$rights[] = '\'IDF_Commit\'';
IDF_Scm::syncTimeline($request->project);
}
if (true === IDF_Precondition::accessIssues($request)) {
$rights[] = '\'IDF_Issue\'';
$rights[] = '\'IDF_IssueComment\'';
}
if (true === IDF_Precondition::accessDownloads($request)) {
$rights[] = '\'IDF_Upload\'';
}
if (true === IDF_Precondition::accessWiki($request)) {
$rights[] = '\'IDF_WikiPage\'';
$rights[] = '\'IDF_WikiRevision\'';
}
if (true === IDF_Precondition::accessReview($request)) {
$rights[] = '\'IDF_Review_Comment\'';
$rights[] = '\'IDF_Review_Patch\'';
}
if (count($rights) == 0) {
$rights[] = '\'IDF_Dummy\'';
}
$sql = sprintf('model_class IN (%s)', implode(', ', $rights));
$pag->forced_where = new Pluf_SQL('project=%s AND '.$sql, $pag->forced_where = new Pluf_SQL('project=%s AND '.$sql,
array($prj->id)); array($prj->id));
$pag->sort_order = array('creation_dtime', 'ASC'); $pag->sort_order = array('creation_dtime', 'ASC');
@ -113,32 +156,23 @@ class IDF_Views_Project
$pag->items_per_page = 20; $pag->items_per_page = 20;
$pag->no_results_text = __('No changes were found.'); $pag->no_results_text = __('No changes were found.');
$pag->setFromRequest($request); $pag->setFromRequest($request);
$downloads = array();
if ($request->rights['hasDownloadsAccess']) {
$tags = IDF_Views_Download::getDownloadTags($prj);
// the first tag is the featured, the last is the deprecated.
$downloads = $tags[0]->get_idf_upload_list();
}
$pages = array();
if ($request->rights['hasWikiAccess']) {
$tags = IDF_Views_Wiki::getWikiTags($prj);
$pages = $tags[0]->get_idf_wikipage_list();
}
if (!$request->user->isAnonymous() and $prj->isRestricted()) { if (!$request->user->isAnonymous() and $prj->isRestricted()) {
$feedurl = Pluf_HTTP_URL_urlForView('idf_project_timeline_feed_auth', $feedurl = Pluf_HTTP_URL_urlForView('idf_project_timeline_feed_auth',
array($prj->shortname, array($prj->shortname,
$model_filter,
IDF_Precondition::genFeedToken($prj, $request->user))); IDF_Precondition::genFeedToken($prj, $request->user)));
} else { } else {
$feedurl = Pluf_HTTP_URL_urlForView('idf_project_timeline_feed', $feedurl = Pluf_HTTP_URL_urlForView('idf_project_timeline_feed',
array($prj->shortname)); array($prj->shortname, $model_filter));
} }
return Pluf_Shortcuts_RenderToResponse('idf/project/timeline.html', return Pluf_Shortcuts_RenderToResponse('idf/project/timeline.html',
array( array(
'page_title' => $title, 'page_title' => $title,
'feedurl' => $feedurl, 'feedurl' => $feedurl,
'timeline' => $pag, 'timeline' => $pag,
'team' => $team, 'model_filter' => $model_filter,
'downloads' => $downloads, 'all_model_filters' => $all_model_filters,
), ),
$request); $request);
@ -156,31 +190,17 @@ class IDF_Views_Project
public function timelineFeed($request, $match) public function timelineFeed($request, $match)
{ {
$prj = $request->project; $prj = $request->project;
// Need to check the rights $model_filter = @$match[2];
$rights = array();
if (true === IDF_Precondition::accessSource($request)) { $model_filter = @$match[2];
$rights[] = '\'IDF_Commit\''; $all_model_filters = self::getAvailableModelFilters();
IDF_Scm::syncTimeline($request->project); if (!array_key_exists($model_filter, $all_model_filters)) {
$model_filter = 'all';
} }
if (true === IDF_Precondition::accessIssues($request)) { $title = $all_model_filters[$model_filter];
$rights[] = '\'IDF_Issue\'';
$rights[] = '\'IDF_IssueComment\''; $classes = self::determineModelClasses($request, $model_filter);
} $sqls = sprintf('model_class IN (%s)', implode(', ', $classes));
if (true === IDF_Precondition::accessDownloads($request)) {
$rights[] = '\'IDF_Upload\'';
}
if (true === IDF_Precondition::accessWiki($request)) {
$rights[] = '\'IDF_WikiPage\'';
$rights[] = '\'IDF_WikiRevision\'';
}
if (true === IDF_Precondition::accessReview($request)) {
$rights[] = '\'IDF_Review_Comment\'';
$rights[] = '\'IDF_Review_Patch\'';
}
if (count($rights) == 0) {
$rights[] = '\'IDF_Dummy\'';
}
$sqls = sprintf('model_class IN (%s)', implode(', ', $rights));
$sql = new Pluf_SQL('project=%s AND '.$sqls, array($prj->id)); $sql = new Pluf_SQL('project=%s AND '.$sqls, array($prj->id));
$params = array( $params = array(
'filter' => $sql->gen(), 'filter' => $sql->gen(),
@ -203,7 +223,6 @@ class IDF_Views_Project
} }
$out = Pluf_Template::markSafe(implode("\n", $out)); $out = Pluf_Template::markSafe(implode("\n", $out));
$tmpl = new Pluf_Template('idf/index.atom'); $tmpl = new Pluf_Template('idf/index.atom');
$title = __('Updates');
$feedurl = Pluf::f('url_base').Pluf::f('idf_base').$request->query; $feedurl = Pluf::f('url_base').Pluf::f('idf_base').$request->query;
$viewurl = Pluf_HTTP_URL_urlForView('IDF_Views_Project::timeline', $viewurl = Pluf_HTTP_URL_urlForView('IDF_Views_Project::timeline',
array($prj->shortname)); array($prj->shortname));
@ -277,7 +296,8 @@ class IDF_Views_Project
} }
} else { } else {
$params = array(); $params = array();
$keys = array('labels_issue_open', 'labels_issue_closed', $keys = array('labels_issue_template',
'labels_issue_open', 'labels_issue_closed',
'labels_issue_predefined', 'labels_issue_one_max'); 'labels_issue_predefined', 'labels_issue_one_max');
foreach ($keys as $key) { foreach ($keys as $key) {
$_val = $conf->getVal($key, false); $_val = $conf->getVal($key, false);

View File

@ -533,8 +533,10 @@ class IDF_Views_Source
if (0 === strpos($fileinfo[0], 'text/')) { if (0 === strpos($fileinfo[0], 'text/')) {
return true; return true;
} }
$ext = 'mdtext php-dist h gitignore diff patch' $ext = 'mdtext php-dist h gitignore diff patch';
.Pluf::f('idf_extra_text_ext', ''); $extra_ext = trim(Pluf::f('idf_extra_text_ext', ''));
if (!empty($extra_ext))
$ext .= ' ' . $extra_ext;
$ext = array_merge(self::$supportedExtenstions, explode(' ' , $ext)); $ext = array_merge(self::$supportedExtenstions, explode(' ' , $ext));
return (in_array($fileinfo[2], $ext)); return (in_array($fileinfo[2], $ext));
} }

View File

@ -73,90 +73,31 @@ $cfg['git_write_remote_url'] = 'git@localhost:%s.git';
$cfg['svn_repositories'] = 'file:///home/svn/repositories/%s'; $cfg['svn_repositories'] = 'file:///home/svn/repositories/%s';
$cfg['svn_remote_url'] = 'http://localhost/svn/%s'; $cfg['svn_remote_url'] = 'http://localhost/svn/%s';
# Path to the monotone binary (you need mtn 0.99 or newer) #
# You can setup monotone for use with indefero in several ways.
# Please look into doc/syncmonotone.mdtext for more information.
#
# Path to the monotone binary
$cfg['mtn_path'] = 'mtn'; $cfg['mtn_path'] = 'mtn';
# Additional options for the started monotone process # Additional options for the started monotone process
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles'); $cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles');
#
# You can setup monotone for use with indefero in several ways. The # The path to a specific database (local use) or a writable project
# two most-used should be: # directory (remote / usher use). %s is replaced with the project name
#
# 1) One database for everything:
#
# Set 'mtn_repositories' below to a fixed database path, such as
# '/home/mtn/repositories/all_projects.mtn'
#
# Pro: - easy to setup and to manage
# Con: - while read access can be configured per-branch,
# granting write access rights to a user means that
# he can write anything in the global database
# - database lock problem: the database from which
# indefero reads its data cannot be used to serve the
# contents to the users, as the serve process locks
# the database
#
# 2) One database for every project with 'usher':
#
# Download and configure 'usher'
# (mtn clone mtn://monotone.ca?net.venge.monotone.contrib.usher)
# which acts as proxy in front of all single project databases.
# Create a basic configuration file for it and add a secret admin
# username and password. Finally, point the below variable
# 'mtn_usher_conf' to this configuration file.
#
# Then set 'mtn_remote_url' below to a string which matches your setup.
# Again, the '%s' placeholder will be expanded to the project's
# short name. Note that 'mtn_remote_url' is used as internal
# URI (to access the data for indefero) as well as external URI
# (for end users) at the same time. 'mtn_repositories' should then
# point to a directory where all project-related files (databases,
# keys, configurations) are kept, as these are automatically created
# on project creation by IDF.
#
# Example: 'mtn_repositories' is configured to be '/var/monotone/%s'
#
# - IDF tries to create /var/monotone/<projectname> as root directory
# - The database is placed in as /var/monotone/<projectname>/database.mtn
# - The server key is put into /var/monotone/<projectname>/keys and
# is named "<projectname>-server@<host>", where host is the host part
# of 'mtn_remote_url'
#
# therefor /var/monotone MUST be read/writable for the www user and all
# files which are created underknees MUST be read/writable by the user
# who is executing the usher instance! The best way to achieve this is with
# default (POSIX) ACLs on /var/monotone.
#
#
# You could also choose to setup usher by hand, i.e. with individual
# databases, in this case leave 'mtn_usher_conf' below commented out.
#
# Pro: - read and write access can be granted per project
# - no database locking issues
# - one public server running on the one well-known port
# Con: - harder to setup
#
# Usher can also be used to forward sync requests to remote servers,
# please consult its README file for more information.
#
# monotone also allows to use SSH as transport protocol, so if you do not plan
# to setup a netsync server as described above, then just enter a URI like
# 'ssh://my-host.biz/home/mtn/repositories/%s.mtn' in 'mtn_remote_url'.
#
$cfg['mtn_repositories'] = '/home/mtn/repositories/%s.mtn'; $cfg['mtn_repositories'] = '/home/mtn/repositories/%s.mtn';
# The URL which is displayed as sync URL to the user and which is also
# used to connect to a remote usher
$cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s'; $cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s';
#
# Whether the particular database(s) are accessed locally (via automate stdio) # Whether the particular database(s) are accessed locally (via automate stdio)
# or remotely (via automate remote_stdio). 'remote' is the default for # or remotely (via automate remote_stdio). 'remote' is the default for
# netsync setups, while 'local' access should be choosed for ssh access. # use with usher and the SyncMonotone plugin, while 'local' access should be
# # choosed for manual setups and / or ssh access.
# Note that you need to setup the hook 'get_remote_automate_permitted' for $cfg['mtn_db_access'] = 'local';
# each remotely accessible database. A full HOWTO set this up is beyond this
# scope, please refer to the documentation of monotone and / or ask on the
# mailing list (monotone-users@nongnu.org) or IRC channel
# (irc.oftc.net/#monotone)
#
$cfg['mtn_db_access'] = 'remote';
#
# If true, each access to the database is authenticated with an auto-generated # If true, each access to the database is authenticated with an auto-generated
# project key which is stored in the IDF project configuration # project key which is stored in the IDF project configuration
# ('mtn_client_key_*') and written out to $cfg['tmp_folder']/mtn-client-keys # ('mtn_client_key_*') and written out to $cfg['tmp_folder']/mtn-client-keys
@ -167,18 +108,17 @@ $cfg['mtn_db_access'] = 'remote';
# the remote monotone server instance. In this case no project-specific # the remote monotone server instance. In this case no project-specific
# keys are generated and the server must be configured to allow at least # keys are generated and the server must be configured to allow at least
# anonymous read access to the main functions. # anonymous read access to the main functions.
# #$cfg['mtn_remote_auth'] = true;
$cfg['mtn_remote_auth'] = true;
# # Needs to be configured for remote / usher usage.
# If configured, this allows basic control of a running usher process # This allows basic control of a running usher process via the forge
# via the forge administration. The variable must point to the full (writable) # administration. The variable must point to the full (writable)
# path of the usher configuration file which gets updated when new projects # path of the usher configuration file which gets updated when new projects
# are added # are added
#
#$cfg['mtn_usher_conf'] = '/path/to/usher.conf'; #$cfg['mtn_usher_conf'] = '/path/to/usher.conf';
# Mercurial repositories path # Mercurial repositories path
#$cfg['mercurial_repositories'] = '/home/mercurial/repositories/%s'; $cfg['mercurial_repositories'] = '/home/mercurial/repositories/%s';
#$cfg['mercurial_remote_url'] = 'http://projects.ceondo.com/hg/%s'; #$cfg['mercurial_remote_url'] = 'http://projects.ceondo.com/hg/%s';
# admins will get an email in case of errors in the system in non # admins will get an email in case of errors in the system in non
@ -265,7 +205,7 @@ $cfg['db_database'] = 'website'; # put absolute path to the db if you
# are using SQLite. # are using SQLite.
# #
# The extension of the downloads are limited. You can add extra # The extension of the downloads are limited. You can add extra
# extensions here. The list must start with a space. # extensions here.
# $cfg['idf_extra_upload_ext'] = 'ext1 ext2'; # $cfg['idf_extra_upload_ext'] = 'ext1 ext2';
# #
# By default, the size of the downloads is limited to 2MB. # By default, the size of the downloads is limited to 2MB.
@ -316,6 +256,13 @@ $cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git',
'mtn' => 'IDF_Scm_Monotone', 'mtn' => 'IDF_Scm_Monotone',
); );
# Set to true when uploaded public keys should not only be validated
# syntactically, but also by the specific backend. For SSH public
# keys, ssh-keygen(3) must be available and usable in PATH, for
# monotone public keys, the monotone binary (as configured above)
# is used.
# $cfg['idf_strong_key_check'] = false;
# If you want to use another memtypes database # If you want to use another memtypes database
# $cfg['idf_mimetypes_db'] = '/etc/mime.types'; # $cfg['idf_mimetypes_db'] = '/etc/mime.types';

View File

@ -74,18 +74,18 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/$#',
'model' => 'IDF_Views_Project', 'model' => 'IDF_Views_Project',
'method' => 'home'); 'method' => 'home');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/timeline/$#', $ctl[] = array('regex' => '#^/p/([\-\w]+)/timeline/(\w+)/$#',
'base' => $base, 'base' => $base,
'model' => 'IDF_Views_Project', 'model' => 'IDF_Views_Project',
'method' => 'timeline'); 'method' => 'timeline');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/$#', $ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/(\w+)/$#',
'base' => $base, 'base' => $base,
'model' => 'IDF_Views_Project', 'model' => 'IDF_Views_Project',
'method' => 'timelineFeed', 'method' => 'timelineFeed',
'name' => 'idf_project_timeline_feed'); 'name' => 'idf_project_timeline_feed');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/token/(.*)/$#', $ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/(\w+)/token/(.*)/$#',
'base' => $base, 'base' => $base,
'model' => 'IDF_Views_Project', 'model' => 'IDF_Views_Project',
'method' => 'timelineFeed', 'method' => 'timelineFeed',

View File

@ -96,7 +96,7 @@ Pluf_Signal::connect('IDF_Key::postSave',
array('IDF_Plugin_SyncMonotone', 'entry')); array('IDF_Plugin_SyncMonotone', 'entry'));
Pluf_Signal::connect('IDF_Key::preDelete', Pluf_Signal::connect('IDF_Key::preDelete',
array('IDF_Plugin_SyncMonotone', 'entry')); array('IDF_Plugin_SyncMonotone', 'entry'));
Pluf_Signal::connect('phppostpush.php::run', Pluf_Signal::connect('mtnpostpush.php::run',
array('IDF_Plugin_SyncMonotone', 'entry')); array('IDF_Plugin_SyncMonotone', 'entry'));
# #

View File

@ -4,6 +4,12 @@
<form method="post" action="."> <form method="post" action=".">
<table class="form" summary=""> <table class="form" summary="">
<tr> <tr>
<td colspan="2"><strong>{$form.f.labels_issue_template.labelTag}:</strong><br />
{if $form.f.labels_issue_template.errors}{$form.f.labels_issue_template.fieldErrors}{/if}
{$form.f.labels_issue_template|unsafe}
</td>
</tr>
<tr>
<td colspan="2"><strong>{$form.f.labels_issue_open.labelTag}:</strong><br /> <td colspan="2"><strong>{$form.f.labels_issue_open.labelTag}:</strong><br />
{if $form.f.labels_issue_open.errors}{$form.f.labels_issue_open.fieldErrors}{/if} {if $form.f.labels_issue_open.errors}{$form.f.labels_issue_open.fieldErrors}{/if}
{$form.f.labels_issue_open|unsafe} {$form.f.labels_issue_open|unsafe}

View File

@ -29,16 +29,13 @@
<![endif]--> <![endif]-->
{block extraheader}{/block} {block extraheader}{/block}
<title>{block pagetitle}{$page_title|strip_tags}{/block}{if $project} - {$project.shortdesc}{/if}</title> <title>{block pagetitle}{$page_title|strip_tags}{/block}{if $project} - {$project.shortdesc}{/if}</title>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
</head> </head>
<body> <body>
<div id="{block docid}doc3{/block}"> <div id="{block docid}doc3{/block}">
<div id="hd"> <div id="hd">
{if $project}<h1 class="project-title">{$project}</h1>{/if} {if $project}<h1 class="project-title">{$project}</h1>{/if}
<p class="top"><a href="#title" accesskey="2"></a> {include 'idf/main-menu.html'}
{if !$user.isAnonymous()}{aurl 'url', 'idf_dashboard'}{blocktrans}Welcome, <strong><a class="userw" href="{$url}">{$user}</a></strong>.{/blocktrans} <a href="{url 'IDF_Views::logout'}">{trans 'Sign Out'}</a>{else}<a href="{url 'IDF_Views::login'}">{trans 'Sign in or create your account'}</a>{/if}
{if $project} | <a href="{url 'IDF_Views::index'}">{trans 'Project List'}</a>{/if}
| <a href="{url 'IDF_Views::faq'}" title="{trans 'Help and accessibility features'}">{trans 'Help'}</a>
</p>
<div id="header"> <div id="header">
<div id="main-tabs"> <div id="main-tabs">
{if $project} {if $project}
@ -69,7 +66,6 @@
</div> </div>
<div id="ft">{block foot}{/block}</div> <div id="ft">{block foot}{/block}</div>
</div> </div>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
{include 'idf/js-hotkeys.html'} {include 'idf/js-hotkeys.html'}
{block javascript}{/block} {block javascript}{/block}
{if $project} {if $project}

View File

@ -29,15 +29,12 @@
<![endif]--> <![endif]-->
{block extraheader}{/block} {block extraheader}{/block}
<title>{block pagetitle}{$page_title|strip_tags}{/block}</title> <title>{block pagetitle}{$page_title|strip_tags}{/block}</title>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
</head> </head>
<body> <body>
<div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}"> <div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}">
<div id="hd"> <div id="hd">
<p class="top"><a href="#title" accesskey="2"></a> {include 'idf/main-menu.html'}
{if !$user.isAnonymous()}{aurl 'url', 'idf_dashboard'}{blocktrans}Welcome, <strong><a class="userw" href="{$url}">{$user}</a></strong>.{/blocktrans} <a href="{url 'IDF_Views::logout'}">{trans 'Sign Out'}</a>{else}<a href="{url 'IDF_Views::login'}">{trans 'Sign in or create your account'}</a>{/if}
| <a href="{url 'IDF_Views::index'}">{trans 'Project List'}</a> {if $isAdmin}| <a href="{url 'IDF_Views_Admin::projects'}">{trans 'Forge Management'}</a>{/if}
| <a href="{url 'IDF_Views::faq'}" title="{trans 'Help and accessibility features'}">{trans 'Help'}</a>
</p>
<h1 id="title" class="title">{block title}{$page_title}{/block}</h1> <h1 id="title" class="title">{block title}{$page_title}{/block}</h1>
</div> </div>
<div id="bd"> <div id="bd">
@ -53,7 +50,6 @@
</div> </div>
<div id="ft">{block foot}{/block}</div> <div id="ft">{block foot}{/block}</div>
</div> </div>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
{include 'idf/js-hotkeys.html'} {include 'idf/js-hotkeys.html'}
{block javascript}{/block} {block javascript}{/block}
</body> </body>

View File

@ -29,17 +29,13 @@
<![endif]--> <![endif]-->
{block extraheader}{/block} {block extraheader}{/block}
<title>{block pagetitle}{$page_title|strip_tags}{/block}{if $project} - {$project.shortdesc}{/if}</title> <title>{block pagetitle}{$page_title|strip_tags}{/block}{if $project} - {$project.shortdesc}{/if}</title>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
</head> </head>
<body> <body>
<div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}"> <div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}">
<div id="hd"> <div id="hd">
{if $project}<h1 class="project-title">{$project}</h1>{/if} {if $project}<h1 class="project-title">{$project}</h1>{/if}
<p class="top"><a href="#title" accesskey="2"></a> {include 'idf/main-menu.html'}
{if !$user.isAnonymous()}{aurl 'url', 'idf_dashboard'}{blocktrans}Welcome, <strong><a class="userw" href="{$url}">{$user}</a></strong>.{/blocktrans} <a href="{url 'IDF_Views::logout'}">{trans 'Sign Out'}</a>{else}<a href="{url 'IDF_Views::login'}">{trans 'Sign in or create your account'}</a>{/if}
{if $project} | <a href="{url 'IDF_Views::index'}">{trans 'Project List'}</a>{/if}
{if $isAdmin}| <a href="{url 'IDF_Views_Admin::projects'}">{trans 'Forge Management'}</a>{/if}
| <a href="{url 'IDF_Views::faq'}" title="{trans 'Help and accessibility features'}">{trans 'Help'}</a>
</p>
<div id="header"> <div id="header">
<div id="main-tabs"> <div id="main-tabs">
{if $project} {if $project}
@ -71,7 +67,6 @@
</div> </div>
<div id="ft">{block foot}{/block}</div> <div id="ft">{block foot}{/block}</div>
</div> </div>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
{include 'idf/js-hotkeys.html'} {include 'idf/js-hotkeys.html'}
{block javascript}{/block} {block javascript}{/block}
{if $project} {if $project}

View File

@ -29,16 +29,12 @@
<![endif]--> <![endif]-->
{block extraheader}{/block} {block extraheader}{/block}
<title>{block pagetitle}{$page_title|strip_tags}{/block}</title> <title>{block pagetitle}{$page_title|strip_tags}{/block}</title>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
</head> </head>
<body> <body>
<div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}"> <div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}">
<div id="hd"> <div id="hd">
<p class="top"><a href="#title" accesskey="2"></a> {include 'idf/main-menu.html'}
{aurl 'url', 'IDF_Views_User::dashboard'}{blocktrans}Welcome, <strong><a class="userw" href="{$url}">{$user}</a></strong>.{/blocktrans} <a href="{url 'IDF_Views::logout'}">{trans 'Sign Out'}</a>
| <a href="{url 'IDF_Views::index'}">{trans 'Project List'}</a>
| <a href="{url 'IDF_Views_Admin::projects'}">{trans 'Forge Management'}</a>
| <a href="{url 'IDF_Views::faq'}" title="{trans 'Help and accessibility features'}">{trans 'Help'}</a>
</p>
<div id="header"> <div id="header">
<div id="main-tabs"> <div id="main-tabs">
<a href="{url 'IDF_Views_Admin::projects'}"{block tabprojects}{/block}>{trans 'Projects'}</a> <a href="{url 'IDF_Views_Admin::projects'}"{block tabprojects}{/block}>{trans 'Projects'}</a>
@ -64,7 +60,6 @@
</div> </div>
<div id="ft">{block foot}{/block}</div> <div id="ft">{block foot}{/block}</div>
</div> </div>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
{include 'idf/js-hotkeys.html'} {include 'idf/js-hotkeys.html'}
{block javascript}{/block} {block javascript}{/block}
</body> </body>

View File

@ -3,7 +3,7 @@
// <!-- // <!--
{hotkey 'Shift+h', 'IDF_Views::faq'} {hotkey 'Shift+h', 'IDF_Views::faq'}
{if $project} {if $project}
{hotkey 'Shift+u', 'IDF_Views_Project::timeline', array($project.shortname)} {hotkey 'Shift+u', 'IDF_Views_Project::timeline', array($project.shortname, 'all')}
{if $hasIssuesAccess}{hotkey 'Shift+a', 'IDF_Views_Issue::create', array($project.shortname)} {if $hasIssuesAccess}{hotkey 'Shift+a', 'IDF_Views_Issue::create', array($project.shortname)}
{hotkey 'Shift+i', 'IDF_Views_Issue::index', array($project.shortname)}{/if} {hotkey 'Shift+i', 'IDF_Views_Issue::index', array($project.shortname)}{/if}
{if $hasDownloadsAccess}{hotkey 'Shift+d', 'IDF_Views_Download::index', array($project.shortname)}{/if} {if $hasDownloadsAccess}{hotkey 'Shift+d', 'IDF_Views_Download::index', array($project.shortname)}{/if}

View File

@ -0,0 +1,31 @@
<a href="#title" accesskey="2"></a>
<ul id="main-menu">
{if !$user.isAnonymous()}
{aurl 'url', 'idf_dashboard'}
<li>{blocktrans}Welcome, <strong><a class="userw" href="{$url}">{$user}</a></strong>.{/blocktrans}
<a href="{url 'IDF_Views::logout'}">{trans 'Sign Out'}</a></li>{else}<li>
<a href="{url 'IDF_Views::login'}">{trans 'Sign in or create your account'}</a></li>
{/if}<li id="project-list"><a href="{url 'IDF_Views::index'}">{trans 'Project List'} &#x25be;</a>
{if $allProjects.count() != 0}
<ul>{foreach $allProjects as $p}
<li>{if $p.private}<img style="vertical-align: text-bottom;" src="{media '/idf/img/lock.png'}" alt="{trans 'Private project'}" /> {/if}
<a href="{url 'IDF_Views_Project::home', array($p.shortname)}">{$p}</a></li>
{/foreach}</ul>
{/if}</li>{if $isAdmin}<li><a href="{url 'IDF_Views_Admin::projects'}">{trans 'Forge Management'}</a></li>{/if}<li>
<a href="{url 'IDF_Views::faq'}" title="{trans 'Help and accessibility features'}">{trans 'Help'}</a></li>
</ul>
{if $allProjects.count() != 0}
<script type="text/javascript" charset="utf-8">
{literal}
$(document).ready(function() {
$('#project-list').bind('mouseenter', function(ev) {
$(this).find('ul').show();
}).bind('mouseleave', function(ev) {
$(this).find('ul').hide();
});
});
{/literal}
</script>
{/if}

View File

@ -3,7 +3,7 @@
{block tabhome} class="active"{/block} {block tabhome} class="active"{/block}
{block subtabs} {block subtabs}
<div id="sub-tabs"> <div id="sub-tabs">
{trans 'Welcome'} | <strong><a href="{url 'IDF_Views_Project::timeline', array($project.shortname)}">{trans 'Latest Updates'}</a></strong>{superblock} {trans 'Welcome'} | <strong><a href="{url 'IDF_Views_Project::timeline', array($project.shortname, 'all')}">{trans 'Latest Updates'}</a></strong>{superblock}
</div> </div>
{/block} {/block}
{block body} {block body}

View File

@ -4,7 +4,7 @@
{block tabhome} class="active"{/block} {block tabhome} class="active"{/block}
{block subtabs} {block subtabs}
<div id="sub-tabs"> <div id="sub-tabs">
<a href="{url 'IDF_Views_Project::home', array($project.shortname)}">{trans 'Welcome'}</a> | <strong><a href="{url 'IDF_Views_Project::timeline', array($project.shortname)}" class="active">{trans 'Latest Updates'}</a></strong>{superblock} <a href="{url 'IDF_Views_Project::home', array($project.shortname)}">{trans 'Welcome'}</a> | <strong><a href="{url 'IDF_Views_Project::timeline', array($project.shortname, 'all')}" class="active">{trans 'Latest Updates'}</a></strong>{superblock}
</div> </div>
{/block} {/block}
@ -13,26 +13,10 @@
{/block} {/block}
{block context} {block context}
{if count($downloads) > 0} <p><strong>{trans 'Filter by type'}</strong><br />
<p><strong>{trans 'Featured Downloads'}</strong><br /> {foreach $all_model_filters as $filter_key => $filter_name}
{foreach $downloads as $download} <span class="label{if $filter_key == $model_filter} active{/if}"><a href="{url 'IDF_Views_Project::timeline', array($project.shortname, $filter_key)}">{$filter_name}</a></span><br />
<span class="label"><a href="{url 'IDF_Views_Download::view', array($project.shortname, $download.id)}" title="{$download.summary}">{$download}</a></span><br />
{/foreach} {/foreach}
<span class="label"> </span><span class="note"><a href="{url 'IDF_Views_Download::index', array($project.shortname)}">{trans 'show more...'}</a></span>
{/if}
{assign $ko = 'owners'}
{assign $km = 'members'}
<p><strong>{trans 'Development Team'}</strong><br />
{trans 'Admins'}<br />
{foreach $team[$ko] as $owner}{aurl 'url', 'IDF_Views_User::view', array($owner.login)}
<span class="label"><a class="label" href="{$url}">{$owner}</a></span><br />
{/foreach}
{if count($team[$km]) > 0}
{trans 'Happy Crew'}<br />
{foreach $team[$km] as $member}{aurl 'url', 'IDF_Views_User::view', array($member.login)}
<span class="label"><a class="label" href="{$url}">{$member}</a></span><br />
{/foreach}
{/if}
</p> </p>
{/block} {/block}

View File

@ -601,7 +601,6 @@ table.code td.code {
white-space: -o-pre-wrap; /* Opera 7 */ white-space: -o-pre-wrap; /* Opera 7 */
white-space: -pre-wrap; /* Opera 4-6 */ white-space: -pre-wrap; /* Opera 4-6 */
white-space: pre-wrap; /* CSS 2.1 */ white-space: pre-wrap; /* CSS 2.1 */
white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */
word-wrap: break-word; /* IE */ word-wrap: break-word; /* IE */
padding-left: 5px; padding-left: 5px;
} }
@ -717,3 +716,90 @@ div.deprecated-page {
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
} }
/**
* main menu
*/
#main-menu {
padding: 0;
margin: 5px 0 13px;
}
#main-menu > li {
list-style-type: none;
margin-left: 5px;
padding-left: 5px;
border-left: 1px solid black;
display: inline-block;
line-height: 1em;
}
#main-menu > li:first-child {
margin-left: 0;
padding-left: 0;
border-left: none;
}
/**
* project list popup
*/
#project-list {
position: relative;
padding-left: 0 !important;
}
#project-list > a {
padding-left: 5px;
padding-right: 5px;
margin-top: -3px;
padding-top: 3px;
}
#project-list + li {
margin-left: 0;
}
#project-list ul {
display: none;
background: #A5E26A;
border-top: 0;
position: absolute;
margin: 0;
z-index: 1000;
top: 1.1em;
-moz-border-radius: 0 0 3px 3px;
border-radius: 0 0 3px 3px;
-moz-box-shadow: 0 10px 20px #333;
-webkit-box-shadow: 0 10px 20px #333;
box-shadow: 0 10px 20px #333;
max-height: 400px;
min-width: 100%;
overflow-x: hidden;
overflow-y: auto;
}
#project-list ul li {
margin: 7px;
white-space: nowrap;
font-size: 0.95em;
list-style-type: square;
list-style-position: inside;
}
#project-list ul li:first-child {
margin-top: 10px;
}
#project-list ul li a {
text-decoration: none;
}
#project-list:hover > a {
background: #A5E26A;
text-decoration: none;
}
#project-list:hover a {
color: #2E3436;
}