Merge branch 'develop'

This commit is contained in:
Loïc d'Anterroches 2010-11-17 09:45:22 +01:00
commit deb1ea4d2b
59 changed files with 6036 additions and 803 deletions

115
CONTRIBUTE.mdtext Normal file
View 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.

View File

@ -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/

View File

@ -0,0 +1,20 @@
Copyright (C) 2007-2009 Paul Duncan <pabs@pablotron.org>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,42 @@
ZipStream 0.2.2 README
======================
Please see the file COPYING for licensing and warranty information. The
latest version of this software is available at the following URL:
http://pablotron.org/software/zipstream-php/
Overview
========
A fast and simple streaming zip file downloader for PHP. Here's a
simple example:
# create a new zipstream object
$zip = new ZipStream('example.zip');
# create a file named 'hello.txt'
$zip->add_file('some_image.jpg', 'This is the contents of hello.txt');
# add a file named 'image.jpg' from a local file 'path/to/image.jpg'
$zip->add_file_from_path('some_image.jpg', 'path/to/image.jpg');
# finish the zip stream
$zip->finish();
You can also add comments, modify file timestamps, and customize (or
disable) the HTTP headers. See the class file for details. There are a
couple of additional examples in the initial release announcement at the
following URL:
http://pablotron.org/?cid=1535
Requirements
============
* PHP version 5.1.2 or newer (specifically, the hash_init and
hash_file functions).
About the Author
================
Paul Duncan <pabs@pablotron.org>
http://pablotron.org/

View File

@ -0,0 +1,2 @@
Based on PKZIP appnotes, which are included here.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
<?php
# load zipstream class
require '../zipstream.php';
# get path to current file
$pwd = dirname(__FILE__);
# add some random files
$files = array(
'../extras/zip-appnote-6.3.1-20070411.txt',
'../zipstream.php',
);
# create new zip stream object
$zip = new ZipStream('test.zip', array(
'comment' => 'this is a zip file comment. hello?'
));
# common file options
$file_opt = array(
# file creation time (2 hours ago)
'time' => time() - 2 * 3600,
# file comment
'comment' => 'this is a file comment. hi!',
);
# add files under folder 'asdf'
foreach ($files as $file) {
# build absolute path and get file data
$path = ($file[0] == '/') ? $file : "$pwd/$file";
$data = file_get_contents($path);
# add file to archive
$zip->add_file('asdf/' . basename($file), $data, $file_opt);
}
# add same files again wihtout a folder
foreach ($files as $file) {
# build absolute path and get file data
$path = ($file[0] == '/') ? $file : "$pwd/$file";
$data = file_get_contents($path);
# add file to archive
$zip->add_file(basename($file), $data, $file_opt);
}
# finish archive
$zip->finish();
?>

View File

