Merge branch 'develop'
This commit is contained in:
commit
deb1ea4d2b
115
CONTRIBUTE.mdtext
Normal file
115
CONTRIBUTE.mdtext
Normal file
@ -0,0 +1,115 @@
|
||||
[Indefero][idf] is not only a software you can use either hosted for
|
||||
you or hosted by you, but also a free software you can contribute to.
|
||||
|
||||
Here you will get how to contribute and what to contribute.
|
||||
|
||||
[idf]: http://www.indefero.net
|
||||
|
||||
# Quick Way on How to Contribute
|
||||
|
||||
Simple contribution:
|
||||
|
||||
1. Open a ticket with your idea. You can directly propose a patch if
|
||||
you have it.
|
||||
|
||||
2. Wait for it to be checked by the devs or meet us on the #indefero
|
||||
channel on [FreeNode][freenode].
|
||||
|
||||
Bigger contribution:
|
||||
|
||||
1. Fork Indefero where you want (fork from the develop branch).
|
||||
|
||||
2. Code your change and document it.
|
||||
|
||||
3. Open a ticket with a pull request and talk about it on IRC.
|
||||
|
||||
# The General Contribution Workflow for Regular Contributors
|
||||
|
||||
1. Fork Indefero from the **develop** branch.
|
||||
2. Request a pull request if you do not have write access on the repository.
|
||||
3. Merge your changes without fast forward in develop. This keeps track of
|
||||
the history of the changes and makes understanding what is going on easy.
|
||||
4. Merge your changes with fast forward **only if a single commit**.
|
||||
|
||||
Indefero is composed of two main branches:
|
||||
|
||||
1. **master**: this is the shipped branch, only a select number of people
|
||||
can push into it.
|
||||
2. **develop**: this is the development branch, all the people having write
|
||||
access to the repository are welcomed to push in.
|
||||
|
||||
**Note:** The branching model we use is [explained in details here][bmi]. You
|
||||
**must** understand it to really contribute to the code base in an
|
||||
efficient way.
|
||||
|
||||
[bmi]: http://nvie.com/git-model "A successful Git branching model"
|
||||
|
||||
# What to Contribute
|
||||
|
||||
Contribution is easy, you can contribute in a lot of different fields,
|
||||
contributions small or big are always appreciated. Here is an example
|
||||
list of what you can do:
|
||||
|
||||
- Install InDefero on your system and report the problem you had.
|
||||
- Find the bad English and typos and propose corrections.
|
||||
- Help with the translation effort.
|
||||
- Find little bugs or usability problems and provide ideas on how to fix them.
|
||||
- Register to the [discussion group][group] and help new users.
|
||||
- Come and chat on IRC #indefero on the [FreeNode][freenode] servers.
|
||||
- Find ways to improve the design while keeping it **beautifully simple**.
|
||||
- Write a blog post about the project, what you think is good or bad.
|
||||
- Translate InDefero for the sake of the community.
|
||||
- Or maybe really hack into the code.
|
||||
|
||||
As you can see, the real hacking into the code is just a small part of the work, so even if you are not a coder you can do a lot.
|
||||
|
||||
[group]: http://groups.google.com/group/indefero-users
|
||||
[freenode]: http://freenode.net/
|
||||
|
||||
## I am a simple user
|
||||
|
||||
Thanks a lot! Really! As a project leader, I consider **you** as
|
||||
**the most important person in the success of the project**. So do not
|
||||
worry, I will really listen to your needs and make you love this
|
||||
project.
|
||||
|
||||
What you can do to help:
|
||||
|
||||
- Use the software and each time you find something a bit annoying in your daily use, report a bug. Usability issues are high priority issues.
|
||||
- Find typos, grammar mistakes, etc. and report a bug.
|
||||
- Write about InDefero on your blog/website.
|
||||
- Read the issues submitted by the users and provide answers if you have them.
|
||||
- ...
|
||||
|
||||
## I am a designer
|
||||
|
||||
A lot of things to do for you:
|
||||
|
||||
- Check the design and find the flaws in it. Is the space well used, does it look really nice and is it also functional for the first users?
|
||||
- Do we have good support of all the major browsers?
|
||||
- ...
|
||||
|
||||
## I am a coder
|
||||
|
||||
Checkout the code and have fun, but keep in mind that your results
|
||||
must be simple to use. Do not worry about the beautiful part, the
|
||||
designers can work on that.
|
||||
|
||||
## I am a security guy
|
||||
|
||||
Please, do try to break it, if you find a problem, come on IRC or
|
||||
contact the developers to get the issue fixed as soon as
|
||||
possible. Please, be nice, do not release the issue in the wild
|
||||
without first talking to us.
|
||||
|
||||
## I am a translator
|
||||
|
||||
We currently use (transifex)[http://trac.transifex.org] to help our
|
||||
users to translate indefero. You don't have to use it, but it's an
|
||||
easy way to do the job. You can visit the indefero page at transifex
|
||||
here : http://www.transifex.net/projects/p/indefero/c/indefero/
|
||||
|
||||
Please understand that your changes will not be commited instantly,
|
||||
but are sent to the maintainers e-mails before. Then, your changes
|
||||
will not be in the main repository until da-loic push the changes. In
|
||||
that way, try to do big changes with less submissions.
|
@ -213,3 +213,13 @@ may have problems as your certificate is not trusted, check the
|
||||
[procedure provided here][svnfix] to solve the problem.
|
||||
|
||||
[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/
|
20
contrib/zipstream-php-0.2.2/COPYING
Normal file
20
contrib/zipstream-php-0.2.2/COPYING
Normal 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.
|
42
contrib/zipstream-php-0.2.2/README
Normal file
42
contrib/zipstream-php-0.2.2/README
Normal 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/
|
2
contrib/zipstream-php-0.2.2/extras/README
Normal file
2
contrib/zipstream-php-0.2.2/extras/README
Normal file
@ -0,0 +1,2 @@
|
||||
Based on PKZIP appnotes, which are included here.
|
||||
|
3071
contrib/zipstream-php-0.2.2/extras/zip-appnote-6.3.1-20070411.txt
Normal file
3071
contrib/zipstream-php-0.2.2/extras/zip-appnote-6.3.1-20070411.txt
Normal file
File diff suppressed because it is too large
Load Diff
52
contrib/zipstream-php-0.2.2/test/index.php
Normal file
52
contrib/zipstream-php-0.2.2/test/index.php
Normal 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();
|
||||
|
||||
?>
|
580
contrib/zipstream-php-0.2.2/zipstream.php
Normal file
580
contrib/zipstream-php-0.2.2/zipstream.php
Normal 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);
|
||||
}
|
||||
};
|
||||
|
||||
?>
|
@ -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
224
doc/syncmonotone.mdtext
Normal 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.
|
||||
|
@ -15,8 +15,7 @@ res=$(cd "$dir" && /bin/pwd || "$dir")
|
||||
SCRIPTDIR="$res/$(readlink $0)"
|
||||
PHP_POST_PUSH=$SCRIPTDIR/mtnpostpush.php
|
||||
|
||||
base=$(basename "$0")
|
||||
TMPFILE=$(mktemp /tmp/${tempfoo}.XXXXXX) || exit 1
|
||||
TMPFILE=$(mktemp /tmp/mtn-post-push.XXXXXX) || exit 1
|
||||
while read rev; do echo $rev >> $TMPFILE; done
|
||||
|
||||
echo php $PHP_POST_PUSH "$1" \< $TMPFILE \&\& rm -f $TMPFILE |\
|
||||
|
@ -72,6 +72,9 @@ class IDF_Diff
|
||||
$indiff = true;
|
||||
continue;
|
||||
} 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
|
||||
$current_file = self::getMtnFile($this->lines[$i+1]);
|
||||
// mtn 0.48 and newer set /dev/null as file path for dropped files
|
||||
|
@ -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".'),
|
||||
));
|
||||
|
||||
$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(
|
||||
array('required' => true,
|
||||
'label' => __('Repository type'),
|
||||
@ -97,6 +105,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
|
||||
array('required' => false,
|
||||
'label' => __('Master branch'),
|
||||
'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.'),
|
||||
));
|
||||
|
||||
@ -195,7 +204,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
|
||||
}
|
||||
|
||||
$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()));
|
||||
if ($l->count() > 0) {
|
||||
throw new Pluf_Form_Invalid(__(
|
||||
@ -272,6 +281,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
|
||||
$project = new IDF_Project();
|
||||
$project->name = $this->cleaned_data['name'];
|
||||
$project->shortname = $this->cleaned_data['shortname'];
|
||||
$project->shortdesc = $this->cleaned_data['shortdesc'];
|
||||
|
||||
if ($this->cleaned_data['template'] != '--') {
|
||||
// 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_wiki_predefined' => IDF_Form_WikiConf::init_predefined,
|
||||
'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_closed' => IDF_Form_IssueTrackingConf::init_closed,
|
||||
'labels_issue_predefined' => IDF_Form_IssueTrackingConf::init_predefined,
|
||||
|
@ -37,12 +37,32 @@ class IDF_Form_Admin_ProjectUpdate extends Pluf_Form
|
||||
{
|
||||
$this->project = $extra['project'];
|
||||
$members = $this->project->getMembershipData('string');
|
||||
$conf = $this->project->getConf();
|
||||
|
||||
$this->fields['name'] = new Pluf_Form_Field_Varchar(
|
||||
array('required' => true,
|
||||
'label' => __('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(
|
||||
array('required' => false,
|
||||
'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()
|
||||
{
|
||||
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->project->membershipsUpdated();
|
||||
$this->project->name = $this->cleaned_data['name'];
|
||||
$this->project->shortdesc = $this->cleaned_data['shortdesc'];
|
||||
$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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,9 @@ class IDF_Form_IssueCreate extends Pluf_Form
|
||||
or $this->user->hasPerm('IDF.project-member', $this->project)) {
|
||||
$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(
|
||||
array('required' => true,
|
||||
'label' => __('Summary'),
|
||||
@ -57,7 +60,7 @@ class IDF_Form_IssueCreate extends Pluf_Form
|
||||
$this->fields['content'] = new Pluf_Form_Field_Varchar(
|
||||
array('required' => true,
|
||||
'label' => __('Description'),
|
||||
'initial' => '',
|
||||
'initial' => $contentTemplate,
|
||||
'widget' => 'Pluf_Form_Widget_TextareaInput',
|
||||
'widget_attrs' => array(
|
||||
'cols' => 58,
|
||||
@ -105,14 +108,39 @@ class IDF_Form_IssueCreate extends Pluf_Form
|
||||
'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++) {
|
||||
$initial = '';
|
||||
switch ($i) {
|
||||
case 1:
|
||||
$initial = 'Type:Defect';
|
||||
$initial = $predefined_type;
|
||||
break;
|
||||
case 2:
|
||||
$initial = 'Priority:Medium';
|
||||
$initial = $predefined_priority;
|
||||
break;
|
||||
}
|
||||
$this->fields['label'.$i] = new Pluf_Form_Field_Varchar(
|
||||
|
@ -31,6 +31,15 @@ class IDF_Form_IssueTrackingConf extends Pluf_Form
|
||||
* Defined as constants to easily access the value in the
|
||||
* 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
|
||||
Accepted = Problem reproduced / Need acknowledged
|
||||
Started = Work on this issue has begun';
|
||||
@ -66,6 +75,15 @@ Maintainability = Hinders future changes';
|
||||
|
||||
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(
|
||||
array('required' => true,
|
||||
'label' => __('Open issue status values'),
|
||||
@ -87,6 +105,7 @@ Maintainability = Hinders future changes';
|
||||
array('required' => true,
|
||||
'label' => __('Predefined issue labels'),
|
||||
'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,
|
||||
'cols' => 75),
|
||||
'widget' => 'Pluf_Form_Widget_TextareaInput',
|
||||
@ -99,8 +118,6 @@ Maintainability = Hinders future changes';
|
||||
'widget_attrs' => array('size' => 60),
|
||||
));
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,7 +204,7 @@ class IDF_Form_UserAccount extends Pluf_Form
|
||||
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);
|
||||
|
||||
if (Pluf::f('idf_strong_key_check', false)) {
|
||||
|
@ -80,7 +80,7 @@ class IDF_Key extends Pluf_Model
|
||||
if (preg_match('#^\[pubkey ([^\]]+)\]\s*(\S+)\s*\[end\]$#', $this->content, $m)) {
|
||||
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]);
|
||||
}
|
||||
|
||||
|
@ -112,6 +112,7 @@ function IDF_Middleware_ContextPreProcessor($request)
|
||||
$c = array_merge($c, $request->rights);
|
||||
}
|
||||
$c['usherConfigured'] = Pluf::f("mtn_usher_conf", null) !== null;
|
||||
$c['allProjects'] = IDF_Views::getProjects($request->user);
|
||||
return $c;
|
||||
}
|
||||
|
||||
|
@ -35,48 +35,66 @@ class IDF_Plugin_SyncMonotone
|
||||
$plug = new IDF_Plugin_SyncMonotone();
|
||||
switch ($signal) {
|
||||
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;
|
||||
case 'mtnpostpush.php::run':
|
||||
$plug->processSyncTimeline($params);
|
||||
$plug->processSyncTimeline($params['project']);
|
||||
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
|
||||
* 'mtn_repositories'
|
||||
* 2) create a new server key in the same directory
|
||||
* 3) write monotonerc for access control
|
||||
* 4) add the database as new local server in the usher configuration
|
||||
* 5) reload the running usher instance so it acknowledges the new
|
||||
* server
|
||||
* 3) create a new client key for IDF and store it in the project conf
|
||||
* 4) write monotonerc
|
||||
* 5) add the database as new local server in the usher configuration
|
||||
* 6) reload the running usher instance so it acknowledges the new server
|
||||
*
|
||||
* The initial right setup happens in processMembershipsUpdated()
|
||||
*
|
||||
* @param IDF_Project
|
||||
*/
|
||||
function processMonotoneCreate($project)
|
||||
function processProjectCreate($project)
|
||||
{
|
||||
if ($project->getConf()->getVal('scm') != 'mtn') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Pluf::f('mtn_db_access', 'local') == 'local') {
|
||||
return;
|
||||
}
|
||||
|
||||
$projecttempl = Pluf::f('mtn_repositories', false);
|
||||
if ($projecttempl === false) {
|
||||
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);
|
||||
if (!$usher_config || !is_writable($usher_config)) {
|
||||
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)) {
|
||||
throw new IDF_Scm_Exception(sprintf(
|
||||
__('Could not find mtn-post-push script "%s".'), $mtnpostpush
|
||||
@ -101,18 +119,8 @@ class IDF_Plugin_SyncMonotone
|
||||
// step 1) create a new database
|
||||
//
|
||||
$dbfile = $projectpath.'/database.mtn';
|
||||
$cmd = sprintf(
|
||||
Pluf::f('mtn_path', 'mtn').' db init -d %s',
|
||||
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
|
||||
));
|
||||
}
|
||||
$cmd = sprintf('db init -d %s', escapeshellarg($dbfile));
|
||||
self::_mtn_exec($cmd);
|
||||
|
||||
//
|
||||
// step 2) create a server key
|
||||
@ -126,43 +134,91 @@ class IDF_Plugin_SyncMonotone
|
||||
$server = $parsed['host'];
|
||||
}
|
||||
|
||||
$keyname = $shortname.'-server@'.$server;
|
||||
$cmd = sprintf(
|
||||
Pluf::f('mtn_path', 'mtn').' au generate_key --confdir=%s %s ""',
|
||||
$serverkey = $shortname.'-server@'.$server;
|
||||
$cmd = sprintf('au generate_key --confdir=%s %s ""',
|
||||
escapeshellarg($projectpath),
|
||||
escapeshellarg($keyname)
|
||||
escapeshellarg($serverkey)
|
||||
);
|
||||
self::_mtn_exec($cmd);
|
||||
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
$output = $return = null;
|
||||
$ll = exec($cmd, $output, $return);
|
||||
if ($return != 0) {
|
||||
//
|
||||
// step 3) create a client key, and save it in IDF
|
||||
//
|
||||
$clientkey_hash = '';
|
||||
$monotonerc_tpl = 'monotonerc-noauth.tpl';
|
||||
|
||||
if (Pluf::f('mtn_remote_auth', true)) {
|
||||
$monotonerc_tpl = 'monotonerc-auth.tpl';
|
||||
$keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
|
||||
if (!file_exists($keydir)) {
|
||||
if (!mkdir($keydir)) {
|
||||
throw new IDF_Scm_Exception(sprintf(
|
||||
__('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
|
||||
// FIXME: netsync access control is still missing!
|
||||
// step 4) write monotonerc
|
||||
//
|
||||
$monotonerc = file_get_contents(dirname(__FILE__) . "/SyncMonotone/monotonerc.tpl");
|
||||
$monotonerc = file_get_contents(
|
||||
dirname(__FILE__).'/SyncMonotone/'.$monotonerc_tpl
|
||||
);
|
||||
$monotonerc = str_replace(
|
||||
array("%%MTNPOSTPUSH%%", "%%PROJECT%%"),
|
||||
array($mtnpostpush, $shortname),
|
||||
array('%%MTNPOSTPUSH%%', '%%PROJECT%%', '%%MTNCLIENTKEY%%'),
|
||||
array($mtnpostpush, $shortname, $clientkey_hash),
|
||||
$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(
|
||||
__('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);
|
||||
$parsed_config = array();
|
||||
@ -177,13 +233,10 @@ class IDF_Plugin_SyncMonotone
|
||||
}
|
||||
|
||||
// ensure we haven't configured a server with this name already
|
||||
foreach ($parsed_config as $stanzas)
|
||||
{
|
||||
foreach ($stanzas as $stanza_line)
|
||||
{
|
||||
foreach ($parsed_config as $stanzas) {
|
||||
foreach ($stanzas as $stanza_line) {
|
||||
if ($stanza_line['key'] == 'server' &&
|
||||
$stanza_line['values'][0] == $shortname)
|
||||
{
|
||||
$stanza_line['values'][0] == $shortname) {
|
||||
throw new IDF_Scm_Exception(sprintf(
|
||||
__('usher configuration already contains a server '.
|
||||
'entry named "%s"'),
|
||||
@ -197,7 +250,9 @@ class IDF_Plugin_SyncMonotone
|
||||
array('key' => 'server', 'values' => array($shortname)),
|
||||
array('key' => 'local', 'values' => array(
|
||||
'--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
|
||||
// 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(
|
||||
__('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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
public function processSyncTimeline($params)
|
||||
public function processSyncTimeline($project_name)
|
||||
{
|
||||
$pname = $params['project'];
|
||||
try {
|
||||
$project = IDF_Project::getOr404($pname);
|
||||
$project = IDF_Project::getOr404($project_name);
|
||||
} catch (Pluf_HTTP_Error404 $e) {
|
||||
Pluf_Log::event(array(
|
||||
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
||||
'Project not found.',
|
||||
array($pname, $params)
|
||||
array($project_name, $params)
|
||||
));
|
||||
return false; // Project not found
|
||||
}
|
||||
|
||||
Pluf_Log::debug(array(
|
||||
'IDF_Plugin_SyncMonotone::processSyncTimeline',
|
||||
'Project found', $pname, $project->id
|
||||
'Project found', $project_name, $project->id
|
||||
));
|
||||
IDF_Scm::syncTimeline($project, true);
|
||||
Pluf_Log::event(array(
|
||||
'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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
79
src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl
Normal file
79
src/IDF/Plugin/SyncMonotone/monotonerc-auth.tpl
Normal 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")
|
||||
|
@ -1,6 +1,6 @@
|
||||
-- ***** BEGIN LICENSE BLOCK *****
|
||||
-- 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
|
||||
-- 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
|
||||
-- and other interested parties
|
||||
--
|
||||
function get_remote_automate_permitted(key_identity, command, options)
|
||||
local read_only_commands = {
|
||||
@ -30,7 +31,8 @@ function get_remote_automate_permitted(key_identity, command, options)
|
||||
"leaves", "ancestry_difference", "toposort", "erase_ancestors",
|
||||
"descendents", "ancestors", "heads", "get_file_of", "get_file",
|
||||
"interface_version", "get_attributes", "content_diff",
|
||||
"file_merge", "show_conflicts", "certs", "keys"
|
||||
"file_merge", "show_conflicts", "certs", "keys", "get_file_size",
|
||||
"get_extended_manifest_of"
|
||||
}
|
||||
|
||||
for _,v in ipairs(read_only_commands) do
|
||||
@ -42,23 +44,27 @@ function get_remote_automate_permitted(key_identity, command, options)
|
||||
return false
|
||||
end
|
||||
|
||||
--
|
||||
-- let IDF know of new arriving revisions to fill its timeline
|
||||
--
|
||||
_idf_revs = {}
|
||||
function note_netsync_start(session_id)
|
||||
push_hook_functions({
|
||||
["start"] = function (session_id)
|
||||
_idf_revs[session_id] = {}
|
||||
end
|
||||
|
||||
function note_netsync_revision_received(new_id, revision, certs, session_id)
|
||||
return "continue",nil
|
||||
end,
|
||||
["revision_received"] = function (new_id, revision, certs, session_id)
|
||||
table.insert(_idf_revs[session_id], new_id)
|
||||
end
|
||||
|
||||
function note_netsync_end (session_id, ...)
|
||||
return "continue",nil
|
||||
end,
|
||||
["end"] = function (session_id, ...)
|
||||
if table.getn(_idf_revs[session_id]) == 0 then
|
||||
return
|
||||
return "continue",nil
|
||||
end
|
||||
|
||||
local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%");
|
||||
if pid == -1 then
|
||||
print("could execute %%MTNPOSTPUSH%%")
|
||||
print("could not execute %%MTNPOSTPUSH%%")
|
||||
return
|
||||
end
|
||||
|
||||
@ -68,4 +74,19 @@ function note_netsync_end (session_id, ...)
|
||||
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")
|
@ -166,13 +166,28 @@ class IDF_Scm
|
||||
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
|
||||
* @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();
|
||||
}
|
||||
@ -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 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();
|
||||
}
|
||||
|
@ -296,10 +296,12 @@ class IDF_Scm_Git extends IDF_Scm
|
||||
}
|
||||
|
||||
|
||||
public function isValidRevision($commit)
|
||||
public function validateRevision($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[0]->changes = '';
|
||||
}
|
||||
|
||||
$out[0]['branch'] = $this->inBranches($commit, null);
|
||||
return $out[0];
|
||||
}
|
||||
|
||||
@ -545,13 +549,14 @@ class IDF_Scm_Git extends IDF_Scm
|
||||
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',
|
||||
escapeshellarg($this->repo),
|
||||
escapeshellarg($prefix),
|
||||
escapeshellarg($commit));
|
||||
return new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -87,14 +87,19 @@ class IDF_Scm_Mercurial extends IDF_Scm
|
||||
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',
|
||||
escapeshellarg($this->repo),
|
||||
escapeshellarg($rev));
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
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];
|
||||
} elseif ($match[1] == 'summary') {
|
||||
$c['title'] = $match[2];
|
||||
} elseif ($match[1] == 'branch') {
|
||||
$c['branch'] = $match[2];
|
||||
} else {
|
||||
$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['branch'] = empty($c['branch']) ? 'default' : $c['branch'];
|
||||
$c['full_message'] = !empty($c['full_message']) ? trim($c['full_message']) : '';
|
||||
$res[] = (object) $c;
|
||||
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 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 -',
|
||||
escapeshellarg($this->repo),
|
||||
escapeshellarg($commit));
|
||||
return new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,8 @@
|
||||
#
|
||||
# ***** END LICENSE BLOCK ***** */
|
||||
|
||||
require_once(dirname(__FILE__) . "/Monotone/Stdio.php");
|
||||
require_once(dirname(__FILE__) . "/Monotone/BasicIO.php");
|
||||
//require_once(dirname(__FILE__) . "/Monotone/Stdio.php");
|
||||
//require_once(dirname(__FILE__) . "/Monotone/BasicIO.php");
|
||||
|
||||
/**
|
||||
* Monotone scm class
|
||||
@ -47,6 +47,16 @@ class IDF_Scm_Monotone extends IDF_Scm
|
||||
$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()
|
||||
*/
|
||||
@ -125,6 +135,20 @@ class IDF_Scm_Monotone extends IDF_Scm
|
||||
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
|
||||
* multiple 40 byte revision ids
|
||||
@ -147,9 +171,11 @@ class IDF_Scm_Monotone extends IDF_Scm
|
||||
*/
|
||||
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));
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
*/
|
||||
@ -286,6 +284,84 @@ class IDF_Scm_Monotone extends IDF_Scm
|
||||
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()
|
||||
*/
|
||||
@ -297,52 +373,21 @@ class IDF_Scm_Monotone extends IDF_Scm
|
||||
}
|
||||
|
||||
$out = $this->stdio->exec(array(
|
||||
'get_manifest_of', $revs[0]
|
||||
'get_extended_manifest_of', $revs[0]
|
||||
));
|
||||
|
||||
$files = array();
|
||||
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
|
||||
$folder = $folder == '/' || empty($folder) ? '' : $folder.'/';
|
||||
$folder = $folder == '/' || empty($folder) ? '' : $folder;
|
||||
|
||||
foreach ($stanzas as $stanza) {
|
||||
if ($stanza[0]['key'] == 'format_version')
|
||||
continue;
|
||||
|
||||
$path = $stanza[0]['values'][0];
|
||||
if (!preg_match('#^'.$folder.'([^/]+)$#', $path, $m))
|
||||
$file = $this->_fillFileEntry($stanza, $folder);
|
||||
if ($file === null)
|
||||
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;
|
||||
}
|
||||
return $files;
|
||||
@ -382,7 +427,7 @@ class IDF_Scm_Monotone extends IDF_Scm
|
||||
$certs = $scm->_getCerts($revs[0]);
|
||||
// for the very seldom case that a revision
|
||||
// has no branch certificate
|
||||
if (count($certs['branch']) == 0) {
|
||||
if (!array_key_exists('branch', $certs)) {
|
||||
$branch = '*';
|
||||
}
|
||||
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);
|
||||
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;
|
||||
|
||||
$out = $this->stdio->exec(array(
|
||||
'get_manifest_of', $revs[0]
|
||||
'get_extended_manifest_of', $revs[0]
|
||||
));
|
||||
|
||||
$files = array();
|
||||
@ -457,43 +543,10 @@ class IDF_Scm_Monotone extends IDF_Scm
|
||||
if ($stanza[0]['key'] == 'format_version')
|
||||
continue;
|
||||
|
||||
$path = $stanza[0]['values'][0];
|
||||
if (!preg_match('#^'.$file.'$#', $path, $m))
|
||||
if ($stanza[0]['values'][0] != $file)
|
||||
continue;
|
||||
|
||||
$file = array();
|
||||
$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']);
|
||||
}
|
||||
|
||||
$file = $this->_fillFileEntry($stanza);
|
||||
return (object) $file;
|
||||
}
|
||||
return false;
|
||||
@ -565,8 +618,12 @@ class IDF_Scm_Monotone extends IDF_Scm
|
||||
$dates[] = date('Y-m-d H:i:s', strtotime($date));
|
||||
$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['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
|
||||
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'];
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
$log = array();
|
||||
$log['author'] = implode(", ", $certs['author']);
|
||||
$log['author'] = implode(', ', $certs['author']);
|
||||
|
||||
$dates = array();
|
||||
foreach ($certs['date'] as $date)
|
||||
|
@ -38,14 +38,15 @@ class IDF_Scm_Monotone_BasicIO
|
||||
{
|
||||
$pos = 0;
|
||||
$stanzas = array();
|
||||
$length = strlen($in);
|
||||
|
||||
while ($pos < strlen($in)) {
|
||||
while ($pos < $length) {
|
||||
$stanza = array();
|
||||
while ($pos < strlen($in)) {
|
||||
while ($pos < $length) {
|
||||
if ($in[$pos] == "\n") break;
|
||||
|
||||
$stanzaLine = array('key' => '', 'values' => array(), 'hash' => null);
|
||||
while ($pos < strlen($in)) {
|
||||
while ($pos < $length) {
|
||||
$ch = $in[$pos];
|
||||
if ($ch == '"' || $ch == '[') break;
|
||||
++$pos;
|
||||
@ -53,6 +54,9 @@ class IDF_Scm_Monotone_BasicIO
|
||||
$stanzaLine['key'] .= $ch;
|
||||
}
|
||||
|
||||
// symbol w/o a value list
|
||||
if ($pos >= $length || $in[$pos] == "\n") break;
|
||||
|
||||
if ($in[$pos] == '[') {
|
||||
unset($stanzaLine['values']);
|
||||
++$pos; // opening square bracket
|
||||
@ -64,30 +68,38 @@ class IDF_Scm_Monotone_BasicIO
|
||||
{
|
||||
unset($stanzaLine['hash']);
|
||||
$valCount = 0;
|
||||
while ($in[$pos] == '"') {
|
||||
++$pos; // opening quote
|
||||
// if hashs and plain values are encountered in the same
|
||||
// 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] = '';
|
||||
while ($pos < strlen($in)) {
|
||||
while ($pos < $length) {
|
||||
$ch = $in[$pos]; $pr = $in[$pos-1];
|
||||
if ($ch == '"' && $pr != '\\') break;
|
||||
if (($isHashValue && $ch == ']')
|
||||
||(!$isHashValue && $ch == '"' && $pr != '\\'))
|
||||
break;
|
||||
++$pos;
|
||||
$stanzaLine['values'][$valCount] .= $ch;
|
||||
}
|
||||
++$pos; // closing quote
|
||||
|
||||
if (!$isHashValue) {
|
||||
$stanzaLine['values'][$valCount] = str_replace(
|
||||
array("\\\\", "\\\""),
|
||||
array("\\", "\""),
|
||||
$stanzaLine['values'][$valCount]
|
||||
);
|
||||
}
|
||||
|
||||
if ($pos >= $length)
|
||||
break;
|
||||
|
||||
if ($in[$pos] == ' ') {
|
||||
++$pos; // space
|
||||
++$valCount;
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i <= $valCount; $i++) {
|
||||
$stanzaLine['values'][$i] = str_replace(
|
||||
array("\\\\", "\\\""),
|
||||
array("\\", "\""),
|
||||
$stanzaLine['values'][$i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$stanza[] = $stanzaLine;
|
||||
|
@ -62,6 +62,55 @@ class IDF_Scm_Monotone_Stdio
|
||||
$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
|
||||
*/
|
||||
@ -80,9 +129,8 @@ class IDF_Scm_Monotone_Stdio
|
||||
$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) {
|
||||
$cmd .= $this->_getAuthOptions();
|
||||
$host = sprintf(Pluf::f('mtn_remote_url'), $this->project->shortname);
|
||||
$cmd .= sprintf('automate remote_stdio %s', escapeshellarg($host));
|
||||
}
|
||||
@ -104,7 +152,6 @@ class IDF_Scm_Monotone_Stdio
|
||||
);
|
||||
|
||||
$env = array('LANG' => 'en_US.UTF-8');
|
||||
|
||||
$this->proc = proc_open($cmd, $descriptors, $this->pipes,
|
||||
null, $env);
|
||||
|
||||
|
@ -76,7 +76,7 @@ class IDF_Scm_Monotone_Usher
|
||||
$single_conns = preg_split('/[ ]/', $conn);
|
||||
$ret = array();
|
||||
foreach ($single_conns as $conn) {
|
||||
preg_match('/\(\w+\)([^:]):(\d+)/', $conn, $matches);
|
||||
preg_match('/\((\w+)\)([^:]+):(\d+)/', $conn, $matches);
|
||||
$ret[$matches[1]][] = (object)array(
|
||||
'server' => $matches[1],
|
||||
'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;
|
||||
}
|
||||
|
||||
|
78
src/IDF/Scm/Monotone/ZipRender.php
Normal file
78
src/IDF/Scm/Monotone/ZipRender.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -138,7 +138,7 @@ class IDF_Scm_Svn extends IDF_Scm
|
||||
/**
|
||||
* Subversion revisions are either a number or 'HEAD'.
|
||||
*/
|
||||
public function isValidRevision($rev)
|
||||
public function validateRevision($rev)
|
||||
{
|
||||
if ($rev == 'HEAD') {
|
||||
return true;
|
||||
@ -149,8 +149,11 @@ class IDF_Scm_Svn extends IDF_Scm
|
||||
escapeshellarg($this->repo),
|
||||
escapeshellarg($rev));
|
||||
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
|
||||
self::exec('IDF_Scm_Svn::isValidRevision', $cmd, $out, $ret);
|
||||
return (0 == $ret);
|
||||
self::exec('IDF_Scm_Svn::validateRevision', $cmd, $out, $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['changes'] = ($getdiff) ? $this->getDiff($commit) : '';
|
||||
$res['tree'] = '';
|
||||
$res['branch'] = '';
|
||||
return (object) $res;
|
||||
}
|
||||
|
||||
|
@ -61,6 +61,66 @@ class IDF_Views_Project
|
||||
$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.
|
||||
*/
|
||||
@ -68,38 +128,21 @@ class IDF_Views_Project
|
||||
public function timeline($request, $match)
|
||||
{
|
||||
$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->class = 'recent-issues';
|
||||
$pag->item_extra_props = array('request' => $request);
|
||||
$pag->summary = __('This table shows the project updates.');
|
||||
// Need to check the rights
|
||||
$rights = array();
|
||||
if (true === IDF_Precondition::accessSource($request)) {
|
||||
$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));
|
||||
|
||||
$classes = self::determineModelClasses($request, $model_filter);
|
||||
$sql = sprintf('model_class IN (%s)', implode(', ', $classes));
|
||||
$pag->forced_where = new Pluf_SQL('project=%s AND '.$sql,
|
||||
array($prj->id));
|
||||
$pag->sort_order = array('creation_dtime', 'ASC');
|
||||
@ -113,32 +156,23 @@ class IDF_Views_Project
|
||||
$pag->items_per_page = 20;
|
||||
$pag->no_results_text = __('No changes were found.');
|
||||
$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()) {
|
||||
$feedurl = Pluf_HTTP_URL_urlForView('idf_project_timeline_feed_auth',
|
||||
array($prj->shortname,
|
||||
$model_filter,
|
||||
IDF_Precondition::genFeedToken($prj, $request->user)));
|
||||
} else {
|
||||
$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',
|
||||
array(
|
||||
'page_title' => $title,
|
||||
'feedurl' => $feedurl,
|
||||
'timeline' => $pag,
|
||||
'team' => $team,
|
||||
'downloads' => $downloads,
|
||||
'model_filter' => $model_filter,
|
||||
'all_model_filters' => $all_model_filters,
|
||||
),
|
||||
$request);
|
||||
|
||||
@ -156,31 +190,17 @@ class IDF_Views_Project
|
||||
public function timelineFeed($request, $match)
|
||||
{
|
||||
$prj = $request->project;
|
||||
// Need to check the rights
|
||||
$rights = array();
|
||||
if (true === IDF_Precondition::accessSource($request)) {
|
||||
$rights[] = '\'IDF_Commit\'';
|
||||
IDF_Scm::syncTimeline($request->project);
|
||||
$model_filter = @$match[2];
|
||||
|
||||
$model_filter = @$match[2];
|
||||
$all_model_filters = self::getAvailableModelFilters();
|
||||
if (!array_key_exists($model_filter, $all_model_filters)) {
|
||||
$model_filter = 'all';
|
||||
}
|
||||
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\'';
|
||||
}
|
||||
$sqls = sprintf('model_class IN (%s)', implode(', ', $rights));
|
||||
$title = $all_model_filters[$model_filter];
|
||||
|
||||
$classes = self::determineModelClasses($request, $model_filter);
|
||||
$sqls = sprintf('model_class IN (%s)', implode(', ', $classes));
|
||||
$sql = new Pluf_SQL('project=%s AND '.$sqls, array($prj->id));
|
||||
$params = array(
|
||||
'filter' => $sql->gen(),
|
||||
@ -203,7 +223,6 @@ class IDF_Views_Project
|
||||
}
|
||||
$out = Pluf_Template::markSafe(implode("\n", $out));
|
||||
$tmpl = new Pluf_Template('idf/index.atom');
|
||||
$title = __('Updates');
|
||||
$feedurl = Pluf::f('url_base').Pluf::f('idf_base').$request->query;
|
||||
$viewurl = Pluf_HTTP_URL_urlForView('IDF_Views_Project::timeline',
|
||||
array($prj->shortname));
|
||||
@ -277,7 +296,8 @@ class IDF_Views_Project
|
||||
}
|
||||
} else {
|
||||
$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');
|
||||
foreach ($keys as $key) {
|
||||
$_val = $conf->getVal($key, false);
|
||||
|
@ -35,11 +35,12 @@ class IDF_Views_Source
|
||||
* Extension supported by the syntax highlighter.
|
||||
*/
|
||||
public static $supportedExtenstions = array(
|
||||
'ascx', 'ashx', 'asmx', 'aspx', 'browser', 'bsh', 'c', 'cc',
|
||||
'config', 'cpp', 'cs', 'csh', 'csproj', 'css', 'cv', 'cyc',
|
||||
'html', 'html', 'java', 'js', 'master', 'perl', 'php', 'pl',
|
||||
'pm', 'py', 'rb', 'sh', 'sitemap', 'skin', 'sln', 'svc', 'vala',
|
||||
'vb', 'vbproj', 'wsdl', 'xhtml', 'xml', 'xsd', 'xsl', 'xslt');
|
||||
'ascx', 'ashx', 'asmx', 'aspx', 'browser', 'bsh', 'c', 'cl', 'cc',
|
||||
'config', 'cpp', 'cs', 'csh', 'csproj', 'css', 'cv', 'cyc', 'el', 'fs',
|
||||
'h', 'hh', 'hpp', 'hs', 'html', 'html', 'java', 'js', 'lisp', 'master',
|
||||
'pas', 'perl', 'php', 'pl', 'pm', 'py', 'rb', 'scm', 'sh', 'sitemap',
|
||||
'skin', 'sln', 'svc', 'vala', 'vb', 'vbproj', 'vbs', 'wsdl', 'xhtml',
|
||||
'xml', 'xsd', 'xsl', 'xslt');
|
||||
|
||||
/**
|
||||
* Display help on how to checkout etc.
|
||||
@ -59,30 +60,56 @@ class IDF_Views_Source
|
||||
$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)
|
||||
{
|
||||
$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();
|
||||
$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,
|
||||
$this->getScmType($request));
|
||||
$changes = $scm->getChangeLog($commit, 25);
|
||||
@ -111,22 +138,17 @@ class IDF_Views_Source
|
||||
$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)
|
||||
{
|
||||
$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];
|
||||
|
||||
$cobject = $scm->getCommit($commit);
|
||||
if (!$cobject) {
|
||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
|
||||
array($request->project->shortname,
|
||||
$scm->getMainBranch()));
|
||||
return new Pluf_HTTP_Response_Redirect($url);
|
||||
throw new Exception('could not retrieve commit object for '. $commit);
|
||||
}
|
||||
$title = sprintf(__('%1$s %2$s Source Tree'),
|
||||
$request->project, $this->getScmType($request));
|
||||
@ -159,20 +181,14 @@ class IDF_Views_Source
|
||||
$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)
|
||||
{
|
||||
$scm = IDF_Scm::get($request->project);
|
||||
$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];
|
||||
if (substr($request_file, -1) == '/') {
|
||||
$request_file = substr($request_file, 0, -1);
|
||||
@ -181,13 +197,13 @@ class IDF_Views_Source
|
||||
$request_file));
|
||||
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);
|
||||
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);
|
||||
}
|
||||
$branches = $scm->getBranches();
|
||||
@ -277,26 +293,17 @@ class IDF_Views_Source
|
||||
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)
|
||||
{
|
||||
$scm = IDF_Scm::get($request->project);
|
||||
$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);
|
||||
$cobject = $scm->getCommit($commit, !$large);
|
||||
if (!$cobject) {
|
||||
// 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);
|
||||
throw new Exception('could not retrieve commit object for '. $commit);
|
||||
}
|
||||
$title = sprintf(__('%s Commit Details'), (string) $request->project);
|
||||
$page_title = sprintf(__('%s Commit Details - %s'), (string) $request->project, $commit);
|
||||
@ -326,19 +333,17 @@ class IDF_Views_Source
|
||||
$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)
|
||||
{
|
||||
$scm = IDF_Scm::get($request->project);
|
||||
$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);
|
||||
if (!$cobject) {
|
||||
throw new Exception('could not retrieve commit object for '. $commit);
|
||||
}
|
||||
$rep = new Pluf_HTTP_Response($cobject->changes, 'text/plain');
|
||||
$rep->headers['Content-Disposition'] = 'attachment; filename="'.$commit.'.diff"';
|
||||
return $rep;
|
||||
@ -394,19 +399,14 @@ class IDF_Views_Source
|
||||
* 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)
|
||||
{
|
||||
$scm = IDF_Scm::get($request->project);
|
||||
$commit = $match[2];
|
||||
$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);
|
||||
if (!$request_file_info or $request_file_info->type == 'tree') {
|
||||
// Redirect to the first branch
|
||||
@ -427,27 +427,20 @@ class IDF_Views_Source
|
||||
* 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)
|
||||
{
|
||||
$commit = trim($match[2]);
|
||||
$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;
|
||||
$cmd = $scm->getArchiveCommand($commit, $base.'/');
|
||||
$rep = new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
|
||||
$rep = $scm->getArchiveStream($commit, $base.'/');
|
||||
$rep->headers['Content-Transfer-Encoding'] = 'binary';
|
||||
$rep->headers['Content-Disposition'] = 'attachment; filename="'.$base.'.zip"';
|
||||
return $rep;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the mime type of a requested file.
|
||||
*
|
||||
@ -495,7 +488,6 @@ class IDF_Views_Source
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the mime type of a file.
|
||||
*
|
||||
@ -541,8 +533,10 @@ class IDF_Views_Source
|
||||
if (0 === strpos($fileinfo[0], 'text/')) {
|
||||
return true;
|
||||
}
|
||||
$ext = 'mdtext php-dist h gitignore diff patch'
|
||||
.Pluf::f('idf_extra_text_ext', '');
|
||||
$ext = 'mdtext php-dist h gitignore diff patch';
|
||||
$extra_ext = trim(Pluf::f('idf_extra_text_ext', ''));
|
||||
if (!empty($extra_ext))
|
||||
$ext .= ' ' . $extra_ext;
|
||||
$ext = array_merge(self::$supportedExtenstions, explode(' ' , $ext));
|
||||
return (in_array($fileinfo[2], $ext));
|
||||
}
|
||||
@ -609,4 +603,3 @@ function IDF_Views_Source_ShortenString($string, $length)
|
||||
return substr($string, 0, $preflen).$ellipse.
|
||||
substr($string, -($length - $preflen - mb_strlen($ellipse)));
|
||||
}
|
||||
|
||||
|
74
src/IDF/Views/Source/Precondition.php
Normal file
74
src/IDF/Views/Source/Precondition.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||
/*
|
||||
# ***** BEGIN LICENSE BLOCK *****
|
||||
# This file is part of InDefero, an open source project management application.
|
||||
# Copyright (C) 2010 Céondo Ltd and contributors.
|
||||
#
|
||||
# InDefero is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# InDefero is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
#
|
||||
# ***** END LICENSE BLOCK ***** */
|
||||
|
||||
class IDF_Views_Source_Precondition
|
||||
{
|
||||
/**
|
||||
* Ensures that the configured SCM for the project is available
|
||||
*
|
||||
* @param $request
|
||||
* @return true | Pluf_HTTP_Response_Redirect
|
||||
*/
|
||||
static public function scmAvailable($request)
|
||||
{
|
||||
$scm = IDF_Scm::get($request->project);
|
||||
if (!$scm->isAvailable()) {
|
||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help',
|
||||
array($request->project->shortname));
|
||||
return new Pluf_HTTP_Response_Redirect($url);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the revision given in the URL path and acts accordingly
|
||||
*
|
||||
* @param $request
|
||||
* @return true | Pluf_HTTP_Response_Redirect
|
||||
* @throws Exception
|
||||
*/
|
||||
static public function revisionValid($request)
|
||||
{
|
||||
list($url_info, $url_matches) = $request->view;
|
||||
list(, $project, $commit) = $url_matches;
|
||||
|
||||
$scm = IDF_Scm::get($request->project);
|
||||
$res = $scm->validateRevision($commit);
|
||||
switch ($res) {
|
||||
case IDF_Scm::REVISION_VALID:
|
||||
return true;
|
||||
case IDF_Scm::REVISION_INVALID:
|
||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::invalidRevision',
|
||||
array($request->project->shortname, $commit));
|
||||
return new Pluf_HTTP_Response_Redirect($url);
|
||||
case IDF_Scm::REVISION_AMBIGUOUS:
|
||||
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::disambiguateRevision',
|
||||
array($request->project->shortname,
|
||||
$commit,
|
||||
$url_info['model'].'::'.$url_info['method']));
|
||||
return new Pluf_HTTP_Response_Redirect($url);
|
||||
default:
|
||||
throw new Exception('unknown validation result: '. $res);
|
||||
}
|
||||
}
|
||||
}
|
@ -73,99 +73,52 @@ $cfg['git_write_remote_url'] = 'git@localhost:%s.git';
|
||||
$cfg['svn_repositories'] = 'file:///home/svn/repositories/%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';
|
||||
|
||||
# Additional options for the started monotone process
|
||||
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles', '--key=');
|
||||
#
|
||||
# You can setup monotone for use with indefero in several ways. The
|
||||
# two most-used should be:
|
||||
#
|
||||
# 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_opts'] = array('--no-workspace', '--no-standard-rcfiles');
|
||||
|
||||
# The path to a specific database (local use) or a writable project
|
||||
# directory (remote / usher use). %s is replaced with the project name
|
||||
$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';
|
||||
#
|
||||
|
||||
# Whether the particular database(s) are accessed locally (via automate stdio)
|
||||
# or remotely (via automate remote_stdio). 'remote' is the default for
|
||||
# netsync setups, while 'local' access should be choosed for ssh access.
|
||||
#
|
||||
# Note that you need to setup the hook 'get_remote_automate_permitted' for
|
||||
# each remotely accessible database. A full HOWTO set this up is beyond this
|
||||
# scope, please refer to the documentation of monotone and / or ask on the
|
||||
# mailing list (monotone-users@nongnu.org) or IRC channel
|
||||
# (irc.oftc.net/#monotone)
|
||||
#
|
||||
$cfg['mtn_db_access'] = 'remote';
|
||||
#
|
||||
# If configured, this allows basic control of a running usher process
|
||||
# via the forge administration. The variable must point to the full (writable)
|
||||
# use with usher and the SyncMonotone plugin, while 'local' access should be
|
||||
# choosed for manual setups and / or ssh access.
|
||||
$cfg['mtn_db_access'] = 'local';
|
||||
|
||||
# If true, each access to the database is authenticated with an auto-generated
|
||||
# project key which is stored in the IDF project configuration
|
||||
# ('mtn_client_key_*') and written out to $cfg['tmp_folder']/mtn-client-keys
|
||||
# for its actual use. This key is then configured on the server to have
|
||||
# full read / write access to all functions, while anonymous access can be
|
||||
# completely disabled.
|
||||
# If false, IDF tries to connect anonymously, without authentication, to
|
||||
# the remote monotone server instance. In this case no project-specific
|
||||
# keys are generated and the server must be configured to allow at least
|
||||
# anonymous read access to the main functions.
|
||||
#$cfg['mtn_remote_auth'] = true;
|
||||
|
||||
# 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
|
||||
# are added
|
||||
#
|
||||
#$cfg['mtn_usher_conf'] = '/path/to/usher.conf';
|
||||
|
||||
# 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';
|
||||
|
||||
# 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.
|
||||
#
|
||||
# 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';
|
||||
#
|
||||
# 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',
|
||||
);
|
||||
|
||||
# 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
|
||||
# $cfg['idf_mimetypes_db'] = '/etc/mime.types';
|
||||
|
||||
|
@ -74,18 +74,18 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/$#',
|
||||
'model' => 'IDF_Views_Project',
|
||||
'method' => 'home');
|
||||
|
||||
$ctl[] = array('regex' => '#^/p/([\-\w]+)/timeline/$#',
|
||||
$ctl[] = array('regex' => '#^/p/([\-\w]+)/timeline/(\w+)/$#',
|
||||
'base' => $base,
|
||||
'model' => 'IDF_Views_Project',
|
||||
'method' => 'timeline');
|
||||
|
||||
$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/$#',
|
||||
$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/(\w+)/$#',
|
||||
'base' => $base,
|
||||
'model' => 'IDF_Views_Project',
|
||||
'method' => 'timelineFeed',
|
||||
'name' => 'idf_project_timeline_feed');
|
||||
|
||||
$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/token/(.*)/$#',
|
||||
$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/(\w+)/token/(.*)/$#',
|
||||
'base' => $base,
|
||||
'model' => 'IDF_Views_Project',
|
||||
'method' => 'timelineFeed',
|
||||
@ -148,6 +148,16 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/source/help/$#',
|
||||
'model' => 'IDF_Views_Source',
|
||||
'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/([^/]+)/$#',
|
||||
'base' => $base,
|
||||
'model' => 'IDF_Views_Source',
|
||||
|
@ -88,7 +88,15 @@ Pluf_Signal::connect('gitpostupdate.php::run',
|
||||
# monotone synchronization
|
||||
Pluf_Signal::connect('IDF_Project::created',
|
||||
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'));
|
||||
|
||||
#
|
||||
|
@ -4,6 +4,12 @@
|
||||
<form method="post" action=".">
|
||||
<table class="form" summary="">
|
||||
<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 />
|
||||
{if $form.f.labels_issue_open.errors}{$form.f.labels_issue_open.fieldErrors}{/if}
|
||||
{$form.f.labels_issue_open|unsafe}
|
||||
@ -18,7 +24,8 @@
|
||||
<tr>
|
||||
<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}
|
||||
{$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>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -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 *****
|
||||
# This file is part of InDefero, an open source project management application.
|
||||
@ -29,16 +30,13 @@
|
||||
<![endif]-->
|
||||
{block extraheader}{/block}
|
||||
<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>
|
||||
<body>
|
||||
<div id="{block docid}doc3{/block}">
|
||||
<div id="hd">
|
||||
{if $project}<h1 class="project-title">{$project}</h1>{/if}
|
||||
<p class="top"><a href="#title" accesskey="2"></a>
|
||||
{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>
|
||||
{include 'idf/main-menu.html'}
|
||||
<div id="header">
|
||||
<div id="main-tabs">
|
||||
{if $project}
|
||||
@ -69,7 +67,6 @@
|
||||
</div>
|
||||
<div id="ft">{block foot}{/block}</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
|
||||
{include 'idf/js-hotkeys.html'}
|
||||
{block javascript}{/block}
|
||||
{if $project}
|
||||
|
@ -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 *****
|
||||
# This file is part of InDefero, an open source project management application.
|
||||
@ -29,15 +30,12 @@
|
||||
<![endif]-->
|
||||
{block extraheader}{/block}
|
||||
<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>
|
||||
<body>
|
||||
<div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}">
|
||||
<div id="hd">
|
||||
<p class="top"><a href="#title" accesskey="2"></a>
|
||||
{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>
|
||||
{include 'idf/main-menu.html'}
|
||||
<h1 id="title" class="title">{block title}{$page_title}{/block}</h1>
|
||||
</div>
|
||||
<div id="bd">
|
||||
@ -53,7 +51,6 @@
|
||||
</div>
|
||||
<div id="ft">{block foot}{/block}</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
|
||||
{include 'idf/js-hotkeys.html'}
|
||||
{block javascript}{/block}
|
||||
</body>
|
||||
|
@ -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 *****
|
||||
# This file is part of InDefero, an open source project management application.
|
||||
@ -29,17 +30,13 @@
|
||||
<![endif]-->
|
||||
{block extraheader}{/block}
|
||||
<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>
|
||||
<body>
|
||||
<div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}">
|
||||
<div id="hd">
|
||||
{if $project}<h1 class="project-title">{$project}</h1>{/if}
|
||||
<p class="top"><a href="#title" accesskey="2"></a>
|
||||
{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>
|
||||
{include 'idf/main-menu.html'}
|
||||
<div id="header">
|
||||
<div id="main-tabs">
|
||||
{if $project}
|
||||
@ -71,7 +68,6 @@
|
||||
</div>
|
||||
<div id="ft">{block foot}{/block}</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
|
||||
{include 'idf/js-hotkeys.html'}
|
||||
{block javascript}{/block}
|
||||
{if $project}
|
||||
|
@ -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 *****
|
||||
# This file is part of InDefero, an open source project management application.
|
||||
@ -29,16 +30,12 @@
|
||||
<![endif]-->
|
||||
{block extraheader}{/block}
|
||||
<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>
|
||||
<body>
|
||||
<div id="{block docid}doc3{/block}" class="{block docclass}yui-t3{/block}">
|
||||
<div id="hd">
|
||||
<p class="top"><a href="#title" accesskey="2"></a>
|
||||
{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>
|
||||
{include 'idf/main-menu.html'}
|
||||
<div id="header">
|
||||
<div id="main-tabs">
|
||||
<a href="{url 'IDF_Views_Admin::projects'}"{block tabprojects}{/block}>{trans 'Projects'}</a>
|
||||
@ -64,7 +61,6 @@
|
||||
</div>
|
||||
<div id="ft">{block foot}{/block}</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
|
||||
{include 'idf/js-hotkeys.html'}
|
||||
{block javascript}{/block}
|
||||
</body>
|
||||
|
@ -29,6 +29,14 @@
|
||||
</td>
|
||||
</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>
|
||||
<td>{if $form.f.scm.errors}{$form.f.scm.fieldErrors}{/if}
|
||||
{$form.f.scm|unsafe}
|
||||
|
@ -18,6 +18,23 @@
|
||||
</td>
|
||||
</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>
|
||||
<td>
|
||||
{if $form.f.owners.errors}{$form.f.owners.fieldErrors}{/if}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{extends "idf/issues/base.html"}
|
||||
{block docclass}yui-t2{/block}
|
||||
{block docclass}yui-t2{assign $inOpenIssues=true}{/block}
|
||||
{block body}
|
||||
{$issues.render}
|
||||
{if !$user.isAnonymous()}
|
||||
|
@ -3,7 +3,7 @@
|
||||
// <!--
|
||||
{hotkey 'Shift+h', 'IDF_Views::faq'}
|
||||
{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)}
|
||||
{hotkey 'Shift+i', 'IDF_Views_Issue::index', array($project.shortname)}{/if}
|
||||
{if $hasDownloadsAccess}{hotkey 'Shift+d', 'IDF_Views_Download::index', array($project.shortname)}{/if}
|
||||
|
31
src/IDF/templates/idf/main-menu.html
Normal file
31
src/IDF/templates/idf/main-menu.html
Normal 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'} ▾</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}
|
||||
|
@ -3,7 +3,7 @@
|
||||
{block tabhome} class="active"{/block}
|
||||
{block subtabs}
|
||||
<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>
|
||||
{/block}
|
||||
{block body}
|
||||
|
@ -4,7 +4,7 @@
|
||||
{block tabhome} class="active"{/block}
|
||||
{block subtabs}
|
||||
<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>
|
||||
{/block}
|
||||
|
||||
@ -13,26 +13,10 @@
|
||||
{/block}
|
||||
|
||||
{block context}
|
||||
{if count($downloads) > 0}
|
||||
<p><strong>{trans 'Featured Downloads'}</strong><br />
|
||||
{foreach $downloads as $download}
|
||||
<span class="label"><a href="{url 'IDF_Views_Download::view', array($project.shortname, $download.id)}" title="{$download.summary}">{$download}</a></span><br />
|
||||
<p><strong>{trans 'Filter by type'}</strong><br />
|
||||
{foreach $all_model_filters as $filter_key => $filter_name}
|
||||
<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 />
|
||||
{/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>
|
||||
{/block}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{extends "idf/base.html"}
|
||||
{block tabsource} class="active"{/block}
|
||||
{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">
|
||||
<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>
|
||||
|
@ -10,6 +10,9 @@
|
||||
<th><strong>{trans 'Author:'}</strong></th><td>{showuser $rcommit.get_author(), $request, $cobject.author}</td>
|
||||
</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>
|
||||
</tr>
|
||||
<tr>
|
||||
|
33
src/IDF/templates/idf/source/disambiguate_revision.html
Normal file
33
src/IDF/templates/idf/source/disambiguate_revision.html
Normal file
@ -0,0 +1,33 @@
|
||||
{extends "idf/source/base.html"}
|
||||
{block docclass}yui-t2{assign $inError=true}{/block}
|
||||
{block body}
|
||||
|
||||
<p>{blocktrans}The revision identifier <b>{$commit}</b> is ambiguous and can be
|
||||
expanded to multiple valid revisions - please choose one:{/blocktrans}</p>
|
||||
|
||||
<table summary="" class="tree-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{trans 'Title'}</th>
|
||||
<th>{trans 'Author'}</th>
|
||||
<th>{trans 'Date'}</th>
|
||||
<th>{trans 'Branch'}</th>
|
||||
<th>{trans 'Revision'}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach $revisions as $revision}
|
||||
{aurl 'url', $redirect, array($project.shortname, $revision.commit)}
|
||||
<tr class="log">
|
||||
<td>{$revision.title}</td>
|
||||
<td>{$revision.author}</td>
|
||||
<td>{$revision.date}</td>
|
||||
<td>{$revision.branch}</td>
|
||||
<td><a href="{$url}">{$revision.commit}</a></td>
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
{/block}
|
||||
|
@ -35,7 +35,7 @@
|
||||
<td{if $file.type == 'tree'} colspan="4"{/if}><a href="{$url}">{$file.file}</a></td>{else}<td><a href="#" title="{$file.hash}">{$file.file}</a></td>{/if}
|
||||
{if $file.type == 'blob'}
|
||||
{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>
|
||||
{else}<td colspan="2"></td>{/if}
|
||||
<td>{$file.size|size}</td>{/if}
|
||||
|
17
src/IDF/templates/idf/source/invalid_revision.html
Normal file
17
src/IDF/templates/idf/source/invalid_revision.html
Normal file
@ -0,0 +1,17 @@
|
||||
{extends "idf/source/base.html"}
|
||||
{block docclass}yui-t2{assign $inError=true}{/block}
|
||||
{block body}
|
||||
|
||||
<p>{blocktrans}The revision <b>{$commit}</b> is not valid or does not exist
|
||||
in this repository.{/blocktrans}</p>
|
||||
|
||||
{if $isOwner or $isMember}
|
||||
{aurl 'url', 'IDF_Views_Source::help', array($project.shortname)}
|
||||
<p>{blocktrans}If this is a new repository, the reason for this error
|
||||
could be that you have not committed and / or pushed any change so far.
|
||||
In this case please take a look at the <a href="{$url}">Help</a> page
|
||||
how to access your repository.{/blocktrans}</p>
|
||||
{/if}
|
||||
|
||||
{/block}
|
||||
|
@ -34,7 +34,7 @@
|
||||
<td{if $file.type != 'blob'} colspan="4"{/if}><a href="{$url}">{$file.file}</a></td>
|
||||
{if $file.type == 'blob'}
|
||||
{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>
|
||||
{else}<td colspan="2"></td>{/if}
|
||||
<td></td>{/if}
|
||||
|
@ -48,7 +48,7 @@
|
||||
</table>
|
||||
{aurl 'url', 'IDF_Views_Source::download', array($project.shortname, $commit)}
|
||||
<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>
|
||||
</p>
|
||||
|
||||
|
@ -45,7 +45,7 @@
|
||||
<td class="fileicon"><img src="{media '/idf/img/'~$file.type~'.png'}" alt="{$file.type}" /></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{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'}
|
||||
|
@ -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}
|
||||
<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>To directly include a file content from the repository, embrace its path with triple square brackets: [[[path/to/file.txt]]].</p>
|
||||
{/blocktrans}
|
||||
|
@ -601,7 +601,6 @@ table.code td.code {
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: pre-wrap; /* CSS 2.1 */
|
||||
white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */
|
||||
word-wrap: break-word; /* IE */
|
||||
padding-left: 5px;
|
||||
}
|
||||
@ -717,3 +716,90 @@ div.deprecated-page {
|
||||
padding: 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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user