diff --git a/doc/readme-monotone.mdtext b/doc/readme-monotone.mdtext deleted file mode 100644 index cbaf8ba..0000000 --- a/doc/readme-monotone.mdtext +++ /dev/null @@ -1,62 +0,0 @@ -# monotone implementation notes - -## general - - This version of indefero contains an implementation of the monotone - automation interface. It needs at least monotone version 0.99 - (interface version 13.0) or newer. - - To set up a new IDF project with monotone quickly, all you need to do - is to create a new monotone database with - - $ mtn db init -d project.mtn - - in the configured repository path `$cfg['mtn_repositories']` and - configure `$cfg['mtn_db_access']` to "local". - - To have a really workable setup, this database needs an initial commit - on the configured master branch of the project. This can be done easily - with - - $ mkdir tmp && touch tmp/remove_me - $ mtn import -d project.mtn -b master.branch.name \ - -m "initial commit" tmp - $ rm -rf tmp - - Its expected that more scripts arrive soon to automate this and other - tasks in the future for (multi)forge setups. - -## current state / internals - - The implementation should be fairly stable and fast, though some - information, such as individual file sizes or last change information, - won't scale well with the tree size. Its expected that the mtn - automation interface improves in this area in the future and that - these parts can then be rewritten with speed in mind. - - As the idf.conf-dist explains more in detail, different access patterns - are possible to retrieve changeset data from monotone. Please refer - to the documentation there for more information. - -## indefero critique: - - It was not always 100% clear what some of the abstract SCM API method - wanted in return. While it helped a lot to have prior art in form of the - SVN and git implementation, the documentation of the abstract IDF_Scm - should probably still be improved. - - Since branch and tag names can be of arbitrary size, it was not possible - to display them completely in the default layout. This might be a problem - in other SCMs as well, in particular for the monotone implementation I - introduced a special filter, called "IDF_Views_Source_ShortenString". - - The API methods getPathInfo() and getTree() return similar VCS "objects" - which unfortunately do not have a well-defined structure - this should - probably addressed in future indefero releases. - - While the returned objects from getTree() contain all the needed - information, indefero doesn't seem to use them to sort the output - f.e. alphabetically or in such a way that directories are outputted - before files. It was unclear if the SCM implementor should do this - task or not and what the admired default sorting should be. - diff --git a/doc/syncmonotone.mdtext b/doc/syncmonotone.mdtext new file mode 100644 index 0000000..89c1350 --- /dev/null +++ b/doc/syncmonotone.mdtext @@ -0,0 +1,175 @@ +# Plugin SyncMonotone by Thomas Keller (me@thomaskeller.biz) + +The SyncMonotone plugin allow the direct creation and synchronisation of +monotone repositories with the InDefero database. It has been built to +work together with monotone's "super server" usher, which is used to control +several repositories at once, acts as proxy and single entrance. + +## Prerequisites + +* a unixoid operating system +* monotone >= 0.99 +* for a proxy setup with usher: + * boost headers (for usher compilation) + * a current version of usher + * a daemonizer, like supervise + +## Installation of monotone + +If you install monotone from a distribution package, ensure you do not +install and / or activate the server component. We just need a plain +client installation which usually consists only of the `mtn` binary and +a few docs. + +If you install monotone from source (), +please follow the `INSTALL` document which comes with the software. +It contains detailed instructions, including all needed dependencies. + +## Choose your indefero setup + +The monotone plugin can be used in several different ways: + +1. One database for everything. This is the easiest setup and of possible + use in case you do not want indefero to manage the access to your project. + Your `idf.php` should look like this: + + $ cat idf.php + ... + $cfg['mtn_path'] = 'mtn'; + $cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles'); + $cfg['mtn_repositories'] = '/home/monotone/all_projects.mtn'; + $cfg['mtn_remote_url'] = 'ssh://monotone@my.server.com:~all_projects.mtn'; + $cfg['mtn_db_access'] = 'local'; + ... + + Pro: + * easy to setup and to manage + + Con: + * you need to give committers SSH access to your machine + * database lock problem: the database from which + indefero reads its data might be locked in case a user + syncs at the very moment via SSH + +2. One database for every project. Similar to the above setup, but this + time you use the '%s' placeholder which is replaced with the short name + of the indefero project: + + $ cat idf.php + ... + $cfg['mtn_path'] = 'mtn'; + $cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles'); + $cfg['mtn_repositories'] = '/home/monotone/%s.mtn'; + $cfg['mtn_remote_url'] = 'ssh://monotone@my.server.com:~%s.mtn'; + $cfg['mtn_db_access'] = 'local'; + ... + + The same pro's and con's apply. Additionally you have to be careful about + not giving people physical read/write access of another project's database. + + Furthermore, if you do not want to use `ssh`, but `netsync` transport, + each project's database must be served over a separate port. + +3. One database for every project, all managed with usher. This is the + recommended setup for a mid-size forge setup. The remaining part of this + document will describe the process to set this up in detail. + + Pro: + * access rights can be granted per project and are automatically + managed by indefero, just like the user's public monotone keys + * no database locking issues + * one public server running on the one well-known port + + Con: + * harder to setup + +## Installation and configuration of usher + +1. Clone usher's monotone repository: + + $ mtn clone "mtn://monotone.ca?net.venge.monotone.contrib.usher" + +2. Compile usher: + + $ autoreconf -i + $ ./configure && make + $ sudo make install + + This installs the usher binary in $prefix/bin. + +3. Create a new usher user: + + $ adduser --system --disabled-login --home /var/lib/usher usher + +4. Create the basic usher setup: + + $ cd /var/lib/usher + $ mkdir projects logs + $ cat > usher.conf + userpass "admin" "" + adminaddr "127.0.0.1:12345" + logdir "log" + ^D + $ chmod 600 usher.conf + + Your indefero www user needs later write access to `usher.conf` and + `projects/`. There are two ways of setting this up: + + * Make the usher user the web user, for example via Apache's `suexec` + * Use acls, like this: + + $ setfacl -m u:www:rw usher.conf + $ setfacl -m d:u:www:rwx projects/ + +5. Wrap a daemonizer around usher, for example supervise from daemontools + (): + + $ cat > run + #!/bin/sh + cd /var/lib/usher + exec 2>&1 + exec \ + setuidgid usher \ + usher usher.conf + ^D + + The service can now be started through supervise: + + $ supervise /var/lib/usher + +## Configuration of indefero + +Based on the above setup, the configuration in `src/IDF/conf/idf.php` should +look like this: + + $ cat idf.php + ... + $cfg['mtn_path'] = 'mtn'; + $cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles'); + $cfg['mtn_repositories'] = '/var/lib/usher/projects/%s/'; + $cfg['mtn_remote_url'] = 'mtn://my.server.com/%s'; + $cfg['mtn_db_access'] = 'remote'; + $cfg['mtn_remote_auth'] = true; + $cfg['mtn_usher_conf'] = '/var/lib/usher/usher.conf'; + ... + +The `%s` placeholders are automatically replaced by the name of the +indefero project. The plugin assumes that every project is separated +by a distinct server name in the monotone URL (hence the use of `/%s`), +so if a user calls + + $ mtn sync mtn://my.server.com/project1 + +then the database / repository of the indefero `project1` is used. +Note that 'mtn_remote_url' is also used as internal URI to query the data +for indefero's source view, so it *must* be a valid host! + +Usher also allows the identification of a project repository by hostname, +which would allow an URL template like `mtn://%s.my.server.com`, however +the plugin does not write out the configuration which is needed for this +yet. + +For even more advanced setups, usher can also be used to forward sync +requests to other remote servers for load balancing, please consult the +README file for more information. + diff --git a/scripts/mtn-post-push b/scripts/mtn-post-push index 46f5d3b..e41b41f 100755 --- a/scripts/mtn-post-push +++ b/scripts/mtn-post-push @@ -15,8 +15,7 @@ res=$(cd "$dir" && /bin/pwd || "$dir") SCRIPTDIR="$res/$(readlink $0)" PHP_POST_PUSH=$SCRIPTDIR/mtnpostpush.php -base=$(basename "$0") -TMPFILE=$(mktemp /tmp/${tempfoo}.XXXXXX) || exit 1 +TMPFILE=$(mktemp /tmp/mtn-post-push.XXXXXX) || exit 1 while read rev; do echo $rev >> $TMPFILE; done echo php $PHP_POST_PUSH "$1" \< $TMPFILE \&\& rm -f $TMPFILE |\ diff --git a/src/IDF/Diff.php b/src/IDF/Diff.php index c51e883..d6abe56 100644 --- a/src/IDF/Diff.php +++ b/src/IDF/Diff.php @@ -72,6 +72,9 @@ class IDF_Diff $indiff = true; continue; } else if (0 === strpos($line, '=========')) { + // ignore pseudo stanzas with a hint of a binary file + if (preg_match("/^# (.+) is binary/", $this->lines[$i])) + continue; // by default always use the new name of a possibly renamed file $current_file = self::getMtnFile($this->lines[$i+1]); // mtn 0.48 and newer set /dev/null as file path for dropped files diff --git a/src/IDF/Form/Admin/ProjectCreate.php b/src/IDF/Form/Admin/ProjectCreate.php index d297f14..f6b75a8 100644 --- a/src/IDF/Form/Admin/ProjectCreate.php +++ b/src/IDF/Form/Admin/ProjectCreate.php @@ -313,6 +313,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form 'labels_download_one_max' => IDF_Form_UploadConf::init_one_max, 'labels_wiki_predefined' => IDF_Form_WikiConf::init_predefined, 'labels_wiki_one_max' => IDF_Form_WikiConf::init_one_max, + 'labels_issue_template' => IDF_Form_IssueTrackingConf::init_template, 'labels_issue_open' => IDF_Form_IssueTrackingConf::init_open, 'labels_issue_closed' => IDF_Form_IssueTrackingConf::init_closed, 'labels_issue_predefined' => IDF_Form_IssueTrackingConf::init_predefined, diff --git a/src/IDF/Form/IssueCreate.php b/src/IDF/Form/IssueCreate.php index 56f9ceb..3c38e7b 100644 --- a/src/IDF/Form/IssueCreate.php +++ b/src/IDF/Form/IssueCreate.php @@ -45,6 +45,9 @@ class IDF_Form_IssueCreate extends Pluf_Form or $this->user->hasPerm('IDF.project-member', $this->project)) { $this->show_full = true; } + $contentTemplate = $this->project->getConf()->getVal( + 'labels_issue_template', IDF_Form_IssueTrackingConf::init_template + ); $this->fields['summary'] = new Pluf_Form_Field_Varchar( array('required' => true, 'label' => __('Summary'), @@ -57,7 +60,7 @@ class IDF_Form_IssueCreate extends Pluf_Form $this->fields['content'] = new Pluf_Form_Field_Varchar( array('required' => true, 'label' => __('Description'), - 'initial' => '', + 'initial' => $contentTemplate, 'widget' => 'Pluf_Form_Widget_TextareaInput', 'widget_attrs' => array( 'cols' => 58, diff --git a/src/IDF/Form/IssueTrackingConf.php b/src/IDF/Form/IssueTrackingConf.php index ae7a9d0..0aa4f59 100644 --- a/src/IDF/Form/IssueTrackingConf.php +++ b/src/IDF/Form/IssueTrackingConf.php @@ -31,6 +31,15 @@ class IDF_Form_IssueTrackingConf extends Pluf_Form * Defined as constants to easily access the value in the * IssueUpdate/Create form in the case nothing is in the db yet. */ + const init_template = 'Steps to reproduce the problem: +1. +2. +3. + +Expected result: + +Actual result: +'; const init_open = 'New = Issue has not had initial review yet Accepted = Problem reproduced / Need acknowledged Started = Work on this issue has begun'; @@ -66,6 +75,15 @@ Maintainability = Hinders future changes'; public function initFields($extra=array()) { + $this->fields['labels_issue_template'] = new Pluf_Form_Field_Varchar( + array('required' => false, + 'label' => __('Define an issue template to hint the reporter to provide certain information'), + 'initial' => self::init_template, + 'widget_attrs' => array('rows' => 7, + 'cols' => 75), + 'widget' => 'Pluf_Form_Widget_TextareaInput', + )); + $this->fields['labels_issue_open'] = new Pluf_Form_Field_Varchar( array('required' => true, 'label' => __('Open issue status values'), @@ -99,8 +117,6 @@ Maintainability = Hinders future changes'; 'widget_attrs' => array('size' => 60), )); - - } } diff --git a/src/IDF/Key.php b/src/IDF/Key.php index c9d7eff..2320d8f 100644 --- a/src/IDF/Key.php +++ b/src/IDF/Key.php @@ -80,7 +80,7 @@ class IDF_Key extends Pluf_Model if (preg_match('#^\[pubkey ([^\]]+)\]\s*(\S+)\s*\[end\]$#', $this->content, $m)) { return array('mtn', $m[1], $m[2]); } - else if (preg_match('#^ssh\-[a-z]{3}\s(\S+)\s(\S+)$#', $this->content, $m)) { + else if (preg_match('#^ssh\-[a-z]{3}\s(\S+)(?:\s(\S+))?$#', $this->content, $m)) { return array('ssh', $m[2], $m[1]); } diff --git a/src/IDF/Middleware.php b/src/IDF/Middleware.php index 424cc01..b836e25 100644 --- a/src/IDF/Middleware.php +++ b/src/IDF/Middleware.php @@ -112,6 +112,7 @@ function IDF_Middleware_ContextPreProcessor($request) $c = array_merge($c, $request->rights); } $c['usherConfigured'] = Pluf::f("mtn_usher_conf", null) !== null; + $c['allProjects'] = IDF_Views::getProjects($request->user); return $c; } diff --git a/src/IDF/Plugin/SyncMonotone.php b/src/IDF/Plugin/SyncMonotone.php index 19854e0..2da68cd 100644 --- a/src/IDF/Plugin/SyncMonotone.php +++ b/src/IDF/Plugin/SyncMonotone.php @@ -76,6 +76,10 @@ class IDF_Plugin_SyncMonotone return; } + if (Pluf::f('mtn_db_access', 'local') == 'local') { + return; + } + $projecttempl = Pluf::f('mtn_repositories', false); if ($projecttempl === false) { throw new IDF_Scm_Exception( @@ -246,7 +250,9 @@ class IDF_Plugin_SyncMonotone array('key' => 'server', 'values' => array($shortname)), array('key' => 'local', 'values' => array( '--confdir', $projectpath, - '-d', $dbfile + '-d', $dbfile, + '--timestamps', + '--ticker=dot' )), ); @@ -278,6 +284,10 @@ class IDF_Plugin_SyncMonotone return; } + if (Pluf::f('mtn_db_access', 'local') == 'local') { + return; + } + $mtn = IDF_Scm_Monotone::factory($project); $stdio = $mtn->getStdio(); @@ -338,6 +348,10 @@ class IDF_Plugin_SyncMonotone return; } + if (Pluf::f('mtn_db_access', 'local') == 'local') { + return; + } + $usher_config = Pluf::f('mtn_usher_conf', false); if (!$usher_config || !is_writable($usher_config)) { throw new IDF_Scm_Exception( @@ -419,8 +433,13 @@ class IDF_Plugin_SyncMonotone */ public function processKeyCreate($key) { - if ($key->getType() != 'mtn') + if ($key->getType() != 'mtn') { return; + } + + if (Pluf::f('mtn_db_access', 'local') == 'local') { + return; + } foreach (Pluf::factory('IDF_Project')->getList() as $project) { $conf = new IDF_Conf(); @@ -525,8 +544,13 @@ class IDF_Plugin_SyncMonotone */ public function processKeyDelete($key) { - if ($key->getType() != 'mtn') + if ($key->getType() != 'mtn') { return; + } + + if (Pluf::f('mtn_db_access', 'local') == 'local') { + return; + } foreach (Pluf::factory('IDF_Project')->getList() as $project) { $conf = new IDF_Conf(); diff --git a/src/IDF/Scm/Monotone.php b/src/IDF/Scm/Monotone.php index a0b01c6..74c6b78 100644 --- a/src/IDF/Scm/Monotone.php +++ b/src/IDF/Scm/Monotone.php @@ -157,9 +157,11 @@ class IDF_Scm_Monotone extends IDF_Scm */ private function _getCerts($rev) { - static $certCache = array(); - - if (!array_key_exists($rev, $certCache)) { + $cache = Pluf_Cache::factory(); + $cachekey = 'mtn-plugin-certs-for-rev-' . $rev; + $certs = $cache->get($cachekey); + + if ($certs === null) { $out = $this->stdio->exec(array('certs', $rev)); $stanzas = IDF_Scm_Monotone_BasicIO::parse($out); @@ -183,10 +185,10 @@ class IDF_Scm_Monotone extends IDF_Scm } } } - $certCache[$rev] = $certs; + $cache->set($cachekey, $certs); } - return $certCache[$rev]; + return $certs; } /** @@ -212,34 +214,6 @@ class IDF_Scm_Monotone extends IDF_Scm return array_unique($certValues); } - /** - * Returns the revision in which the file has been last changed, - * starting from the start rev - * - * @param string - * @param string - * @return string - */ - private function _getLastChangeFor($file, $startrev) - { - $out = $this->stdio->exec(array( - 'get_content_changed', $startrev, $file - )); - - $stanzas = IDF_Scm_Monotone_BasicIO::parse($out); - - // FIXME: we only care about the first returned content mark - // everything else seem to be very, very rare cases - foreach ($stanzas as $stanza) { - foreach ($stanza as $stanzaline) { - if ($stanzaline['key'] == 'content_mark') { - return $stanzaline['hash']; - } - } - } - return null; - } - /** * @see IDF_Scm::inBranches() */ @@ -296,6 +270,84 @@ class IDF_Scm_Monotone extends IDF_Scm return $this->_getUniqueCertValuesFor($revs, 'tag', 't:'); } + /** + * Takes a single stanza coming from an extended manifest output + * and converts it into a file structure used by IDF + * + * @param string $forceBasedir If given then the element's path is checked + * to be directly beneath the given directory. + * If not, null is returned and the parsing is + * aborted. + * @return array | null + */ + private function _fillFileEntry(array $manifestEntry, $forceBasedir = null) + { + $fullpath = $manifestEntry[0]['values'][0]; + $filename = basename($fullpath); + $dirname = dirname($fullpath); + $dirname = $dirname == '.' ? '' : $dirname; + + if ($forceBasedir !== null && $forceBasedir != $dirname) { + return null; + } + + $file = array(); + $file['file'] = $filename; + $file['fullpath'] = $fullpath; + $file['efullpath'] = self::smartEncode($fullpath); + + $wanted_mark = ''; + if ($manifestEntry[0]['key'] == 'dir') { + $file['type'] = 'tree'; + $file['size'] = 0; + $wanted_mark = 'path_mark'; + } + else { + $file['type'] = 'blob'; + $file['hash'] = $manifestEntry[1]['hash']; + $size = 0; + foreach ($manifestEntry as $line) { + if ($line['key'] == 'size') { + $size = $line['values'][0]; + break; + } + } + $file['size'] = $size; + $wanted_mark = 'content_mark'; + } + + $rev_mark = null; + foreach ($manifestEntry as $line) { + if ($line['key'] == $wanted_mark) { + $rev_mark = $line['hash']; + break; + } + } + + if ($rev_mark !== null) { + $file['rev'] = $rev_mark; + $certs = $this->_getCerts($rev_mark); + + // FIXME: this assumes that author, date and changelog are always given + $file['author'] = implode(", ", $certs['author']); + + $dates = array(); + foreach ($certs['date'] as $date) + $dates[] = date('Y-m-d H:i:s', strtotime($date)); + $file['date'] = implode(', ', $dates); + $combinedChangelog = implode("\n---\n", $certs['changelog']); + $split = preg_split("/[\n\r]/", $combinedChangelog, 2); + // FIXME: the complete log message is currently not used in the + // tree view (the same is true for the other SCM implementations) + // but we _should_ really use or at least return that here + // in case we want to do fancy stuff like described in + // issue 492 + $file['log'] = $split[0]; + } + + return $file; + } + /** * @see IDF_Scm::getTree() */ @@ -307,59 +359,21 @@ class IDF_Scm_Monotone extends IDF_Scm } $out = $this->stdio->exec(array( - 'get_manifest_of', $revs[0] + 'get_extended_manifest_of', $revs[0] )); $files = array(); $stanzas = IDF_Scm_Monotone_BasicIO::parse($out); - $folder = $folder == '/' || empty($folder) ? '' : $folder.'/'; + $folder = $folder == '/' || empty($folder) ? '' : $folder; foreach ($stanzas as $stanza) { if ($stanza[0]['key'] == 'format_version') continue; - $path = $stanza[0]['values'][0]; - if (!preg_match('#^'.$folder.'([^/]+)$#', $path, $m)) + $file = $this->_fillFileEntry($stanza, $folder); + if ($file === null) continue; - $file = array(); - $file['file'] = $m[1]; - $file['fullpath'] = $path; - $file['efullpath'] = self::smartEncode($path); - - if ($stanza[0]['key'] == 'dir') { - $file['type'] = 'tree'; - $file['size'] = 0; - } - else - { - $file['type'] = 'blob'; - $file['hash'] = $stanza[1]['hash']; - $file['size'] = strlen($this->getFile((object)$file)); - } - - $rev = $this->_getLastChangeFor($file['fullpath'], $revs[0]); - if ($rev !== null) { - $file['rev'] = $rev; - $certs = $this->_getCerts($rev); - - // FIXME: this assumes that author, date and changelog are always given - $file['author'] = implode(", ", $certs['author']); - - $dates = array(); - foreach ($certs['date'] as $date) - $dates[] = date('Y-m-d H:i:s', strtotime($date)); - $file['date'] = implode(', ', $dates); - $combinedChangelog = implode("\n---\n", $certs['changelog']); - $split = preg_split("/[\n\r]/", $combinedChangelog, 2); - // FIXME: the complete log message is currently not used in the - // tree view (the same is true for the other SCM implementations) - // but we _should_ really use or at least return that here - // in case we want to do fancy stuff like described in - // issue 492 - $file['log'] = $split[0]; - } - $files[] = (object) $file; } return $files; @@ -399,7 +413,7 @@ class IDF_Scm_Monotone extends IDF_Scm $certs = $scm->_getCerts($revs[0]); // for the very seldom case that a revision // has no branch certificate - if (count($certs['branch']) == 0) { + if (!array_key_exists('branch', $certs)) { $branch = '*'; } else @@ -505,7 +519,7 @@ class IDF_Scm_Monotone extends IDF_Scm return false; $out = $this->stdio->exec(array( - 'get_manifest_of', $revs[0] + 'get_extended_manifest_of', $revs[0] )); $files = array(); @@ -515,43 +529,10 @@ class IDF_Scm_Monotone extends IDF_Scm if ($stanza[0]['key'] == 'format_version') continue; - $path = $stanza[0]['values'][0]; - if (!preg_match('#^'.$file.'$#', $path, $m)) + if ($stanza[0]['values'][0] != $file) continue; - - $file = array(); - $file['fullpath'] = $path; - - if ($stanza[0]['key'] == "dir") { - $file['type'] = "tree"; - $file['hash'] = null; - $file['size'] = 0; - } - else - { - $file['type'] = 'blob'; - $file['hash'] = $stanza[1]['hash']; - $file['size'] = strlen($this->getFile((object)$file)); - } - - $pathinfo = pathinfo($file['fullpath']); - $file['file'] = $pathinfo['basename']; - - $rev = $this->_getLastChangeFor($file['fullpath'], $revs[0]); - if ($rev !== null) { - $file['rev'] = $rev; - $certs = $this->_getCerts($rev); - - // FIXME: this assumes that author, date and changelog are always given - $file['author'] = implode(", ", $certs['author']); - - $dates = array(); - foreach ($certs['date'] as $date) - $dates[] = date('Y-m-d H:i:s', strtotime($date)); - $file['date'] = implode(', ', $dates); - $file['log'] = implode("\n---\n", $certs['changelog']); - } - + + $file = $this->_fillFileEntry($stanza); return (object) $file; } return false; @@ -623,10 +604,12 @@ class IDF_Scm_Monotone extends IDF_Scm $dates[] = date('Y-m-d H:i:s', strtotime($date)); $res['date'] = implode(', ', $dates); - $res['title'] = implode("\n---\n", $certs['changelog']); + $combinedChangelog = implode("\n---\n", $certs['changelog']); + $split = preg_split("/[\n\r]/", $combinedChangelog, 2); + $res['title'] = $split[0]; + $res['full_message'] = (isset($split[1])) ? trim($split[1]) : ''; $res['commit'] = $revs[0]; - $res['changes'] = ($getdiff) ? $this->_getDiff($revs[0]) : ''; return (object) $res; @@ -680,11 +663,18 @@ class IDF_Scm_Monotone extends IDF_Scm // read in the initial branches we should follow if (count($initialBranches) == 0) { + if (!isset($certs['branch'])) { + throw new IDF_Scm_Exception(sprintf( + __("revision %s has no branch cert - cannot start ". + "logging from this revision"), $rev + )); + } $initialBranches = $certs['branch']; } // only add it to our log if it is on one of the initial branches - if (count(array_intersect($initialBranches, $certs['branch'])) > 0) { + // ignore revisions without any branch certificate + if (count(array_intersect($initialBranches, (array)@$certs['branch'])) > 0) { --$n; $log = array(); diff --git a/src/IDF/Scm/Monotone/BasicIO.php b/src/IDF/Scm/Monotone/BasicIO.php index 78707c5..e783a90 100644 --- a/src/IDF/Scm/Monotone/BasicIO.php +++ b/src/IDF/Scm/Monotone/BasicIO.php @@ -38,14 +38,15 @@ class IDF_Scm_Monotone_BasicIO { $pos = 0; $stanzas = array(); + $length = strlen($in); - while ($pos < strlen($in)) { + while ($pos < $length) { $stanza = array(); - while ($pos < strlen($in)) { + while ($pos < $length) { if ($in[$pos] == "\n") break; $stanzaLine = array('key' => '', 'values' => array(), 'hash' => null); - while ($pos < strlen($in)) { + while ($pos < $length) { $ch = $in[$pos]; if ($ch == '"' || $ch == '[') break; ++$pos; @@ -53,6 +54,9 @@ class IDF_Scm_Monotone_BasicIO $stanzaLine['key'] .= $ch; } + // symbol w/o a value list + if ($pos >= $length || $in[$pos] == "\n") break; + if ($in[$pos] == '[') { unset($stanzaLine['values']); ++$pos; // opening square bracket @@ -64,18 +68,31 @@ class IDF_Scm_Monotone_BasicIO { unset($stanzaLine['hash']); $valCount = 0; - while ($in[$pos] == '"') { - ++$pos; // opening quote + // if hashs and plain values are encountered in the same + // value list, we add the hash values as simple values as well + while ($in[$pos] == '"' || $in[$pos] == '[') { + $isHashValue = $in[$pos] == '['; + ++$pos; // opening quote / bracket $stanzaLine['values'][$valCount] = ''; - while ($pos < strlen($in)) { + while ($pos < $length) { $ch = $in[$pos]; $pr = $in[$pos-1]; - if ($ch == '"' && $pr != '\\') break; + if (($isHashValue && $ch == ']') + ||(!$isHashValue && $ch == '"' && $pr != '\\')) + break; ++$pos; $stanzaLine['values'][$valCount] .= $ch; } ++$pos; // closing quote - if ($pos >= strlen($in)) + if (!$isHashValue) { + $stanzaLine['values'][$valCount] = str_replace( + array("\\\\", "\\\""), + array("\\", "\""), + $stanzaLine['values'][$valCount] + ); + } + + if ($pos >= $length) break; if ($in[$pos] == ' ') { @@ -83,14 +100,6 @@ class IDF_Scm_Monotone_BasicIO ++$valCount; } } - - for ($i = 0; $i <= $valCount; $i++) { - $stanzaLine['values'][$i] = str_replace( - array("\\\\", "\\\""), - array("\\", "\""), - $stanzaLine['values'][$i] - ); - } } $stanza[] = $stanzaLine; diff --git a/src/IDF/Scm/Monotone/Usher.php b/src/IDF/Scm/Monotone/Usher.php index 5e55e08..ed423ea 100644 --- a/src/IDF/Scm/Monotone/Usher.php +++ b/src/IDF/Scm/Monotone/Usher.php @@ -76,7 +76,7 @@ class IDF_Scm_Monotone_Usher $single_conns = preg_split('/[ ]/', $conn); $ret = array(); foreach ($single_conns as $conn) { - preg_match('/\(\w+\)([^:]):(\d+)/', $conn, $matches); + preg_match('/\((\w+)\)([^:]+):(\d+)/', $conn, $matches); $ret[$matches[1]][] = (object)array( 'server' => $matches[1], 'address' => $matches[2], @@ -84,6 +84,12 @@ class IDF_Scm_Monotone_Usher ); } + if ($server !== null) { + if (array_key_exists($server, $ret)) + return $ret[$server]; + return array(); + } + return $ret; } diff --git a/src/IDF/Views/Project.php b/src/IDF/Views/Project.php index 48ed153..20854d9 100644 --- a/src/IDF/Views/Project.php +++ b/src/IDF/Views/Project.php @@ -61,6 +61,66 @@ class IDF_Views_Project $request); } + /** + * Returns an associative array with available model filters + * + * @return array + */ + private static function getAvailableModelFilters() + { + return array( + 'all' => __('All Updates'), + 'commits' => __('Commits'), + 'issues' => __('Issues and Comments'), + 'downloads' => __('Downloads'), + 'documents' => __('Documents'), + 'reviews' => __('Reviews and Patches'), + ); + } + + /** + * Returns an array of model classes for which the current user + * has rights and which should be used according to his filter + * + * @param object $request + * @param string $model_filter + * @return array + */ + private static function determineModelClasses($request, $model_filter = 'all') + { + $classes = array(); + if (true === IDF_Precondition::accessSource($request) && + ($model_filter == 'all' || $model_filter == 'commits')) { + $classes[] = '\'IDF_Commit\''; + // FIXME: this looks like a hack... + IDF_Scm::syncTimeline($request->project); + } + if (true === IDF_Precondition::accessIssues($request) && + ($model_filter == 'all' || $model_filter == 'issues')) { + $classes[] = '\'IDF_Issue\''; + $classes[] = '\'IDF_IssueComment\''; + } + if (true === IDF_Precondition::accessDownloads($request) && + ($model_filter == 'all' || $model_filter == 'downloads')) { + $classes[] = '\'IDF_Upload\''; + } + if (true === IDF_Precondition::accessWiki($request) && + ($model_filter == 'all' || $model_filter == 'documents')) { + $classes[] = '\'IDF_WikiPage\''; + $classes[] = '\'IDF_WikiRevision\''; + } + if (true === IDF_Precondition::accessReview($request) && + ($model_filter == 'all' || $model_filter == 'reviews')) { + $classes[] = '\'IDF_Review_Comment\''; + $classes[] = '\'IDF_Review_Patch\''; + } + if (count($classes) == 0) { + $classes[] = '\'IDF_Dummy\''; + } + + return $classes; + } + /** * Timeline of the project. */ @@ -68,38 +128,21 @@ class IDF_Views_Project public function timeline($request, $match) { $prj = $request->project; - $title = sprintf(__('%s Updates'), (string) $prj); - $team = $prj->getMembershipData(); + + $model_filter = @$match[2]; + $all_model_filters = self::getAvailableModelFilters(); + if (!array_key_exists($model_filter, $all_model_filters)) { + $model_filter = 'all'; + } + $title = (string)$prj . ' ' . $all_model_filters[$model_filter]; $pag = new IDF_Timeline_Paginator(new IDF_Timeline()); $pag->class = 'recent-issues'; $pag->item_extra_props = array('request' => $request); $pag->summary = __('This table shows the project updates.'); - // Need to check the rights - $rights = array(); - if (true === IDF_Precondition::accessSource($request)) { - $rights[] = '\'IDF_Commit\''; - IDF_Scm::syncTimeline($request->project); - } - if (true === IDF_Precondition::accessIssues($request)) { - $rights[] = '\'IDF_Issue\''; - $rights[] = '\'IDF_IssueComment\''; - } - if (true === IDF_Precondition::accessDownloads($request)) { - $rights[] = '\'IDF_Upload\''; - } - if (true === IDF_Precondition::accessWiki($request)) { - $rights[] = '\'IDF_WikiPage\''; - $rights[] = '\'IDF_WikiRevision\''; - } - if (true === IDF_Precondition::accessReview($request)) { - $rights[] = '\'IDF_Review_Comment\''; - $rights[] = '\'IDF_Review_Patch\''; - } - if (count($rights) == 0) { - $rights[] = '\'IDF_Dummy\''; - } - $sql = sprintf('model_class IN (%s)', implode(', ', $rights)); + + $classes = self::determineModelClasses($request, $model_filter); + $sql = sprintf('model_class IN (%s)', implode(', ', $classes)); $pag->forced_where = new Pluf_SQL('project=%s AND '.$sql, array($prj->id)); $pag->sort_order = array('creation_dtime', 'ASC'); @@ -113,32 +156,23 @@ class IDF_Views_Project $pag->items_per_page = 20; $pag->no_results_text = __('No changes were found.'); $pag->setFromRequest($request); - $downloads = array(); - if ($request->rights['hasDownloadsAccess']) { - $tags = IDF_Views_Download::getDownloadTags($prj); - // the first tag is the featured, the last is the deprecated. - $downloads = $tags[0]->get_idf_upload_list(); - } - $pages = array(); - if ($request->rights['hasWikiAccess']) { - $tags = IDF_Views_Wiki::getWikiTags($prj); - $pages = $tags[0]->get_idf_wikipage_list(); - } + if (!$request->user->isAnonymous() and $prj->isRestricted()) { $feedurl = Pluf_HTTP_URL_urlForView('idf_project_timeline_feed_auth', array($prj->shortname, + $model_filter, IDF_Precondition::genFeedToken($prj, $request->user))); } else { $feedurl = Pluf_HTTP_URL_urlForView('idf_project_timeline_feed', - array($prj->shortname)); + array($prj->shortname, $model_filter)); } return Pluf_Shortcuts_RenderToResponse('idf/project/timeline.html', array( 'page_title' => $title, 'feedurl' => $feedurl, 'timeline' => $pag, - 'team' => $team, - 'downloads' => $downloads, + 'model_filter' => $model_filter, + 'all_model_filters' => $all_model_filters, ), $request); @@ -156,31 +190,17 @@ class IDF_Views_Project public function timelineFeed($request, $match) { $prj = $request->project; - // Need to check the rights - $rights = array(); - if (true === IDF_Precondition::accessSource($request)) { - $rights[] = '\'IDF_Commit\''; - IDF_Scm::syncTimeline($request->project); + $model_filter = @$match[2]; + + $model_filter = @$match[2]; + $all_model_filters = self::getAvailableModelFilters(); + if (!array_key_exists($model_filter, $all_model_filters)) { + $model_filter = 'all'; } - if (true === IDF_Precondition::accessIssues($request)) { - $rights[] = '\'IDF_Issue\''; - $rights[] = '\'IDF_IssueComment\''; - } - if (true === IDF_Precondition::accessDownloads($request)) { - $rights[] = '\'IDF_Upload\''; - } - if (true === IDF_Precondition::accessWiki($request)) { - $rights[] = '\'IDF_WikiPage\''; - $rights[] = '\'IDF_WikiRevision\''; - } - if (true === IDF_Precondition::accessReview($request)) { - $rights[] = '\'IDF_Review_Comment\''; - $rights[] = '\'IDF_Review_Patch\''; - } - if (count($rights) == 0) { - $rights[] = '\'IDF_Dummy\''; - } - $sqls = sprintf('model_class IN (%s)', implode(', ', $rights)); + $title = $all_model_filters[$model_filter]; + + $classes = self::determineModelClasses($request, $model_filter); + $sqls = sprintf('model_class IN (%s)', implode(', ', $classes)); $sql = new Pluf_SQL('project=%s AND '.$sqls, array($prj->id)); $params = array( 'filter' => $sql->gen(), @@ -203,7 +223,6 @@ class IDF_Views_Project } $out = Pluf_Template::markSafe(implode("\n", $out)); $tmpl = new Pluf_Template('idf/index.atom'); - $title = __('Updates'); $feedurl = Pluf::f('url_base').Pluf::f('idf_base').$request->query; $viewurl = Pluf_HTTP_URL_urlForView('IDF_Views_Project::timeline', array($prj->shortname)); @@ -277,7 +296,8 @@ class IDF_Views_Project } } else { $params = array(); - $keys = array('labels_issue_open', 'labels_issue_closed', + $keys = array('labels_issue_template', + 'labels_issue_open', 'labels_issue_closed', 'labels_issue_predefined', 'labels_issue_one_max'); foreach ($keys as $key) { $_val = $conf->getVal($key, false); @@ -535,4 +555,4 @@ class IDF_Views_Project ), $request); } -} \ No newline at end of file +} diff --git a/src/IDF/Views/Source.php b/src/IDF/Views/Source.php index 3e8d3ff..357a38a 100644 --- a/src/IDF/Views/Source.php +++ b/src/IDF/Views/Source.php @@ -533,8 +533,10 @@ class IDF_Views_Source if (0 === strpos($fileinfo[0], 'text/')) { return true; } - $ext = 'mdtext php-dist h gitignore diff patch' - .Pluf::f('idf_extra_text_ext', ''); + $ext = 'mdtext php-dist h gitignore diff patch'; + $extra_ext = trim(Pluf::f('idf_extra_text_ext', '')); + if (!empty($extra_ext)) + $ext .= ' ' . $extra_ext; $ext = array_merge(self::$supportedExtenstions, explode(' ' , $ext)); return (in_array($fileinfo[2], $ext)); } diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index d4bcd5c..e1dbd4a 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -73,90 +73,31 @@ $cfg['git_write_remote_url'] = 'git@localhost:%s.git'; $cfg['svn_repositories'] = 'file:///home/svn/repositories/%s'; $cfg['svn_remote_url'] = 'http://localhost/svn/%s'; -# Path to the monotone binary (you need mtn 0.99 or newer) +# +# You can setup monotone for use with indefero in several ways. +# Please look into doc/syncmonotone.mdtext for more information. +# + +# Path to the monotone binary $cfg['mtn_path'] = 'mtn'; + # Additional options for the started monotone process $cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles'); -# -# You can setup monotone for use with indefero in several ways. The -# two most-used should be: -# -# 1) One database for everything: -# -# Set 'mtn_repositories' below to a fixed database path, such as -# '/home/mtn/repositories/all_projects.mtn' -# -# Pro: - easy to setup and to manage -# Con: - while read access can be configured per-branch, -# granting write access rights to a user means that -# he can write anything in the global database -# - database lock problem: the database from which -# indefero reads its data cannot be used to serve the -# contents to the users, as the serve process locks -# the database -# -# 2) One database for every project with 'usher': -# -# Download and configure 'usher' -# (mtn clone mtn://monotone.ca?net.venge.monotone.contrib.usher) -# which acts as proxy in front of all single project databases. -# Create a basic configuration file for it and add a secret admin -# username and password. Finally, point the below variable -# 'mtn_usher_conf' to this configuration file. -# -# Then set 'mtn_remote_url' below to a string which matches your setup. -# Again, the '%s' placeholder will be expanded to the project's -# short name. Note that 'mtn_remote_url' is used as internal -# URI (to access the data for indefero) as well as external URI -# (for end users) at the same time. 'mtn_repositories' should then -# point to a directory where all project-related files (databases, -# keys, configurations) are kept, as these are automatically created -# on project creation by IDF. -# -# Example: 'mtn_repositories' is configured to be '/var/monotone/%s' -# -# - IDF tries to create /var/monotone/ as root directory -# - The database is placed in as /var/monotone//database.mtn -# - The server key is put into /var/monotone//keys and -# is named "-server@", where host is the host part -# of 'mtn_remote_url' -# -# therefor /var/monotone MUST be read/writable for the www user and all -# files which are created underknees MUST be read/writable by the user -# who is executing the usher instance! The best way to achieve this is with -# default (POSIX) ACLs on /var/monotone. -# -# -# You could also choose to setup usher by hand, i.e. with individual -# databases, in this case leave 'mtn_usher_conf' below commented out. -# -# Pro: - read and write access can be granted per project -# - no database locking issues -# - one public server running on the one well-known port -# Con: - harder to setup -# -# Usher can also be used to forward sync requests to remote servers, -# please consult its README file for more information. -# -# monotone also allows to use SSH as transport protocol, so if you do not plan -# to setup a netsync server as described above, then just enter a URI like -# 'ssh://my-host.biz/home/mtn/repositories/%s.mtn' in 'mtn_remote_url'. -# + +# The path to a specific database (local use) or a writable project +# directory (remote / usher use). %s is replaced with the project name $cfg['mtn_repositories'] = '/home/mtn/repositories/%s.mtn'; + +# The URL which is displayed as sync URL to the user and which is also +# used to connect to a remote usher $cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s'; -# + # Whether the particular database(s) are accessed locally (via automate stdio) # or remotely (via automate remote_stdio). 'remote' is the default for -# netsync setups, while 'local' access should be choosed for ssh access. -# -# Note that you need to setup the hook 'get_remote_automate_permitted' for -# each remotely accessible database. A full HOWTO set this up is beyond this -# scope, please refer to the documentation of monotone and / or ask on the -# mailing list (monotone-users@nongnu.org) or IRC channel -# (irc.oftc.net/#monotone) -# -$cfg['mtn_db_access'] = 'remote'; -# +# use with usher and the SyncMonotone plugin, while 'local' access should be +# choosed for manual setups and / or ssh access. +$cfg['mtn_db_access'] = 'local'; + # If true, each access to the database is authenticated with an auto-generated # project key which is stored in the IDF project configuration # ('mtn_client_key_*') and written out to $cfg['tmp_folder']/mtn-client-keys @@ -167,18 +108,17 @@ $cfg['mtn_db_access'] = 'remote'; # the remote monotone server instance. In this case no project-specific # keys are generated and the server must be configured to allow at least # anonymous read access to the main functions. -# -$cfg['mtn_remote_auth'] = true; -# -# If configured, this allows basic control of a running usher process -# via the forge administration. The variable must point to the full (writable) +#$cfg['mtn_remote_auth'] = true; + +# Needs to be configured for remote / usher usage. +# This allows basic control of a running usher process via the forge +# administration. The variable must point to the full (writable) # path of the usher configuration file which gets updated when new projects # are added -# #$cfg['mtn_usher_conf'] = '/path/to/usher.conf'; # Mercurial repositories path -#$cfg['mercurial_repositories'] = '/home/mercurial/repositories/%s'; +$cfg['mercurial_repositories'] = '/home/mercurial/repositories/%s'; #$cfg['mercurial_remote_url'] = 'http://projects.ceondo.com/hg/%s'; # admins will get an email in case of errors in the system in non @@ -265,8 +205,8 @@ $cfg['db_database'] = 'website'; # put absolute path to the db if you # are using SQLite. # # The extension of the downloads are limited. You can add extra -# extensions here. The list must start with a space. -# $cfg['idf_extra_upload_ext'] = ' ext1 ext2'; +# extensions here. +# $cfg['idf_extra_upload_ext'] = 'ext1 ext2'; # # By default, the size of the downloads is limited to 2MB. # The php.ini upload_max_filesize configuration setting will @@ -316,6 +256,13 @@ $cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git', 'mtn' => 'IDF_Scm_Monotone', ); +# Set to true when uploaded public keys should not only be validated +# syntactically, but also by the specific backend. For SSH public +# keys, ssh-keygen(3) must be available and usable in PATH, for +# monotone public keys, the monotone binary (as configured above) +# is used. +# $cfg['idf_strong_key_check'] = false; + # If you want to use another memtypes database # $cfg['idf_mimetypes_db'] = '/etc/mime.types'; diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index c01e565..1afe134 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -74,18 +74,18 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/$#', 'model' => 'IDF_Views_Project', 'method' => 'home'); -$ctl[] = array('regex' => '#^/p/([\-\w]+)/timeline/$#', +$ctl[] = array('regex' => '#^/p/([\-\w]+)/timeline/(\w+)/$#', 'base' => $base, 'model' => 'IDF_Views_Project', 'method' => 'timeline'); -$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/$#', +$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/(\w+)/$#', 'base' => $base, 'model' => 'IDF_Views_Project', 'method' => 'timelineFeed', 'name' => 'idf_project_timeline_feed'); -$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/token/(.*)/$#', +$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/(\w+)/token/(.*)/$#', 'base' => $base, 'model' => 'IDF_Views_Project', 'method' => 'timelineFeed', diff --git a/src/IDF/relations.php b/src/IDF/relations.php index 9442aba..6848e1a 100644 --- a/src/IDF/relations.php +++ b/src/IDF/relations.php @@ -96,7 +96,7 @@ Pluf_Signal::connect('IDF_Key::postSave', array('IDF_Plugin_SyncMonotone', 'entry')); Pluf_Signal::connect('IDF_Key::preDelete', array('IDF_Plugin_SyncMonotone', 'entry')); -Pluf_Signal::connect('phppostpush.php::run', +Pluf_Signal::connect('mtnpostpush.php::run', array('IDF_Plugin_SyncMonotone', 'entry')); # diff --git a/src/IDF/templates/idf/admin/issue-tracking.html b/src/IDF/templates/idf/admin/issue-tracking.html index f835a68..fb5766c 100644 --- a/src/IDF/templates/idf/admin/issue-tracking.html +++ b/src/IDF/templates/idf/admin/issue-tracking.html @@ -4,6 +4,12 @@
+ + +
{$form.f.labels_issue_template.labelTag}:
+{if $form.f.labels_issue_template.errors}{$form.f.labels_issue_template.fieldErrors}{/if} +{$form.f.labels_issue_template|unsafe} +
{$form.f.labels_issue_open.labelTag}:
{if $form.f.labels_issue_open.errors}{$form.f.labels_issue_open.fieldErrors}{/if} {$form.f.labels_issue_open|unsafe} diff --git a/src/IDF/templates/idf/base-full.html b/src/IDF/templates/idf/base-full.html index fbb7280..23b5863 100644 --- a/src/IDF/templates/idf/base-full.html +++ b/src/IDF/templates/idf/base-full.html @@ -29,22 +29,19 @@ {block extraheader}{/block} {block pagetitle}{$page_title|strip_tags}{/block}{if $project} - {$project.shortdesc}{/if} +
-{if $project}