@ -0,0 +1,580 @@
<?php
##########################################################################
# ZipStream - Streamed, dynamically generated zip archives. #
# by Paul Duncan <pabs@pablotron.org> #
# #
# Copyright (C) 2007-2009 Paul Duncan <pabs@pablotron.org> #
# #
# Permission is hereby granted, free of charge, to any person obtaining #
# a copy of this software and associated documentation files (the #
# "Software"), to deal in the Software without restriction, including #
# without limitation the rights to use, copy, modify, merge, publish, #
# distribute, sublicense, and/or sell copies of the Software, and to #
# permit persons to whom the Software is furnished to do so, subject to #
# the following conditions: #
# #
# The above copyright notice and this permission notice shall be #
# included in all copies or substantial portions of the of the Software. #
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, #
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF #
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR #
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, #
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR #
# OTHER DEALINGS IN THE SOFTWARE. #
##########################################################################
#
# ZipStream - Streamed, dynamically generated zip archives.
# by Paul Duncan <pabs@pablotron.org>
#
# Requirements:
#
# * PHP version 5.1.2 or newer.
#
# Usage:
#
# Streaming zip archives is a simple, three-step process:
#
# 1. Create the zip stream:
#
# $zip = new ZipStream('example.zip');
#
# 2. Add one or more files to the archive:
#
# # add first file
# $data = file_get_contents('some_file.gif');
# $zip->add_file('some_file.gif', $data);
#
# # add second file
# $data = file_get_contents('some_file.gif');
# $zip->add_file('another_file.png', $data);
#
# 3. Finish the zip stream:
#
# $zip->finish();
#
# You can also add an archive comment, add comments to individual files,
# and adjust the timestamp of files. See the API documentation for each
# method below for additional information.
#
# Example:
#
# # create a new zip stream object
# $zip = new ZipStream('some_files.zip');
#
# # list of local files
# $files = array('foo.txt', 'bar.jpg');
#
# # read and add each file to the archive
# foreach ($files as $path)
# $zip->add_file($path, file_get_contents($path));
#
# # write archive footer to stream
# $zip->finish();
#
class ZipStream {
const VERSION = '0.2.2';
var $opt = array(),
$files = array(),
$cdr_ofs = 0,
$need_headers = false,
$ofs = 0;
#
# Create a new ZipStream object.
#
# Parameters:
#
# $name - Name of output file (optional).
# $opt - Hash of archive options (optional, see "Archive Options"
# below).
#
# Archive Options:
#
# comment - Comment for this archive.
# content_type - HTTP Content-Type. Defaults to 'application/x-zip'.
# content_disposition - HTTP Content-Disposition. Defaults to
# 'attachment; filename=\"FILENAME\"', where
# FILENAME is the specified filename.
# large_file_size - Size, in bytes, of the largest file to try
# and load into memory (used by
# add_file_from_path()). Large files may also
# be compressed differently; see the
# 'large_file_method' option.
# large_file_method - How to handle large files. Legal values are
# 'store' (the default), or 'deflate'. Store
# sends the file raw and is significantly
# faster, while 'deflate' compresses the file
# and is much, much slower. Note that deflate
# must compress the file twice and extremely
# slow.
# send_http_headers - Boolean indicating whether or not to send
# the HTTP headers for this file.
#
# Note that content_type and content_disposition do nothing if you are
# not sending HTTP headers.
#
# Large File Support:
#
# By default, the method add_file_from_path() will send send files
# larger than 20 megabytes along raw rather than attempting to
# compress them. You can change both the maximum size and the
# compression behavior using the large_file_* options above, with the
# following caveats:
#
# * For "small" files (e.g. files smaller than large_file_size), the
# memory use can be up to twice that of the actual file. In other
# words, adding a 10 megabyte file to the archive could potentially
# occupty 20 megabytes of memory.
#
# * Enabling compression on large files (e.g. files larger than
# large_file_size) is extremely slow, because ZipStream has to pass
# over the large file once to calculate header information, and then
# again to compress and send the actual data.
#
# Examples:
#
# # create a new zip file named 'foo.zip'
# $zip = new ZipStream('foo.zip');
#
# # create a new zip file named 'bar.zip' with a comment
# $zip = new ZipStream('bar.zip', array(
# 'comment' => 'this is a comment for the zip file.',
# ));
#
# Notes:
#
# If you do not set a filename, then this library _DOES NOT_ send HTTP
# headers by default. This behavior is to allow software to send its
# own headers (including the filename), and still use this library.
#
function __construct($name = null, $opt = array()) {
# save options
$this->opt = $opt;
# set large file defaults: size = 20 megabytes, method = store
if (!isset($this->opt['large_file_size']))
$this->opt['large_file_size'] = 20 * 1024 * 1024;
if (!isset($this->opt['large_file_method']))
$this->opt['large_file_method'] = 'store';
$this->output_name = $name;
if ($name || (isset($opt['send_http_headers'])
&& $opt['send_http_headers']))
$this->need_headers = true;
}
#
# add_file - add a file to the archive
#
# Parameters:
#
# $name - path of file in archive (including directory).
# $data - contents of file
# $opt - Hash of options for file (optional, see "File Options"
# below).
#
# File Options:
# time - Last-modified timestamp (seconds since the epoch) of
# this file. Defaults to the current time.
# comment - Comment related to this file.
#
# Examples:
#
# # add a file named 'foo.txt'
# $data = file_get_contents('foo.txt');
# $zip->add_file('foo.txt', $data);
#
# # add a file named 'bar.jpg' with a comment and a last-modified
# # time of two hours ago
# $data = file_get_contents('bar.jpg');
# $zip->add_file('bar.jpg', $data, array(
# 'time' => time() - 2 * 3600,
# 'comment' => 'this is a comment about bar.jpg',
# ));
#
function add_file($name, $data, $opt = array()) {
# compress data
$zdata = gzdeflate($data);
# calculate header attributes
$crc = crc32($data);
$zlen = strlen($zdata);
$len = strlen($data);
$meth = 0x08;
# send file header
$this->add_file_header($name, $opt, $meth, $crc, $zlen, $len);
# print data
$this->send($zdata);
}
#
# add_file_from_path - add a file at path to the archive.
#
# Note that large files may be compresed differently than smaller
# files; see the "Large File Support" section above for more
# information.
#
# Parameters:
#
# $name - name of file in archive (including directory path).
# $path - path to file on disk (note: paths should be encoded using
# UNIX-style forward slashes -- e.g '/path/to/some/file').
# $opt - Hash of options for file (optional, see "File Options"
# below).
#
# File Options:
# time - Last-modified timestamp (seconds since the epoch) of
# this file. Defaults to the current time.
# comment - Comment related to this file.
#
# Examples:
#
# # add a file named 'foo.txt' from the local file '/tmp/foo.txt'
# $zip->add_file_from_path('foo.txt', '/tmp/foo.txt');
#
# # add a file named 'bigfile.rar' from the local file
# # '/usr/share/bigfile.rar' with a comment and a last-modified
# # time of two hours ago
# $path = '/usr/share/bigfile.rar';
# $zip->add_file_from_path('bigfile.rar', $path, array(
# 'time' => time() - 2 * 3600,
# 'comment' => 'this is a comment about bar.jpg',
# ));
#
function add_file_from_path($name, $path, $opt = array()) {
if ($this->is_large_file($path)) {
# file is too large to be read into memory; add progressively
$this->add_large_file($name, $path, $opt);
} else {
# file is small enough to read into memory; read file contents and
# handle with add_file()
$data = file_get_contents($path);
$this->add_file($name, $data, $opt);
}
}
#
# finish - Write zip footer to stream.
#
# Example:
#
# # add a list of files to the archive
# $files = array('foo.txt', 'bar.jpg');
# foreach ($files as $path)
# $zip->add_file($path, file_get_contents($path));
#
# # write footer to stream
# $zip->finish();
#
function finish() {
# add trailing cdr record
$this->add_cdr($this->opt);
$this->clear();
}
###################
# PRIVATE METHODS #
###################
#
# Create and send zip header for this file.
#
private function add_file_header($name, $opt, $meth, $crc, $zlen, $len) {
# strip leading slashes from file name
# (fixes bug in windows archive viewer)
$name = preg_replace('/^\\/+/', '', $name);
# calculate name length
$nlen = strlen($name);
# create dos timestamp
$opt['time'] = isset($opt['time']) ? $opt['time'] : time();
$dts = $this->dostime($opt['time']);
# build file header
$fields = array( # (from V.A of APPNOTE.TXT)
array('V', 0x04034b50), # local file header signature
array('v', (6 << 8) + 3), # version needed to extract
array('v', 0x00), # general purpose bit flag
array('v', $meth), # compresion method (deflate or store)
array('V', $dts), # dos timestamp
array('V', $crc), # crc32 of data
array('V', $zlen), # compressed data length
array('V', $len), # uncompressed data length
array('v', $nlen), # filename length
array('v', 0), # extra data len
);
# pack fields and calculate "total" length
$ret = $this->pack_fields($fields);
$cdr_len = strlen($ret) + $nlen + $zlen;
# print header and filename
$this->send($ret . $name);
# add to central directory record and increment offset
$this->add_to_cdr($name, $opt, $meth, $crc, $zlen, $len, $cdr_len);
}
#
# Add a large file from the given path.
#
private function add_large_file($name, $path, $opt = array()) {
$st = stat($path);
$block_size = 1048576; # process in 1 megabyte chunks
$algo = 'crc32b';
# calculate header attributes
$zlen = $len = $st['size'];
$meth_str = $this->opt['large_file_method'];
if ($meth_str == 'store') {
# store method
$meth = 0x00;
$crc = unpack('V', hash_file($algo, $path, true));
$crc = $crc[1];
} elseif ($meth_str == 'deflate') {
# deflate method
$meth = 0x08;
# open file, calculate crc and compressed file length
$fh = fopen($path, 'rb');
$hash_ctx = hash_init($algo);
$zlen = 0;
# read each block, update crc and zlen
while ($data = fgets($fh, $block_size)) {
hash_update($hash_ctx, $data);
$data = gzdeflate($data);
$zlen += strlen($data);
}
# close file and finalize crc
fclose($fh);
$crc = unpack('V', hash_final($hash_ctx, true));
$crc = $crc[1];
} else {
die("unknown large_file_method: $meth_str");
}
# send file header
$this->add_file_header($name, $opt, $meth, $crc, $zlen, $len);
# open input file
$fh = fopen($path, 'rb');
# send file blocks
while ($data = fgets($fh, $block_size)) {
if ($meth_str == 'deflate')
$data = gzdeflate($data);
# send data
$this->send($data);
}
# close input file
fclose($fh);
}
#
# Is this file larger than large_file_size?
#
function is_large_file($path) {
$st = stat($path);
return ($this->opt['large_file_size'] > 0) &&
($st['size'] > $this->opt['large_file_size']);
}
#
# Save file attributes for trailing CDR record.
#
private function add_to_cdr($name, $opt, $meth, $crc, $zlen, $len, $rec_len) {
$this->files[] = array($name, $opt, $meth, $crc, $zlen, $len, $this->ofs);
$this->ofs += $rec_len;
}
#
# Send CDR record for specified file.
#
private function add_cdr_file($args) {
list ($name, $opt, $meth, $crc, $zlen, $len, $ofs) = $args;
# get attributes
$comment = isset($opt['comment']) ? $opt['comment'] : '';
# get dos timestamp
$dts = $this->dostime($opt['time']);
$fields = array( # (from V,F of APPNOTE.TXT)
array('V', 0x02014b50), # central file header signature
array('v', (6 << 8) + 3), # version made by
array('v', (6 << 8) + 3), # version needed to extract
array('v', 0x00), # general purpose bit flag
array('v', $meth), # compresion method (deflate or store)
array('V', $dts), # dos timestamp
array('V', $crc), # crc32 of data
array('V', $zlen), # compressed data length
array('V', $len), # uncompressed data length
array('v', strlen($name)), # filename length
array('v', 0), # extra data len
array('v', strlen($comment)), # file comment length
array('v', 0), # disk number start
array('v', 0), # internal file attributes
array('V', 32), # external file attributes
array('V', $ofs), # relative offset of local header
);
# pack fields, then append name and comment
$ret = $this->pack_fields($fields) . $name . $comment;
$this->send($ret);
# increment cdr offset
$this->cdr_ofs += strlen($ret);
}
#
# Send CDR EOF (Central Directory Record End-of-File) record.
#
private function add_cdr_eof($opt = null) {
$num = count($this->files);
$cdr_len = $this->cdr_ofs;
$cdr_ofs = $this->ofs;
# grab comment (if specified)
$comment = '';
if ($opt && isset($opt['comment']))
$comment = $opt['comment'];
$fields = array( # (from V,F of APPNOTE.TXT)
array('V', 0x06054b50), # end of central file header signature
array('v', 0x00), # this disk number
array('v', 0x00), # number of disk with cdr
array('v', $num), # number of entries in the cdr on this disk
array('v', $num), # number of entries in the cdr
array('V', $cdr_len), # cdr size
array('V', $cdr_ofs), # cdr ofs
array('v', strlen($comment)), # zip file comment length
);
$ret = $this->pack_fields($fields) . $comment;
$this->send($ret);
}
#
# Add CDR (Central Directory Record) footer.
#
private function add_cdr($opt = null) {
foreach ($this->files as $file)
$this->add_cdr_file($file);
$this->add_cdr_eof($opt);
}
#
# Clear all internal variables. Note that the stream object is not
# usable after this.
#
function clear() {
$this->files = array();
$this->ofs = 0;
$this->cdr_ofs = 0;
$this->opt = array();
}
###########################
# PRIVATE UTILITY METHODS #
###########################
#
# Send HTTP headers for this stream.
#
private function send_http_headers() {
# grab options
$opt = $this->opt;
# grab content type from options
$content_type = 'application/x-zip';
if (isset($opt['content_type']))
$content_type = $this->opt['content_type'];
# grab content disposition
$disposition = 'attachment';
if (isset($opt['content_disposition']))
$disposition = $opt['content_disposition'];
if ($this->output_name)
$disposition .= "; filename=\"{$this->output_name}\"";
$headers = array(
'Content-Type' => $content_type,
'Content-Disposition' => $disposition,
'Pragma' => 'public',
'Cache-Control' => 'public, must-revalidate',
'Content-Transfer-Encoding' => 'binary',
);
foreach ($headers as $key => $val)
header("$key: $val");
}
#
# Send string, sending HTTP headers if necessary.
#
private function send($str) {
if ($this->need_headers)
$this->send_http_headers();
$this->need_headers = false;
echo $str;
}
#
# Convert a UNIX timestamp to a DOS timestamp.
#
function dostime($when = 0) {
# get date array for timestamp
$d = getdate($when);
# set lower-bound on dates
if ($d['year'] < 1980) {
$d = array('year' => 1980, 'mon' => 1, 'mday' => 1,
'hours' => 0, 'minutes' => 0, 'seconds' => 0);
}
# remove extra years from 1980
$d['year'] -= 1980;
# return date string
return ($d['year'] << 25) | ($d['mon'] << 21) | ($d['mday'] << 16) |
($d['hours'] << 11) | ($d['minutes'] << 5) | ($d['seconds'] >> 1);
}
#
# Create a format string and argument list for pack(), then call
# pack() and return the result.
#
function pack_fields($fields) {
list ($fmt, $args) = array('', array());
# populate format string and argument list
foreach ($fields as $field) {
$fmt .= $field[0];
$args[] = $field[1];
}
# prepend format string to argument list
array_unshift($args, $fmt);
# build output string from header and compressed data
return call_user_func_array('pack', $args);
}
};
?>

View File

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

224
doc/syncmonotone.mdtext Normal file
View File

