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

This commit is contained in:
Loïc d'Anterroches 2010-11-17 09:33:10 +01:00
commit 3aac4d528a
29 changed files with 4121 additions and 83 deletions

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -173,3 +173,52 @@ For even more advanced setups, usher can also be used to forward sync
requests to other remote servers for load balancing, please consult the requests to other remote servers for load balancing, please consult the
README file for more information. README file for more information.
## Security and remote access
Indefero distinguishs between public and private projects and so does
the monotone plugin.
Public projects can be pulled by everybody and pushed by team members
or additional invited people. Remote command execution is enabled, but
only for read-only commands.
Remote commands can be helpful for a user or a 3rd party tool (like
[mtn-browse](http://mtn-browse.sourceforge.net) or
[guitone](http://guitone.thomaskeller.biz)) to browse the database
contents remotely without having to pull everything in first instance.
Private projects on the other hand can only be synced by team members
or additional invited people. Also noo remote command execution is enabled
by default.
## Notifications
If you have successfully set up your monotone instance, you probably want
to notify 3rd party systems for incoming changes or simply mirror them
somewhere else for backup purposes. The monotone source tree already comes
with [many example scripts and hooks](http://code.monotone.ca/p/monotone/source/tree/h:net.venge.monotone/contrib)
which serve these purposes, after only little additional configuration.
The usher/indefero-controlled setup automatically looks for *.lua files
in a directory called `hooks.d` right under the project's base directory
(configured via $cfg['mtn_repositories']) and this is the ideal place to
put or link these additional lua sources.
## Q&A
### I pushed a branch to my server, but it does not show up in IDF. Whats wrong?
Check if the heads of your branch are not suspended, i.e. do not carry a
`suspend` certificate. This usually hides the branch and all of its history
from monotone's eyes and therefor also from indefero. You can either choose
to "unsuspend" the branch simply by committing and pushing another head or
by letting monotone ignore all suspend certs. For the latter, its usually
enough to add `--ignore-suspend-certs` to the list of options in `$cfg['mtn_opts']`.
### I want to display another default branch when I click the "Source" tab. How can I do that?
Let the forge admin know the new master branch for your project. He is able
to change that quickly. Depending on the backend / server setup this might
also require some changes in the usher configuration, but only if usher
recognizes and proxies your database on a branch name level.

View File

@ -66,7 +66,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
$this->fields['shortdesc'] = new Pluf_Form_Field_Varchar( $this->fields['shortdesc'] = new Pluf_Form_Field_Varchar(
array('required' => true, array('required' => true,
'label' => __('short description'), 'label' => __('Short description'),
'help_text' => __('A one line description of the project.'), 'help_text' => __('A one line description of the project.'),
'initial' => '', 'initial' => '',
'widget_attrs' => array('size' => '35'), 'widget_attrs' => array('size' => '35'),
@ -105,6 +105,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
array('required' => false, array('required' => false,
'label' => __('Master branch'), 'label' => __('Master branch'),
'initial' => '', 'initial' => '',
'widget_attrs' => array('size' => '35'),
'help_text' => __('This should be a world-wide unique identifier for your project. A reverse DNS notation like "com.my-domain.my-project" is a good idea.'), 'help_text' => __('This should be a world-wide unique identifier for your project. A reverse DNS notation like "com.my-domain.my-project" is a good idea.'),
)); ));
@ -203,7 +204,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
} }
$sql = new Pluf_SQL('vkey=%s AND vdesc=%s', $sql = new Pluf_SQL('vkey=%s AND vdesc=%s',
array("mtn_master_branch", $mtn_master_branch)); array('mtn_master_branch', $mtn_master_branch));
$l = Pluf::factory('IDF_Conf')->getList(array('filter'=>$sql->gen())); $l = Pluf::factory('IDF_Conf')->getList(array('filter'=>$sql->gen()));
if ($l->count() > 0) { if ($l->count() > 0) {
throw new Pluf_Form_Invalid(__( throw new Pluf_Form_Invalid(__(

View File

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

View File

@ -108,14 +108,39 @@ class IDF_Form_IssueCreate extends Pluf_Form
'size' => 15, 'size' => 15,
), ),
)); ));
/*
* get predefined tags for issues from current project
*
* first Type:<...> and Priority:<...> will be used
*
*/
$predefined = preg_split("/[\r\n]+/", $extra['project']->getConf()->getVal(
'labels_issue_predefined'
));
$predefined_type = 'Type:Defect';
foreach ($predefined as $tag) {
if (strpos($tag, 'Type:') === 0) {
$predefined_type = $tag;
break;
}
}
$predefined_priority = 'Priority:Medium';
foreach ($predefined as $tag) {
if (strpos($tag, 'Priority:') === 0) {
$predefined_priority = $tag;
break;
}
}
for ($i=1;$i<7;$i++) { for ($i=1;$i<7;$i++) {
$initial = ''; $initial = '';
switch ($i) { switch ($i) {
case 1: case 1:
$initial = 'Type:Defect'; $initial = $predefined_type;
break; break;
case 2: case 2:
$initial = 'Priority:Medium'; $initial = $predefined_priority;
break; break;
} }
$this->fields['label'.$i] = new Pluf_Form_Field_Varchar( $this->fields['label'.$i] = new Pluf_Form_Field_Varchar(

View File

@ -105,6 +105,7 @@ Maintainability = Hinders future changes';
array('required' => true, array('required' => true,
'label' => __('Predefined issue labels'), 'label' => __('Predefined issue labels'),
'initial' => self::init_predefined, 'initial' => self::init_predefined,
'help_text' => __('The first "Type:" and "Priority:" entries found in this list are automatically chosen as defaults for new issues.'),
'widget_attrs' => array('rows' => 7, 'widget_attrs' => array('rows' => 7,
'cols' => 75), 'cols' => 75),
'widget' => 'Pluf_Form_Widget_TextareaInput', 'widget' => 'Pluf_Form_Widget_TextareaInput',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,8 +21,8 @@
# #
# ***** END LICENSE BLOCK ***** */ # ***** END LICENSE BLOCK ***** */
require_once(dirname(__FILE__) . "/Monotone/Stdio.php"); //require_once(dirname(__FILE__) . "/Monotone/Stdio.php");
require_once(dirname(__FILE__) . "/Monotone/BasicIO.php"); //require_once(dirname(__FILE__) . "/Monotone/BasicIO.php");
/** /**
* Monotone scm class * Monotone scm class
@ -135,6 +135,20 @@ class IDF_Scm_Monotone extends IDF_Scm
return $branch; return $branch;
} }
/**
* @see IDF_Scm::getArchiveStream
*/
public function getArchiveStream($commit, $prefix='repository/')
{
$revs = $this->_resolveSelector($commit);
// sanity: this should actually not happen, because the
// revision is validated before already
if (count($revs) == 0) {
return new Pluf_HTTP_Response_NotFound();
}
return new IDF_Scm_Monotone_ZipRender($this->stdio, $revs[0]);
}
/** /**
* expands a selector or a partial revision id to zero, one or * expands a selector or a partial revision id to zero, one or
* multiple 40 byte revision ids * multiple 40 byte revision ids
@ -609,7 +623,9 @@ class IDF_Scm_Monotone extends IDF_Scm
$res['title'] = $split[0]; $res['title'] = $split[0];
$res['full_message'] = (isset($split[1])) ? trim($split[1]) : ''; $res['full_message'] = (isset($split[1])) ? trim($split[1]) : '';
$res['branch'] = implode(', ', $certs['branch']);
$res['commit'] = $revs[0]; $res['commit'] = $revs[0];
$res['changes'] = ($getdiff) ? $this->_getDiff($revs[0]) : ''; $res['changes'] = ($getdiff) ? $this->_getDiff($revs[0]) : '';
return (object) $res; return (object) $res;

View File

@ -0,0 +1,78 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# Plume Framework is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Plume Framework is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
require_once(IDF_PATH.'/../contrib/zipstream-php-0.2.2/zipstream.php');
/**
* Special response object to output
*
* The Content-Length will not be set as it is not possible to predict it.
*
* Note: The ZipArchive version 0.2.2 has been patched in-tree with this
* patch http://pastebin.ca/1977584 to avoid a couple of PHP notices
*
*/
class IDF_Scm_Monotone_ZipRender extends Pluf_HTTP_Response
{
/**
* The revision argument must be a safe string!
*
* @param Object stdio context
* @param string revision
* @param string Mimetype (null)
*/
private $stdio = null;
private $revision = null;
function __construct($stdio, $revision)
{
parent::__construct($revision, 'application/x-zip');
$this->stdio = $stdio;
$this->revision = $revision;
}
/**
* Render a response object.
*/
function render($output_body=true)
{
$this->outputHeaders();
if ($output_body) {
$manifest = $this->stdio->exec(array('get_manifest_of', $this->revision));
$stanzas = IDF_Scm_Monotone_BasicIO::parse($manifest);
$zip = new ZipStream();
foreach ($stanzas as $stanza) {
if ($stanza[0]['key'] != 'file')
continue;
$content = $this->stdio->exec(array('get_file', $stanza[1]['hash']));
$zip->add_file($stanza[0]['values'][0], $content);
}
$zip->finish();
}
}
}

View File

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

View File

@ -35,11 +35,12 @@ class IDF_Views_Source
* Extension supported by the syntax highlighter. * Extension supported by the syntax highlighter.
*/ */
public static $supportedExtenstions = array( public static $supportedExtenstions = array(
'ascx', 'ashx', 'asmx', 'aspx', 'browser', 'bsh', 'c', 'cc', 'ascx', 'ashx', 'asmx', 'aspx', 'browser', 'bsh', 'c', 'cl', 'cc',
'config', 'cpp', 'cs', 'csh', 'csproj', 'css', 'cv', 'cyc', 'config', 'cpp', 'cs', 'csh', 'csproj', 'css', 'cv', 'cyc', 'el', 'fs',
'html', 'html', 'java', 'js', 'master', 'pas', 'perl', 'php', 'pl', 'h', 'hh', 'hpp', 'hs', 'html', 'html', 'java', 'js', 'lisp', 'master',
'pm', 'py', 'rb', 'sh', 'sitemap', 'skin', 'sln', 'svc', 'vala', 'pas', 'perl', 'php', 'pl', 'pm', 'py', 'rb', 'scm', 'sh', 'sitemap',
'vb', 'vbproj', 'wsdl', 'xhtml', 'xml', 'xsd', 'xsl', 'xslt'); 'skin', 'sln', 'svc', 'vala', 'vb', 'vbproj', 'vbs', 'wsdl', 'xhtml',
'xml', 'xsd', 'xsl', 'xslt');
/** /**
* Display help on how to checkout etc. * Display help on how to checkout etc.
@ -434,8 +435,7 @@ class IDF_Views_Source
$commit = trim($match[2]); $commit = trim($match[2]);
$scm = IDF_Scm::get($request->project); $scm = IDF_Scm::get($request->project);
$base = $request->project->shortname.'-'.$commit; $base = $request->project->shortname.'-'.$commit;
$cmd = $scm->getArchiveCommand($commit, $base.'/'); $rep = $scm->getArchiveStream($commit, $base.'/');
$rep = new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
$rep->headers['Content-Transfer-Encoding'] = 'binary'; $rep->headers['Content-Transfer-Encoding'] = 'binary';
$rep->headers['Content-Disposition'] = 'attachment; filename="'.$base.'.zip"'; $rep->headers['Content-Disposition'] = 'attachment; filename="'.$base.'.zip"';
return $rep; return $rep;

View File

@ -24,7 +24,8 @@
<tr> <tr>
<td colspan="2"><strong>{$form.f.labels_issue_predefined.labelTag}:</strong><br /> <td colspan="2"><strong>{$form.f.labels_issue_predefined.labelTag}:</strong><br />
{if $form.f.labels_issue_predefined.errors}{$form.f.labels_issue_predefined.fieldErrors}{/if} {if $form.f.labels_issue_predefined.errors}{$form.f.labels_issue_predefined.fieldErrors}{/if}
{$form.f.labels_issue_predefined|unsafe} {$form.f.labels_issue_predefined|unsafe}<br />
<span class="helptext">{$form.f.labels_issue_predefined.help_text}</span>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -1,4 +1,5 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
{* {*
# ***** BEGIN LICENSE BLOCK ***** # ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application. # This file is part of InDefero, an open source project management application.

View File

@ -1,4 +1,5 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
{* {*
# ***** BEGIN LICENSE BLOCK ***** # ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application. # This file is part of InDefero, an open source project management application.

View File

@ -1,4 +1,5 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
{* {*
# ***** BEGIN LICENSE BLOCK ***** # ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application. # This file is part of InDefero, an open source project management application.

View File

@ -1,4 +1,5 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
{* {*
# ***** BEGIN LICENSE BLOCK ***** # ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application. # This file is part of InDefero, an open source project management application.

View File

@ -25,6 +25,15 @@
<span class="helptext">{$form.f.shortdesc.help_text}</span> <span class="helptext">{$form.f.shortdesc.help_text}</span>
</td> </td>
</tr> </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> <tr>
<th><strong>{$form.f.owners.labelTag}:</strong></th> <th><strong>{$form.f.owners.labelTag}:</strong></th>
<td> <td>

View File

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

View File

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