{$project}

{/if} -

-{if !$user.isAnonymous()}{aurl 'url', 'idf_dashboard'}{blocktrans}Welcome, {$user}.{/blocktrans} {trans 'Sign Out'}{else}{trans 'Sign in or create your account'}{/if} -{if $project} | {trans 'Project List'}{/if} -| {trans 'Help'} -

+ {if $project}

{$project}

{/if} + {include 'idf/main-menu.html'}
-
+
-
+
{if $user and $user.id}{getmsgs $user}{/if} -
{block body}{/block}
-
+
{block body}{/block}
+
{block foot}{/block}
- {include 'idf/js-hotkeys.html'} {block javascript}{/block} {if $project} {/if} diff --git a/src/IDF/templates/idf/base-simple.html b/src/IDF/templates/idf/base-simple.html index a88f42d..98dd8d1 100644 --- a/src/IDF/templates/idf/base-simple.html +++ b/src/IDF/templates/idf/base-simple.html @@ -29,31 +29,27 @@ {block extraheader}{/block} {block pagetitle}{$page_title|strip_tags}{/block} +
-

-{if !$user.isAnonymous()}{aurl 'url', 'idf_dashboard'}{blocktrans}Welcome, {$user}.{/blocktrans} {trans 'Sign Out'}{else}{trans 'Sign in or create your account'}{/if} - | {trans 'Project List'} {if $isAdmin}| {trans 'Forge Management'}{/if} -| {trans 'Help'} -