@ -0,0 +1,224 @@
# Plugin SyncMonotone by Thomas Keller (me@thomaskeller.biz)
The SyncMonotone plugin allow the direct creation and synchronisation of
monotone repositories with the InDefero database. It has been built to
work together with monotone's "super server" usher, which is used to control
several repositories at once, acts as proxy and single entrance.
## Prerequisites
* a unixoid operating system
* monotone >= 0.99
* for a proxy setup with usher:
* boost headers (for usher compilation)
* a current version of usher
* a daemonizer, like supervise
## Installation of monotone
If you install monotone from a distribution package, ensure you do not
install and / or activate the server component. We just need a plain
client installation which usually consists only of the `mtn` binary and
a few docs.
If you install monotone from source (<http://monotone.ca/downloads.php>),
please follow the `INSTALL` document which comes with the software.
It contains detailed instructions, including all needed dependencies.
## Choose your indefero setup
The monotone plugin can be used in several different ways:
1. One database for everything. This is the easiest setup and of possible
use in case you do not want indefero to manage the access to your project.
Your `idf.php` should look like this:
$ cat idf.php
...
$cfg['mtn_path'] = 'mtn';
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles');
$cfg['mtn_repositories'] = '/home/monotone/all_projects.mtn';
$cfg['mtn_remote_url'] = 'ssh://monotone@my.server.com:~all_projects.mtn';
$cfg['mtn_db_access'] = 'local';
...
Pro:
* easy to setup and to manage
Con:
* you need to give committers SSH access to your machine
* database lock problem: the database from which
indefero reads its data might be locked in case a user
syncs at the very moment via SSH
2. One database for every project. Similar to the above setup, but this
time you use the '%s' placeholder which is replaced with the short name
of the indefero project:
$ cat idf.php
...
$cfg['mtn_path'] = 'mtn';
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles');
$cfg['mtn_repositories'] = '/home/monotone/%s.mtn';
$cfg['mtn_remote_url'] = 'ssh://monotone@my.server.com:~%s.mtn';
$cfg['mtn_db_access'] = 'local';
...
The same pro's and con's apply. Additionally you have to be careful about
not giving people physical read/write access of another project's database.
Furthermore, if you do not want to use `ssh`, but `netsync` transport,
each project's database must be served over a separate port.
3. One database for every project, all managed with usher. This is the
recommended setup for a mid-size forge setup. The remaining part of this
document will describe the process to set this up in detail.
Pro:
* access rights can be granted per project and are automatically
managed by indefero, just like the user's public monotone keys
* no database locking issues
* one public server running on the one well-known port
Con:
* harder to setup
## Installation and configuration of usher
1. Clone usher's monotone repository:
$ mtn clone "mtn://monotone.ca?net.venge.monotone.contrib.usher"
2. Compile usher:
$ autoreconf -i
$ ./configure && make
$ sudo make install
This installs the usher binary in $prefix/bin.
3. Create a new usher user:
$ adduser --system --disabled-login --home /var/lib/usher usher
4. Create the basic usher setup:
$ cd /var/lib/usher
$ mkdir projects logs
$ cat > usher.conf
userpass "admin" "<secret-password>"
adminaddr "127.0.0.1:12345"
logdir "log"
^D
$ chmod 600 usher.conf
Your indefero www user needs later write access to `usher.conf` and
`projects/`. There are two ways of setting this up:
* Make the usher user the web user, for example via Apache's `suexec`
* Use acls, like this:
$ setfacl -m u:www:rw usher.conf
$ setfacl -m d:u:www:rwx projects/
5. Wrap a daemonizer around usher, for example supervise from daemontools
(<http://cr.yp.to/damontools.html>):
$ cat > run
#!/bin/sh
cd /var/lib/usher
exec 2>&1
exec \
setuidgid usher \
usher usher.conf
^D
The service can now be started through supervise:
$ supervise /var/lib/usher
## Configuration of indefero
Based on the above setup, the configuration in `src/IDF/conf/idf.php` should
look like this:
$ cat idf.php
...
$cfg['mtn_path'] = 'mtn';
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles');
$cfg['mtn_repositories'] = '/var/lib/usher/projects/%s/';
$cfg['mtn_remote_url'] = 'mtn://my.server.com/%s';
$cfg['mtn_db_access'] = 'remote';
$cfg['mtn_remote_auth'] = true;
$cfg['mtn_usher_conf'] = '/var/lib/usher/usher.conf';
...
The `%s` placeholders are automatically replaced by the name of the
indefero project. The plugin assumes that every project is separated
by a distinct server name in the monotone URL (hence the use of `/%s`),
so if a user calls
$ mtn sync mtn://my.server.com/project1
then the database / repository of the indefero `project1` is used.
Note that 'mtn_remote_url' is also used as internal URI to query the data
for indefero's source view, so it *must* be a valid host!
Usher also allows the identification of a project repository by hostname,
which would allow an URL template like `mtn://%s.my.server.com`, however
the plugin does not write out the configuration which is needed for this
yet.
For even more advanced setups, usher can also be used to forward sync
requests to other remote servers for load balancing, please consult the
README file for more information.
## Security and remote access
Indefero distinguishs between public and private projects and so does
the monotone plugin.
Public projects can be pulled by everybody and pushed by team members
or additional invited people. Remote command execution is enabled, but
only for read-only commands.
Remote commands can be helpful for a user or a 3rd party tool (like
[mtn-browse](http://mtn-browse.sourceforge.net) or
[guitone](http://guitone.thomaskeller.biz)) to browse the database
contents remotely without having to pull everything in first instance.
Private projects on the other hand can only be synced by team members
or additional invited people. Also noo remote command execution is enabled
by default.
## Notifications
If you have successfully set up your monotone instance, you probably want
to notify 3rd party systems for incoming changes or simply mirror them
somewhere else for backup purposes. The monotone source tree already comes
with [many example scripts and hooks](http://code.monotone.ca/p/monotone/source/tree/h:net.venge.monotone/contrib)
which serve these purposes, after only little additional configuration.
The usher/indefero-controlled setup automatically looks for *.lua files
in a directory called `hooks.d` right under the project's base directory
(configured via $cfg['mtn_repositories']) and this is the ideal place to
put or link these additional lua sources.
## Q&A
### I pushed a branch to my server, but it does not show up in IDF. Whats wrong?
Check if the heads of your branch are not suspended, i.e. do not carry a
`suspend` certificate. This usually hides the branch and all of its history
from monotone's eyes and therefor also from indefero. You can either choose
to "unsuspend" the branch simply by committing and pushing another head or
by letting monotone ignore all suspend certs. For the latter, its usually
enough to add `--ignore-suspend-certs` to the list of options in `$cfg['mtn_opts']`.
### I want to display another default branch when I click the "Source" tab. How can I do that?
Let the forge admin know the new master branch for your project. He is able
to change that quickly. Depending on the backend / server setup this might
also require some changes in the usher configuration, but only if usher
recognizes and proxies your database on a branch name level.

View File

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

View File

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

View File

@ -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'),
@ -97,6 +105,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
array('required' => false, array('required' => false,
'label' => __('Master branch'), 'label' => __('Master branch'),
'initial' => '', 'initial' => '',
'widget_attrs' => array('size' => '35'),
'help_text' => __('This should be a world-wide unique identifier for your project. A reverse DNS notation like "com.my-domain.my-project" is a good idea.'), 'help_text' => __('This should be a world-wide unique identifier for your project. A reverse DNS notation like "com.my-domain.my-project" is a good idea.'),
)); ));
@ -195,7 +204,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
} }
$sql = new Pluf_SQL('vkey=%s AND vdesc=%s', $sql = new Pluf_SQL('vkey=%s AND vdesc=%s',
array("mtn_master_branch", $mtn_master_branch)); array('mtn_master_branch', $mtn_master_branch));
$l = Pluf::factory('IDF_Conf')->getList(array('filter'=>$sql->gen())); $l = Pluf::factory('IDF_Conf')->getList(array('filter'=>$sql->gen()));
if ($l->count() > 0) { if ($l->count() > 0) {
throw new Pluf_Form_Invalid(__( throw new Pluf_Form_Invalid(__(
@ -272,6 +281,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
@ -304,6 +314,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
'labels_download_one_max' => IDF_Form_UploadConf::init_one_max, 'labels_download_one_max' => IDF_Form_UploadConf::init_one_max,
'labels_wiki_predefined' => IDF_Form_WikiConf::init_predefined, 'labels_wiki_predefined' => IDF_Form_WikiConf::init_predefined,
'labels_wiki_one_max' => IDF_Form_WikiConf::init_one_max, 'labels_wiki_one_max' => IDF_Form_WikiConf::init_one_max,
'labels_issue_template' => IDF_Form_IssueTrackingConf::init_template,
'labels_issue_open' => IDF_Form_IssueTrackingConf::init_open, 'labels_issue_open' => IDF_Form_IssueTrackingConf::init_open,
'labels_issue_closed' => IDF_Form_IssueTrackingConf::init_closed, 'labels_issue_closed' => IDF_Form_IssueTrackingConf::init_closed,
'labels_issue_predefined' => IDF_Form_IssueTrackingConf::init_predefined, 'labels_issue_predefined' => IDF_Form_IssueTrackingConf::init_predefined,

View File

@ -37,12 +37,32 @@ class IDF_Form_Admin_ProjectUpdate extends Pluf_Form
{ {
$this->project = $extra['project']; $this->project = $extra['project'];
$members = $this->project->getMembershipData('string'); $members = $this->project->getMembershipData('string');
$conf = $this->project->getConf();
$this->fields['name'] = new Pluf_Form_Field_Varchar( $this->fields['name'] = new Pluf_Form_Field_Varchar(
array('required' => true, array('required' => true,
'label' => __('Name'), 'label' => __('Name'),
'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'),
));
if ($this->project->getConf()->getVal('scm') == 'mtn') {
$this->fields['mtn_master_branch'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Master branch'),
'initial' => $conf->getVal('mtn_master_branch'),
'widget_attrs' => array('size' => '35'),
'help_text' => __('This should be a world-wide unique identifier for your project. A reverse DNS notation like "com.my-domain.my-project" is a good idea.'),
));
}
$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'),
@ -61,6 +81,30 @@ class IDF_Form_Admin_ProjectUpdate extends Pluf_Form
)); ));
} }
public function clean_mtn_master_branch()
{
$mtn_master_branch = mb_strtolower($this->cleaned_data['mtn_master_branch']);
if (!preg_match('/^([\w\d]+([-][\w\d]+)*)(\.[\w\d]+([-][\w\d]+)*)*$/',
$mtn_master_branch)) {
throw new Pluf_Form_Invalid(__(
'The master branch is empty or contains illegal characters, '.
'please use only letters, digits, dashs and dots as separators.'
));
}
$sql = new Pluf_SQL('vkey=%s AND vdesc=%s AND project!=%s',
array('mtn_master_branch', $mtn_master_branch,
(string)$this->project->id));
$l = Pluf::factory('IDF_Conf')->getList(array('filter'=>$sql->gen()));
if ($l->count() > 0) {
throw new Pluf_Form_Invalid(__(
'This master branch is already used. Please select another one.'
));
}
return $mtn_master_branch;
}
public function clean_owners() public function clean_owners()
{ {
return IDF_Form_MembersConf::checkBadLogins($this->cleaned_data['owners']); return IDF_Form_MembersConf::checkBadLogins($this->cleaned_data['owners']);
@ -80,7 +124,15 @@ 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();
$keys = array('mtn_master_branch');
foreach ($keys as $key) {
if (!empty($this->cleaned_data[$key])) {
$this->project->getConf()->setVal($key, $this->cleaned_data[$key]);
}
}
} }
} }

View File

@ -45,6 +45,9 @@ class IDF_Form_IssueCreate extends Pluf_Form
or $this->user->hasPerm('IDF.project-member', $this->project)) { or $this->user->hasPerm('IDF.project-member', $this->project)) {
$this->show_full = true; $this->show_full = true;
} }
$contentTemplate = $this->project->getConf()->getVal(
'labels_issue_template', IDF_Form_IssueTrackingConf::init_template
);
$this->fields['summary'] = new Pluf_Form_Field_Varchar( $this->fields['summary'] = new Pluf_Form_Field_Varchar(
array('required' => true, array('required' => true,
'label' => __('Summary'), 'label' => __('Summary'),
@ -57,7 +60,7 @@ class IDF_Form_IssueCreate extends Pluf_Form
$this->fields['content'] = new Pluf_Form_Field_Varchar( $this->fields['content'] = new Pluf_Form_Field_Varchar(
array('required' => true, array('required' => true,
'label' => __('Description'), 'label' => __('Description'),
'initial' => '', 'initial' => $contentTemplate,
'widget' => 'Pluf_Form_Widget_TextareaInput', 'widget' => 'Pluf_Form_Widget_TextareaInput',
'widget_attrs' => array( 'widget_attrs' => array(
'cols' => 58, 'cols' => 58,
@ -105,14 +108,39 @@ class IDF_Form_IssueCreate extends Pluf_Form
'size' => 15, 'size' => 15,
), ),
)); ));
/*
* get predefined tags for issues from current project
*
* first Type:<...> and Priority:<...> will be used
*
*/
$predefined = preg_split("/[\r\n]+/", $extra['project']->getConf()->getVal(
'labels_issue_predefined'
));
$predefined_type = 'Type:Defect';
foreach ($predefined as $tag) {
if (strpos($tag, 'Type:') === 0) {
$predefined_type = $tag;
break;
}
}
$predefined_priority = 'Priority:Medium';
foreach ($predefined as $tag) {
if (strpos($tag, 'Priority:') === 0) {
$predefined_priority = $tag;
break;
}
}
for ($i=1;$i<7;$i++) { for ($i=1;$i<7;$i++) {
$initial = ''; $initial = '';
switch ($i) { switch ($i) {
case 1: case 1:
$initial = 'Type:Defect'; $initial = $predefined_type;
break; break;
case 2: case 2:
$initial = 'Priority:Medium'; $initial = $predefined_priority;
break; break;
} }
$this->fields['label'.$i] = new Pluf_Form_Field_Varchar( $this->fields['label'.$i] = new Pluf_Form_Field_Varchar(

View File

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

View File

@ -204,7 +204,7 @@ class IDF_Form_UserAccount extends Pluf_Form
return ''; return '';
} }
if (preg_match('#^ssh\-[a-z]{3}\s\S+\s\S+$#', $key)) { if (preg_match('#^ssh\-[a-z]{3}\s\S+==(\s\S+)?$#', $key)) {
$key = str_replace(array("\n", "\r"), '', $key); $key = str_replace(array("\n", "\r"), '', $key);
if (Pluf::f('idf_strong_key_check', false)) { if (Pluf::f('idf_strong_key_check', false)) {

View File

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

View File

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

View File

@ -35,48 +35,66 @@ 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::membershipsUpdated':
$plug->processMembershipsUpdated($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; break;
case 'mtnpostpush.php::run': case 'mtnpostpush.php::run':
$plug->processSyncTimeline($params); $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
*
* The initial right setup happens in processMembershipsUpdated()
* *
* @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;
} }
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
$projecttempl = Pluf::f('mtn_repositories', false); $projecttempl = Pluf::f('mtn_repositories', false);
if ($projecttempl === false) { if ($projecttempl === false) {
throw new IDF_Scm_Exception( throw new IDF_Scm_Exception(
'"mtn_repositories" must be defined in your configuration file.' __('"mtn_repositories" must be defined in your configuration file.')
); );
} }
$usher_config = Pluf::f('mtn_usher_conf', false); $usher_config = Pluf::f('mtn_usher_conf', false);
if (!$usher_config || !is_writable($usher_config)) { if (!$usher_config || !is_writable($usher_config)) {
throw new IDF_Scm_Exception( throw new IDF_Scm_Exception(
'"mtn_usher_conf" does not exist or is not writable.' __('"mtn_usher_conf" does not exist or is not writable.')
); );
} }
$mtnpostpush = realpath(dirname(__FILE__) . "/../../../scripts/mtn-post-push"); $mtnpostpush = realpath(dirname(__FILE__) . '/../../../scripts/mtn-post-push');
if (!file_exists($mtnpostpush)) { if (!file_exists($mtnpostpush)) {
throw new IDF_Scm_Exception(sprintf( throw new IDF_Scm_Exception(sprintf(
__('Could not find mtn-post-push script "%s".'), $mtnpostpush __('Could not find mtn-post-push script "%s".'), $mtnpostpush
@ -101,18 +119,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;
$output = $return = null;
$ll = exec($cmd, $output, $return);
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
@ -126,43 +134,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 generate_key --confdir=%s %s ""',
escapeshellarg($projectpath), escapeshellarg($projectpath),
escapeshellarg($keyname) escapeshellarg($serverkey)
); );
self::_mtn_exec($cmd);
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; //
$output = $return = null; // step 3) create a client key, and save it in IDF
$ll = exec($cmd, $output, $return); //
if ($return != 0) { $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'];
$clientkey_file = $keydir . '/' . $clientkey_name . '.' . $clientkey_hash;
$clientkey_data = file_get_contents($clientkey_file);
$project->getConf()->setVal('mtn_client_key_name', $clientkey_name);
$project->getConf()->setVal('mtn_client_key_hash', $clientkey_hash);
$project->getConf()->setVal('mtn_client_key_data', $clientkey_data);
// add the public client key to the server
$cmd = sprintf('au get_public_key --keydir=%s %s',
escapeshellarg($keydir),
escapeshellarg($clientkey_hash)
);
$clientkey_pubdata = self::_mtn_exec($cmd);
$cmd = sprintf('au put_public_key --db=%s %s',
escapeshellarg($dbfile),
escapeshellarg($clientkey_pubdata)
);
self::_mtn_exec($cmd);
}
// //
// step 3) write monotonerc for access control // step 4) write monotonerc
// FIXME: netsync access control is still missing!
// //
$monotonerc = file_get_contents(dirname(__FILE__) . "/SyncMonotone/monotonerc.tpl"); $monotonerc = file_get_contents(
dirname(__FILE__).'/SyncMonotone/'.$monotonerc_tpl
);
$monotonerc = str_replace( $monotonerc = str_replace(
array("%%MTNPOSTPUSH%%", "%%PROJECT%%"), array('%%MTNPOSTPUSH%%', '%%PROJECT%%', '%%MTNCLIENTKEY%%'),
array($mtnpostpush, $shortname), array($mtnpostpush, $shortname, $clientkey_hash),
$monotonerc $monotonerc
); );
$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();
@ -177,13 +233,10 @@ class IDF_Plugin_SyncMonotone
} }
// 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"'),
@ -197,7 +250,9 @@ class IDF_Plugin_SyncMonotone
array('key' => 'server', 'values' => array($shortname)), array('key' => 'server', 'values' => array($shortname)),
array('key' => 'local', 'values' => array( array('key' => 'local', 'values' => array(
'--confdir', $projectpath, '--confdir', $projectpath,
'-d', $dbfile '-d', $dbfile,
'--timestamps',
'--ticker=dot'
)), )),
); );
@ -206,44 +261,478 @@ class IDF_Plugin_SyncMonotone
// 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();
} }
/**
* Updates the read / write permissions for the monotone database
*
* @param IDF_Project
*/
public function processMembershipsUpdated($project)
{
if ($project->getConf()->getVal('scm') != 'mtn') {
return;
}
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
$mtn = IDF_Scm_Monotone::factory($project);
$stdio = $mtn->getStdio();
$projectpath = self::_get_project_path($project);
$auth_ids = self::_get_authorized_user_ids($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;
}
if (Pluf::f('mtn_db_access', 'local') == 'local') {
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.')
);
}
$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;
}
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
$conf = new IDF_Conf();
$conf->setProject($project);
$scm = $conf->getVal('scm', 'mtn');
if ($scm != 'mtn')
continue;
$projectpath = self::_get_project_path($project);
$auth_ids = self::_get_authorized_user_ids($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;
for ($i=0; $i<count($parsed_read_perms); ++$i) {
foreach ($parsed_read_perms[$i] as $stanza_line) {
if ($stanza_line['key'] == 'pattern' &&
$stanza_line['values'][0] == '*') {
$wildcard_section =& $parsed_read_perms[$i];
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, -1, PREG_SPLIT_NO_EMPTY);
if (!in_array('*', $lines) && !in_array($mtn_key_id, $lines)) {
$lines[] = $mtn_key_id;
}
if (file_put_contents($projectpath.'/write-permissions',
implode("\n", $lines) . "\n", 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;
}
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
$conf = new IDF_Conf();
$conf->setProject($project);
$scm = $conf->getVal('scm', 'mtn');
if ($scm != 'mtn')
continue;
$projectpath = self::_get_project_path($project);
$auth_ids = self::_get_authorized_user_ids($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) {
$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
for ($h=0; $h<count($parsed_read_perms); ++$h) {
for ($i=0; $i<count($parsed_read_perms[$h]); ++$i) {
if ($parsed_read_perms[$h][$i]['key'] == 'allow' &&
$parsed_read_perms[$h][$i]['values'][0] == $mtn_key_id) {
unset($parsed_read_perms[$h][$i]);
continue;
}
}
}
$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, -1, PREG_SPLIT_NO_EMPTY);
for ($i=0; $i<count($lines); ++$i) {
if ($lines[$i] == $mtn_key_id) {
unset($lines[$i]);
// the key should actually only exist once in the
// file, but we're paranoid
continue;
}
}
if (file_put_contents($projectpath.'/write-permissions',
implode("\n", $lines) . "\n", 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;
}
}
}
/** /**
* Update the timeline after a push * Update the timeline after a push
* *
*/ */
public function processSyncTimeline($params) public function processSyncTimeline($project_name)
{ {
$pname = $params['project'];
try { try {
$project = IDF_Project::getOr404($pname); $project = IDF_Project::getOr404($project_name);
} catch (Pluf_HTTP_Error404 $e) { } catch (Pluf_HTTP_Error404 $e) {
Pluf_Log::event(array( Pluf_Log::event(array(
'IDF_Plugin_SyncMonotone::processSyncTimeline', 'IDF_Plugin_SyncMonotone::processSyncTimeline',
'Project not found.', 'Project not found.',
array($pname, $params) array($project_name, $params)
)); ));
return false; // Project not found return false; // Project not found
} }
Pluf_Log::debug(array( Pluf_Log::debug(array(
'IDF_Plugin_SyncMonotone::processSyncTimeline', 'IDF_Plugin_SyncMonotone::processSyncTimeline',
'Project found', $pname, $project->id 'Project found', $project_name, $project->id
)); ));
IDF_Scm::syncTimeline($project, true); IDF_Scm::syncTimeline($project, true);
Pluf_Log::event(array( Pluf_Log::event(array(
'IDF_Plugin_SyncMonotone::processSyncTimeline', 'IDF_Plugin_SyncMonotone::processSyncTimeline',
'sync', array($pname, $project->id) 'sync', array($project_name, $project->id)
)); ));
} }
private static function _get_authorized_user_ids($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;
}
private static function _get_project_path($project)
{
$projecttempl = Pluf::f('mtn_repositories', false);
if ($projecttempl === false) {
throw new IDF_Scm_Exception(
__('"mtn_repositories" must be defined in your configuration file.')
);
}
$projectpath = sprintf($projecttempl, $project->shortname);
if (!file_exists($projectpath)) {
throw new IDF_Scm_Exception(sprintf(
__('The project path %s does not exists.'), $projectpath
));
}
return $projectpath;
}
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;
}
}
} }

View File

@ -0,0 +1,79 @@
-- ***** 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 = {}
push_hook_functions({
["start"] = function (session_id)
_idf_revs[session_id] = {}
return "continue",nil
end,
["revision_received"] = function (new_id, revision, certs, session_id)
table.insert(_idf_revs[session_id], new_id)
return "continue",nil
end,
["end"] = function (session_id, ...)
if table.getn(_idf_revs[session_id]) == 0 then
return "continue",nil
end
local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%");
if pid == -1 then
print("could not execute %%MTNPOSTPUSH%%")
return
end
for _,r in ipairs(_idf_revs[session_id]) do
pin:write(r .. "\n")
end
pin:close()
wait(pid)
return "continue",nil
end
})
--
-- Load local hooks if they exist.
--
-- The way this is supposed to work is that hooks.d can contain symbolic
-- links to lua scripts. These links MUST have the extension .lua
-- If the script needs some configuration, a corresponding file with
-- the extension .conf is the right spot.
--
-- First load the configuration of the hooks, if applicable
includedirpattern(get_confdir() .. "/hooks.d/", "*.conf")
-- Then load the hooks themselves
includedirpattern(get_confdir() .. "/hooks.d/", "*.lua")

View File

@ -1,6 +1,6 @@
-- ***** 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
@ -20,6 +20,7 @@
-- --
-- controls the access rights for remote_stdio which is used by IDFs frontend -- 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) function get_remote_automate_permitted(key_identity, command, options)
local read_only_commands = { local read_only_commands = {
@ -30,7 +31,8 @@ function get_remote_automate_permitted(key_identity, command, options)
"leaves", "ancestry_difference", "toposort", "erase_ancestors", "leaves", "ancestry_difference", "toposort", "erase_ancestors",
"descendents", "ancestors", "heads", "get_file_of", "get_file", "descendents", "ancestors", "heads", "get_file_of", "get_file",
"interface_version", "get_attributes", "content_diff", "interface_version", "get_attributes", "content_diff",
"file_merge", "show_conflicts", "certs", "keys" "file_merge", "show_conflicts", "certs", "keys", "get_file_size",
"get_extended_manifest_of"
} }
for _,v in ipairs(read_only_commands) do for _,v in ipairs(read_only_commands) do
@ -42,23 +44,27 @@ function get_remote_automate_permitted(key_identity, command, options)
return false return false
end end
--
-- let IDF know of new arriving revisions to fill its timeline
--
_idf_revs = {} _idf_revs = {}
function note_netsync_start(session_id) push_hook_functions({
["start"] = function (session_id)
_idf_revs[session_id] = {} _idf_revs[session_id] = {}
end return "continue",nil
end,
function note_netsync_revision_received(new_id, revision, certs, session_id) ["revision_received"] = function (new_id, revision, certs, session_id)
table.insert(_idf_revs[session_id], new_id) table.insert(_idf_revs[session_id], new_id)
end return "continue",nil
end,
function note_netsync_end (session_id, ...) ["end"] = function (session_id, ...)
if table.getn(_idf_revs[session_id]) == 0 then if table.getn(_idf_revs[session_id]) == 0 then
return return "continue",nil
end end
local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%"); local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%");
if pid == -1 then if pid == -1 then
print("could execute %%MTNPOSTPUSH%%") print("could not execute %%MTNPOSTPUSH%%")
return return
end end
@ -68,4 +74,19 @@ function note_netsync_end (session_id, ...)
pin:close() pin:close()
wait(pid) wait(pid)
return "continue",nil
end end
})
--
-- Load local hooks if they exist.
--
-- The way this is supposed to work is that hooks.d can contain symbolic
-- links to lua scripts. These links MUST have the extension .lua
-- If the script needs some configuration, a corresponding file with
-- the extension .conf is the right spot.
--
-- First load the configuration of the hooks, if applicable
includedirpattern(get_confdir() .. "/hooks.d/", "*.conf")
-- Then load the hooks themselves
includedirpattern(get_confdir() .. "/hooks.d/", "*.lua")

View File

@ -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();
} }
@ -378,13 +393,13 @@ class IDF_Scm
} }
/** /**
* Generate the command to create a zip archive at a given commit. * Generate a zip archive at a given commit, wrapped in a HTTP response, suitable for pushing to client.
* *
* @param string Commit * @param string Commit
* @param string Prefix ('repository/') * @param string Prefix ('repository/')
* @return string Command * @return Pluf_HTTP_Response The HTTP Response containing the zip archive
*/ */
public function getArchiveCommand($commit, $prefix='repository/') public function getArchiveStream($commit, $prefix='repository/')
{ {
throw new Pluf_Exception_NotImplemented(); throw new Pluf_Exception_NotImplemented();
} }

