Merge branch 'develop'

This commit is contained in:
Loïc d'Anterroches 2011-01-08 21:35:20 +01:00
commit 023a3ce879
32 changed files with 787 additions and 269 deletions

View File

@ -149,7 +149,6 @@ look like this:
$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';
...
@ -188,8 +187,10 @@ Remote commands can be helpful for a user or a 3rd party tool (like
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.
or additional invited people. Remote command execution is still enabled
by default - if you want to disable that, simply remove the symlink to
the file `indefero_authorize_remote_automate.conf` in your project's `hooks.d`
directory or copy the file from the original location and adapt it.
## Notifications
@ -204,8 +205,54 @@ 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.
## Custom project configurations and templates
If a new project is created in IDF, the SyncMonotone plugin creates a new
configuration tree for the project into the project's configuration directory,
determined by `$cfg['mtn_repositories']`. IDF ships with the minimum set of
files for this configuration tree and sets up everything automatically for you.
Even more, most of the configuration files from the newly created tree are only
symlinked to the original configuration directory which is configurable via
`$cfg['mtn_confdir']` and defaults to `src/IDF/Plugin/SyncMonotone/`. This has
the advantage that your standard IDF setup automatically receives updates to
existing (symlinked) configuration files as soon as you update to a newer
version.
You could, however, also choose to place the directory tree somewhere else
and adapt the contents of the individual files yourself, so these changes get
automatically applied to all new projects you create. You could even go so far
and add new files to the tree and let them be processed automatically just
as the basic files! All you need to do is to copy your files and / or directories
underknees your `$cfg['mtn_confdir']` and add their relative paths to
`$cfg['mtn_confdir_extra']`.
By convention, all entries which end with a slash are considered directories,
so mkdir(1) is issued for these entries, all files which do not end up with
".in" are considered to be static script files which are just symlinked from
the basic configuration dir and all entries ending on ".in" are considered
configuration files or templates, which are copied over to the project's
configuration tree and which get some basic project-specific values replaced.
The following placeholders are currently recognized and replaced for these files:
* %%PROJECT%% - the name of the created project
* %%MTNPOSTPUSH%% - the absolute path to the `mtn-post-push` script
* %%MTNCLIENTKEY%% - the public key hash of the key which is used by IDF
to authenticate remote stdio access
Thats it - I hope you find it useful :)
## Q&A
### After I created a new project, IDF throws an exception and tells me that it couldn't save the membership data with a cryptic error message. Whats wrong?
Multiple issues could cause that. If you've set up usher, make sure the usher
can fork your database at all and look out for specific errors in the log file
of your project. If you stumble upon permission issues, ensure that the user
who runs the usher can access all files in your project's configuration directory,
including symlinked files.
### 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

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
id="svg2"
sodipodi:version="0.32"
inkscape:version="0.48.0 r9654"
version="1.0"
sodipodi:docname="indefero-logo-lite.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape">
<defs
id="defs4">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 526.18109 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="744.09448 : 526.18109 : 1"
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
id="perspective10" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
gridtolerance="10000"
guidetolerance="10"
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.313708"
inkscape:cx="31.568929"
inkscape:cy="-0.35578703"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1280"
inkscape:window-height="723"
inkscape:window-x="0"
inkscape:window-y="24"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-728.09451)">
<g
id="g2401"
transform="matrix(0.13580542,0,0,0.13580542,-47.580342,708.10521)"
style="fill:#8ae234;stroke:#4e9a06;stroke-width:2.4000001;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:export-filename="/home/loa/Projects/indefero/logo/powered-by-indefero.png"
inkscape:export-xdpi="12.330909"
inkscape:export-ydpi="12.330909">
<path
id="path2383"
d="m 396.19089,173.14471 c -7.67621,0.80661 -14.40195,5.39406 -19.58101,10.89131 -7.23597,7.88004 -11.69742,18.07908 -13.32198,28.60362 -1.7236,11.28173 -0.25925,23.20635 5.07686,33.37271 3.78607,7.24384 9.53161,13.92339 17.29701,16.96772 3.86478,1.53937 8.98362,1.03284 11.67912,-2.41036 2.64357,-3.5671 2.69463,-8.234 2.85756,-12.48867 0.045,-7.61054 -0.54749,-15.25544 0.45618,-22.83193 0.87131,-9.50623 4.03944,-18.56751 6.71612,-27.66851 1.16242,-4.44333 2.25094,-9.02808 1.97499,-13.64988 -0.48817,-4.62476 -3.58059,-9.31042 -8.2964,-10.4067 -1.57489,-0.44882 -3.23412,-0.48948 -4.85845,-0.37931 z"
style="fill:#8ae234;fill-opacity:1;fill-rule:nonzero;stroke:#4e9a06;stroke-width:2.4000001;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
inkscape:connector-curvature="0" />
<path
id="path2391"
d="m 433.14691,149.28687 c 7.2059,2.76589 12.51512,8.93778 16.09494,15.58815 4.94991,9.48434 6.61962,20.49058 5.46486,31.07695 -1.25505,11.34342 -5.75582,22.48271 -13.54134,30.92159 -5.53192,6.01709 -12.81048,10.98198 -21.09918,11.91276 -4.13154,0.4866 -8.94486,-1.32748 -10.65734,-5.35104 -1.63027,-4.12976 -0.4717,-8.65084 0.47212,-12.80269 1.92628,-7.36287 4.47721,-14.59393 5.4687,-22.17201 1.61875,-9.40784 0.90381,-18.98034 0.67386,-28.46402 0.0272,-4.59278 0.1624,-9.30303 1.62515,-13.69592 1.66851,-4.34082 5.86829,-8.06645 10.70716,-7.90484 1.63738,-0.0259 3.25061,0.36424 4.79107,0.89107 z"
style="fill:#8ae234;fill-opacity:1;fill-rule:nonzero;stroke:#4e9a06;stroke-width:2.4000001;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -55,6 +55,7 @@ Pluf_Dispatcher::loadControllers(Pluf::f('idf_views'));
$params = array('repo_dir' => $argv[1],
'revision' => $argv[2],
'env' => array_merge($_ENV, $_SERVER));
Pluf_Log::event(array('svnpostcommit.php', 'Send run signal.', $params));
Pluf_Signal::send('svnpostcommit.php::run', 'svnpostcommit.php', $params);