-

{block title}{$page_title}{/block}

+ {include 'idf/main-menu.html'} +

{block title}{$page_title}{/block}

-
+
-
+
{if $user and $user.id}{getmsgs $user}{/if} -
{block body}{/block}
-
+
{block body}{/block}
+
{block context}{/block}
{block foot}{/block}
- {include 'idf/js-hotkeys.html'} {block javascript}{/block} diff --git a/src/IDF/templates/idf/base.html b/src/IDF/templates/idf/base.html index 6a8182b..772bbd5 100644 --- a/src/IDF/templates/idf/base.html +++ b/src/IDF/templates/idf/base.html @@ -29,23 +29,19 @@ {block extraheader}{/block} {block pagetitle}{$page_title|strip_tags}{/block}{if $project} - {$project.shortdesc}{/if} +
-{if $project}

{$project}

{/if} -

-{if !$user.isAnonymous()}{aurl 'url', 'idf_dashboard'}{blocktrans}Welcome, {$user}.{/blocktrans} {trans 'Sign Out'}{else}{trans 'Sign in or create your account'}{/if} -{if $project} | {trans 'Project List'}{/if} -{if $isAdmin}| {trans 'Forge Management'}{/if} -| {trans 'Help'} -

+ {if $project}

{$project}

{/if} + {include 'idf/main-menu.html'}
-
+
-
+
{if $user and $user.id}{getmsgs $user}{/if} -
{block body}{/block}
-
+
{block body}{/block}
+
{block context}{/block}
{block foot}{/block}
- {include 'idf/js-hotkeys.html'} {block javascript}{/block} {if $project} {/if} diff --git a/src/IDF/templates/idf/gadmin/base.html b/src/IDF/templates/idf/gadmin/base.html index 1858682..b68a99d 100644 --- a/src/IDF/templates/idf/gadmin/base.html +++ b/src/IDF/templates/idf/gadmin/base.html @@ -29,16 +29,12 @@ {block extraheader}{/block} {block pagetitle}{$page_title|strip_tags}{/block} +
-

-{aurl 'url', 'IDF_Views_User::dashboard'}{blocktrans}Welcome, {$user}.{/blocktrans} {trans 'Sign Out'} -| {trans 'Project List'} -| {trans 'Forge Management'} -| {trans 'Help'} -

+ {include 'idf/main-menu.html'} - {include 'idf/js-hotkeys.html'} {block javascript}{/block} diff --git a/src/IDF/templates/idf/js-hotkeys.html b/src/IDF/templates/idf/js-hotkeys.html index ee39cac..66793fb 100644 --- a/src/IDF/templates/idf/js-hotkeys.html +++ b/src/IDF/templates/idf/js-hotkeys.html @@ -3,7 +3,7 @@ //