View File

@ -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;
} }
/** /**
@ -434,6 +436,8 @@ class IDF_Scm_Git extends IDF_Scm
$out = self::parseLog($out); $out = self::parseLog($out);
$out[0]->changes = ''; $out[0]->changes = '';
} }
$out[0]['branch'] = $this->inBranches($commit, null);
return $out[0]; return $out[0];
} }
@ -545,13 +549,14 @@ class IDF_Scm_Git extends IDF_Scm
return $res; return $res;
} }
public function getArchiveCommand($commit, $prefix='repository/') public function getArchiveStream($commit, $prefix='repository/')
{ {
return sprintf(Pluf::f('idf_exec_cmd_prefix', ''). $cmd = sprintf(Pluf::f('idf_exec_cmd_prefix', '').
'GIT_DIR=%s '.Pluf::f('git_path', 'git').' archive --format=zip --prefix=%s %s', 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' archive --format=zip --prefix=%s %s',
escapeshellarg($this->repo), escapeshellarg($this->repo),
escapeshellarg($prefix), escapeshellarg($prefix),
escapeshellarg($commit)); escapeshellarg($commit));
return new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
} }
/* /*

View File

@ -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;
} }
/** /**
@ -424,6 +429,8 @@ class IDF_Scm_Mercurial extends IDF_Scm
$c['author'] = $match[2]; $c['author'] = $match[2];
} elseif ($match[1] == 'summary') { } elseif ($match[1] == 'summary') {
$c['title'] = $match[2]; $c['title'] = $match[2];
} elseif ($match[1] == 'branch') {
$c['branch'] = $match[2];
} else { } else {
$c[$match[1]] = trim($match[2]); $c[$match[1]] = trim($match[2]);
} }
@ -438,23 +445,25 @@ class IDF_Scm_Mercurial extends IDF_Scm
} }
} }
$c['tree'] = !empty($c['commit']) ? trim($c['commit']) : ''; $c['tree'] = !empty($c['commit']) ? trim($c['commit']) : '';
$c['branch'] = empty($c['branch']) ? 'default' : $c['branch'];
$c['full_message'] = !empty($c['full_message']) ? trim($c['full_message']) : ''; $c['full_message'] = !empty($c['full_message']) ? trim($c['full_message']) : '';
$res[] = (object) $c; $res[] = (object) $c;
return $res; return $res;
} }
/** /**
* Generate the command to create a zip archive at a given commit. * Generate a zip archive at a given commit.
* *
* @param string Commit * @param string Commit
* @param string Prefix ('git-repo-dump') * @param string Prefix ('git-repo-dump')
* @return string Command * @return Pluf_HTTP_Response The HTTP response containing the zip archive
*/ */
public function getArchiveCommand($commit, $prefix='') protected function getArchiveStream($commit, $prefix='')
{ {
return sprintf(Pluf::f('idf_exec_cmd_prefix', ''). $cmd = sprintf(Pluf::f('idf_exec_cmd_prefix', '').
Pluf::f('hg_path', 'hg').' archive --type=zip -R %s -r %s -', Pluf::f('hg_path', 'hg').' archive --type=zip -R %s -r %s -',
escapeshellarg($this->repo), escapeshellarg($this->repo),
escapeshellarg($commit)); escapeshellarg($commit));
return new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
} }
} }