View File

@ -295,7 +295,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)) {

View File

@ -230,6 +230,24 @@ class IDF_Plugin_SyncGit_Serve
}
Pluf_Log::debug(array('IDF_Plugin_Git_Serve::initRepository',
'Added post-update hook.', $fullpath));
// Configure the core.quotepath option
$quotepath = (Pluf::f('git_core_quotepath', true) == true) ? 'true' : 'false';
$out = array();
$res = 0;
exec(sprintf(Pluf::f('idf_exec_cmd_prefix', '').
Pluf::f('git_path', 'git').' config -f %s/config --add core.quotepath %s',
escapeshellarg($fullpath),
escapeshellarg($quotepath)
),
$out, $res);
if ($res != 0) {
Pluf_Log::warn(array('IDF_Plugin_Git_Serve::initRepository',
'core.quotepath configuration error.',
$quotepath));
return;
}
Pluf_Log::debug(array('IDF_Plugin_Git_Serve::initRepository',
'core.quotepath configured.', $quotepath));
}
/**

View File

@ -62,7 +62,7 @@ class IDF_Plugin_SyncMonotone
* 'mtn_repositories'
* 2) create a new server key in the same directory
* 3) create a new client key for IDF and store it in the project conf
* 4) write monotonerc
* 4) setup the configuration
* 5) add the database as new local server in the usher configuration
* 6) reload the running usher instance so it acknowledges the new server
*
@ -101,6 +101,36 @@ class IDF_Plugin_SyncMonotone
));
}
// check some static configuration files
$confdir = Pluf::f('mtn_confdir', false);
if ($confdir === false) {
$confdir = dirname(__FILE__).'/SyncMonotone/';
}
$confdir_contents = array(
'monotonerc.in',
'remote-automate-permissions.in',
'hooks.d/',
// this is linked and not copied to be able to update
// the list of read-only commands on upgrades
'hooks.d/indefero_authorize_remote_automate.conf',
'hooks.d/indefero_authorize_remote_automate.lua',
'hooks.d/indefero_post_push.conf.in',
'hooks.d/indefero_post_push.lua',
);
// check whether we should handle additional files in the config directory
$confdir_extra_contents = Pluf::f('mtn_confdir_extra', false);
if ($confdir_extra_contents !== false) {
$confdir_contents =
array_merge($confdir_contents, $confdir_extra_contents);
}
foreach ($confdir_contents as $content) {
if (!file_exists($confdir.$content)) {
throw new IDF_Scm_Exception(sprintf(
__('The configuration file %s is missing.'), $content
));
}
}
$shortname = $project->shortname;
$projectpath = sprintf($projecttempl, $shortname);
if (file_exists($projectpath)) {
@ -144,11 +174,6 @@ class IDF_Plugin_SyncMonotone
//
// 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)) {
@ -195,27 +220,50 @@ class IDF_Plugin_SyncMonotone
escapeshellarg($clientkey_pubdata)
);
self::_mtn_exec($cmd);
//
// step 4) setup the configuration
//
// we assume that all confdir entries ending with a slash mean a
// directory that has to be created, that all files ending on ".in"
// have to be processed and copied in place and that all other files
// just need to be symlinked from the original location
foreach ($confdir_contents as $content) {
$filepath = $projectpath.'/'.$content;
if (substr($content, -1) == '/') {
if (!mkdir($filepath)) {
throw new IDF_Scm_Exception(sprintf(
__('Could not create configuration directory "%s"'), $filepath
));
}
continue;
}
//
// step 4) write monotonerc
//
$monotonerc = file_get_contents(
dirname(__FILE__).'/SyncMonotone/'.$monotonerc_tpl
);
$monotonerc = str_replace(
if (substr($content, -3) != '.in') {
if (!symlink($confdir.$content, $filepath)) {
IDF_Scm_Exception(sprintf(
__('Could not create symlink "%s"'), $filepath
));
}
continue;
}
$filecontents = file_get_contents($confdir.'/'.$content);
$filecontents = str_replace(
array('%%MTNPOSTPUSH%%', '%%PROJECT%%', '%%MTNCLIENTKEY%%'),
array($mtnpostpush, $shortname, $clientkey_hash),
$monotonerc
$filecontents
);
$rcfile = $projectpath.'/monotonerc';
if (file_put_contents($rcfile, $monotonerc, LOCK_EX) === false) {
// remove the .in
$filepath = substr($filepath, 0, -3);
if (file_put_contents($filepath, $filecontents, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write mtn configuration file "%s"'), $rcfile
__('Could not write configuration file "%s"'), $filepath
));
}
}
//
// step 5) read in and append the usher config with the new server
@ -378,7 +426,6 @@ class IDF_Plugin_SyncMonotone
}
}
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);
@ -390,7 +437,6 @@ class IDF_Plugin_SyncMonotone
));
}
}
}
$usher_rc = file_get_contents($usher_config);
$parsed_config = array();
@ -721,7 +767,7 @@ class IDF_Plugin_SyncMonotone
private static function _delete_recursive($path)
{
if (is_file($path)) {
if (is_file($path) || is_link($path)) {
return @unlink($path);
}

View File

@ -0,0 +1,10 @@
ARA_safe_commands = {
"get_corresponding_path", "get_content_changed", "tags", "branches",
"common_ancestors", "packet_for_fdelta", "packet_for_fdata",
"packets_for_certs", "packet_for_rdata", "get_manifest_of",
"get_revision", "select", "graph", "children", "parents", "roots",
"leaves", "ancestry_difference", "toposort", "erase_ancestors",
"descendents", "ancestors", "heads", "get_file_of", "get_file",
"interface_version", "get_attributes", "content_diff",
"file_merge", "show_conflicts", "certs", "keys", "get_extended_manifest_of"
}

View File

@ -0,0 +1,88 @@
-- ***** BEGIN LICENSE BLOCK *****
-- This file is part of InDefero, an open source project management application.
-- Copyright (C) 2011 Céondo Ltd and contributors.
-- Copyright (C) 2010 Thomas Keller <me@thomaskeller.biz>
-- Richard Levitte <richard@levitte.org>
--
-- InDefero is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.
--
-- InDefero is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
--
-- ***** END LICENSE BLOCK *****
--
-- This script reads key identities from a file "remote-automate-permissions"
-- in the configuration directory and permits those authenticating with one
-- of those keys to perform dangerous (read/write) remote automate operations.
-- The format of the file is very simple, one key identity on every line.
-- Lines starting with # are ignore, as well as empty lines.
--
-- It's possible to configure this script to allow the performance of some
-- remote automate commands anonymously, through the variable
-- ARA_safe_commands, which has to be a table of commands as strings.
-- One example configuration, taken from the setup at code.monotone.ca, could
-- be this:
--
-- ARA_safe_commands = {
-- "get_corresponding_path", "get_content_changed", "tags", "branches",
-- "common_ancestors", "packet_for_fdelta", "packet_for_fdata",
-- "packets_for_certs", "packet_for_rdata", "get_manifest_of",
-- "get_revision", "select", "graph", "children", "parents", "roots",
-- "leaves", "ancestry_difference", "toposort", "erase_ancestors",
-- "descendents", "ancestors", "heads", "get_file_of", "get_file",
-- "interface_version", "get_attributes", "content_diff",
-- "file_merge", "show_conflicts", "certs", "keys", "get_extended_manifest_of"
-- }
--
do
local _safe_commands = {}
if ARA_safe_commands then
_safe_commands = ARA_safe_commands
end
local _save_get_remote_automate_permitted = get_remote_automate_permitted
function get_remote_automate_permitted(key_identity, command, options)
local permfile =
io.open(get_confdir() .. "/remote-automate-permissions", "r")
if (permfile == nil) then
return false
end
-- See if the incoming key matches any of the key identities or
-- patterns found in the permissions file.
local matches = false
local line = permfile:read()
while (not matches and line ~= nil) do
if not globish_match("#*", line) then
local _, _, ln = string.find(line, "%s*([^%s]*)%s*")
if ln == "*" then matches = true end
if ln == key_identity.id then matches = true end
if globish_match(ln, key_identity.name) then matches = true end
line = permfile:read()
end
end
io.close(permfile)
if matches then return true end
-- No matching key found, let's see if the command matches one the
-- admin allowed to be performed anonymously
for _,v in ipairs(_safe_commands) do
if (v == command[1]) then
return true
end
end
-- No matches found anywhere, then don't permit this operation
return false
end
end

View File

@ -0,0 +1,2 @@
IDF_project = "%%PROJECT%%"
IDF_push_script = "%%MTNPOSTPUSH%%"

View File

@ -0,0 +1,58 @@
-- ***** BEGIN LICENSE BLOCK *****
-- This file is part of InDefero, an open source project management application.
-- Copyright (C) 2011 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 *****
--
-- 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(IDF_push_script, IDF_project);
if pid == -1 then
print("could not execute " .. IDF_push_script)
return "continue",nil
end
for _,r in ipairs(_idf_revs[session_id]) do
pin:write(r .. "\n")
end
pin:close()
wait(pid)
return "continue",nil
end
})

View File

@ -1,79 +0,0 @@
-- ***** BEGIN LICENSE BLOCK *****
-- This file is part of InDefero, an open source project management application.
-- Copyright (C) 2010 Céondo Ltd and contributors.
--
-- InDefero is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.
--
-- InDefero is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
--
-- ***** END LICENSE BLOCK *****
--
-- controls the access rights for remote_stdio which is used by IDFs frontend
-- and other interested parties
--
function get_remote_automate_permitted(key_identity, command, options)
if (key_identity.id == "%%MTNCLIENTKEY%%") then
return true
end
return false
end
--
-- let IDF know of new arriving revisions to fill its timeline
--
_idf_revs = {}
push_hook_functions({
["start"] = function (session_id)
_idf_revs[session_id] = {}
return "continue",nil
end,
["revision_received"] = function (new_id, revision, certs, session_id)
table.insert(_idf_revs[session_id], new_id)
return "continue",nil
end,
["end"] = function (session_id, ...)
if table.getn(_idf_revs[session_id]) == 0 then
return "continue",nil
end
local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%");
if pid == -1 then
print("could not execute %%MTNPOSTPUSH%%")
return
end
for _,r in ipairs(_idf_revs[session_id]) do
pin:write(r .. "\n")
end
pin:close()
wait(pid)
return "continue",nil
end
})
--
-- Load local hooks if they exist.
--
-- The way this is supposed to work is that hooks.d can contain symbolic
-- links to lua scripts. These links MUST have the extension .lua
-- If the script needs some configuration, a corresponding file with
-- the extension .conf is the right spot.
--
-- First load the configuration of the hooks, if applicable
includedirpattern(get_confdir() .. "/hooks.d/", "*.conf")
-- Then load the hooks themselves
includedirpattern(get_confdir() .. "/hooks.d/", "*.lua")

View File

@ -1,92 +0,0 @@
-- ***** BEGIN LICENSE BLOCK *****
-- This file is part of InDefero, an open source project management application.
-- Copyright (C) 2010 Céondo Ltd and contributors.
--
-- InDefero is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.
--
-- InDefero is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
--
-- ***** END LICENSE BLOCK *****
--
-- controls the access rights for remote_stdio which is used by IDFs frontend
-- and other interested parties
--
function get_remote_automate_permitted(key_identity, command, options)
local read_only_commands = {
"get_corresponding_path", "get_content_changed", "tags", "branches",
"common_ancestors", "packet_for_fdelta", "packet_for_fdata",
"packets_for_certs", "packet_for_rdata", "get_manifest_of",
"get_revision", "select", "graph", "children", "parents", "roots",
"leaves", "ancestry_difference", "toposort", "erase_ancestors",
"descendents", "ancestors", "heads", "get_file_of", "get_file",
"interface_version", "get_attributes", "content_diff",
"file_merge", "show_conflicts", "certs", "keys", "get_file_size",
"get_extended_manifest_of"
}
for _,v in ipairs(read_only_commands) do
if (v == command[1]) then
return true
end
end
return false
end
--
-- let IDF know of new arriving revisions to fill its timeline
--
_idf_revs = {}
push_hook_functions({
["start"] = function (session_id)
_idf_revs[session_id] = {}
return "continue",nil
end,
["revision_received"] = function (new_id, revision, certs, session_id)
table.insert(_idf_revs[session_id], new_id)
return "continue",nil
end,
["end"] = function (session_id, ...)
if table.getn(_idf_revs[session_id]) == 0 then
return "continue",nil
end
local pin,pout,pid = spawn_pipe("%%MTNPOSTPUSH%%", "%%PROJECT%%");
if pid == -1 then
print("could not execute %%MTNPOSTPUSH%%")
return
end
for _,r in ipairs(_idf_revs[session_id]) do
pin:write(r .. "\n")
end
pin:close()
wait(pid)
return "continue",nil
end
})
--
-- Load local hooks if they exist.
--
-- The way this is supposed to work is that hooks.d can contain symbolic
-- links to lua scripts. These links MUST have the extension .lua
-- If the script needs some configuration, a corresponding file with
-- the extension .conf is the right spot.
--
-- First load the configuration of the hooks, if applicable
includedirpattern(get_confdir() .. "/hooks.d/", "*.conf")
-- Then load the hooks themselves
includedirpattern(get_confdir() .. "/hooks.d/", "*.lua")

View File

@ -0,0 +1,30 @@
-- ***** BEGIN LICENSE BLOCK *****
-- This file is part of InDefero, an open source project management application.
-- Copyright (C) 2011 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 *****
---- Load local hooks if they exist.
-- The way this is supposed to work is that hooks.d can contain symbolic
-- links to lua scripts. These links MUST have the extension .lua
-- If the script needs some configuration, a corresponding file with
-- the extension .conf is the right spot.
----
-- First load the configuration of the hooks, if applicable
includedirpattern(get_confdir() .. "/hooks.d/","*.conf")
-- Then load the hooks themselves
includedirpattern(get_confdir() .. "/hooks.d/","*.lua")

View File

@ -0,0 +1 @@
%%MTNCLIENTKEY%%

View File

@ -280,6 +280,14 @@ class IDF_Scm_Git extends IDF_Scm
public static function getAuthAccessUrl($project, $user, $commit=null)
{
// if the user haven't registred a public ssh key,
// he can't use the write url which use the SSH authentification
if ($user != null) {
$keys = $user->get_idf_key_list();
if (count ($keys) == 0)
return self::getAnonymousAccessUrl($project);
}
return sprintf(Pluf::f('git_write_remote_url'), $project->shortname);
}

View File

@ -70,11 +70,6 @@ class IDF_Scm_Monotone_Stdio
*/
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);

