Merge branch 'develop' of projects.ceondo.com:indefero
This commit is contained in:
commit
2106a5fbdc
115
CONTRIBUTE.mdtext
Normal file
115
CONTRIBUTE.mdtext
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
[Indefero][idf] is not only a software you can use either hosted for
|
||||||
|
you or hosted by you, but also a free software you can contribute to.
|
||||||
|
|
||||||
|
Here you will get how to contribute and what to contribute.
|
||||||
|
|
||||||
|
[idf]: http://www.indefero.net
|
||||||
|
|
||||||
|
# Quick Way on How to Contribute
|
||||||
|
|
||||||
|
Simple contribution:
|
||||||
|
|
||||||
|
1. Open a ticket with your idea. You can directly propose a patch if
|
||||||
|
you have it.
|
||||||
|
|
||||||
|
2. Wait for it to be checked by the devs or meet us on the #indefero
|
||||||
|
channel on [FreeNode][freenode].
|
||||||
|
|
||||||
|
Bigger contribution:
|
||||||
|
|
||||||
|
1. Fork Indefero where you want (fork from the develop branch).
|
||||||
|
|
||||||
|
2. Code your change and document it.
|
||||||
|
|
||||||
|
3. Open a ticket with a pull request and talk about it on IRC.
|
||||||
|
|
||||||
|
# The General Contribution Workflow for Regular Contributors
|
||||||
|
|
||||||
|
1. Fork Indefero from the **develop** branch.
|
||||||
|
2. Request a pull request if you do not have write access on the repository.
|
||||||
|
3. Merge your changes without fast forward in develop. This keeps track of
|
||||||
|
the history of the changes and makes understanding what is going on easy.
|
||||||
|
4. Merge your changes with fast forward **only if a single commit**.
|
||||||
|
|
||||||
|
Indefero is composed of two main branches:
|
||||||
|
|
||||||
|
1. **master**: this is the shipped branch, only a select number of people
|
||||||
|
can push into it.
|
||||||
|
2. **develop**: this is the development branch, all the people having write
|
||||||
|
access to the repository are welcomed to push in.
|
||||||
|
|
||||||
|
**Note:** The branching model we use is [explained in details here][bmi]. You
|
||||||
|
**must** understand it to really contribute to the code base in an
|
||||||
|
efficient way.
|
||||||
|
|
||||||
|
[bmi]: http://nvie.com/git-model "A successful Git branching model"
|
||||||
|
|
||||||
|
# What to Contribute
|
||||||
|
|
||||||
|
Contribution is easy, you can contribute in a lot of different fields,
|
||||||
|
contributions small or big are always appreciated. Here is an example
|
||||||
|
list of what you can do:
|
||||||
|
|
||||||
|
- Install InDefero on your system and report the problem you had.
|
||||||
|
- Find the bad English and typos and propose corrections.
|
||||||
|
- Help with the translation effort.
|
||||||
|
- Find little bugs or usability problems and provide ideas on how to fix them.
|
||||||
|
- Register to the [discussion group][group] and help new users.
|
||||||
|
- Come and chat on IRC #indefero on the [FreeNode][freenode] servers.
|
||||||
|
- Find ways to improve the design while keeping it **beautifully simple**.
|
||||||
|
- Write a blog post about the project, what you think is good or bad.
|
||||||
|
- Translate InDefero for the sake of the community.
|
||||||
|
- Or maybe really hack into the code.
|
||||||
|
|
||||||
|
As you can see, the real hacking into the code is just a small part of the work, so even if you are not a coder you can do a lot.
|
||||||
|
|
||||||
|
[group]: http://groups.google.com/group/indefero-users
|
||||||
|
[freenode]: http://freenode.net/
|
||||||
|
|
||||||
|
## I am a simple user
|
||||||
|
|
||||||
|
Thanks a lot! Really! As a project leader, I consider **you** as
|
||||||
|
**the most important person in the success of the project**. So do not
|
||||||
|
worry, I will really listen to your needs and make you love this
|
||||||
|
project.
|
||||||
|
|
||||||
|
What you can do to help:
|
||||||
|
|
||||||
|
- Use the software and each time you find something a bit annoying in your daily use, report a bug. Usability issues are high priority issues.
|
||||||
|
- Find typos, grammar mistakes, etc. and report a bug.
|
||||||
|
- Write about InDefero on your blog/website.
|
||||||
|
- Read the issues submitted by the users and provide answers if you have them.
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## I am a designer
|
||||||
|
|
||||||
|
A lot of things to do for you:
|
||||||
|
|
||||||
|
- Check the design and find the flaws in it. Is the space well used, does it look really nice and is it also functional for the first users?
|
||||||
|
- Do we have good support of all the major browsers?
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## I am a coder
|
||||||
|
|
||||||
|
Checkout the code and have fun, but keep in mind that your results
|
||||||
|
must be simple to use. Do not worry about the beautiful part, the
|
||||||
|
designers can work on that.
|
||||||
|
|
||||||
|
## I am a security guy
|
||||||
|
|
||||||
|
Please, do try to break it, if you find a problem, come on IRC or
|
||||||
|
contact the developers to get the issue fixed as soon as
|
||||||
|
possible. Please, be nice, do not release the issue in the wild
|
||||||
|
without first talking to us.
|
||||||
|
|
||||||
|
## I am a translator
|
||||||
|
|
||||||
|
We currently use (transifex)[http://trac.transifex.org] to help our
|
||||||
|
users to translate indefero. You don't have to use it, but it's an
|
||||||
|
easy way to do the job. You can visit the indefero page at transifex
|
||||||
|
here : http://www.transifex.net/projects/p/indefero/c/indefero/
|
||||||
|
|
||||||
|
Please understand that your changes will not be commited instantly,
|
||||||
|
but are sent to the maintainers e-mails before. Then, your changes
|
||||||
|
will not be in the main repository until da-loic push the changes. In
|
||||||
|
that way, try to do big changes with less submissions.
|
@ -213,3 +213,13 @@ may have problems as your certificate is not trusted, check the
|
|||||||
[procedure provided here][svnfix] to solve the problem.
|
[procedure provided here][svnfix] to solve the problem.
|
||||||
|
|
||||||
[svnfix]: http://projects.ceondo.com/p/indefero/issues/319/#ic1358
|
[svnfix]: http://projects.ceondo.com/p/indefero/issues/319/#ic1358
|
||||||
|
|
||||||
|
## If the registration links are not working
|
||||||
|
|
||||||
|
If You have standard instalaction of PHP ie in Debian, php.ini sets
|
||||||
|
mbstring.func_overload to value "2" for overloading str*
|
||||||
|
functions. You need to prevent the overload as it does not make sense
|
||||||
|
anyway (magic in the background is bad!).
|
||||||
|
See the [corresponding ticket][reglink].
|
||||||
|
|
||||||
|
[reglink]: http://projects.ceondo.com/p/indefero/issues/481/
|
@ -3,9 +3,8 @@
|
|||||||
## general
|
## general
|
||||||
|
|
||||||
This version of indefero contains an implementation of the monotone
|
This version of indefero contains an implementation of the monotone
|
||||||
automation interface. It needs at least monotone version 0.47
|
automation interface. It needs at least monotone version 0.99
|
||||||
(interface version 12.0) or newer, but as development continues, its
|
(interface version 13.0) or newer.
|
||||||
likely that this dependency has to be raised.
|
|
||||||
|
|
||||||
To set up a new IDF project with monotone quickly, all you need to do
|
To set up a new IDF project with monotone quickly, all you need to do
|
||||||
is to create a new monotone database with
|
is to create a new monotone database with
|
||||||
|
23
scripts/mtn-post-push
Executable file
23
scripts/mtn-post-push
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# This hook informs IDF that new revisions arrived in the database
|
||||||
|
# of the specified project.
|
||||||
|
#
|
||||||
|
# This hook is normally installed automatically at the creation of your
|
||||||
|
# repository if you have everything configured correctly. If you want
|
||||||
|
# to enable it later, you need to call it into your monotonerc file
|
||||||
|
# from the hook "note_netsync_end". (See chapter "Event Notifications
|
||||||
|
# and Triggers" on <http://monotone.ca/docs/Hooks.html#Hooks>.)
|
||||||
|
#
|
||||||
|
|
||||||
|
dir=$(dirname "$0")
|
||||||
|
res=$(cd "$dir" && /bin/pwd || "$dir")
|
||||||
|
SCRIPTDIR="$res/$(readlink $0)"
|
||||||
|
PHP_POST_PUSH=$SCRIPTDIR/mtnpostpush.php
|
||||||
|
|
||||||
|
base=$(basename "$0")
|
||||||
|
TMPFILE=$(mktemp /tmp/${tempfoo}.XXXXXX) || exit 1
|
||||||
|
while read rev; do echo $rev >> $TMPFILE; done
|
||||||
|
|
||||||
|
echo php $PHP_POST_PUSH "$1" \< $TMPFILE \&\& rm -f $TMPFILE |\
|
||||||
|
at now > /dev/null 2>&1
|
63
scripts/mtnpostpush.php
Normal file
63
scripts/mtnpostpush.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||||
|
/*
|
||||||
|
# ***** BEGIN LICENSE BLOCK *****
|
||||||
|
# This file is part of InDefero, an open source project management application.
|
||||||
|
# Copyright (C) 2008-2010 Céondo Ltd and contributors.
|
||||||
|
#
|
||||||
|
# InDefero is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# InDefero is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
#
|
||||||
|
# ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This script will send the notifications after a push in your
|
||||||
|
* repository.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require dirname(__FILE__).'/../src/IDF/conf/path.php';
|
||||||
|
require 'Pluf.php';
|
||||||
|
Pluf::start(dirname(__FILE__).'/../src/IDF/conf/idf.php');
|
||||||
|
Pluf_Dispatcher::loadControllers(Pluf::f('idf_views'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [signal]
|
||||||
|
*
|
||||||
|
* mtnpostpush.php::run
|
||||||
|
*
|
||||||
|
* [sender]
|
||||||
|
*
|
||||||
|
* mtnpostpush.php
|
||||||
|
*
|
||||||
|
* [description]
|
||||||
|
*
|
||||||
|
* This signal allows an application to perform a set of tasks
|
||||||
|
* after a push to a monotone repository.
|
||||||
|
*
|
||||||
|
* [parameters]
|
||||||
|
*
|
||||||
|
* array('project' => 'name-of-the-project',
|
||||||
|
* 'revisions' => array('123abc...', '456def...', ...));
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
fwrite(STDERR, "waiting for revisions on STDIN...\n");
|
||||||
|
$stdin = file_get_contents('php://stdin');
|
||||||
|
|
||||||
|
$params = array('project' => $argv[1],
|
||||||
|
'revisions' => explode("\n", chop($stdin)));
|
||||||
|
Pluf_Signal::send('mtnpostpush.php::run', 'mtnpostpush.php', $params);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -64,6 +64,14 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
|
|||||||
'help_text' => __('It must be unique for each project and composed only of letters, digits and dash (-) like "my-project".'),
|
'help_text' => __('It must be unique for each project and composed only of letters, digits and dash (-) like "my-project".'),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
$this->fields['shortdesc'] = new Pluf_Form_Field_Varchar(
|
||||||
|
array('required' => true,
|
||||||
|
'label' => __('short description'),
|
||||||
|
'help_text' => __('A one line description of the project.'),
|
||||||
|
'initial' => '',
|
||||||
|
'widget_attrs' => array('size' => '35'),
|
||||||
|
));
|
||||||
|
|
||||||
$this->fields['scm'] = new Pluf_Form_Field_Varchar(
|
$this->fields['scm'] = new Pluf_Form_Field_Varchar(
|
||||||
array('required' => true,
|
array('required' => true,
|
||||||
'label' => __('Repository type'),
|
'label' => __('Repository type'),
|
||||||
@ -272,6 +280,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
|
|||||||
$project = new IDF_Project();
|
$project = new IDF_Project();
|
||||||
$project->name = $this->cleaned_data['name'];
|
$project->name = $this->cleaned_data['name'];
|
||||||
$project->shortname = $this->cleaned_data['shortname'];
|
$project->shortname = $this->cleaned_data['shortname'];
|
||||||
|
$project->shortdesc = $this->cleaned_data['shortdesc'];
|
||||||
|
|
||||||
if ($this->cleaned_data['template'] != '--') {
|
if ($this->cleaned_data['template'] != '--') {
|
||||||
// Find the template project
|
// Find the template project
|
||||||
|
@ -43,6 +43,14 @@ class IDF_Form_Admin_ProjectUpdate extends Pluf_Form
|
|||||||
'initial' => $this->project->name,
|
'initial' => $this->project->name,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
$this->fields['shortdesc'] = new Pluf_Form_Field_Varchar(
|
||||||
|
array('required' => true,
|
||||||
|
'label' => __('short description'),
|
||||||
|
'help_text' => __('A one line description of the project.'),
|
||||||
|
'initial' => $this->project->shortdesc,
|
||||||
|
'widget_attrs' => array('size' => '35'),
|
||||||
|
));
|
||||||
|
|
||||||
$this->fields['owners'] = new Pluf_Form_Field_Varchar(
|
$this->fields['owners'] = new Pluf_Form_Field_Varchar(
|
||||||
array('required' => false,
|
array('required' => false,
|
||||||
'label' => __('Project owners'),
|
'label' => __('Project owners'),
|
||||||
@ -80,6 +88,7 @@ class IDF_Form_Admin_ProjectUpdate extends Pluf_Form
|
|||||||
$this->cleaned_data);
|
$this->cleaned_data);
|
||||||
$this->project->membershipsUpdated();
|
$this->project->membershipsUpdated();
|
||||||
$this->project->name = $this->cleaned_data['name'];
|
$this->project->name = $this->cleaned_data['name'];
|
||||||
|
$this->project->shortdesc = $this->cleaned_data['shortdesc'];
|
||||||
$this->project->update();
|
$this->project->update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ class IDF_Key extends Pluf_Model
|
|||||||
return array('ssh', $m[2], $m[1]);
|
return array('ssh', $m[2], $m[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IDF_Exception('invalid or unknown key data detected');
|
throw new Exception('invalid or unknown key data detected');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -123,7 +123,7 @@ class IDF_Key extends Pluf_Model
|
|||||||
{
|
{
|
||||||
list($type, $keyName, $keyData) = $this->parseContent();
|
list($type, $keyName, $keyData) = $this->parseContent();
|
||||||
if ($type != 'mtn')
|
if ($type != 'mtn')
|
||||||
throw new IDF_Exception('key is not a monotone public key');
|
throw new Exception('key is not a monotone public key');
|
||||||
return sha1($keyName.":".$keyData);
|
return sha1($keyName.":".$keyData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
/*
|
/*
|
||||||
# ***** BEGIN LICENSE BLOCK *****
|
# ***** BEGIN LICENSE BLOCK *****
|
||||||
# This file is part of InDefero, an open source project management application.
|
# This file is part of InDefero, an open source project management application.
|
||||||
# Copyright (C) 2008 Céondo Ltd and contributors.
|
# Copyright (C) 2010 Céondo Ltd and contributors.
|
||||||
#
|
#
|
||||||
# InDefero is free software; you can redistribute it and/or modify
|
# InDefero is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -35,25 +35,39 @@ class IDF_Plugin_SyncMonotone
|
|||||||
$plug = new IDF_Plugin_SyncMonotone();
|
$plug = new IDF_Plugin_SyncMonotone();
|
||||||
switch ($signal) {
|
switch ($signal) {
|
||||||
case 'IDF_Project::created':
|
case 'IDF_Project::created':
|
||||||
$plug->processMonotoneCreate($params['project']);
|
$plug->processProjectCreate($params['project']);
|
||||||
|
break;
|
||||||
|
case 'IDF_Project::preDelete':
|
||||||
|
$plug->processProjectDelete($params['project']);
|
||||||
|
break;
|
||||||
|
case 'IDF_Key::postSave':
|
||||||
|
$plug->processKeyCreate($params['key']);
|
||||||
|
break;
|
||||||
|
case 'IDF_Key::preDelete':
|
||||||
|
$plug->processKeyDelete($params['key']);
|
||||||
|
break;
|
||||||
|
case 'mtnpostpush.php::run':
|
||||||
|
$plug->processSyncTimeline($params['project']);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Four steps to setup a new monotone project:
|
* Initial steps to setup a new monotone project:
|
||||||
*
|
*
|
||||||
* 1) run mtn db init to initialize a new database underknees
|
* 1) run mtn db init to initialize a new database underknees
|
||||||
* 'mtn_repositories'
|
* 'mtn_repositories'
|
||||||
* 2) create a new server key in the same directory
|
* 2) create a new server key in the same directory
|
||||||
* 3) write monotonerc for access control
|
* 3) create a new client key for IDF and store it in the project conf
|
||||||
* 4) add the database as new local server in the usher configuration
|
* 4) write monotonerc
|
||||||
* 5) reload the running usher instance so it acknowledges the new
|
* 5) add the database as new local server in the usher configuration
|
||||||
* server
|
* 6) reload the running usher instance so it acknowledges the new server
|
||||||
|
* 7) create read-/write-permissions for the project and add all public
|
||||||
|
* keys to the project
|
||||||
*
|
*
|
||||||
* @param IDF_Project
|
* @param IDF_Project
|
||||||
*/
|
*/
|
||||||
function processMonotoneCreate($project)
|
function processProjectCreate($project)
|
||||||
{
|
{
|
||||||
if ($project->getConf()->getVal('scm') != 'mtn') {
|
if ($project->getConf()->getVal('scm') != 'mtn') {
|
||||||
return;
|
return;
|
||||||
@ -73,6 +87,13 @@ class IDF_Plugin_SyncMonotone
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$mtnpostpush = realpath(dirname(__FILE__) . "/../../../scripts/mtn-post-push");
|
||||||
|
if (!file_exists($mtnpostpush)) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not find mtn-post-push script "%s".'), $mtnpostpush
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
$shortname = $project->shortname;
|
$shortname = $project->shortname;
|
||||||
$projectpath = sprintf($projecttempl, $shortname);
|
$projectpath = sprintf($projecttempl, $shortname);
|
||||||
if (file_exists($projectpath)) {
|
if (file_exists($projectpath)) {
|
||||||
@ -91,17 +112,8 @@ class IDF_Plugin_SyncMonotone
|
|||||||
// step 1) create a new database
|
// step 1) create a new database
|
||||||
//
|
//
|
||||||
$dbfile = $projectpath.'/database.mtn';
|
$dbfile = $projectpath.'/database.mtn';
|
||||||
$cmd = sprintf(
|
$cmd = sprintf('db init -d %s', escapeshellarg($dbfile));
|
||||||
Pluf::f('mtn_path', 'mtn').' db init -d %s',
|
self::_mtn_exec($cmd);
|
||||||
escapeshellarg($dbfile)
|
|
||||||
);
|
|
||||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
|
||||||
$ll = exec($cmd, $output = array(), $return = 0);
|
|
||||||
if ($return != 0) {
|
|
||||||
throw new IDF_Scm_Exception(sprintf(
|
|
||||||
__('The database file %s could not be created.'), $dbfile
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// step 2) create a server key
|
// step 2) create a server key
|
||||||
@ -115,56 +127,91 @@ class IDF_Plugin_SyncMonotone
|
|||||||
$server = $parsed['host'];
|
$server = $parsed['host'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$keyname = $shortname.'-server@'.$server;
|
$serverkey = $shortname.'-server@'.$server;
|
||||||
$cmd = sprintf(
|
$cmd = sprintf('au generate_key --confdir=%s %s ""',
|
||||||
Pluf::f('mtn_path', 'mtn').' au genkey --confdir=%s %s ""',
|
|
||||||
escapeshellarg($projectpath),
|
escapeshellarg($projectpath),
|
||||||
escapeshellarg($keyname)
|
escapeshellarg($serverkey)
|
||||||
);
|
);
|
||||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
self::_mtn_exec($cmd);
|
||||||
$ll = exec($cmd, $output = array(), $return = 0);
|
|
||||||
if ($return != 0) {
|
//
|
||||||
|
// step 3) create a client key, and save it in IDF
|
||||||
|
//
|
||||||
|
$clientkey_hash = '';
|
||||||
|
$monotonerc_tpl = 'monotonerc-noauth.tpl';
|
||||||
|
|
||||||
|
if (Pluf::f('mtn_remote_auth', true)) {
|
||||||
|
$monotonerc_tpl = 'monotonerc-auth.tpl';
|
||||||
|
$keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
|
||||||
|
if (!file_exists($keydir)) {
|
||||||
|
if (!mkdir($keydir)) {
|
||||||
throw new IDF_Scm_Exception(sprintf(
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
__('The server key %s could not be created.'), $keyname
|
__('The key directory %s could not be created.'), $keydir
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientkey_name = $shortname.'-client@'.$server;
|
||||||
|
$cmd = sprintf('au generate_key --keydir=%s %s ""',
|
||||||
|
escapeshellarg($keydir),
|
||||||
|
escapeshellarg($clientkey_name)
|
||||||
|
);
|
||||||
|
$keyinfo = self::_mtn_exec($cmd);
|
||||||
|
|
||||||
|
$parsed_keyinfo = array();
|
||||||
|
try {
|
||||||
|
$parsed_keyinfo = IDF_Scm_Monotone_BasicIO::parse($keyinfo);
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not parse key information: %s'), $e->getMessage()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
$clientkey_hash = $parsed_keyinfo[0][1]['hash'];
|
||||||
// step 3) write monotonerc for access control
|
$clientkey_file = $keydir . '/' . $clientkey_name . '.' . $clientkey_hash;
|
||||||
// FIXME: netsync access control is still missing!
|
$clientkey_data = file_get_contents($clientkey_file);
|
||||||
//
|
|
||||||
$monotonerc =<<<END
|
$project->getConf()->setVal('mtn_client_key_name', $clientkey_name);
|
||||||
function get_remote_automate_permitted(key_identity, command, options)
|
$project->getConf()->setVal('mtn_client_key_hash', $clientkey_hash);
|
||||||
local read_only_commands = {
|
$project->getConf()->setVal('mtn_client_key_data', $clientkey_data);
|
||||||
"get_corresponding_path", "get_content_changed", "tags", "branches",
|
|
||||||
"common_ancestors", "packet_for_fdelta", "packet_for_fdata",
|
// add the public client key to the server
|
||||||
"packets_for_certs", "packet_for_rdata", "get_manifest_of",
|
$cmd = sprintf('au get_public_key --keydir=%s %s',
|
||||||
"get_revision", "select", "graph", "children", "parents", "roots",
|
escapeshellarg($keydir),
|
||||||
"leaves", "ancestry_difference", "toposort", "erase_ancestors",
|
escapeshellarg($clientkey_hash)
|
||||||
"descendents", "ancestors", "heads", "get_file_of", "get_file",
|
);
|
||||||
"interface_version", "get_attributes", "content_diff",
|
$clientkey_pubdata = self::_mtn_exec($cmd);
|
||||||
"file_merge", "show_conflicts", "certs", "keys"
|
|
||||||
|
$cmd = sprintf('au put_public_key --db=%s %s',
|
||||||
|
escapeshellarg($dbfile),
|
||||||
|
escapeshellarg($clientkey_pubdata)
|
||||||
|
);
|
||||||
|
self::_mtn_exec($cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
for _,v in ipairs(read_only_commands) do
|
//
|
||||||
if (v == command[1]) then
|
// step 4) write monotonerc
|
||||||
return true
|
//
|
||||||
end
|
$monotonerc = file_get_contents(
|
||||||
end
|
dirname(__FILE__).'/SyncMonotone/'.$monotonerc_tpl
|
||||||
|
);
|
||||||
|
$monotonerc = str_replace(
|
||||||
|
array('%%MTNPOSTPUSH%%', '%%PROJECT%%', '%%MTNCLIENTKEY%%'),
|
||||||
|
array($mtnpostpush, $shortname, $clientkey_hash),
|
||||||
|
$monotonerc
|
||||||
|
);
|
||||||
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
END;
|
|
||||||
$rcfile = $projectpath.'/monotonerc';
|
$rcfile = $projectpath.'/monotonerc';
|
||||||
|
|
||||||
if (!file_put_contents($rcfile, $monotonerc, LOCK_EX)) {
|
if (file_put_contents($rcfile, $monotonerc, LOCK_EX) === false) {
|
||||||
throw new IDF_Scm_Exception(sprintf(
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
__('Could not write mtn configuration file "%s"'), $rcfile)
|
__('Could not write mtn configuration file "%s"'), $rcfile
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// step 4) read in and append the usher config with the new server
|
// step 5) read in and append the usher config with the new server
|
||||||
//
|
//
|
||||||
$usher_rc = file_get_contents($usher_config);
|
$usher_rc = file_get_contents($usher_config);
|
||||||
$parsed_config = array();
|
$parsed_config = array();
|
||||||
@ -179,13 +226,10 @@ END;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ensure we haven't configured a server with this name already
|
// ensure we haven't configured a server with this name already
|
||||||
foreach ($parsed_config as $stanzas)
|
foreach ($parsed_config as $stanzas) {
|
||||||
{
|
foreach ($stanzas as $stanza_line) {
|
||||||
foreach ($stanzas as $stanza_line)
|
|
||||||
{
|
|
||||||
if ($stanza_line['key'] == 'server' &&
|
if ($stanza_line['key'] == 'server' &&
|
||||||
$stanza_line['values'][0] == $shortname)
|
$stanza_line['values'][0] == $shortname) {
|
||||||
{
|
|
||||||
throw new IDF_Scm_Exception(sprintf(
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
__('usher configuration already contains a server '.
|
__('usher configuration already contains a server '.
|
||||||
'entry named "%s"'),
|
'entry named "%s"'),
|
||||||
@ -208,15 +252,468 @@ END;
|
|||||||
|
|
||||||
// FIXME: more sanity - what happens on failing writes? we do not
|
// FIXME: more sanity - what happens on failing writes? we do not
|
||||||
// have a backup copy of usher.conf around...
|
// have a backup copy of usher.conf around...
|
||||||
if (!file_put_contents($usher_config, $usher_rc, LOCK_EX)) {
|
if (file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) {
|
||||||
throw new IDF_Scm_Exception(sprintf(
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
__('Could not write usher configuration file "%s"'), $usher_config)
|
__('Could not write usher configuration file "%s"'), $usher_config
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// step 5) reload usher to pick up the new configuration
|
// step 6) reload usher to pick up the new configuration
|
||||||
//
|
//
|
||||||
IDF_Scm_Monotone_Usher::reload();
|
IDF_Scm_Monotone_Usher::reload();
|
||||||
|
|
||||||
|
//
|
||||||
|
// step 7) add public monotone keys for the project to
|
||||||
|
// read-permissions, write-permissions and the database
|
||||||
|
//
|
||||||
|
$mtn = IDF_Scm_Monotone::factory($project);
|
||||||
|
$stdio = $mtn->getStdio();
|
||||||
|
|
||||||
|
$auth_ids = self::getAuthorizedUserIds($project);
|
||||||
|
$key_ids = array();
|
||||||
|
foreach ($auth_ids as $auth_id) {
|
||||||
|
$sql = new Pluf_SQL('user=%s', array($auth_id));
|
||||||
|
$keys = Pluf::factory('IDF_Key')->getList(array('filter' => $sql->gen()));
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if ($key->getType() != 'mtn')
|
||||||
|
continue;
|
||||||
|
$stdio->exec(array('put_public_key', $key->content));
|
||||||
|
$key_ids[] = $key->getMtnId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$write_permissions = implode("\n", $key_ids);
|
||||||
|
$rcfile = $projectpath.'/write-permissions';
|
||||||
|
if (file_put_contents($rcfile, $write_permissions, LOCK_EX) === false) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not write write-permissions file "%s"'), $rcfile
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($project->private) {
|
||||||
|
$stanza = array(
|
||||||
|
array('key' => 'pattern', 'values' => array('*')),
|
||||||
|
);
|
||||||
|
foreach ($key_ids as $key_id)
|
||||||
|
{
|
||||||
|
$stanza[] = array('key' => 'allow', 'values' => array($key_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$stanza = array(
|
||||||
|
array('key' => 'pattern', 'values' => array('*')),
|
||||||
|
array('key' => 'allow', 'values' => array('*')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$read_permissions = IDF_Scm_Monotone_BasicIO::compile(array($stanza));
|
||||||
|
$rcfile = $projectpath.'/read-permissions';
|
||||||
|
if (file_put_contents($rcfile, $read_permissions, LOCK_EX) === false) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not write read-permissions file "%s"'), $rcfile
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up after a mtn project was deleted
|
||||||
|
*
|
||||||
|
* @param IDF_Project
|
||||||
|
*/
|
||||||
|
public function processProjectDelete($project)
|
||||||
|
{
|
||||||
|
if ($project->getConf()->getVal('scm') != 'mtn') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$usher_config = Pluf::f('mtn_usher_conf', false);
|
||||||
|
if (!$usher_config || !is_writable($usher_config)) {
|
||||||
|
throw new IDF_Scm_Exception(
|
||||||
|
'"mtn_usher_conf" does not exist or is not writable.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$shortname = $project->shortname;
|
||||||
|
IDF_Scm_Monotone_Usher::killServer($shortname);
|
||||||
|
|
||||||
|
$projecttempl = Pluf::f('mtn_repositories', false);
|
||||||
|
if ($projecttempl === false) {
|
||||||
|
throw new IDF_Scm_Exception(
|
||||||
|
'"mtn_repositories" must be defined in your configuration file.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$usher_config = Pluf::f('mtn_usher_conf', false);
|
||||||
|
if (!$usher_config || !is_writable($usher_config)) {
|
||||||
|
throw new IDF_Scm_Exception(
|
||||||
|
'"mtn_usher_conf" does not exist or is not writable.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$projectpath = sprintf($projecttempl, $shortname);
|
||||||
|
if (file_exists($projectpath)) {
|
||||||
|
if (!self::_delete_recursive($projectpath)) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('One or more paths underknees %s could not be deleted.'), $projectpath
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Pluf::f('mtn_remote_auth', true)) {
|
||||||
|
$keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
|
||||||
|
$keyname = $project->getConf()->getVal('mtn_client_key_name', false);
|
||||||
|
$keyhash = $project->getConf()->getVal('mtn_client_key_hash', false);
|
||||||
|
if ($keyname && $keyhash &&
|
||||||
|
file_exists($keydir .'/'. $keyname . '.' . $keyhash)) {
|
||||||
|
if (!@unlink($keydir .'/'. $keyname . '.' . $keyhash)) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not delete client private key %s'), $keyname
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$usher_rc = file_get_contents($usher_config);
|
||||||
|
$parsed_config = array();
|
||||||
|
try {
|
||||||
|
$parsed_config = IDF_Scm_Monotone_BasicIO::parse($usher_rc);
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not parse usher configuration in "%s": %s'),
|
||||||
|
$usher_config, $e->getMessage()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($parsed_config as $idx => $stanzas) {
|
||||||
|
foreach ($stanzas as $stanza_line) {
|
||||||
|
if ($stanza_line['key'] == 'server' &&
|
||||||
|
$stanza_line['values'][0] == $shortname) {
|
||||||
|
unset($parsed_config[$idx]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$usher_rc = IDF_Scm_Monotone_BasicIO::compile($parsed_config);
|
||||||
|
|
||||||
|
// FIXME: more sanity - what happens on failing writes? we do not
|
||||||
|
// have a backup copy of usher.conf around...
|
||||||
|
if (file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not write usher configuration file "%s"'), $usher_config
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
IDF_Scm_Monotone_Usher::reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the (monotone) key to all monotone projects of this forge
|
||||||
|
* where the user of the key has write access to
|
||||||
|
*/
|
||||||
|
public function processKeyCreate($key)
|
||||||
|
{
|
||||||
|
if ($key->getType() != 'mtn')
|
||||||
|
return;
|
||||||
|
|
||||||
|
$projecttempl = Pluf::f('mtn_repositories', false);
|
||||||
|
if ($projecttempl === false) {
|
||||||
|
throw new IDF_Scm_Exception(
|
||||||
|
'"mtn_repositories" must be defined in your configuration file.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
|
||||||
|
$conf = new IDF_Conf();
|
||||||
|
$conf->setProject($project);
|
||||||
|
$scm = $conf->getVal('scm', 'mtn');
|
||||||
|
if ($scm != 'mtn')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$shortname = $project->shortname;
|
||||||
|
$projectpath = sprintf($projecttempl, $shortname);
|
||||||
|
if (!file_exists($projectpath)) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('The project path %s does not exists.'), $projectpath
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$auth_ids = self::getAuthorizedUserIds($project);
|
||||||
|
if (!in_array($key->user, $auth_ids))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$mtn_key_id = $key->getMtnId();
|
||||||
|
|
||||||
|
// if the project is not defined as private, all people have
|
||||||
|
// read access already, so we don't need to write anything
|
||||||
|
// and we currently do not check if read-permissions really
|
||||||
|
// contains
|
||||||
|
// pattern "*"
|
||||||
|
// allow "*"
|
||||||
|
// which is the default for non-private projects
|
||||||
|
if ($project->private == true) {
|
||||||
|
$read_perms = file_get_contents($projectpath.'/read-permissions');
|
||||||
|
$parsed_read_perms = array();
|
||||||
|
try {
|
||||||
|
$parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms);
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not parse read-permissions for project "%s": %s'),
|
||||||
|
$shortname, $e->getMessage()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$wildcard_section = null;
|
||||||
|
foreach ($parsed_read_perms as $stanzas) {
|
||||||
|
foreach ($stanzas as $stanza_line) {
|
||||||
|
if ($stanza_line['key'] == 'pattern' &&
|
||||||
|
$stanza_line['values'][0] == '*') {
|
||||||
|
$wildcard_section =& $stanzas;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($wildcard_section == null)
|
||||||
|
{
|
||||||
|
$wildcard_section = array(
|
||||||
|
array('key' => 'pattern', 'values' => array('*'))
|
||||||
|
);
|
||||||
|
$parsed_read_perms[] =& $wildcard_section;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key_found = false;
|
||||||
|
foreach ($wildcard_section as $line)
|
||||||
|
{
|
||||||
|
if ($line['key'] == 'allow' && $line['values'][0] == $mtn_key_id) {
|
||||||
|
$key_found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$key_found) {
|
||||||
|
$wildcard_section[] = array(
|
||||||
|
'key' => 'allow', 'values' => array($mtn_key_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$read_perms = IDF_Scm_Monotone_BasicIO::compile($parsed_read_perms);
|
||||||
|
|
||||||
|
if (file_put_contents($projectpath.'/read-permissions',
|
||||||
|
$read_perms, LOCK_EX) === false) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not write read-permissions for project "%s"'), $shortname
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$write_perms = file_get_contents($projectpath.'/write-permissions');
|
||||||
|
$lines = preg_split("/(\n|\r\n)/", $write_perms);
|
||||||
|
if (!in_array('*', $lines) && !in_array($mtn_key_id, $lines)) {
|
||||||
|
$lines[] = $mtn_key_id;
|
||||||
|
}
|
||||||
|
if (file_put_contents($projectpath.'/write-permissions',
|
||||||
|
implode("\n", $lines), LOCK_EX) === false) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not write write-permissions file for project "%s"'),
|
||||||
|
$shortname
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$mtn = IDF_Scm_Monotone::factory($project);
|
||||||
|
$stdio = $mtn->getStdio();
|
||||||
|
$stdio->exec(array('put_public_key', $key->content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the (monotone) key from all monotone projects of this forge
|
||||||
|
* where the user of the key has write access to
|
||||||
|
*/
|
||||||
|
public function processKeyDelete($key)
|
||||||
|
{
|
||||||
|
if ($key->getType() != 'mtn')
|
||||||
|
return;
|
||||||
|
|
||||||
|
$projecttempl = Pluf::f('mtn_repositories', false);
|
||||||
|
if ($projecttempl === false) {
|
||||||
|
throw new IDF_Scm_Exception(
|
||||||
|
'"mtn_repositories" must be defined in your configuration file.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
|
||||||
|
$conf = new IDF_Conf();
|
||||||
|
$conf->setProject($project);
|
||||||
|
$scm = $conf->getVal('scm', 'mtn');
|
||||||
|
if ($scm != 'mtn')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$shortname = $project->shortname;
|
||||||
|
$projectpath = sprintf($projecttempl, $shortname);
|
||||||
|
if (!file_exists($projectpath)) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('The project path %s does not exists.'), $projectpath
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$auth_ids = self::getAuthorizedUserIds($project);
|
||||||
|
if (!in_array($key->user, $auth_ids))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$mtn_key_id = $key->getMtnId();
|
||||||
|
|
||||||
|
// if the project is not defined as private, all people have
|
||||||
|
// read access already, so we don't need to write anything
|
||||||
|
// and we currently do not check if read-permissions really
|
||||||
|
// contains
|
||||||
|
// pattern "*"
|
||||||
|
// allow "*"
|
||||||
|
// which is the default for non-private projects
|
||||||
|
if ($project->private === true) {
|
||||||
|
$read_perms = file_get_contents($projectpath.'/read-permissions');
|
||||||
|
$parsed_read_perms = array();
|
||||||
|
try {
|
||||||
|
$parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms);
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not parse read-permissions for project "%s": %s'),
|
||||||
|
$shortname, $e->getMessage()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// while we add new keys only to an existing wild-card entry
|
||||||
|
// we remove dropped keys from all sections since the key
|
||||||
|
// should be simply unavailable for all of them
|
||||||
|
foreach ($parsed_read_perms as $stanzas) {
|
||||||
|
for ($i=0; $i<count($stanzas); ) {
|
||||||
|
if ($stanzas[$i]['key'] == 'allow' &&
|
||||||
|
$stanzas[$i]['values'][0] == $mtn_key_id) {
|
||||||
|
unset($stanzas[$i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++$i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$read_perms = IDF_Scm_Monotone_BasicIO::compile($parsed_read_perms);
|
||||||
|
|
||||||
|
if (file_put_contents($projectpath.'/read-permissions',
|
||||||
|
$read_perms, LOCK_EX) === false) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not write read-permissions for project "%s"'), $shortname
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$write_perms = file_get_contents($projectpath.'/write-permissions');
|
||||||
|
$lines = preg_split("/(\n|\r\n)/", $write_perms);
|
||||||
|
for ($i=0; $i<count($lines); ) {
|
||||||
|
if ($lines[$i] == $mtn_key_id) {
|
||||||
|
unset($lines[$i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++$i;
|
||||||
|
}
|
||||||
|
if (file_put_contents($projectpath.'/write-permissions',
|
||||||
|
implode("\n", $lines), LOCK_EX) === false) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not write write-permissions file for project "%s"'),
|
||||||
|
$shortname
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$mtn = IDF_Scm_Monotone::factory($project);
|
||||||
|
$stdio = $mtn->getStdio();
|
||||||
|
// if the public key did not sign any revisions, drop it from
|
||||||
|
// the database as well
|
||||||
|
try {
|
||||||
|
if (strlen($stdio->exec(array('select', 'k:' . $mtn_key_id))) == 0) {
|
||||||
|
$stdio->exec(array('drop_public_key', $mtn_key_id));
|
||||||
|
}
|
||||||
|
} catch (IDF_Scm_Exception $e) {
|
||||||
|
if (strpos($e->getMessage(), 'there is no key named') === false)
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getAuthorizedUserIds($project)
|
||||||
|
{
|
||||||
|
$mem = $project->getMembershipData();
|
||||||
|
$members = array_merge((array)$mem['members'],
|
||||||
|
(array)$mem['owners'],
|
||||||
|
(array)$mem['authorized']);
|
||||||
|
$userids = array();
|
||||||
|
foreach ($members as $member) {
|
||||||
|
$userids[] = $member->id;
|
||||||
|
}
|
||||||
|
return $userids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the timeline after a push
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function processSyncTimeline($project_name)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$project = IDF_Project::getOr404($project_name);
|
||||||
|
} catch (Pluf_HTTP_Error404 $e) {
|
||||||
|
Pluf_Log::event(array(
|
||||||
|
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
||||||
|
'Project not found.',
|
||||||
|
array($project_name, $params)
|
||||||
|
));
|
||||||
|
return false; // Project not found
|
||||||
|
}
|
||||||
|
|
||||||
|
Pluf_Log::debug(array(
|
||||||
|
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
||||||
|
'Project found', $project_name, $project->id
|
||||||
|
));
|
||||||
|
IDF_Scm::syncTimeline($project, true);
|
||||||
|
Pluf_Log::event(array(
|
||||||
|
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
||||||
|
'sync', array($project_name, $project->id)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function _mtn_exec($cmd)
|
||||||
|
{
|
||||||
|
$fullcmd = sprintf('%s %s %s',
|
||||||
|
Pluf::f('idf_exec_cmd_prefix', ''),
|
||||||
|
Pluf::f('mtn_path', 'mtn'),
|
||||||
|
$cmd
|
||||||
|
);
|
||||||
|
|
||||||
|
$output = $return = null;
|
||||||
|
exec($fullcmd, $output, $return);
|
||||||
|
if ($return != 0) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('The command "%s" could not be executed.'), $cmd
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return implode("\n", $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function _delete_recursive($path)
|
||||||
|
{
|
||||||
|
if (is_file($path)) {
|
||||||
|
return @unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_dir($path)) {
|
||||||
|
$scan = glob(rtrim($path, '/') . '/*');
|
||||||
|
$status = 0;
|
||||||
|
foreach ($scan as $subpath) {
|
||||||
|
$status |= self::_delete_recursive($subpath);
|
||||||
|
}
|
||||||
|
$status |= rmdir($path);
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
63
src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl
Normal file
63
src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
-- ***** BEGIN LICENSE BLOCK *****
|
||||||
|
-- This file is part of InDefero, an open source project management application.
|
||||||
|
-- Copyright (C) 2010 Céondo Ltd and contributors.
|
||||||
|
--
|
||||||
|
-- InDefero is free software; you can redistribute it and/or modify
|
||||||
|
-- it under the terms of the GNU General Public License as published by
|
||||||
|
-- the Free Software Foundation; either version 2 of the License, or
|
||||||
|
-- (at your option) any later version.
|
||||||
|
--
|
||||||
|
-- InDefero is distributed in the hope that it will be useful,
|
||||||
|
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
-- GNU General Public License for more details.
|
||||||
|
--
|
||||||
|
-- You should have received a copy of the GNU General Public License
|
||||||
|
-- along with this program; if not, write to the Free Software
|
||||||
|
-- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
--
|
||||||
|
-- ***** END LICENSE BLOCK *****
|
||||||
|
|
||||||
|
--
|
||||||
|
-- controls the access rights for remote_stdio which is used by IDFs frontend
|
||||||
|
-- and other interested parties
|
||||||
|
--
|
||||||
|
function get_remote_automate_permitted(key_identity, command, options)
|
||||||
|
if (key_identity.id == "%%MTNCLIENTKEY%%") then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- let IDF know of new arriving revisions to fill its timeline
|
||||||
|
--
|
||||||
|
_idf_revs = {}
|
||||||
|
function note_netsync_start(session_id)
|
||||||
|
_idf_revs[session_id] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function note_netsync_revision_received(new_id, revision, certs, session_id)
|
||||||
|
table.insert(_idf_revs[session_id], new_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
function note_netsync_end (session_id, ...)
|
||||||
|
if table.getn(_idf_revs[session_id]) == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%");
|
||||||
|
if pid == -1 then
|
||||||
|
print("could execute %%MTNPOSTPUSH%%")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for _,r in ipairs(_idf_revs[session_id]) do
|
||||||
|
pin:write(r .. "\n")
|
||||||
|
end
|
||||||
|
pin:close()
|
||||||
|
|
||||||
|
wait(pid)
|
||||||
|
end
|
||||||
|
|
75
src/IDF/Plugin/SyncMonotone/monotonerc-noauth.tpl
Normal file
75
src/IDF/Plugin/SyncMonotone/monotonerc-noauth.tpl
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
-- ***** BEGIN LICENSE BLOCK *****
|
||||||
|
-- This file is part of InDefero, an open source project management application.
|
||||||
|
-- Copyright (C) 2010 Céondo Ltd and contributors.
|
||||||
|
--
|
||||||
|
-- InDefero is free software; you can redistribute it and/or modify
|
||||||
|
-- it under the terms of the GNU General Public License as published by
|
||||||
|
-- the Free Software Foundation; either version 2 of the License, or
|
||||||
|
-- (at your option) any later version.
|
||||||
|
--
|
||||||
|
-- InDefero is distributed in the hope that it will be useful,
|
||||||
|
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
-- GNU General Public License for more details.
|
||||||
|
--
|
||||||
|
-- You should have received a copy of the GNU General Public License
|
||||||
|
-- along with this program; if not, write to the Free Software
|
||||||
|
-- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
--
|
||||||
|
-- ***** END LICENSE BLOCK *****
|
||||||
|
|
||||||
|
--
|
||||||
|
-- controls the access rights for remote_stdio which is used by IDFs frontend
|
||||||
|
-- and other interested parties
|
||||||
|
--
|
||||||
|
function get_remote_automate_permitted(key_identity, command, options)
|
||||||
|
local read_only_commands = {
|
||||||
|
"get_corresponding_path", "get_content_changed", "tags", "branches",
|
||||||
|
"common_ancestors", "packet_for_fdelta", "packet_for_fdata",
|
||||||
|
"packets_for_certs", "packet_for_rdata", "get_manifest_of",
|
||||||
|
"get_revision", "select", "graph", "children", "parents", "roots",
|
||||||
|
"leaves", "ancestry_difference", "toposort", "erase_ancestors",
|
||||||
|
"descendents", "ancestors", "heads", "get_file_of", "get_file",
|
||||||
|
"interface_version", "get_attributes", "content_diff",
|
||||||
|
"file_merge", "show_conflicts", "certs", "keys"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _,v in ipairs(read_only_commands) do
|
||||||
|
if (v == command[1]) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- let IDF know of new arriving revisions to fill its timeline
|
||||||
|
--
|
||||||
|
_idf_revs = {}
|
||||||
|
function note_netsync_start(session_id)
|
||||||
|
_idf_revs[session_id] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function note_netsync_revision_received(new_id, revision, certs, session_id)
|
||||||
|
table.insert(_idf_revs[session_id], new_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
function note_netsync_end (session_id, ...)
|
||||||
|
if table.getn(_idf_revs[session_id]) == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%");
|
||||||
|
if pid == -1 then
|
||||||
|
print("could execute %%MTNPOSTPUSH%%")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for _,r in ipairs(_idf_revs[session_id]) do
|
||||||
|
pin:write(r .. "\n")
|
||||||
|
end
|
||||||
|
pin:close()
|
||||||
|
|
||||||
|
wait(pid)
|
||||||
|
end
|
@ -166,13 +166,28 @@ class IDF_Scm
|
|||||||
throw new Pluf_Exception_NotImplemented();
|
throw new Pluf_Exception_NotImplemented();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REVISION_VALID = 0;
|
||||||
|
const REVISION_INVALID = 1;
|
||||||
|
const REVISION_AMBIGUOUS = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a revision or commit is valid.
|
* Check if a revision or commit is valid, invalid or ambiguous.
|
||||||
*
|
*
|
||||||
* @param string Revision or commit
|
* @param string Revision or commit
|
||||||
* @return bool
|
* @return int One of REVISION_VALID, REVISION_INVALID or REVISION_AMBIGIOUS
|
||||||
*/
|
*/
|
||||||
public function isValidRevision($rev)
|
public function validateRevision($rev)
|
||||||
|
{
|
||||||
|
throw new Pluf_Exception_NotImplemented();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of single commit objects for ambiguous commit identifiers
|
||||||
|
*
|
||||||
|
* @param string Ambiguous commit identifier
|
||||||
|
* @return array of objects
|
||||||
|
*/
|
||||||
|
public function disambiguateRevision($commit)
|
||||||
{
|
{
|
||||||
throw new Pluf_Exception_NotImplemented();
|
throw new Pluf_Exception_NotImplemented();
|
||||||
}
|
}
|
||||||
|
@ -296,10 +296,12 @@ class IDF_Scm_Git extends IDF_Scm
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function isValidRevision($commit)
|
public function validateRevision($commit)
|
||||||
{
|
{
|
||||||
$type = $this->testHash($commit);
|
$type = $this->testHash($commit);
|
||||||
return ('commit' == $type || 'tag' == $type);
|
if ('commit' == $type || 'tag' == $type)
|
||||||
|
return IDF_Scm::REVISION_VALID;
|
||||||
|
return IDF_Scm::REVISION_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -87,14 +87,19 @@ class IDF_Scm_Mercurial extends IDF_Scm
|
|||||||
return sprintf(Pluf::f('mercurial_remote_url'), $project->shortname);
|
return sprintf(Pluf::f('mercurial_remote_url'), $project->shortname);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isValidRevision($rev)
|
public function validateRevision($rev)
|
||||||
{
|
{
|
||||||
$cmd = sprintf(Pluf::f('hg_path', 'hg').' log -R %s -r %s',
|
$cmd = sprintf(Pluf::f('hg_path', 'hg').' log -R %s -r %s',
|
||||||
escapeshellarg($this->repo),
|
escapeshellarg($this->repo),
|
||||||
escapeshellarg($rev));
|
escapeshellarg($rev));
|
||||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||||
self::exec('IDF_Scm_Mercurial::isValidRevision', $cmd, $out, $ret);
|
self::exec('IDF_Scm_Mercurial::isValidRevision', $cmd, $out, $ret);
|
||||||
return ($ret == 0) && (count($out) > 0);
|
|
||||||
|
// FIXME: apparently a given hg revision can also be ambigious -
|
||||||
|
// handle this case here sometime
|
||||||
|
if ($ret == 0 && count($out) > 0)
|
||||||
|
return IDF_Scm::REVISION_VALID;
|
||||||
|
return IDF_Scm::REVISION_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,6 +47,16 @@ class IDF_Scm_Monotone extends IDF_Scm
|
|||||||
$this->stdio = new IDF_Scm_Monotone_Stdio($project);
|
$this->stdio = new IDF_Scm_Monotone_Stdio($project);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the stdio instance in use
|
||||||
|
*
|
||||||
|
* @return IDF_Scm_Monotone_Stdio
|
||||||
|
*/
|
||||||
|
public function getStdio()
|
||||||
|
{
|
||||||
|
return $this->stdio;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see IDF_Scm::getRepositorySize()
|
* @see IDF_Scm::getRepositorySize()
|
||||||
*/
|
*/
|
||||||
@ -340,7 +350,14 @@ class IDF_Scm_Monotone extends IDF_Scm
|
|||||||
foreach ($certs['date'] as $date)
|
foreach ($certs['date'] as $date)
|
||||||
$dates[] = date('Y-m-d H:i:s', strtotime($date));
|
$dates[] = date('Y-m-d H:i:s', strtotime($date));
|
||||||
$file['date'] = implode(', ', $dates);
|
$file['date'] = implode(', ', $dates);
|
||||||
$file['log'] = implode("\n---\n", $certs['changelog']);
|
$combinedChangelog = implode("\n---\n", $certs['changelog']);
|
||||||
|
$split = preg_split("/[\n\r]/", $combinedChangelog, 2);
|
||||||
|
// FIXME: the complete log message is currently not used in the
|
||||||
|
// tree view (the same is true for the other SCM implementations)
|
||||||
|
// but we _should_ really use or at least return that here
|
||||||
|
// in case we want to do fancy stuff like described in
|
||||||
|
// issue 492
|
||||||
|
$file['log'] = $split[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
$files[] = (object) $file;
|
$files[] = (object) $file;
|
||||||
@ -425,12 +442,53 @@ class IDF_Scm_Monotone extends IDF_Scm
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see IDF_Scm::isValidRevision()
|
* @see IDF_Scm::validateRevision()
|
||||||
*/
|
*/
|
||||||
public function isValidRevision($commit)
|
public function validateRevision($commit)
|
||||||
{
|
{
|
||||||
$revs = $this->_resolveSelector($commit);
|
$revs = $this->_resolveSelector($commit);
|
||||||
return count($revs) == 1;
|
if (count($revs) == 0)
|
||||||
|
return IDF_Scm::REVISION_INVALID;
|
||||||
|
|
||||||
|
if (count($revs) > 1)
|
||||||
|
return IDF_Scm::REVISION_AMBIGUOUS;
|
||||||
|
|
||||||
|
return IDF_Scm::REVISION_VALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see IDF_Scm::disambiguateRevision
|
||||||
|
*/
|
||||||
|
public function disambiguateRevision($commit)
|
||||||
|
{
|
||||||
|
$revs = $this->_resolveSelector($commit);
|
||||||
|
|
||||||
|
$out = array();
|
||||||
|
foreach ($revs as $rev)
|
||||||
|
{
|
||||||
|
$certs = $this->_getCerts($rev);
|
||||||
|
|
||||||
|
$log = array();
|
||||||
|
$log['author'] = implode(', ', $certs['author']);
|
||||||
|
|
||||||
|
$log['branch'] = implode(', ', $certs['branch']);
|
||||||
|
|
||||||
|
$dates = array();
|
||||||
|
foreach ($certs['date'] as $date)
|
||||||
|
$dates[] = date('Y-m-d H:i:s', strtotime($date));
|
||||||
|
$log['date'] = implode(', ', $dates);
|
||||||
|
|
||||||
|
$combinedChangelog = implode("\n---\n", $certs['changelog']);
|
||||||
|
$split = preg_split("/[\n\r]/", $combinedChangelog, 2);
|
||||||
|
$log['title'] = $split[0];
|
||||||
|
$log['full_message'] = (isset($split[1])) ? trim($split[1]) : '';
|
||||||
|
|
||||||
|
$log['commit'] = $rev;
|
||||||
|
|
||||||
|
$out[] = (object)$log;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -630,7 +688,7 @@ class IDF_Scm_Monotone extends IDF_Scm
|
|||||||
--$n;
|
--$n;
|
||||||
|
|
||||||
$log = array();
|
$log = array();
|
||||||
$log['author'] = implode(", ", $certs['author']);
|
$log['author'] = implode(', ', $certs['author']);
|
||||||
|
|
||||||
$dates = array();
|
$dates = array();
|
||||||
foreach ($certs['date'] as $date)
|
foreach ($certs['date'] as $date)
|
||||||
|
@ -75,6 +75,9 @@ class IDF_Scm_Monotone_BasicIO
|
|||||||
}
|
}
|
||||||
++$pos; // closing quote
|
++$pos; // closing quote
|
||||||
|
|
||||||
|
if ($pos >= strlen($in))
|
||||||
|
break;
|
||||||
|
|
||||||
if ($in[$pos] == ' ') {
|
if ($in[$pos] == ' ') {
|
||||||
++$pos; // space
|
++$pos; // space
|
||||||
++$valCount;
|
++$valCount;
|
||||||
|
@ -62,6 +62,55 @@ class IDF_Scm_Monotone_Stdio
|
|||||||
$this->stop();
|
$this->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string with additional options which are passed to
|
||||||
|
* an mtn instance connecting to remote databases
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function _getAuthOptions()
|
||||||
|
{
|
||||||
|
// no remote authentication - the simple case
|
||||||
|
if (!Pluf::f('mtn_remote_auth', true)) {
|
||||||
|
return '--key= ';
|
||||||
|
}
|
||||||
|
|
||||||
|
$prjconf = $this->project->getConf();
|
||||||
|
$name = $prjconf->getVal('mtn_client_key_name', false);
|
||||||
|
$hash = $prjconf->getVal('mtn_client_key_hash', false);
|
||||||
|
|
||||||
|
if (!$name || !$hash) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Monotone client key name or hash not in project conf.')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
|
||||||
|
if (!file_exists($keydir)) {
|
||||||
|
if (!mkdir($keydir)) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('The key directory %s could not be created.'), $keydir
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case somebody cleaned out the cache, we restore the key here
|
||||||
|
$keyfile = $keydir . '/' . $name .'.'. $hash;
|
||||||
|
if (!file_exists($keyfile)) {
|
||||||
|
$data = $prjconf->getVal('mtn_client_key_data');
|
||||||
|
if (!file_put_contents($keyfile, $data, LOCK_EX)) {
|
||||||
|
throw new IDF_Scm_Exception(sprintf(
|
||||||
|
__('Could not write client key "%s"'), $keyfile
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('--keydir=%s --key=%s ',
|
||||||
|
escapeshellarg($keydir),
|
||||||
|
escapeshellarg($hash)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the stdio process and resets the command counter
|
* Starts the stdio process and resets the command counter
|
||||||
*/
|
*/
|
||||||
@ -80,9 +129,8 @@ class IDF_Scm_Monotone_Stdio
|
|||||||
$cmd .= sprintf('%s ', escapeshellarg($opt));
|
$cmd .= sprintf('%s ', escapeshellarg($opt));
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: we might want to add an option for anonymous / no key
|
|
||||||
// access, but upstream bug #30237 prevents that for now
|
|
||||||
if ($remote_db_access) {
|
if ($remote_db_access) {
|
||||||
|
$cmd .= $this->_getAuthOptions();
|
||||||
$host = sprintf(Pluf::f('mtn_remote_url'), $this->project->shortname);
|
$host = sprintf(Pluf::f('mtn_remote_url'), $this->project->shortname);
|
||||||
$cmd .= sprintf('automate remote_stdio %s', escapeshellarg($host));
|
$cmd .= sprintf('automate remote_stdio %s', escapeshellarg($host));
|
||||||
}
|
}
|
||||||
@ -104,7 +152,6 @@ class IDF_Scm_Monotone_Stdio
|
|||||||
);
|
);
|
||||||
|
|
||||||
$env = array('LANG' => 'en_US.UTF-8');
|
$env = array('LANG' => 'en_US.UTF-8');
|
||||||
|
|
||||||
$this->proc = proc_open($cmd, $descriptors, $this->pipes,
|
$this->proc = proc_open($cmd, $descriptors, $this->pipes,
|
||||||
null, $env);
|
null, $env);
|
||||||
|
|
||||||
@ -146,8 +193,9 @@ class IDF_Scm_Monotone_Stdio
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
$read = array($this->pipes[1], $this->pipes[2]);
|
$read = array($this->pipes[1], $this->pipes[2]);
|
||||||
|
$write = $except = null;
|
||||||
$streamsChanged = stream_select(
|
$streamsChanged = stream_select(
|
||||||
$read, $write = null, $except = null, 0, 20000
|
$read, $write, $except, 0, 20000
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($streamsChanged === false) {
|
if ($streamsChanged === false) {
|
||||||
|
@ -138,7 +138,7 @@ class IDF_Scm_Svn extends IDF_Scm
|
|||||||
/**
|
/**
|
||||||
* Subversion revisions are either a number or 'HEAD'.
|
* Subversion revisions are either a number or 'HEAD'.
|
||||||
*/
|
*/
|
||||||
public function isValidRevision($rev)
|
public function validateRevision($rev)
|
||||||
{
|
{
|
||||||
if ($rev == 'HEAD') {
|
if ($rev == 'HEAD') {
|
||||||
return true;
|
return true;
|
||||||
@ -149,8 +149,11 @@ class IDF_Scm_Svn extends IDF_Scm
|
|||||||
escapeshellarg($this->repo),
|
escapeshellarg($this->repo),
|
||||||
escapeshellarg($rev));
|
escapeshellarg($rev));
|
||||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||||
self::exec('IDF_Scm_Svn::isValidRevision', $cmd, $out, $ret);
|
self::exec('IDF_Scm_Svn::validateRevision', $cmd, $out, $ret);
|
||||||
return (0 == $ret);
|
|
||||||
|
if ($ret == 0)
|
||||||
|
return IDF_Scm::REVISION_VALID;
|
||||||
|
return IDF_Scm::REVISION_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,25 +46,32 @@ class IDF_Template_Markdown extends Pluf_Template_Tag
|
|||||||
array($this, 'callbackEmbeddedDoc'),
|
array($this, 'callbackEmbeddedDoc'),
|
||||||
$text);
|
$text);
|
||||||
}
|
}
|
||||||
|
// Replace [Page]([[PageName]]) with corresponding link to the page, with link text being Page.
|
||||||
|
$text = preg_replace_callback('#\[([^\]]+)\]\(\[\[([A-Za-z0-9\-]+)\]\]\)#im',
|
||||||
|
array($this, 'callbackWikiPage'),
|
||||||
|
$text);
|
||||||
// Replace [[PageName]] with corresponding link to the page.
|
// Replace [[PageName]] with corresponding link to the page.
|
||||||
$text = preg_replace_callback('#\[\[([A-Za-z0-9\-]+)\]\]#im',
|
$text = preg_replace_callback('#\[\[([A-Za-z0-9\-]+)\]\]#im',
|
||||||
array($this, 'callbackWikiPage'),
|
array($this, 'callbackWikiPageNoName'),
|
||||||
$text);
|
$text);
|
||||||
$filter = new IDF_Template_MarkdownPrefilter();
|
$filter = new IDF_Template_MarkdownPrefilter();
|
||||||
echo $filter->go(Pluf_Text_MarkDown_parse($text));
|
echo $filter->go(Pluf_Text_MarkDown_parse($text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function callbackWikiPageNoName($m)
|
||||||
|
{
|
||||||
|
$m[2] = $m[1]; //Set the link text to be the same as the page name.
|
||||||
|
return $this->callbackWikiPage($m);
|
||||||
|
}
|
||||||
|
|
||||||
function callbackWikiPage($m)
|
function callbackWikiPage($m)
|
||||||
{
|
{
|
||||||
$sql = new Pluf_SQL('project=%s AND title=%s',
|
$sql = new Pluf_SQL('project=%s AND title=%s',
|
||||||
array($this->project->id, $m[1]));
|
array($this->project->id, $m[2]));
|
||||||
$pages = Pluf::factory('IDF_WikiPage')->getList(array('filter'=>$sql->gen()));
|
$pages = Pluf::factory('IDF_WikiPage')->getList(array('filter'=>$sql->gen()));
|
||||||
if ($pages->count() != 1 and !$this->request->rights['hasWikiAccess']) {
|
|
||||||
return $m[0];
|
|
||||||
}
|
|
||||||
if ($pages->count() != 1 and $this->request->rights['hasWikiAccess']
|
if ($pages->count() != 1 and $this->request->rights['hasWikiAccess']
|
||||||
and !$this->request->user->isAnonymous()) {
|
and !$this->request->user->isAnonymous()) {
|
||||||
return '<img style="vertical-align: text-bottom;" alt=" " src="'.Pluf::f('url_media').'/idf/img/add.png" /><a href="'.Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::create', array($this->project->shortname), array('name'=>$m[1])).'" title="'.__('Create this documentation page').'">'.$m[1].'</a>';
|
return '<img style="vertical-align: text-bottom;" alt=" " src="'.Pluf::f('url_media').'/idf/img/add.png" /><a href="'.Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::create', array($this->project->shortname), array('name'=>$m[2])).'" title="'.__('Create this documentation page').'">'.$m[1].'</a>';
|
||||||
}
|
}
|
||||||
if (!$this->request->rights['hasWikiAccess'] or $pages->count() == 0) {
|
if (!$this->request->rights['hasWikiAccess'] or $pages->count() == 0) {
|
||||||
return $m[1];
|
return $m[1];
|
||||||
|
@ -59,30 +59,56 @@ class IDF_Views_Source
|
|||||||
$params, $request);
|
$params, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public $changeLog_precond = array('IDF_Precondition::accessSource');
|
/**
|
||||||
|
* Is displayed in case an invalid revision is requested
|
||||||
|
*/
|
||||||
|
public $invalidRevision_precond = array('IDF_Precondition::accessSource');
|
||||||
|
public function invalidRevision($request, $match)
|
||||||
|
{
|
||||||
|
$title = sprintf(__('%s Invalid Revision'), (string) $request->project);
|
||||||
|
$commit = $match[2];
|
||||||
|
$params = array(
|
||||||
|
'page_title' => $title,
|
||||||
|
'title' => $title,
|
||||||
|
'commit' => $commit,
|
||||||
|
);
|
||||||
|
return Pluf_Shortcuts_RenderToResponse('idf/source/invalid_revision.html',
|
||||||
|
$params, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is displayed in case a revision identifier cannot be uniquely resolved
|
||||||
|
* to one single revision
|
||||||
|
*/
|
||||||
|
public $disambiguateRevision_precond = array('IDF_Precondition::accessSource',
|
||||||
|
'IDF_Views_Source_Precondition::scmAvailable');
|
||||||
|
public function disambiguateRevision($request, $match)
|
||||||
|
{
|
||||||
|
$title = sprintf(__('%s Ambiguous Revision'), (string) $request->project);
|
||||||
|
$commit = $match[2];
|
||||||
|
$redirect = $match[3];
|
||||||
|
$scm = IDF_Scm::get($request->project);
|
||||||
|
$revisions = $scm->disambiguateRevision($commit);
|
||||||
|
$params = array(
|
||||||
|
'page_title' => $title,
|
||||||
|
'title' => $title,
|
||||||
|
'commit' => $commit,
|
||||||
|
'revisions' => $revisions,
|
||||||
|
'redirect' => $redirect,
|
||||||
|
);
|
||||||
|
return Pluf_Shortcuts_RenderToResponse('idf/source/disambiguate_revision.html',
|
||||||
|
$params, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public $changeLog_precond = array('IDF_Precondition::accessSource',
|
||||||
|
'IDF_Views_Source_Precondition::scmAvailable',
|
||||||
|
'IDF_Views_Source_Precondition::revisionValid');
|
||||||
public function changeLog($request, $match)
|
public function changeLog($request, $match)
|
||||||
{
|
{
|
||||||
$scm = IDF_Scm::get($request->project);
|
$scm = IDF_Scm::get($request->project);
|
||||||
if (!$scm->isAvailable()) {
|
|
||||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help',
|
|
||||||
array($request->project->shortname));
|
|
||||||
return new Pluf_HTTP_Response_Redirect($url);
|
|
||||||
}
|
|
||||||
$branches = $scm->getBranches();
|
$branches = $scm->getBranches();
|
||||||
$commit = $match[2];
|
$commit = $match[2];
|
||||||
if (!$scm->isValidRevision($commit)) {
|
|
||||||
if (count($branches) == 0) {
|
|
||||||
// Redirect to the project source help
|
|
||||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help',
|
|
||||||
array($request->project->shortname));
|
|
||||||
return new Pluf_HTTP_Response_Redirect($url);
|
|
||||||
}
|
|
||||||
// Redirect to the first branch
|
|
||||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::changeLog',
|
|
||||||
array($request->project->shortname,
|
|
||||||
$scm->getMainBranch()));
|
|
||||||
return new Pluf_HTTP_Response_Redirect($url);
|
|
||||||
}
|
|
||||||
$title = sprintf(__('%1$s %2$s Change Log'), (string) $request->project,
|
$title = sprintf(__('%1$s %2$s Change Log'), (string) $request->project,
|
||||||
$this->getScmType($request));
|
$this->getScmType($request));
|
||||||
$changes = $scm->getChangeLog($commit, 25);
|
$changes = $scm->getChangeLog($commit, 25);
|
||||||
@ -111,22 +137,17 @@ class IDF_Views_Source
|
|||||||
$request);
|
$request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public $treeBase_precond = array('IDF_Precondition::accessSource');
|
public $treeBase_precond = array('IDF_Precondition::accessSource',
|
||||||
|
'IDF_Views_Source_Precondition::scmAvailable',
|
||||||
|
'IDF_Views_Source_Precondition::revisionValid');
|
||||||
public function treeBase($request, $match)
|
public function treeBase($request, $match)
|
||||||
{
|
{
|
||||||
$scm = IDF_Scm::get($request->project);
|
$scm = IDF_Scm::get($request->project);
|
||||||
if (!$scm->isAvailable()) {
|
|
||||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help',
|
|
||||||
array($request->project->shortname));
|
|
||||||
return new Pluf_HTTP_Response_Redirect($url);
|
|
||||||
}
|
|
||||||
$commit = $match[2];
|
$commit = $match[2];
|
||||||
|
|
||||||
$cobject = $scm->getCommit($commit);
|
$cobject = $scm->getCommit($commit);
|
||||||
if (!$cobject) {
|
if (!$cobject) {
|
||||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
|
throw new Exception('could not retrieve commit object for '. $commit);
|
||||||
array($request->project->shortname,
|
|
||||||
$scm->getMainBranch()));
|
|
||||||
return new Pluf_HTTP_Response_Redirect($url);
|
|
||||||
}
|
}
|
||||||
$title = sprintf(__('%1$s %2$s Source Tree'),
|
$title = sprintf(__('%1$s %2$s Source Tree'),
|
||||||
$request->project, $this->getScmType($request));
|
$request->project, $this->getScmType($request));
|
||||||
@ -159,20 +180,14 @@ class IDF_Views_Source
|
|||||||
$request);
|
$request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public $tree_precond = array('IDF_Precondition::accessSource');
|
public $tree_precond = array('IDF_Precondition::accessSource',
|
||||||
|
'IDF_Views_Source_Precondition::scmAvailable',
|
||||||
|
'IDF_Views_Source_Precondition::revisionValid');
|
||||||
public function tree($request, $match)
|
public function tree($request, $match)
|
||||||
{
|
{
|
||||||
$scm = IDF_Scm::get($request->project);
|
$scm = IDF_Scm::get($request->project);
|
||||||
$commit = $match[2];
|
$commit = $match[2];
|
||||||
|
|
||||||
if (!$scm->isAvailable()) {
|
|
||||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help',
|
|
||||||
array($request->project->shortname));
|
|
||||||
return new Pluf_HTTP_Response_Redirect($url);
|
|
||||||
}
|
|
||||||
$fburl = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
|
|
||||||
array($request->project->shortname,
|
|
||||||
$scm->getMainBranch()));
|
|
||||||
$request_file = $match[3];
|
$request_file = $match[3];
|
||||||
if (substr($request_file, -1) == '/') {
|
if (substr($request_file, -1) == '/') {
|
||||||
$request_file = substr($request_file, 0, -1);
|
$request_file = substr($request_file, 0, -1);
|
||||||
@ -181,13 +196,13 @@ class IDF_Views_Source
|
|||||||
$request_file));
|
$request_file));
|
||||||
return new Pluf_HTTP_Response_Redirect($url, 301);
|
return new Pluf_HTTP_Response_Redirect($url, 301);
|
||||||
}
|
}
|
||||||
if (!$scm->isValidRevision($commit, $request_file)) {
|
|
||||||
// Redirect to the first branch
|
|
||||||
return new Pluf_HTTP_Response_Redirect($fburl);
|
|
||||||
}
|
|
||||||
$request_file_info = $scm->getPathInfo($request_file, $commit);
|
$request_file_info = $scm->getPathInfo($request_file, $commit);
|
||||||
if (!$request_file_info) {
|
if (!$request_file_info) {
|
||||||
// Redirect to the first branch
|
// Redirect to the main branch
|
||||||
|
$fburl = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
|
||||||
|
array($request->project->shortname,
|
||||||
|
$scm->getMainBranch()));
|
||||||
return new Pluf_HTTP_Response_Redirect($fburl);
|
return new Pluf_HTTP_Response_Redirect($fburl);
|
||||||
}
|
}
|
||||||
$branches = $scm->getBranches();
|
$branches = $scm->getBranches();
|
||||||
@ -277,26 +292,17 @@ class IDF_Views_Source
|
|||||||
return '<span class="breadcrumb">'.implode('<span class="sep">'.$sep.'</span>', $out).'</span>';
|
return '<span class="breadcrumb">'.implode('<span class="sep">'.$sep.'</span>', $out).'</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
public $commit_precond = array('IDF_Precondition::accessSource');
|
public $commit_precond = array('IDF_Precondition::accessSource',
|
||||||
|
'IDF_Views_Source_Precondition::scmAvailable',
|
||||||
|
'IDF_Views_Source_Precondition::revisionValid');
|
||||||
public function commit($request, $match)
|
public function commit($request, $match)
|
||||||
{
|
{
|
||||||
$scm = IDF_Scm::get($request->project);
|
$scm = IDF_Scm::get($request->project);
|
||||||
$commit = $match[2];
|
$commit = $match[2];
|
||||||
if (!$scm->isValidRevision($commit)) {
|
|
||||||
// Redirect to the first branch
|
|
||||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
|
|
||||||
array($request->project->shortname,
|
|
||||||
$scm->getMainBranch()));
|
|
||||||
return new Pluf_HTTP_Response_Redirect($url);
|
|
||||||
}
|
|
||||||
$large = $scm->isCommitLarge($commit);
|
$large = $scm->isCommitLarge($commit);
|
||||||
$cobject = $scm->getCommit($commit, !$large);
|
$cobject = $scm->getCommit($commit, !$large);
|
||||||
if (!$cobject) {
|
if (!$cobject) {
|
||||||
// Redirect to the first branch
|
throw new Exception('could not retrieve commit object for '. $commit);
|
||||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
|
|
||||||
array($request->project->shortname,
|
|
||||||
$scm->getMainBranch()));
|
|
||||||
return new Pluf_HTTP_Response_Redirect($url);
|
|
||||||
}
|
}
|
||||||
$title = sprintf(__('%s Commit Details'), (string) $request->project);
|
$title = sprintf(__('%s Commit Details'), (string) $request->project);
|
||||||
$page_title = sprintf(__('%s Commit Details - %s'), (string) $request->project, $commit);
|
$page_title = sprintf(__('%s Commit Details - %s'), (string) $request->project, $commit);
|
||||||
@ -326,19 +332,17 @@ class IDF_Views_Source
|
|||||||
$request);
|
$request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public $downloadDiff_precond = array('IDF_Precondition::accessSource');
|
public $downloadDiff_precond = array('IDF_Precondition::accessSource',
|
||||||
|
'IDF_Views_Source_Precondition::scmAvailable',
|
||||||
|
'IDF_Views_Source_Precondition::revisionValid');
|
||||||
public function downloadDiff($request, $match)
|
public function downloadDiff($request, $match)
|
||||||
{
|
{
|
||||||
$scm = IDF_Scm::get($request->project);
|
$scm = IDF_Scm::get($request->project);
|
||||||
$commit = $match[2];
|
$commit = $match[2];
|
||||||
if (!$scm->isValidRevision($commit)) {
|
|
||||||
// Redirect to the first branch
|
|
||||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
|
|
||||||
array($request->project->shortname,
|
|
||||||
$scm->getMainBranch()));
|
|
||||||
return new Pluf_HTTP_Response_Redirect($url);
|
|
||||||
}
|
|
||||||
$cobject = $scm->getCommit($commit, true);
|
$cobject = $scm->getCommit($commit, true);
|
||||||
|
if (!$cobject) {
|
||||||
|
throw new Exception('could not retrieve commit object for '. $commit);
|
||||||
|
}
|
||||||
$rep = new Pluf_HTTP_Response($cobject->changes, 'text/plain');
|
$rep = new Pluf_HTTP_Response($cobject->changes, 'text/plain');
|
||||||
$rep->headers['Content-Disposition'] = 'attachment; filename="'.$commit.'.diff"';
|
$rep->headers['Content-Disposition'] = 'attachment; filename="'.$commit.'.diff"';
|
||||||
return $rep;
|
return $rep;
|
||||||
@ -394,19 +398,14 @@ class IDF_Views_Source
|
|||||||
* Get a given file at a given commit.
|
* Get a given file at a given commit.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public $getFile_precond = array('IDF_Precondition::accessSource');
|
public $getFile_precond = array('IDF_Precondition::accessSource',
|
||||||
|
'IDF_Views_Source_Precondition::scmAvailable',
|
||||||
|
'IDF_Views_Source_Precondition::revisionValid');
|
||||||
public function getFile($request, $match)
|
public function getFile($request, $match)
|
||||||
{
|
{
|
||||||
$scm = IDF_Scm::get($request->project);
|
$scm = IDF_Scm::get($request->project);
|
||||||
$commit = $match[2];
|
$commit = $match[2];
|
||||||
$request_file = $match[3];
|
$request_file = $match[3];
|
||||||
if (!$scm->isValidRevision($commit)) {
|
|
||||||
// Redirect to the first branch
|
|
||||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
|
|
||||||
array($request->project->shortname,
|
|
||||||
$scm->getMainBranch()));
|
|
||||||
return new Pluf_HTTP_Response_Redirect($url);
|
|
||||||
}
|
|
||||||
$request_file_info = $scm->getPathInfo($request_file, $commit);
|
$request_file_info = $scm->getPathInfo($request_file, $commit);
|
||||||
if (!$request_file_info or $request_file_info->type == 'tree') {
|
if (!$request_file_info or $request_file_info->type == 'tree') {
|
||||||
// Redirect to the first branch
|
// Redirect to the first branch
|
||||||
@ -427,18 +426,13 @@ class IDF_Views_Source
|
|||||||
* Get a zip archive of the current commit.
|
* Get a zip archive of the current commit.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public $download_precond = array('IDF_Precondition::accessSource');
|
public $download_precond = array('IDF_Precondition::accessSource',
|
||||||
|
'IDF_Views_Source_Precondition::scmAvailable',
|
||||||
|
'IDF_Views_Source_Precondition::revisionValid');
|
||||||
public function download($request, $match)
|
public function download($request, $match)
|
||||||
{
|
{
|
||||||
$commit = trim($match[2]);
|
$commit = trim($match[2]);
|
||||||
$scm = IDF_Scm::get($request->project);
|
$scm = IDF_Scm::get($request->project);
|
||||||
if (!$scm->isValidRevision($commit)) {
|
|
||||||
// Redirect to the first branch
|
|
||||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
|
|
||||||
array($request->project->shortname,
|
|
||||||
$scm->getMainBranch()));
|
|
||||||
return new Pluf_HTTP_Response_Redirect($url);
|
|
||||||
}
|
|
||||||
$base = $request->project->shortname.'-'.$commit;
|
$base = $request->project->shortname.'-'.$commit;
|
||||||
$cmd = $scm->getArchiveCommand($commit, $base.'/');
|
$cmd = $scm->getArchiveCommand($commit, $base.'/');
|
||||||
$rep = new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
|
$rep = new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
|
||||||
@ -447,7 +441,6 @@ class IDF_Views_Source
|
|||||||
return $rep;
|
return $rep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the mime type of a requested file.
|
* Find the mime type of a requested file.
|
||||||
*
|
*
|
||||||
@ -495,7 +488,6 @@ class IDF_Views_Source
|
|||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the mime type of a file.
|
* Find the mime type of a file.
|
||||||
*
|
*
|
||||||
@ -609,4 +601,3 @@ function IDF_Views_Source_ShortenString($string, $length)
|
|||||||
return substr($string, 0, $preflen).$ellipse.
|
return substr($string, 0, $preflen).$ellipse.
|
||||||
substr($string, -($length - $preflen - mb_strlen($ellipse)));
|
substr($string, -($length - $preflen - mb_strlen($ellipse)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
74
src/IDF/Views/Source/Precondition.php
Normal file
74
src/IDF/Views/Source/Precondition.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||||
|
/*
|
||||||
|
# ***** BEGIN LICENSE BLOCK *****
|
||||||
|
# This file is part of InDefero, an open source project management application.
|
||||||
|
# Copyright (C) 2010 Céondo Ltd and contributors.
|
||||||
|
#
|
||||||
|
# InDefero is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# InDefero is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
#
|
||||||
|
# ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
class IDF_Views_Source_Precondition
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Ensures that the configured SCM for the project is available
|
||||||
|
*
|
||||||
|
* @param $request
|
||||||
|
* @return true | Pluf_HTTP_Response_Redirect
|
||||||
|
*/
|
||||||
|
static public function scmAvailable($request)
|
||||||
|
{
|
||||||
|
$scm = IDF_Scm::get($request->project);
|
||||||
|
if (!$scm->isAvailable()) {
|
||||||
|
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help',
|
||||||
|
array($request->project->shortname));
|
||||||
|
return new Pluf_HTTP_Response_Redirect($url);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the revision given in the URL path and acts accordingly
|
||||||
|
*
|
||||||
|
* @param $request
|
||||||
|
* @return true | Pluf_HTTP_Response_Redirect
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
static public function revisionValid($request)
|
||||||
|
{
|
||||||
|
list($url_info, $url_matches) = $request->view;
|
||||||
|
list(, $project, $commit) = $url_matches;
|
||||||
|
|
||||||
|
$scm = IDF_Scm::get($request->project);
|
||||||
|
$res = $scm->validateRevision($commit);
|
||||||
|
switch ($res) {
|
||||||
|
case IDF_Scm::REVISION_VALID:
|
||||||
|
return true;
|
||||||
|
case IDF_Scm::REVISION_INVALID:
|
||||||
|
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::invalidRevision',
|
||||||
|
array($request->project->shortname, $commit));
|
||||||
|
return new Pluf_HTTP_Response_Redirect($url);
|
||||||
|
case IDF_Scm::REVISION_AMBIGUOUS:
|
||||||
|
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::disambiguateRevision',
|
||||||
|
array($request->project->shortname,
|
||||||
|
$commit,
|
||||||
|
$url_info['model'].'::'.$url_info['method']));
|
||||||
|
return new Pluf_HTTP_Response_Redirect($url);
|
||||||
|
default:
|
||||||
|
throw new Exception('unknown validation result: '. $res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -73,10 +73,10 @@ $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
|
# Path to the monotone binary (you need mtn 0.99 or newer)
|
||||||
$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', '--norc');
|
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles');
|
||||||
#
|
#
|
||||||
# You can setup monotone for use with indefero in several ways. The
|
# You can setup monotone for use with indefero in several ways. The
|
||||||
# two most-used should be:
|
# two most-used should be:
|
||||||
@ -157,6 +157,19 @@ $cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s';
|
|||||||
#
|
#
|
||||||
$cfg['mtn_db_access'] = 'remote';
|
$cfg['mtn_db_access'] = 'remote';
|
||||||
#
|
#
|
||||||
|
# If true, each access to the database is authenticated with an auto-generated
|
||||||
|
# project key which is stored in the IDF project configuration
|
||||||
|
# ('mtn_client_key_*') and written out to $cfg['tmp_folder']/mtn-client-keys
|
||||||
|
# for its actual use. This key is then configured on the server to have
|
||||||
|
# full read / write access to all functions, while anonymous access can be
|
||||||
|
# completely disabled.
|
||||||
|
# If false, IDF tries to connect anonymously, without authentication, to
|
||||||
|
# the remote monotone server instance. In this case no project-specific
|
||||||
|
# keys are generated and the server must be configured to allow at least
|
||||||
|
# anonymous read access to the main functions.
|
||||||
|
#
|
||||||
|
$cfg['mtn_remote_auth'] = true;
|
||||||
|
#
|
||||||
# If configured, this allows basic control of a running usher process
|
# If configured, this allows basic control of a running usher process
|
||||||
# via the forge administration. The variable must point to the full (writable)
|
# via the forge 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
|
||||||
|
@ -148,6 +148,16 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/source/help/$#',
|
|||||||
'model' => 'IDF_Views_Source',
|
'model' => 'IDF_Views_Source',
|
||||||
'method' => 'help');
|
'method' => 'help');
|
||||||
|
|
||||||
|
$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/invalid/([^/]+)/$#',
|
||||||
|
'base' => $base,
|
||||||
|
'model' => 'IDF_Views_Source',
|
||||||
|
'method' => 'invalidRevision');
|
||||||
|
|
||||||
|
$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/disambiguate/([^/]+)/from/([^/]+)/$#',
|
||||||
|
'base' => $base,
|
||||||
|
'model' => 'IDF_Views_Source',
|
||||||
|
'method' => 'disambiguateRevision');
|
||||||
|
|
||||||
$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/tree/([^/]+)/$#',
|
$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/tree/([^/]+)/$#',
|
||||||
'base' => $base,
|
'base' => $base,
|
||||||
'model' => 'IDF_Views_Source',
|
'model' => 'IDF_Views_Source',
|
||||||
|
@ -970,7 +970,7 @@ msgid ""
|
|||||||
"<ul>\n"
|
"<ul>\n"
|
||||||
"<li>A commit or revision of the current code in the repository from which you started your work.</li>\n"
|
"<li>A commit or revision of the current code in the repository from which you started your work.</li>\n"
|
||||||
"<li>A patch describing your changes with respect to the reference commit.</li>\n"
|
"<li>A patch describing your changes with respect to the reference commit.</li>\n"
|
||||||
"<li><strong>Check your patch to not provide any password or confidential information!</strong></li>\n"
|
"<li><strong>Ensure your patch does not contain any passwords or confidential information!</strong></li>\n"
|
||||||
"</ul>"
|
"</ul>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -970,7 +970,7 @@ msgid ""
|
|||||||
"<ul>\n"
|
"<ul>\n"
|
||||||
"<li>A commit or revision of the current code in the repository from which you started your work.</li>\n"
|
"<li>A commit or revision of the current code in the repository from which you started your work.</li>\n"
|
||||||
"<li>A patch describing your changes with respect to the reference commit.</li>\n"
|
"<li>A patch describing your changes with respect to the reference commit.</li>\n"
|
||||||
"<li><strong>Check your patch to not provide any password or confidential information!</strong></li>\n"
|
"<li><strong>Ensure your patch does not contain any passwords or confidential information!</strong></li>\n"
|
||||||
"</ul>"
|
"</ul>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -1026,7 +1026,7 @@ msgid ""
|
|||||||
"<ul>\n"
|
"<ul>\n"
|
||||||
"<li>A commit or revision of the current code in the repository from which you started your work.</li>\n"
|
"<li>A commit or revision of the current code in the repository from which you started your work.</li>\n"
|
||||||
"<li>A patch describing your changes with respect to the reference commit.</li>\n"
|
"<li>A patch describing your changes with respect to the reference commit.</li>\n"
|
||||||
"<li><strong>Check your patch to not provide any password or confidential information!</strong></li>\n"
|
"<li><strong>Ensure your patch does not contain any passwords or confidential information!</strong></li>\n"
|
||||||
"</ul>"
|
"</ul>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"<p>Pour démarrer une revue de code vous devez fournir :</p>\n"
|
"<p>Pour démarrer une revue de code vous devez fournir :</p>\n"
|
||||||
|
@ -1019,7 +1019,7 @@ msgid ""
|
|||||||
"<ul>\n"
|
"<ul>\n"
|
||||||
"<li>A commit or revision of the current code in the repository from which you started your work.</li>\n"
|
"<li>A commit or revision of the current code in the repository from which you started your work.</li>\n"
|
||||||
"<li>A patch describing your changes with respect to the reference commit.</li>\n"
|
"<li>A patch describing your changes with respect to the reference commit.</li>\n"
|
||||||
"<li><strong>Check your patch to not provide any password or confidential information!</strong></li>\n"
|
"<li><strong>Ensure your patch does not contain any passwords or confidential information!</strong></li>\n"
|
||||||
"</ul>"
|
"</ul>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -997,7 +997,7 @@ msgid ""
|
|||||||
"<ul>\n"
|
"<ul>\n"
|
||||||
"<li>A commit or revision of the current code in the repository from which you started your work.</li>\n"
|
"<li>A commit or revision of the current code in the repository from which you started your work.</li>\n"
|
||||||
"<li>A patch describing your changes with respect to the reference commit.</li>\n"
|
"<li>A patch describing your changes with respect to the reference commit.</li>\n"
|
||||||
"<li><strong>Check your patch to not provide any password or confidential information!</strong></li>\n"
|
"<li><strong>Ensure your patch does not contain any passwords or confidential information!</strong></li>\n"
|
||||||
"</ul>"
|
"</ul>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -88,6 +88,15 @@ Pluf_Signal::connect('gitpostupdate.php::run',
|
|||||||
# monotone synchronization
|
# monotone synchronization
|
||||||
Pluf_Signal::connect('IDF_Project::created',
|
Pluf_Signal::connect('IDF_Project::created',
|
||||||
array('IDF_Plugin_SyncMonotone', 'entry'));
|
array('IDF_Plugin_SyncMonotone', 'entry'));
|
||||||
|
Pluf_Signal::connect('IDF_Project::preDelete',
|
||||||
|
array('IDF_Plugin_SyncMonotone', 'entry'));
|
||||||
|
Pluf_Signal::connect('IDF_Key::postSave',
|
||||||
|
array('IDF_Plugin_SyncMonotone', 'entry'));
|
||||||
|
Pluf_Signal::connect('IDF_Key::preDelete',
|
||||||
|
array('IDF_Plugin_SyncMonotone', 'entry'));
|
||||||
|
Pluf_Signal::connect('phppostpush.php::run',
|
||||||
|
array('IDF_Plugin_SyncMonotone', 'entry'));
|
||||||
|
|
||||||
#
|
#
|
||||||
# -- Processing of the webhook queue --
|
# -- Processing of the webhook queue --
|
||||||
Pluf_Signal::connect('queuecron.php::run',
|
Pluf_Signal::connect('queuecron.php::run',
|
||||||
|
@ -29,6 +29,14 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th><strong>{$form.f.shortdesc.labelTag}:</strong></th>
|
||||||
|
<td>
|
||||||
|
{if $form.f.shortdesc.errors}{$form.f.shortdesc.fieldErrors}{/if}
|
||||||
|
{$form.f.shortdesc|unsafe}<br />
|
||||||
|
<span class="helptext">{$form.f.shortdesc.help_text}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<th><strong>{$form.f.scm.labelTag}:</strong></th>
|
<th><strong>{$form.f.scm.labelTag}:</strong></th>
|
||||||
<td>{if $form.f.scm.errors}{$form.f.scm.fieldErrors}{/if}
|
<td>{if $form.f.scm.errors}{$form.f.scm.fieldErrors}{/if}
|
||||||
{$form.f.scm|unsafe}
|
{$form.f.scm|unsafe}
|
||||||
|
@ -18,6 +18,14 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th><strong>{$form.f.shortdesc.labelTag}:</strong></th>
|
||||||
|
<td>
|
||||||
|
{if $form.f.shortdesc.errors}{$form.f.shortdesc.fieldErrors}{/if}
|
||||||
|
{$form.f.shortdesc|unsafe}<br />
|
||||||
|
<span class="helptext">{$form.f.shortdesc.help_text}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<th><strong>{$form.f.owners.labelTag}:</strong></th>
|
<th><strong>{$form.f.owners.labelTag}:</strong></th>
|
||||||
<td>
|
<td>
|
||||||
{if $form.f.owners.errors}{$form.f.owners.fieldErrors}{/if}
|
{if $form.f.owners.errors}{$form.f.owners.fieldErrors}{/if}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<td>{$server.name}</td>
|
<td>{$server.name}</td>
|
||||||
<td>{$server.status}</td>
|
<td>{$server.status}</td>
|
||||||
<td>
|
<td>
|
||||||
{if preg_match("/ACTIVE|RUNNING|SLEEPING/", $server.status)}
|
{if preg_match("/ACTIVE|WAITING|RUNNING|SLEEPING/", $server.status)}
|
||||||
<a href="{url 'IDF_Views_Admin::usherServerControl', array($server.name, 'stop')}">
|
<a href="{url 'IDF_Views_Admin::usherServerControl', array($server.name, 'stop')}">
|
||||||
{trans 'stop'}</a>
|
{trans 'stop'}</a>
|
||||||
{elseif $server.status == "STOPPED"}
|
{elseif $server.status == "STOPPED"}
|
||||||
|
@ -57,7 +57,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>A commit or revision of the current code in the repository from which you started your work.</li>
|
<li>A commit or revision of the current code in the repository from which you started your work.</li>
|
||||||
<li>A patch describing your changes with respect to the reference commit.</li>
|
<li>A patch describing your changes with respect to the reference commit.</li>
|
||||||
<li><strong>Check your patch to not provide any password or confidential information!</strong></li>
|
<li><strong>Ensure your patch does not contain any passwords or confidential information!</strong></li>
|
||||||
</ul>{/blocktrans}
|
</ul>{/blocktrans}
|
||||||
</div>
|
</div>
|
||||||
{/block}
|
{/block}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{extends "idf/base.html"}
|
{extends "idf/base.html"}
|
||||||
{block tabsource} class="active"{/block}
|
{block tabsource} class="active"{/block}
|
||||||
{block subtabs}
|
{block subtabs}
|
||||||
{if !$inHelp and (in_array($commit, $tree_in) or (in_array($commit, $tags_in)))}{assign $currentCommit = $commit}{else}{assign $currentCommit = $project.getScmRoot()}{/if}
|
{if !$inHelp and !$inError and (in_array($commit, $tree_in) or (in_array($commit, $tags_in)))}{assign $currentCommit = $commit}{else}{assign $currentCommit = $project.getScmRoot()}{/if}
|
||||||
<div id="sub-tabs">
|
<div id="sub-tabs">
|
||||||
<a {if $inSourceTree}class="active" {/if}href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $currentCommit)}">{trans 'Source Tree'}</a> |
|
<a {if $inSourceTree}class="active" {/if}href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $currentCommit)}">{trans 'Source Tree'}</a> |
|
||||||
<a {if $inChangeLog}class="active" {/if}href="{url 'IDF_Views_Source::changeLog', array($project.shortname, $currentCommit)}">{trans 'Change Log'}</a>
|
<a {if $inChangeLog}class="active" {/if}href="{url 'IDF_Views_Source::changeLog', array($project.shortname, $currentCommit)}">{trans 'Change Log'}</a>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
{foreach $changes as $change}
|
{foreach $changes as $change}
|
||||||
{aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $change.scm_id)}
|
{aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $change.scm_id)}
|
||||||
<tr class="log">
|
<tr class="log">
|
||||||
<td><a href="{$url}">{$change.creation_dtime|dateago:"wihtout"}</a></td>
|
<td><a href="{$url}">{$change.creation_dtime|dateago:"without"}</a></td>
|
||||||
<td>{issuetext $change.summary, $request}{if $change.fullmessage}<br /><br />{issuetext $change.fullmessage, $request, true, false, true, true, true}{/if}</td>
|
<td>{issuetext $change.summary, $request}{if $change.fullmessage}<br /><br />{issuetext $change.fullmessage, $request, true, false, true, true, true}{/if}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="extra">
|
<tr class="extra">
|
||||||
|
33
src/IDF/templates/idf/source/disambiguate_revision.html
Normal file
33
src/IDF/templates/idf/source/disambiguate_revision.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{extends "idf/source/base.html"}
|
||||||
|
{block docclass}yui-t2{assign $inError=true}{/block}
|
||||||
|
{block body}
|
||||||
|
|
||||||
|
<p>{blocktrans}The revision identifier <b>{$commit}</b> is ambiguous and can be
|
||||||
|
expanded to multiple valid revisions - please choose one:{/blocktrans}</p>
|
||||||
|
|
||||||
|
<table summary="" class="tree-list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{trans 'Title'}</th>
|
||||||
|
<th>{trans 'Author'}</th>
|
||||||
|
<th>{trans 'Date'}</th>
|
||||||
|
<th>{trans 'Branch'}</th>
|
||||||
|
<th>{trans 'Revision'}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{foreach $revisions as $revision}
|
||||||
|
{aurl 'url', $redirect, array($project.shortname, $revision.commit)}
|
||||||
|
<tr class="log">
|
||||||
|
<td>{$revision.title}</td>
|
||||||
|
<td>{$revision.author}</td>
|
||||||
|
<td>{$revision.date}</td>
|
||||||
|
<td>{$revision.branch}</td>
|
||||||
|
<td><a href="{$url}">{$revision.commit}</a></td>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/foreach}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/block}
|
||||||
|
|
@ -35,7 +35,7 @@
|
|||||||
<td{if $file.type == 'tree'} colspan="4"{/if}><a href="{$url}">{$file.file}</a></td>{else}<td><a href="#" title="{$file.hash}">{$file.file}</a></td>{/if}
|
<td{if $file.type == 'tree'} colspan="4"{/if}><a href="{$url}">{$file.file}</a></td>{else}<td><a href="#" title="{$file.hash}">{$file.file}</a></td>{/if}
|
||||||
{if $file.type == 'blob'}
|
{if $file.type == 'blob'}
|
||||||
{if isset($file.date) and $file.log != '----'}
|
{if isset($file.date) and $file.log != '----'}
|
||||||
<td><span class="smaller">{$file.date|dateago:"wihtout"}</span></td>
|
<td><span class="smaller">{$file.date|dateago:"without"}</span></td>
|
||||||
<td><span class="smaller">{$file.author|strip_tags|trim}{trans ':'} {issuetext $file.log, $request, true, false}</span></td>
|
<td><span class="smaller">{$file.author|strip_tags|trim}{trans ':'} {issuetext $file.log, $request, true, false}</span></td>
|
||||||
{else}<td colspan="2"></td>{/if}
|
{else}<td colspan="2"></td>{/if}
|
||||||
<td>{$file.size|size}</td>{/if}
|
<td>{$file.size|size}</td>{/if}
|
||||||
|
17
src/IDF/templates/idf/source/invalid_revision.html
Normal file
17
src/IDF/templates/idf/source/invalid_revision.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{extends "idf/source/base.html"}
|
||||||
|
{block docclass}yui-t2{assign $inError=true}{/block}
|
||||||
|
{block body}
|
||||||
|
|
||||||
|
<p>{blocktrans}The revision <b>{$commit}</b> is not valid or does not exist
|
||||||
|
in this repository.{/blocktrans}</p>
|
||||||
|
|
||||||
|
{if $isOwner or $isMember}
|
||||||
|
{aurl 'url', 'IDF_Views_Source::help', array($project.shortname)}
|
||||||
|
<p>{blocktrans}If this is a new repository, the reason for this error
|
||||||
|
could be that you have not committed and / or pushed any change so far.
|
||||||
|
In this case please take a look at the <a href="{$url}">Help</a> page
|
||||||
|
how to access your repository.{/blocktrans}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{/block}
|
||||||
|
|
@ -34,7 +34,7 @@
|
|||||||
<td{if $file.type != 'blob'} colspan="4"{/if}><a href="{$url}">{$file.file}</a></td>
|
<td{if $file.type != 'blob'} colspan="4"{/if}><a href="{$url}">{$file.file}</a></td>
|
||||||
{if $file.type == 'blob'}
|
{if $file.type == 'blob'}
|
||||||
{if isset($file.date)}
|
{if isset($file.date)}
|
||||||
<td><span class="smaller">{$file.date|dateago:"wihtout"}</span></td>
|
<td><span class="smaller">{$file.date|dateago:"without"}</span></td>
|
||||||
<td><span class="smaller">{$file.author|strip_tags|trim}{trans ':'} {$file.log}</span></td>
|
<td><span class="smaller">{$file.author|strip_tags|trim}{trans ':'} {$file.log}</span></td>
|
||||||
{else}<td colspan="2"></td>{/if}
|
{else}<td colspan="2"></td>{/if}
|
||||||
<td></td>{/if}
|
<td></td>{/if}
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
<td class="fileicon"><img src="{media '/idf/img/'~$file.type~'.png'}" alt="{$file.type}" /></td>
|
<td class="fileicon"><img src="{media '/idf/img/'~$file.type~'.png'}" alt="{$file.type}" /></td>
|
||||||
|
|
||||||
<td><a href="{$url}">{$file.file}</a></td>
|
<td><a href="{$url}">{$file.file}</a></td>
|
||||||
<td><span class="smaller">{$file.date|dateago:"wihtout"}</span></td>
|
<td><span class="smaller">{$file.date|dateago:"without"}</span></td>
|
||||||
<td>{$file.rev}</td>
|
<td>{$file.rev}</td>
|
||||||
<td{if $file.type != 'blob'} colspan="2"{/if}><span class="smaller">{$file.author|strip_tags|trim}{trans ':'} {issuetext $file.log, $request, true, false}</span></td>
|
<td{if $file.type != 'blob'} colspan="2"{/if}><span class="smaller">{$file.author|strip_tags|trim}{trans ':'} {issuetext $file.log, $request, true, false}</span></td>
|
||||||
{if $file.type == 'blob'}
|
{if $file.type == 'blob'}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
{assign $url = 'http://michelf.com/projects/php-markdown/extra/'}
|
{assign $eurl = 'http://michelf.com/projects/php-markdown/extra/'}
|
||||||
|
{assign $burl = 'http://daringfireball.net/projects/markdown/syntax'}
|
||||||
{blocktrans}
|
{blocktrans}
|
||||||
<p><strong>Instructions:</strong></p>
|
<p><strong>Instructions:</strong></p>
|
||||||
<p>The content of the page can use the <a href="{$url}">Markdown syntax</a>.</p>
|
<p>The content of the page can use the <a href="{$burl}">Markdown syntax</a> with the <a href="{$eurl}"><em>Extra</em> extension</a>.</p>
|
||||||
<p>Website addresses are automatically linked and you can link to another page in the documentation using double square brackets like that [[AnotherPage]].</p>
|
<p>Website addresses are automatically linked and you can link to another page in the documentation using double square brackets like that [[AnotherPage]].</p>
|
||||||
|
<p>To directly include a file content from the repository, embrace its path with triple square brackets: [[[path/to/file.txt]]].</p>
|
||||||
{/blocktrans}
|
{/blocktrans}
|
||||||
|
Loading…
Reference in New Issue
Block a user