View File

@ -21,8 +21,8 @@
# #
# ***** END LICENSE BLOCK ***** */ # ***** END LICENSE BLOCK ***** */
require_once(dirname(__FILE__) . "/Monotone/Stdio.php"); //require_once(dirname(__FILE__) . "/Monotone/Stdio.php");
require_once(dirname(__FILE__) . "/Monotone/BasicIO.php"); //require_once(dirname(__FILE__) . "/Monotone/BasicIO.php");
/** /**
* Monotone scm class * Monotone scm class
@ -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()
*/ */
@ -125,6 +135,20 @@ class IDF_Scm_Monotone extends IDF_Scm
return $branch; return $branch;
} }
/**
* @see IDF_Scm::getArchiveStream
*/
public function getArchiveStream($commit, $prefix='repository/')
{
$revs = $this->_resolveSelector($commit);
// sanity: this should actually not happen, because the
// revision is validated before already
if (count($revs) == 0) {
return new Pluf_HTTP_Response_NotFound();
}
return new IDF_Scm_Monotone_ZipRender($this->stdio, $revs[0]);
}
/** /**
* expands a selector or a partial revision id to zero, one or * expands a selector or a partial revision id to zero, one or
* multiple 40 byte revision ids * multiple 40 byte revision ids
@ -147,9 +171,11 @@ class IDF_Scm_Monotone extends IDF_Scm
*/ */
private function _getCerts($rev) private function _getCerts($rev)
{ {
static $certCache = array(); $cache = Pluf_Cache::factory();
$cachekey = 'mtn-plugin-certs-for-rev-' . $rev;
$certs = $cache->get($cachekey);
if (!array_key_exists($rev, $certCache)) { if ($certs === null) {
$out = $this->stdio->exec(array('certs', $rev)); $out = $this->stdio->exec(array('certs', $rev));
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out); $stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
@ -173,10 +199,10 @@ class IDF_Scm_Monotone extends IDF_Scm
} }
} }
} }
$certCache[$rev] = $certs; $cache->set($cachekey, $certs);
} }
return $certCache[$rev]; return $certs;
} }
/** /**
@ -202,34 +228,6 @@ class IDF_Scm_Monotone extends IDF_Scm
return array_unique($certValues); return array_unique($certValues);
} }
/**
* Returns the revision in which the file has been last changed,
* starting from the start rev
*
* @param string
* @param string
* @return string
*/
private function _getLastChangeFor($file, $startrev)
{
$out = $this->stdio->exec(array(
'get_content_changed', $startrev, $file
));
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
// FIXME: we only care about the first returned content mark
// everything else seem to be very, very rare cases
foreach ($stanzas as $stanza) {
foreach ($stanza as $stanzaline) {
if ($stanzaline['key'] == 'content_mark') {
return $stanzaline['hash'];
}
}
}
return null;
}
/** /**
* @see IDF_Scm::inBranches() * @see IDF_Scm::inBranches()
*/ */
@ -286,6 +284,84 @@ class IDF_Scm_Monotone extends IDF_Scm
return $this->_getUniqueCertValuesFor($revs, 'tag', 't:'); return $this->_getUniqueCertValuesFor($revs, 'tag', 't:');
} }
/**
* Takes a single stanza coming from an extended manifest output
* and converts it into a file structure used by IDF
*
* @param string $forceBasedir If given then the element's path is checked
* to be directly beneath the given directory.
* If not, null is returned and the parsing is
* aborted.
* @return array | null
*/
private function _fillFileEntry(array $manifestEntry, $forceBasedir = null)
{
$fullpath = $manifestEntry[0]['values'][0];
$filename = basename($fullpath);
$dirname = dirname($fullpath);
$dirname = $dirname == '.' ? '' : $dirname;
if ($forceBasedir !== null && $forceBasedir != $dirname) {
return null;
}
$file = array();
$file['file'] = $filename;
$file['fullpath'] = $fullpath;
$file['efullpath'] = self::smartEncode($fullpath);
$wanted_mark = '';
if ($manifestEntry[0]['key'] == 'dir') {
$file['type'] = 'tree';
$file['size'] = 0;
$wanted_mark = 'path_mark';
}
else {
$file['type'] = 'blob';
$file['hash'] = $manifestEntry[1]['hash'];
$size = 0;
foreach ($manifestEntry as $line) {
if ($line['key'] == 'size') {
$size = $line['values'][0];
break;
}
}
$file['size'] = $size;
$wanted_mark = 'content_mark';
}
$rev_mark = null;
foreach ($manifestEntry as $line) {
if ($line['key'] == $wanted_mark) {
$rev_mark = $line['hash'];
break;
}
}
if ($rev_mark !== null) {
$file['rev'] = $rev_mark;
$certs = $this->_getCerts($rev_mark);
// FIXME: this assumes that author, date and changelog are always given
$file['author'] = implode(", ", $certs['author']);
$dates = array();
foreach ($certs['date'] as $date)
$dates[] = date('Y-m-d H:i:s', strtotime($date));
$file['date'] = implode(', ', $dates);
$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];
}
return $file;
}
/** /**
* @see IDF_Scm::getTree() * @see IDF_Scm::getTree()
*/ */
@ -297,52 +373,21 @@ class IDF_Scm_Monotone extends IDF_Scm
} }
$out = $this->stdio->exec(array( $out = $this->stdio->exec(array(
'get_manifest_of', $revs[0] 'get_extended_manifest_of', $revs[0]
)); ));
$files = array(); $files = array();
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out); $stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
$folder = $folder == '/' || empty($folder) ? '' : $folder.'/'; $folder = $folder == '/' || empty($folder) ? '' : $folder;
foreach ($stanzas as $stanza) { foreach ($stanzas as $stanza) {
if ($stanza[0]['key'] == 'format_version') if ($stanza[0]['key'] == 'format_version')
continue; continue;
$path = $stanza[0]['values'][0]; $file = $this->_fillFileEntry($stanza, $folder);
if (!preg_match('#^'.$folder.'([^/]+)$#', $path, $m)) if ($file === null)
continue; continue;
$file = array();
$file['file'] = $m[1];
$file['fullpath'] = $path;
$file['efullpath'] = self::smartEncode($path);
if ($stanza[0]['key'] == 'dir') {
$file['type'] = 'tree';
$file['size'] = 0;
}
else
{
$file['type'] = 'blob';
$file['hash'] = $stanza[1]['hash'];
$file['size'] = strlen($this->getFile((object)$file));
}
$rev = $this->_getLastChangeFor($file['fullpath'], $revs[0]);
if ($rev !== null) {
$file['rev'] = $rev;
$certs = $this->_getCerts($rev);
// FIXME: this assumes that author, date and changelog are always given
$file['author'] = implode(", ", $certs['author']);
$dates = array();
foreach ($certs['date'] as $date)
$dates[] = date('Y-m-d H:i:s', strtotime($date));
$file['date'] = implode(', ', $dates);
$file['log'] = implode("\n---\n", $certs['changelog']);
}
$files[] = (object) $file; $files[] = (object) $file;
} }
return $files; return $files;
@ -382,7 +427,7 @@ class IDF_Scm_Monotone extends IDF_Scm
$certs = $scm->_getCerts($revs[0]); $certs = $scm->_getCerts($revs[0]);
// for the very seldom case that a revision // for the very seldom case that a revision
// has no branch certificate // has no branch certificate
if (count($certs['branch']) == 0) { if (!array_key_exists('branch', $certs)) {
$branch = '*'; $branch = '*';
} }
else else
@ -425,12 +470,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;
} }
/** /**
@ -447,7 +533,7 @@ class IDF_Scm_Monotone extends IDF_Scm
return false; return false;
$out = $this->stdio->exec(array( $out = $this->stdio->exec(array(
'get_manifest_of', $revs[0] 'get_extended_manifest_of', $revs[0]
)); ));
$files = array(); $files = array();
@ -457,43 +543,10 @@ class IDF_Scm_Monotone extends IDF_Scm
if ($stanza[0]['key'] == 'format_version') if ($stanza[0]['key'] == 'format_version')
continue; continue;
$path = $stanza[0]['values'][0]; if ($stanza[0]['values'][0] != $file)
if (!preg_match('#^'.$file.'$#', $path, $m))
continue; continue;
$file = array(); $file = $this->_fillFileEntry($stanza);
$file['fullpath'] = $path;
if ($stanza[0]['key'] == "dir") {
$file['type'] = "tree";
$file['hash'] = null;
$file['size'] = 0;
}
else
{
$file['type'] = 'blob';
$file['hash'] = $stanza[1]['hash'];
$file['size'] = strlen($this->getFile((object)$file));
}
$pathinfo = pathinfo($file['fullpath']);
$file['file'] = $pathinfo['basename'];
$rev = $this->_getLastChangeFor($file['fullpath'], $revs[0]);
if ($rev !== null) {
$file['rev'] = $rev;
$certs = $this->_getCerts($rev);
// FIXME: this assumes that author, date and changelog are always given
$file['author'] = implode(", ", $certs['author']);
$dates = array();
foreach ($certs['date'] as $date)
$dates[] = date('Y-m-d H:i:s', strtotime($date));
$file['date'] = implode(', ', $dates);
$file['log'] = implode("\n---\n", $certs['changelog']);
}
return (object) $file; return (object) $file;
} }
return false; return false;
@ -565,8 +618,12 @@ class IDF_Scm_Monotone extends IDF_Scm
$dates[] = date('Y-m-d H:i:s', strtotime($date)); $dates[] = date('Y-m-d H:i:s', strtotime($date));
$res['date'] = implode(', ', $dates); $res['date'] = implode(', ', $dates);
$res['title'] = implode("\n---\n", $certs['changelog']); $combinedChangelog = implode("\n---\n", $certs['changelog']);
$split = preg_split("/[\n\r]/", $combinedChangelog, 2);
$res['title'] = $split[0];
$res['full_message'] = (isset($split[1])) ? trim($split[1]) : '';
$res['branch'] = implode(', ', $certs['branch']);
$res['commit'] = $revs[0]; $res['commit'] = $revs[0];
$res['changes'] = ($getdiff) ? $this->_getDiff($revs[0]) : ''; $res['changes'] = ($getdiff) ? $this->_getDiff($revs[0]) : '';
@ -622,15 +679,22 @@ class IDF_Scm_Monotone extends IDF_Scm
// read in the initial branches we should follow // read in the initial branches we should follow
if (count($initialBranches) == 0) { if (count($initialBranches) == 0) {
if (!isset($certs['branch'])) {
throw new IDF_Scm_Exception(sprintf(
__("revision %s has no branch cert - cannot start ".
"logging from this revision"), $rev
));
}
$initialBranches = $certs['branch']; $initialBranches = $certs['branch'];
} }
// only add it to our log if it is on one of the initial branches // only add it to our log if it is on one of the initial branches
if (count(array_intersect($initialBranches, $certs['branch'])) > 0) { // ignore revisions without any branch certificate
if (count(array_intersect($initialBranches, (array)@$certs['branch'])) > 0) {
--$n; --$n;
$log = array(); $log = array();
$log['author'] = implode(", ", $certs['author']); $log['author'] = implode(', ', $certs['author']);
$dates = array(); $dates = array();
foreach ($certs['date'] as $date) foreach ($certs['date'] as $date)

View File

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

View File

@ -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);

View File

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

View File

@ -0,0 +1,78 @@
<?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.
#
# Plume Framework is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Plume Framework 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser 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 ***** */
require_once(IDF_PATH.'/../contrib/zipstream-php-0.2.2/zipstream.php');
/**
* Special response object to output
*
* The Content-Length will not be set as it is not possible to predict it.
*
* Note: The ZipArchive version 0.2.2 has been patched in-tree with this
* patch http://pastebin.ca/1977584 to avoid a couple of PHP notices
*
*/
class IDF_Scm_Monotone_ZipRender extends Pluf_HTTP_Response
{
/**
* The revision argument must be a safe string!
*
* @param Object stdio context
* @param string revision
* @param string Mimetype (null)
*/
private $stdio = null;
private $revision = null;
function __construct($stdio, $revision)
{
parent::__construct($revision, 'application/x-zip');
$this->stdio = $stdio;
$this->revision = $revision;
}
/**
* Render a response object.
*/
function render($output_body=true)
{
$this->outputHeaders();
if ($output_body) {
$manifest = $this->stdio->exec(array('get_manifest_of', $this->revision));
$stanzas = IDF_Scm_Monotone_BasicIO::parse($manifest);
$zip = new ZipStream();
foreach ($stanzas as $stanza) {
if ($stanza[0]['key'] != 'file')
continue;
$content = $this->stdio->exec(array('get_file', $stanza[1]['hash']));
$zip->add_file($stanza[0]['values'][0], $content);
}
$zip->finish();
}
}
}