View File

@ -168,6 +168,7 @@ class IDF_Template_MarkdownPrefilter extends Pluf_Text_HTML_Filter
'https',
'ftp',
'mailto',
'irc'
);
// tags which should be removed if they contain no content
// (e.g. "<b></b>" or "<b />")

View File

@ -78,6 +78,164 @@ class IDF_Views_Issue
$params, $request);
}
/**
* View the issues watch list of a given user.
* Limited to a specified project
*/
public $watchList_precond = array('IDF_Precondition::accessIssues',
'Pluf_Precondition::loginRequired');
public function watchList($request, $match)
{
$prj = $request->project;
$otags = $prj->getTagIdsByStatus('open');
$ctags = $prj->getTagIdsByStatus('closed');
if (count($otags) == 0) $otags[] = 0;
if (count($ctags) == 0) $ctags[] = 0;
// Get the id list of issue in the user watch list (for all projects !)
$db =& Pluf::db();
$sql_results = $db->select('SELECT idf_issue_id as id FROM '.Pluf::f('db_table_prefix', '').'idf_issue_pluf_user_assoc WHERE pluf_user_id='.$request->user->id);
$issue_ids = array(0);
foreach ($sql_results as $id) {
$issue_ids[] = $id['id'];
}
$issue_ids = implode (',', $issue_ids);
// Count open and close issues
$sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array($prj->id));
$nb_open = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
$sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array($prj->id));
$nb_closed = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
// Generate a filter for the paginator
switch ($match[2]) {
case 'closed':
$title = sprintf(__('Watch List: Closed Issues for %s'), (string) $prj);
$summary = __('This table shows the closed issues in your watch list for %s project.', (string) $prj);
$f_sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array($prj->id));
break;
case 'open':
default:
$title = sprintf(__('Watch List: Open Issues for %s'), (string) $prj);
$summary = __('This table shows the open issues in your watch list for %s project.', (string) $prj);
$f_sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array($prj->id));
break;
}
// Paginator to paginate the issues
$pag = new Pluf_Paginator(new IDF_Issue());
$pag->class = 'recent-issues';
$pag->item_extra_props = array('project_m' => $prj,
'shortname' => $prj->shortname,
'current_user' => $request->user);
$pag->summary = $summary;
$pag->forced_where = $f_sql;
$pag->action = array('IDF_Views_Issue::watchList', array($prj->shortname, $match[1]));
$pag->sort_order = array('modif_dtime', 'ASC'); // will be reverted
$pag->sort_reverse_order = array('modif_dtime');
$pag->sort_link_title = true;
$pag->extra_classes = array('a-c', '', 'a-c', '');
$list_display = array(
'id' => __('Id'),
array('summary', 'IDF_Views_Issue_SummaryAndLabels', __('Summary')),
array('status', 'IDF_Views_Issue_ShowStatus', __('Status')),
array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')),
);
$pag->configure($list_display, array(), array('id', 'status', 'modif_dtime'));
$pag->items_per_page = 10;
$pag->no_results_text = __('No issues were found.');
$pag->setFromRequest($request);
return Pluf_Shortcuts_RenderToResponse('idf/issues/project-watchlist.html',
array('project' => $prj,
'page_title' => $title,
'open' => $nb_open,
'closed' => $nb_closed,
'issues' => $pag,
),
$request);
}
/**
* View the issues watch list of a given user.
* For all projects
*/
public $forgeWatchList_precond = array('Pluf_Precondition::loginRequired');
public function forgeWatchList($request, $match)
{
$otags = array();
$ctags = array();
// Note that this approach does not scale, we will need to add
// a table to cache the meaning of the tags for large forges.
foreach (IDF_Views::getProjects($request->user) as $project) {
$otags = array_merge($otags, $project->getTagIdsByStatus('open'));
}
foreach (IDF_Views::getProjects($request->user) as $project) {
$ctags = array_merge($ctags, $project->getTagIdsByStatus('closed'));
}
if (count($otags) == 0) $otags[] = 0;
if (count($ctags) == 0) $ctags[] = 0;
// Get the id list of issue in the user watch list (for all projects !)
$db =& Pluf::db();
$sql_results = $db->select('SELECT idf_issue_id as id FROM '.Pluf::f('db_table_prefix', '').'idf_issue_pluf_user_assoc WHERE pluf_user_id='.$request->user->id);
$issue_ids = array(0);
foreach ($sql_results as $id) {
$issue_ids[] = $id['id'];
}
$issue_ids = implode (',', $issue_ids);
// Count open and close issues
$sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array());
$nb_open = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
$sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array());
$nb_closed = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
// Generate a filter for the paginator
switch ($match[1]) {
case 'closed':
$title = sprintf(__('Watch List: Closed Issues'));
$summary = __('This table shows the closed issues in your watch list.');
$f_sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array());
break;
case 'open':
default:
$title = sprintf(__('Watch List: Open Issues'));
$summary = __('This table shows the open issues in your watch list.');
$f_sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array());
break;
}
// Paginator to paginate the issues
$pag = new Pluf_Paginator(new IDF_Issue());
$pag->class = 'recent-issues';
$pag->item_extra_props = array('current_user' => $request->user);
$pag->summary = $summary;
$pag->forced_where = $f_sql;
$pag->action = array('IDF_Views_Issue::forgeWatchList', array($match[1]));
$pag->sort_order = array('modif_dtime', 'ASC'); // will be reverted
$pag->sort_reverse_order = array('modif_dtime');
$pag->sort_link_title = true;
$pag->extra_classes = array('a-c', '', 'a-c', 'a-c', 'a-c');
$list_display = array(
'id' => __('Id'),
array('summary', 'IDF_Views_Issue_SummaryAndLabelsUnknownProject', __('Summary')),
array('project', 'Pluf_Paginator_FkToString', __('Project')),
array('status', 'IDF_Views_Issue_ShowStatus', __('Status')),
array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')),
);
$pag->configure($list_display, array(), array('id', 'project', 'status', 'modif_dtime'));
$pag->items_per_page = 10;
$pag->no_results_text = __('No issues were found.');
$pag->setFromRequest($request);
return Pluf_Shortcuts_RenderToResponse('idf/issues/forge-watchlist.html',
array('page_title' => $title,
'open' => $nb_open,
'closed' => $nb_closed,
'issues' => $pag,
),
$request);
}
/**
* View the issues of a given user.
*
@ -280,6 +438,26 @@ class IDF_Views_Issue
}
}
// Search previous and next issue id
$octags = $prj->getTagIdsByStatus(($closed) ? 'closed' : 'open');
if (count($octags) == 0) $octags[] = 0;
$sql_previous = new Pluf_SQL('project=%s AND status IN ('.implode(', ', $octags).') AND id<%s',
array($prj->id, $match[2])
);
$sql_next = new Pluf_SQL('project=%s AND status IN ('.implode(', ', $octags).') AND id>%s',
array($prj->id, $match[2])
);
$previous_issue = Pluf::factory('IDF_Issue')->getList(array('filter' => $sql_previous->gen(),
'order' => 'id DESC',
'nb' => 1
));
$next_issue = Pluf::factory('IDF_Issue')->getList(array('filter' => $sql_next->gen(),
'order' => 'id ASC',
'nb' => 1
));
$previous_issue_id = (isset($previous_issue[0])) ? $previous_issue[0]->id : 0;
$next_issue_id = (isset($next_issue[0])) ? $next_issue[0]->id : 0;
$arrays = self::autoCompleteArrays($prj);
return Pluf_Shortcuts_RenderToResponse('idf/issues/view.html',
array_merge(
@ -292,6 +470,8 @@ class IDF_Views_Issue
'closed' => $closed,
'preview' => $preview,
'interested' => $interested->count(),
'previous_issue_id' => $previous_issue_id,
'next_issue_id' => $next_issue_id
),
$arrays),
$request);
@ -540,6 +720,17 @@ class IDF_Views_Issue
}
}
/**
* When you access to your forge watch list, issue don't known
* the project shortname.
*/
function IDF_Views_Issue_SummaryAndLabelsUnknownProject($field, $issue, $extra='')
{
$shortname = $issue->get_project()->shortname;
$issue->__set('shortname', $shortname);
return IDF_Views_Issue_SummaryAndLabels ($field, $issue, $extra);
}
/**
* Display the summary of an issue, then on a new line, display the
* list of labels with a link to a view "by label only".
@ -576,3 +767,5 @@ function IDF_Views_Issue_ShowStatus($field, $issue, $extra='')
{
return Pluf_esc($issue->get_status()->name);
}

View File

@ -98,17 +98,16 @@ $cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s';
# 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;
# Full path to the directory tree which contains default configuration files
# that are automatically created for new projects. This is only needed
# if $cfg['mtn_db_access'] is set to remote, i.e. in case the SyncMonotone
# plugin should be used. If unset, it defaults to the tree underknees
# src/IDF/Plugin/SyncMonotone/. Don't forget the trailing slash!
#$cfg['mtn_confdir'] = '/path/to/dir/tree/';
# Additional configuration files you want to create / copy for new setups.
# All these file paths have to be relative to $cfg['mtn_confdir'].
#$cfg['mtn_confdir_extra'] = array('hooks.d/something.lua')
# Needs to be configured for remote / usher usage.
# This allows basic control of a running usher process via the forge
@ -256,6 +255,12 @@ $cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git',
'mtn' => 'IDF_Scm_Monotone',
);
# Specific git config
# The core.quotepath is configured on new repository
# True -> All characters upper than 0x80 will be escape (default)
# False -> Characters is print directly, enable accented character in a UTF-8 shell
# $cfg['git_core_quotepath'] = false;
# 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

View File

@ -141,6 +141,16 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/view/attachment/(\d+)/(.*)$#',
'model' => 'IDF_Views_Issue',
'method' => 'viewAttachment');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/watchlist/(\w+)$#',
'base' => $base,
'model' => 'IDF_Views_Issue',
'method' => 'watchList');
$ctl[] = array('regex' => '#^/watchlist/(\w+)$#',
'base' => $base,
'model' => 'IDF_Views_Issue',
'method' => 'forgeWatchList');
// ---------- SCM ----------------------------------------
$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/help/$#',

View File

@ -25,6 +25,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="{media '/idf/css/yui.css'}" />
<link rel="stylesheet" type="text/css" href="{media '/idf/css/style.css'}" />
<link rel="icon" type="image/png" href="{media '/idf/img/favicon.png'}" />
<!--[if lt IE 7]>
<link rel="stylesheet" type="text/css" href="{media '/idf/css/ie6.css'}" />
<![endif]-->

View File

@ -25,6 +25,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="{media '/idf/css/yui.css'}" />
<link rel="stylesheet" type="text/css" href="{media '/idf/css/style.css'}" />
<link rel="icon" type="image/png" href="{media '/idf/img/favicon.png'}" />
<!--[if lt IE 7]>
<link rel="stylesheet" type="text/css" href="{media '/idf/css/ie6.css'}" />
<![endif]-->

View File

@ -25,6 +25,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="{media '/idf/css/yui.css'}" />
<link rel="stylesheet" type="text/css" href="{media '/idf/css/style.css'}" />
<link rel="icon" type="image/png" href="{media '/idf/img/favicon.png'}" />
<!--[if lt IE 7]>
<link rel="stylesheet" type="text/css" href="{media '/idf/css/ie6.css'}" />
<![endif]-->

View File

@ -3,7 +3,8 @@
{block subtabs}
<div id="sub-tabs">
<a {if $inOpenIssues}class="active" {/if}href="{url 'IDF_Views_Issue::index', array($project.shortname)}">{trans 'Open Issues'}</a>
{if !$user.isAnonymous()} | <a {if $inCreate}class="active" {/if}href="{url 'IDF_Views_Issue::create', array($project.shortname)}">{trans 'New Issue'}</a> | <a {if $inMyIssues}class="active" {/if}href="{url 'IDF_Views_Issue::myIssues', array($project.shortname, 'submit')}">{trans 'My Issues'}</a>{/if} |
{if !$user.isAnonymous()} | <a {if $inCreate}class="active" {/if}href="{url 'IDF_Views_Issue::create', array($project.shortname)}">{trans 'New Issue'}</a> | <a {if $inMyIssues}class="active" {/if}href="{url 'IDF_Views_Issue::myIssues', array($project.shortname, 'submit')}">{trans 'My Issues'}</a>
| <a {if $inWatchList}class="active" {/if}href="{url 'IDF_Views_Issue::watchList', array($project.shortname, 'open')}">{trans 'My watch list'}</a>{/if} |
<form class="star" action="{url 'IDF_Views_Issue::search', array($project.shortname)}" method="get">
<input accesskey="4" type="text" value="{$q}" name="q" size="20" />
<input type="submit" name="s" value="{trans 'Search'}" />

View File

@ -0,0 +1,12 @@
{extends "idf/base-simple.html"}
{block body}
{$issues.render}
{/block}
{block context}
{aurl 'open_url', 'IDF_Views_Issue::forgeWatchList', array('open')}
{aurl 'closed_url', 'IDF_Views_Issue::forgeWatchList', array('closed')}
{blocktrans}<p><strong>Open issues:</strong> <a href="{$open_url}">{$open}</a></p>
<p><strong>Closed issues:</strong> <a href="{$closed_url}">{$closed}</a></p>{/blocktrans}
{/block}

View File

@ -0,0 +1,17 @@
{extends "idf/issues/base.html"}
{block docclass}yui-t2{assign $inWatchList = true}{/block}
{block body}
{$issues.render}
{if !$user.isAnonymous()}
{aurl 'url', 'IDF_Views_Issue::create', array($project.shortname)}
<p><a href="{$url}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/add.png'}" alt="+" align="bottom" /></a> <a href="{$url}">{trans 'New Issue'}</a></p>{/if}
{/block}
{block context}
{aurl 'open_url', 'IDF_Views_Issue::watchList', array($project.shortname, 'open')}
{aurl 'closed_url', 'IDF_Views_Issue::watchList', array($project.shortname, 'closed')}
{blocktrans}<p><strong>Open issues:</strong> <a href="{$open_url}">{$open}</a></p>
<p><strong>Closed issues:</strong> <a href="{$closed_url}">{$closed}</a></p>{/blocktrans}
{/block}

View File

@ -1,6 +1,15 @@
{extends "idf/issues/base.html"}
{block titleicon}{if $form}<form class="star" method="post" action="{url 'IDF_Views_Issue::star', array($project.shortname, $issue.id)}"><input type="image" src="{if $starred}{media '/idf/img/star.png'}{else}{media '/idf/img/star-grey.png'}{/if}" name="submit" title="{if $starred}{trans 'Remove this issue from your watch list'}{else}{trans 'Add this issue to your watch list'}{/if}" /></form> {/if}{/block}
{block body}
<div style="float:right;">
{if $previous_issue_id}
<a href="{url 'IDF_Views_Issue::view', array($project.shortname, $previous_issue_id)}" title="{if $closed}{trans 'Click here to view the previous closed issue'}{else}{trans 'Click here to view the previous open issue'}{/if}">Previous issue</a>
{/if}
{if $previous_issue_id and $next_issue_id} - {/if}
{if $next_issue_id}
<a href="{url 'IDF_Views_Issue::view', array($project.shortname, $next_issue_id)}" title="{if $closed}{trans 'Click here to view the next closed issue'}{else}{trans 'Click here to view the next open issue'}{/if}">Next issue</a>
{/if}
</div>
{assign $i = 0}
{assign $nc = $comments.count()}
{foreach $comments as $c}{ashowuser 'submitter', $c.get_submitter(), $request}

View File

@ -11,5 +11,7 @@
<p>{blocktrans}<a href="{$url}">Update your account</a>.{/blocktrans}</p>
{aurl 'url', 'IDF_Views_User::view', array($user.login)}
<p>{blocktrans}<a href="{$url}">See your public profile</a>.{/blocktrans}</p>
{aurl 'url', 'IDF_Views_Issue::forgeWatchList', array('open')}
<p>{blocktrans}<a href="{$url}">See your forge issue watch list</a>.{/blocktrans}</p>
{/block}

View File

@ -18,6 +18,9 @@ by {$submitter}.{/blocktrans}</p>
</div>
{/if}
<div id="wiki-content">
<div id="wiki-toc"><span id="contentheader">{trans 'Table of Content'}</span><div id="wiki-toc-content"></div></div>
<script type="text/javascript" src="{media '/idf/js/wiki-toc.js'}"></script>
<p class="desc">{$page.summary}</p>
{if !$oldrev}
@ -29,6 +32,7 @@ by {$submitter}.{/blocktrans}</p>
{/if}
{/if}
</div>
{/block}
{block context}
{ashowuser 'submitter', $page.get_submitter(), $request}

View File

@ -225,6 +225,7 @@ div.issue-comment {
div.issue-comment-first {
border-top: 1px solid #d3d7cf;
clear: both;
}
div.issue-comment-signin {
@ -702,6 +703,14 @@ div.deprecated-page {
color: #a00;
}
ul > li {
list-style: disc outside none;
}
ol > li {
list-style: decimal outside none;
}
#branding {
float: right;
position: relative;
@ -737,6 +746,32 @@ div.deprecated-page {
margin: 0px;
}
#wiki-toc {
float: right;
margin-left: 10px;
}
#wiki-toc-content {
border: 1px solid #999999;
border-width: 1px 0;
padding: 10px 0;
padding-bottom: 25px;
background-color: #ffffff;
display: block;
}
#wiki-toc-content a {
display: block;
}
#wiki-toc-content a.wiki-h2 {
margin-left: 1em;
}
#wiki-toc-content a.wiki-h3 {
margin-left: 2em;
}
/**
* main menu
*/

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

View File

@ -0,0 +1,7 @@
$(document).ready(function() {
$(":header", "#wiki-content").map(function (index) {
this.id = "wikititle_" + index;
$("<a href='#" + this.id + "'>" + this.innerText + "</a>").addClass("wiki-" + this.tagName.toLowerCase()).appendTo('#wiki-toc-content');
});
});