View File

@ -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;
} }
@ -412,6 +415,7 @@ class IDF_Scm_Svn extends IDF_Scm
$res['commit'] = (string) $xml->logentry['revision']; $res['commit'] = (string) $xml->logentry['revision'];
$res['changes'] = ($getdiff) ? $this->getDiff($commit) : ''; $res['changes'] = ($getdiff) ? $this->getDiff($commit) : '';
$res['tree'] = ''; $res['tree'] = '';
$res['branch'] = '';
return (object) $res; return (object) $res;
} }

View File

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

View File

@ -35,11 +35,12 @@ class IDF_Views_Source
* Extension supported by the syntax highlighter. * Extension supported by the syntax highlighter.
*/ */
public static $supportedExtenstions = array( public static $supportedExtenstions = array(
'ascx', 'ashx', 'asmx', 'aspx', 'browser', 'bsh', 'c', 'cc', 'ascx', 'ashx', 'asmx', 'aspx', 'browser', 'bsh', 'c', 'cl', 'cc',
'config', 'cpp', 'cs', 'csh', 'csproj', 'css', 'cv', 'cyc', 'config', 'cpp', 'cs', 'csh', 'csproj', 'css', 'cv', 'cyc', 'el', 'fs',
'html', 'html', 'java', 'js', 'master', 'perl', 'php', 'pl', 'h', 'hh', 'hpp', 'hs', 'html', 'html', 'java', 'js', 'lisp', 'master',
'pm', 'py', 'rb', 'sh', 'sitemap', 'skin', 'sln', 'svc', 'vala', 'pas', 'perl', 'php', 'pl', 'pm', 'py', 'rb', 'scm', 'sh', 'sitemap',
'vb', 'vbproj', 'wsdl', 'xhtml', 'xml', 'xsd', 'xsl', 'xslt'); 'skin', 'sln', 'svc', 'vala', 'vb', 'vbproj', 'vbs', 'wsdl', 'xhtml',
'xml', 'xsd', 'xsl', 'xslt');
/** /**
* Display help on how to checkout etc. * Display help on how to checkout etc.
@ -59,30 +60,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 +138,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 +181,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 +197,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)) {
// 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 +293,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 +333,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 +399,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,27 +427,20 @@ 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.'/'); $rep = $scm->getArchiveStream($commit, $base.'/');
$rep = new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
$rep->headers['Content-Transfer-Encoding'] = 'binary'; $rep->headers['Content-Transfer-Encoding'] = 'binary';
$rep->headers['Content-Disposition'] = 'attachment; filename="'.$base.'.zip"'; $rep->headers['Content-Disposition'] = 'attachment; filename="'.$base.'.zip"';
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.
* *
@ -541,8 +533,10 @@ class IDF_Views_Source
if (0 === strpos($fileinfo[0], 'text/')) { if (0 === strpos($fileinfo[0], 'text/')) {
return true; return true;
} }
$ext = 'mdtext php-dist h gitignore diff patch' $ext = 'mdtext php-dist h gitignore diff patch';
.Pluf::f('idf_extra_text_ext', ''); $extra_ext = trim(Pluf::f('idf_extra_text_ext', ''));
if (!empty($extra_ext))
$ext .= ' ' . $extra_ext;
$ext = array_merge(self::$supportedExtenstions, explode(' ' , $ext)); $ext = array_merge(self::$supportedExtenstions, explode(' ' , $ext));
return (in_array($fileinfo[2], $ext)); return (in_array($fileinfo[2], $ext));
} }
@ -609,4 +603,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)));
} }

View 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);
}
}
}

View File

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

View File

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

View File

@ -88,7 +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('phppostpush.php::run', Pluf_Signal::connect('IDF_Project::membershipsUpdated',
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('mtnpostpush.php::run',
array('IDF_Plugin_SyncMonotone', 'entry')); array('IDF_Plugin_SyncMonotone', 'entry'));
# #

View File

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

View File

@ -1,4 +1,5 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
{* {*
# ***** 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.
@ -29,16 +30,13 @@
<![endif]--> <![endif]-->
{block extraheader}{/block} {block extraheader}{/block}
<title>{block pagetitle}{$page_title|strip_tags}{/block}{if $project} - {$project.shortdesc}{/if}</title> <title>{block pagetitle}{$page_title|strip_tags}{/block}{if $project} - {$project.shortdesc}{/if}</title>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
</head> </head>
<body> <body>
<div id="{block docid}doc3{/block}"> <div id="{block docid}doc3{/block}">
<div id="hd"> <div id="hd">
{if $project}<h1 class="project-title">{$project}</h1>{/if} {if $project}<h1 class="project-title">{$project}</h1>{/if}
<p class="top"><a href="#title" accesskey="2"></a> {include 'idf/main-menu.html'}
{if !$user.isAnonymous()}{aurl 'url', 'idf_dashboard'}{blocktrans}Welcome, <strong><a class="userw" href="{$url}">{$user}</a></strong>.{/blocktrans} <a href="{url 'IDF_Views::logout'}">{trans 'Sign Out'}</a>{else}<a href="{url 'IDF_Views::login'}">{trans 'Sign in or create your account'}</a>{/if}
{if $project} | <a href="{url 'IDF_Views::index'}">{trans 'Project List'}</a>{/if}
| <a href="{url 'IDF_Views::faq'}" title="{trans 'Help and accessibility features'}">{trans 'Help'}</a>
</p>
<div id="header"> <div id="header">
<div id="main-tabs"> <div id="main-tabs">
{if $project} {if $project}
@ -69,7 +67,6 @@
</div> </div>
<div id="ft">{block foot}{/block}</div> <div id="ft">{block foot}{/block}</div>
</div> </div>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
{include 'idf/js-hotkeys.html'} {include 'idf/js-hotkeys.html'}
{block javascript}{/block} {block javascript}{/block}
{if $project} {if $project}

View File

@ -1,4 +1,5 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
{* {*
# ***** 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.
@ -29,15 +30,12 @@
<![endif]--> <![endif]-->
{block extraheader}{/block} {block extraheader}{/block}
<title>{block pagetitle}{$page_title|strip_tags}{/block}</title> <title>{block pagetitle}{$page_title|strip_tags}{/block}</title>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
</head> </head>
<body> <body>
<div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}"> <div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}">
<div id="hd"> <div id="hd">
<p class="top"><a href="#title" accesskey="2"></a> {include 'idf/main-menu.html'}
{if !$user.isAnonymous()}{aurl 'url', 'idf_dashboard'}{blocktrans}Welcome, <strong><a class="userw" href="{$url}">{$user}</a></strong>.{/blocktrans} <a href="{url 'IDF_Views::logout'}">{trans 'Sign Out'}</a>{else}<a href="{url 'IDF_Views::login'}">{trans 'Sign in or create your account'}</a>{/if}
| <a href="{url 'IDF_Views::index'}">{trans 'Project List'}</a> {if $isAdmin}| <a href="{url 'IDF_Views_Admin::projects'}">{trans 'Forge Management'}</a>{/if}
| <a href="{url 'IDF_Views::faq'}" title="{trans 'Help and accessibility features'}">{trans 'Help'}</a>
</p>
<h1 id="title" class="title">{block title}{$page_title}{/block}</h1> <h1 id="title" class="title">{block title}{$page_title}{/block}</h1>
</div> </div>
<div id="bd"> <div id="bd">
@ -53,7 +51,6 @@
</div> </div>
<div id="ft">{block foot}{/block}</div> <div id="ft">{block foot}{/block}</div>
</div> </div>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
{include 'idf/js-hotkeys.html'} {include 'idf/js-hotkeys.html'}
{block javascript}{/block} {block javascript}{/block}
</body> </body>

View File

@ -1,4 +1,5 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
{* {*
# ***** 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.
@ -29,17 +30,13 @@
<![endif]--> <![endif]-->
{block extraheader}{/block} {block extraheader}{/block}
<title>{block pagetitle}{$page_title|strip_tags}{/block}{if $project} - {$project.shortdesc}{/if}</title> <title>{block pagetitle}{$page_title|strip_tags}{/block}{if $project} - {$project.shortdesc}{/if}</title>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
</head> </head>
<body> <body>
<div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}"> <div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}">
<div id="hd"> <div id="hd">
{if $project}<h1 class="project-title">{$project}</h1>{/if} {if $project}<h1 class="project-title">{$project}</h1>{/if}
<p class="top"><a href="#title" accesskey="2"></a> {include 'idf/main-menu.html'}
{if !$user.isAnonymous()}{aurl 'url', 'idf_dashboard'}{blocktrans}Welcome, <strong><a class="userw" href="{$url}">{$user}</a></strong>.{/blocktrans} <a href="{url 'IDF_Views::logout'}">{trans 'Sign Out'}</a>{else}<a href="{url 'IDF_Views::login'}">{trans 'Sign in or create your account'}</a>{/if}
{if $project} | <a href="{url 'IDF_Views::index'}">{trans 'Project List'}</a>{/if}
{if $isAdmin}| <a href="{url 'IDF_Views_Admin::projects'}">{trans 'Forge Management'}</a>{/if}
| <a href="{url 'IDF_Views::faq'}" title="{trans 'Help and accessibility features'}">{trans 'Help'}</a>
</p>
<div id="header"> <div id="header">
<div id="main-tabs"> <div id="main-tabs">
{if $project} {if $project}
@ -71,7 +68,6 @@
</div> </div>
<div id="ft">{block foot}{/block}</div> <div id="ft">{block foot}{/block}</div>
</div> </div>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
{include 'idf/js-hotkeys.html'} {include 'idf/js-hotkeys.html'}
{block javascript}{/block} {block javascript}{/block}
{if $project} {if $project}

View File

@ -1,4 +1,5 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
{* {*
# ***** 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.
@ -29,16 +30,12 @@
<![endif]--> <![endif]-->
{block extraheader}{/block} {block extraheader}{/block}
<title>{block pagetitle}{$page_title|strip_tags}{/block}</title> <title>{block pagetitle}{$page_title|strip_tags}{/block}</title>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
</head> </head>
<body> <body>
<div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}"> <div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}">
<div id="hd"> <div id="hd">
<p class="top"><a href="#title" accesskey="2"></a> {include 'idf/main-menu.html'}
{aurl 'url', 'IDF_Views_User::dashboard'}{blocktrans}Welcome, <strong><a class="userw" href="{$url}">{$user}</a></strong>.{/blocktrans} <a href="{url 'IDF_Views::logout'}">{trans 'Sign Out'}</a>
| <a href="{url 'IDF_Views::index'}">{trans 'Project List'}</a>
| <a href="{url 'IDF_Views_Admin::projects'}">{trans 'Forge Management'}</a>
| <a href="{url 'IDF_Views::faq'}" title="{trans 'Help and accessibility features'}">{trans 'Help'}</a>
</p>
<div id="header"> <div id="header">
<div id="main-tabs"> <div id="main-tabs">
<a href="{url 'IDF_Views_Admin::projects'}"{block tabprojects}{/block}>{trans 'Projects'}</a> <a href="{url 'IDF_Views_Admin::projects'}"{block tabprojects}{/block}>{trans 'Projects'}</a>
@ -64,7 +61,6 @@
</div> </div>
<div id="ft">{block foot}{/block}</div> <div id="ft">{block foot}{/block}</div>
</div> </div>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
{include 'idf/js-hotkeys.html'} {include 'idf/js-hotkeys.html'}
{block javascript}{/block} {block javascript}{/block}
</body> </body>

View File

@ -29,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}

View File

@ -18,6 +18,23 @@
</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>
{if $project.getConf().getVal('scm') == 'mtn'}
<tr class="mtn-form">
<th><strong>{$form.f.mtn_master_branch.labelTag}:</strong></th>
<td>{if $form.f.mtn_master_branch.errors}{$form.f.mtn_master_branch.fieldErrors}{/if}
{$form.f.mtn_master_branch|unsafe}<br />
<span class="helptext">{$form.f.mtn_master_branch.help_text}</span>
</td>
</tr>
{/if}
<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}

View File

@ -1,5 +1,5 @@
{extends "idf/issues/base.html"} {extends "idf/issues/base.html"}
{block docclass}yui-t2{/block} {block docclass}yui-t2{assign $inOpenIssues=true}{/block}
{block body} {block body}
{$issues.render} {$issues.render}
{if !$user.isAnonymous()} {if !$user.isAnonymous()}

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

@ -10,6 +10,9 @@
<th><strong>{trans 'Author:'}</strong></th><td>{showuser $rcommit.get_author(), $request, $cobject.author}</td> <th><strong>{trans 'Author:'}</strong></th><td>{showuser $rcommit.get_author(), $request, $cobject.author}</td>
</tr> </tr>
<tr> <tr>
<th><strong>{trans 'Branch:'}</strong></th><td>{$cobject.branch}</td>
</tr>
<tr>
<th><strong>{trans 'Commit:'}</strong></th><td class="mono"><a href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $commit)}" title="{trans 'View corresponding source tree'}">{$cobject.commit}</a></td> <th><strong>{trans 'Commit:'}</strong></th><td class="mono"><a href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $commit)}" title="{trans 'View corresponding source tree'}">{$cobject.commit}</a></td>
</tr> </tr>
<tr> <tr>

View 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}

View File

@ -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}

View 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}

View File

@ -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}

View File

@ -48,7 +48,7 @@
</table> </table>
{aurl 'url', 'IDF_Views_Source::download', array($project.shortname, $commit)} {aurl 'url', 'IDF_Views_Source::download', array($project.shortname, $commit)}
<p class="right soft"> <p class="right soft">
{* <a href="{$url}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/package-grey.png'}" alt="{trans 'Archive'}" align="bottom" /></a> <a href="{$url}">{trans 'Download this version'}</a> {trans 'or'} *} <a href="{$url}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/package-grey.png'}" alt="{trans 'Archive'}" align="bottom" /></a> <a href="{$url}">{trans 'Download this version'}</a> {trans 'or'}
<kbd>mtn clone {$project.getSourceAccessUrl($user, $commit)}</kbd> <a href="{url 'IDF_Views_Source::help', array($project.shortname)}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/help.png'}" alt="{trans 'Help'}" /></a> <kbd>mtn clone {$project.getSourceAccessUrl($user, $commit)}</kbd> <a href="{url 'IDF_Views_Source::help', array($project.shortname)}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/help.png'}" alt="{trans 'Help'}" /></a>
</p> </p>

View File

@ -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'}

View File

@ -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}

View File

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