77 Commits

Author SHA1 Message Date
William MARTIN
813184f06c Fix an issue with "unasigned issues"
Rename the view to userIssues
2011-10-04 10:05:42 +02:00
William MARTIN
d860f299fd The last part of the previous patch. 2011-10-03 10:17:10 +02:00
William MARTIN
33882d4fa7 Update how the myIssue view works.
It's allow to display this view for other members.
In the issue summary, we can now follow make links for each user display in the part "Unresolved: By Assignee".
2011-10-03 10:00:35 +02:00
William MARTIN
13fad756ab Fix issue 732
Commit based on Stéphane Baron patch
2011-10-01 22:43:00 +02:00
Thomas Keller
1f0791df0e Make the '@rev' part in the regex optional (fixes issue 730). 2011-09-12 17:54:40 +02:00
Thomas Keller
a2c832a130 Improve the 'parents' parsing for git and ignore any empty parts; also
react gracefully if we could not parse the parents for some weird reason.
2011-08-17 20:17:14 +02:00
Thomas Keller
b17de014ec Reworked the option / argument handling in the SVN interface to
have less code duplication.
2011-08-13 02:28:15 +02:00
Thomas Keller
b1b72190e1 Dropped a few more not needed files. 2011-08-13 01:19:58 +02:00
Thomas Keller
2ff2f888bc - Make the SVN test case work without specific test configuration.
- Rename the test repo to match the test function so we can create
  more test repositories for other tests at a later stage.
- Remove useless hooks and configs from the repo (they are not used
  for our specific test and just need memory).
- Note the fix for issue 721 in NEWS.mdtext.
2011-08-13 01:11:00 +02:00
Patrick Georgi
57c2389aae Make SVN backend more robust
The SVN backend failed when trying to access historical information on deleted files.

There's also an initial test case for the SVN backend, testing this issue
and issue 364, which is about a similar problem for renamed files.
Reverting any of these fixes breaks the test.
2011-08-12 20:53:26 +02:00
Thomas Keller
d54c86f813 Note the change from issue 716. 2011-07-27 20:06:56 +02:00
Thomas Keller
05a9697007 Merge branch 'feature.content-md5' into develop 2011-07-27 19:59:18 +02:00
Patrick Georgi
945429abf0 Provide MD5 value of downloads to HTTP client
Content-MD5 is a HTTP header to provide end-to-end integrity checks
(see RFC2616, 14.15). This doesn't protect against malicious
modifications, but against transmissions errors and storage errors
on the server.

The change also removes one redirect when downloading files.
2011-07-24 22:12:36 +02:00
William MARTIN
a016bcb51b Merge branch 'develop' of projects.ceondo.com:indefero into develop 2011-07-05 11:31:05 +02:00
William MARTIN
f2b1ce795c Fix issue 247 : cron overwrites authorized_keys during cron run 2011-07-05 11:30:23 +02:00
Thomas Keller
3a8c56acc4 Postgres needs a VARCHAR cast, which MySQL doesn't understand, of
course. *sigh*
2011-07-01 13:35:43 +02:00
Thomas Keller
7b2552f940 Postgres (and probably others as well) needs an explicit char cast. 2011-06-30 00:25:29 +02:00
Thomas Keller
324b202215 Fix the rendering of issue changes in a mail template and the issue feed fragment. 2011-06-29 17:41:18 +02:00
Loïc d'Anterroches
2c2da6082a Fixed stupid missing semicolon. 2011-06-29 14:41:57 +02:00
Loïc d'Anterroches
dd3fbbd7e4 Fixes to support older PHP versions. 2011-06-29 14:30:17 +02:00
William MARTIN
9bbcd571ec Merge branch 'feature.issue-summary' into develop 2011-06-20 11:37:26 +02:00
William MARTIN
bbc185bf3b Add unit test for IDF_Project::getIssueCountByOwner 2011-06-20 11:34:42 +02:00
Thomas Keller
d1bcdcda20 Fix the mtn getChanges() test. 2011-06-17 23:50:35 +02:00
William MARTIN
6d55602ef3 Add IDF_Project::getIssueCountByOwner and use it into IDF_Views_Issue::summary 2011-06-15 17:30:23 +02:00
William MARTIN
6e7c9f7c4b Use css instead of   2011-06-15 17:29:29 +02:00
William MARTIN
5427aab456 Change "ifnull" sql function to "coalesce" which is supported by postgresql 2011-06-15 13:52:58 +02:00
Thomas Keller
4879d64989 If git's author name does not contain valid utf-8 bytes, skip the author
lookup in the database, which would otherwise only bring up errors.
2011-06-15 13:50:02 +02:00
William MARTIN
dab8ea63fc Mark string for translation 2011-06-15 11:35:30 +02:00
William MARTIN
b03d7a04a0 improve rendering of 0% bar 2011-06-15 11:30:11 +02:00
William MARTIN
ef5b93e3f7 Fix the special case of a empty issue tracker, for the issue summary. 2011-06-15 11:18:41 +02:00
William MARTIN
69ae1c08ef Add an sub-tab under Issue to add a summury view 2011-06-15 09:11:47 +02:00
Thomas Keller
8e4f828cc6 Spelling. 2011-06-13 12:43:39 +02:00
Thomas Keller
c4f92f4569 Access all private methods in a non-static way, now that we have a instance-dependent _diagnoseProblem() method 2011-06-13 12:23:22 +02:00
Thomas Keller
c4d2b99656 Wrong object to call commit() on... 2011-06-13 01:59:21 +02:00
Thomas Keller
d4fe88adab Disable the fixed context bar if the content is already to big 2011-06-12 17:59:43 +02:00
Thomas Keller
69d0e8313a Merge branch 'develop' of projects.ceondo.com:indefero into develop 2011-06-11 00:48:38 +02:00
Thomas Keller
11a234e135 Fix the branch links users of the Subversion frontend get when they enter
a wrong revision and only display this list if there are any branches
available for all SCMs.
2011-06-11 00:47:54 +02:00
Thomas Keller
2f30e4e2f6 Fix a regression introduced with commit 20c3f14cc: If we can read from a process'
pipe doesn't mean the exit code of the process in question is zero, this is
actually what pclose() returns.
2011-06-11 00:44:32 +02:00
Thomas Keller
118ca9f11f Now that IDF_Scm::getChanges() is implemented for all SCMs, there is no need for the try-catch anymore. 2011-06-10 23:40:58 +02:00
Thomas Keller
24fc41ee0d XML node access didn't work and as such renames and copies weren't properly detected. 2011-06-10 23:40:22 +02:00
William MARTIN
c00dbac5e0 Merge branch 'feature.download-md5' into develop 2011-06-10 09:26:20 +02:00
Thomas Keller
ac6be0d3c0 Implement IDF_Scm::getChanges() for Subversion (closes issue 622) 2011-06-10 02:23:54 +02:00
Thomas Keller
7ff6f09f67 We can actually differentiate between copies and renames in Hg, so lets
add support for another change type, 'copies'. The previous implementation
for Hg was also slightly flawed in the way that it mixed sources with
targets.
2011-06-10 01:34:33 +02:00
Thomas Keller
00b576c5a3 Merge branch 'feature.issue-links' into develop 2011-06-10 00:39:39 +02:00
Thomas Keller
2a33510c96 \w doesn't match -, so better use [^)] to match server names 2011-06-06 23:24:00 +02:00
Thomas Keller
d1f79d906d Do not try to symlink the same file twice (fixes issue 695) 2011-06-06 23:18:06 +02:00
Thomas Keller
aa043f14ac Two occurrences forgotten 2011-06-06 23:01:25 +02:00
Thomas Keller
638b28399e Fix issues 695 and 697. Still, public keys aren't added to monotone
databases for some weird reason, so issue 696 is still open.
2011-06-05 02:37:47 +02:00
Thomas Keller
1d86f036a3 Add a warning to not configure a global key for all monotone
instances in usher.conf.
2011-06-05 01:31:21 +02:00
Thomas Keller
2f6e0f0a22 Include examples how to setup ACLs for different platforms. 2011-06-05 01:13:00 +02:00
Thomas Keller
1b1b00a10c The server list no longer renders a bogus server entry in case no
monotone server has been configured yet in the connected usher instance.
Also display a short info text if the server or connection list for
a running monotone server is empty.
2011-06-04 23:15:58 +02:00
Thomas Keller
8693418d39 Revert "Improve error handling and reporting (partially resolves issue 695)"
This reverts commit 8ff15368ce.
2011-06-04 01:14:22 +02:00
Thomas Keller
8ff15368ce Improve error handling and reporting (partially resolves issue 695) 2011-06-04 01:03:30 +02:00
Thomas Keller
32cde534bd Fix some errors in the German translation and unify some of the strings.
Thanks to Daniel Steudler <steudlerdaniel@gmail.com> for some of the
pointers. Fixes issue 694.
2011-06-04 00:31:12 +02:00
Thomas Keller
c0e26133bd Keep the context area in the view if a view scrolls down several pages. 2011-06-02 02:04:52 +02:00
Thomas Keller
592c2ff9ff Several f'ups in hg's parseLog() method 2011-06-02 01:21:32 +02:00
Thomas Keller
30efd0a2db Merge branch 'develop' of projects.ceondo.com:indefero into develop 2011-06-02 00:55:17 +02:00
Thomas Keller
20c3f14cc8 git and hg diff views did not show empty context lines, a regression
from the commit(s) from issue 633. The diff parser assumed a properly
formatted diff that denotes empty context lines with a single space in
the first column. This single space however was missing, because the
hg and git backends got the diff through PHP's exec() function and
this returns already line-splitted output, but - and this is the actual
problem - removes trailing whitespace at the end of each line, essentially
making " \n" only "\n". When splitting this string now again with
PREG_SPLIT_NO_EMPTY the empty line was completely lost in the diff output.

To make it clear that an empty line does not mark a context line now, but
should stop the diff parsing, the Diff parser now also defaults to 'false'
as line type.

This commit fixes issue 688.
2011-06-02 00:48:38 +02:00
Thomas Keller
80313c88c8 Properly reverse-sort git's tag list again; the rsort on the original output
of the command looked bogus: since each tag ref is prefixed by an iso date and
since commit 94da55d1 by hash, the whole string was of course sorted by this
date and later hash, but not the tag name. This should now make more sense.
2011-06-02 00:38:14 +02:00
Thomas Keller
cab1c09ffc Note that issue 559 has been fixed with the fix for issue 686. 2011-06-02 00:08:13 +02:00
William MARTIN
c421919092 Fix php syntax 2011-06-01 11:20:03 +02:00
William MARTIN
d350876bc1 Update allowed attributes in the Markdown Prefilter, to be less strict.
Fix issue 578
2011-06-01 10:10:01 +02:00
William MARTIN
de09c8af56 Merge branch 'develop' of projects.ceondo.com:indefero into develop 2011-05-31 15:40:14 +02:00
William MARTIN
998f4576fe Fix div collision 2011-05-31 15:39:46 +02:00
Thomas Keller
2aab4eea3b Apply (and adapt) the patch from issue 364, which should fix the issue. 2011-05-31 13:41:37 +02:00
Thomas Keller
5bbff9f5a6 Rename the "Go to revision" button in the SVN views to "Switch", so it
doesn't break into the next line.
2011-05-31 13:39:55 +02:00
Thomas Keller
b5fcf1636a String improvements. 2011-05-31 13:34:28 +02:00
Thomas Keller
13012be5d7 Merge branch 'develop' of projects.ceondo.com:indefero into develop 2011-05-31 13:14:21 +02:00
Thomas Keller
ca2ef814fb Add support for the display of monotone node attributes.
(also fix the way the attributes view is displayed, i.e. do not
 use single strings, but one string in context which is much easier
 to translate)
2011-05-31 13:11:55 +02:00
Thomas Keller
9bcb5f9456 Fix a PHP notice / Pluf exception in case any issue type field is cleared. 2011-05-31 10:51:24 +02:00
Thomas Keller
f412099f69 Fix whitespace issues in the timeline. 2011-05-30 14:48:36 +02:00
Thomas Keller
0aa5999bb3 Finalize ticket relations (closes issue 638)
- IssueUpdate.php: use dynamically set field validators for dynamically
  created fields; let relation_type0 and relation_issue0 exist at any time;
  check the validity of a user selection and combine the various input fields
  if possible; do the database updates for links; change the "change" format
  for labels to a more precise structure and no longer trust on a leading
  dash for removed labels
- IssueCreate.php: change the validator calls and field names
- Issue.php (getGroupedRelatedIssues): make it possible to return only a
  flat list of integers for easier processing
- 17AddIssueRelations.php: migrate the previous serialized "changes" format
  for issue comments to the new, more structured format (up and down)
- js-autocomplete.html: add support for multiple input fields
- view.html: output relation changes and wrap the related issues stanzas into
  paragraphs
- NEWS.mdtext: note the addition and the need for a specific version of Pluf
2011-05-30 14:02:10 +02:00
Thomas Keller
16dda0743c Basic storage of relations for new issues has been done; the relations
are also properly displayed at the left side in the issue's detail view.
2011-05-28 23:48:00 +02:00
Thomas Keller
bcba64b2a1 Basic model files added. 2011-05-10 17:22:32 +02:00
Thomas Keller
e40d922eef Add two targets to easily install and update the IDF database 2011-05-10 17:21:56 +02:00
Thomas Keller
7e226b43d3 More work on the issue relation infrastructure
- actually query data for the incoming query
- exclude the current issue from being linked with itself
- allow multiple issues to be given in the second input field
- add the form fields to the ticket update view as well
2011-05-10 16:21:29 +02:00
Thomas Keller
9171bfd1ab Start with issue 638 (issue links); nothing workable yet. 2011-05-09 13:52:09 +02:00
88 changed files with 2229 additions and 519 deletions

View File

@@ -12,6 +12,7 @@ Much appreciated contributors (in alphabetical order):
Brian Armstrong <brianar>
Charles Melbye <charlie@yourwiki.net>
Ciaran Gultnieks <ciaran@ciarang.com>
Daniel Steudler <steudlerdaniel@gmail.com>
David Feeney <davidf>
Denis Kot <denis.kot@gmail.com> - Russian translation
Dmitry Dulepov <dmitryd>

View File

@@ -37,6 +37,10 @@ help:
@printf "\tpo-push - Send the all PO files to the transifex server.\n"
@printf "\tpo-pull - Get all PO files from the transifex server.\n"
@printf "\tpo-stats - Show translation statistics of all PO files.\n"
@printf "\nMisc Rules :\n";
@printf "\tdb-install - Install the database schema.\n"
@printf "\tdb-update - Update the database schema.\n"
#
# Internationalization rule, POT & PO file manipulation
@@ -139,3 +143,8 @@ po-stats:
> indefero-$(@:-zipfile=)-`git log $(@:-zipfile=) -n 1 \
--pretty=format:%h`.zip
db-install:
@cd src && php $(PLUF_PATH)/migrate.php --conf=IDF/conf/idf.php -a -d -i
db-update:
@cd src && php $(PLUF_PATH)/migrate.php --conf=IDF/conf/idf.php -a -d

View File

@@ -1,20 +1,50 @@
# InDefero 1.2 - xxx xxx xx xx:xx 2011 UTC
ATTENTION: You need Pluf [324ae60b](http://projects.ceondo.com/p/pluf/source/commit/324ae60b)
or newer to properly run this version of Indefero!
## New Features
- Indefero's issue tracker can now bi-directionally link issues with variable, configurable
terms, such as "is related to", "is blocked by" or "is duplicated by" (issue 638)
- Mercurial source views now show parent revisions (if any) and detailed change information
- File download URLs now contain the file name rather than the upload id; old links still work though (issue 686)
- Subversion source views now show detailed change information (issue 622)
- File download URLs now contain the file name rather than the upload id; old links still work though (issues 559 and 686)
- Display monotone file and directory attributes in the tree and file view
(needs a monotone with an interface version of 13.1 or newer)
- The context area is now kept in view when a page scrolls down several pages
## Bugfixes
- The SVN interface acts more robust if an underlying repository has been restructured (issues 364 and 721)
- monotone zip archive entries now all carry the revision date as mtime (issue 645)
- Timeline only displays filter options for items a user has actually access to (issue 655)
- The log, tags and branches parsers for Mercurial are more robust now (issue 663)
- Fix SSH public key parsing issues and improve the check for existing, uploaded keys (issue 679)
- Diff views now show empty context lines for git and hg again (issue 688)
- Let the SVN command line client not store the login credentials we give him as arguments
- The usher section in the forge administration no longer displays a bogus
server enty in case no monotone server is configured in the connected
usher instance
- Prevent a timeout from popping up when Usher is restarted (issue 695)
- The SyncMonotone plugin now cleans up partial artifacts it created during the addition of
a new project or monotone key, in case an error popped up in the middle (issue 697)
- Indefero now sends the MD5 checksum as HTTP header when downloading a file from the
download area. Additionally, a unneeded redirect has been removed. (issue 716)
- Source links without a specific revision did not work due to a wrong regex (issue 730)
- Better error detection and reporting in the SyncMonotone plugin
ATTENTION: This needs Pluf 46b7f251 or newer!
- Fix the branch links users of the Subversion frontend get when they enter a wrong revision
and only display this list if there are any branches available for all SCMs
- If git's author name is not encoded in an UTF-8 compatible encoding, skip the author lookup,
as we have no information what the author string is actually encoded in
- Indefero no longer displays an empty parents paragraph in the commit view for root revisions of
a git repository
## Documentation
- The documentation on the setup of the monotone plugin has been improved.
## Translations
# InDefero 1.1.2 - Thu May 26 07:42:25 2011 UTC

View File

@@ -25,7 +25,7 @@ If you install monotone from source (<http://monotone.ca/downloads.php>),
please follow the `INSTALL` document which comes with the software.
It contains detailed instructions, including all needed dependencies.
## Choose your indefero setup
## Choose your indefero (IDF) setup
The monotone plugin can be used in several different ways:
@@ -112,14 +112,40 @@ The monotone plugin can be used in several different ways:
^D
$ chmod 600 usher.conf
**ATTENTION:** Do _not_ configure a default monotone server key in `usher.conf`,
otherwise the individual server key that IDF creates for each project is
not used and this could cause problems.
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:
* Make the usher user the web user, for example via Apache's `suexec`.
This is however a bit clumsy.
* Preferred: Use Access Control Lists (ACLs), like this:
#
# Linux
#
$ setfacl -m u:www:rw usher.conf
$ setfacl -m d:u:www:rwx projects/
$ setfacl -m d:u:usher:rwx projects/
#
# FreeBSD
#
$ setfacl -m user:www:rw::allow usher.conf
$ setfacl -m user:www:rwxp:fd:allow projects/
$ setfacl -m user:usher:rwxp:fd:allow projects/
#
# Mac OS X
#
chmod +a '_www allow read,write' usher.conf
chmod +a '_www allow read,write,delete,add_file,add_subdirectory,file_inherit,directory_inherit' projects/
chmod +a 'usher allow read,write,delete,add_file,add_subdirectory,file_inherit,directory_inherit' projects/
In each example's last line, `usher` is the user which is executing
the usher instance. **It is very important to add this line, otherwise
usher won't be able to read and write into the initial file system
setup IDF creates!**
5. Wrap a daemonizer around usher, for example supervise from daemontools
(<http://cr.yp.to/damontools.html>):

View File

@@ -101,7 +101,7 @@ class IDF_Diff
$files[$current_file]['chunks'][] = array();
while ($i < $diffsize && ($addlines >= 0 || $dellines >= 0)) {
$linetype = $this->lines[$i] != '' ? $this->lines[$i][0] : ' ';
$linetype = $this->lines[$i] != '' ? $this->lines[$i][0] : false;
switch ($linetype) {
case ' ':
$files[$current_file]['chunks'][$current_chunk][] =

View File

@@ -319,6 +319,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
'labels_issue_closed' => IDF_Form_IssueTrackingConf::init_closed,
'labels_issue_predefined' => IDF_Form_IssueTrackingConf::init_predefined,
'labels_issue_one_max' => IDF_Form_IssueTrackingConf::init_one_max,
'issue_relations' => IDF_Form_IssueTrackingConf::init_relations,
'webhook_url' => '',
'downloads_access_rights' => 'all',
'review_access_rights' => 'all',

View File

@@ -36,6 +36,7 @@ class IDF_Form_IssueCreate extends Pluf_Form
public $user = null;
public $project = null;
public $show_full = false;
public $relation_types = null;
public function initFields($extra=array())
{
@@ -45,9 +46,12 @@ class IDF_Form_IssueCreate extends Pluf_Form
or $this->user->hasPerm('IDF.project-member', $this->project)) {
$this->show_full = true;
}
$this->relation_types = $this->project->getRelationsFromConfig();
$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'),
@@ -109,6 +113,20 @@ class IDF_Form_IssueCreate extends Pluf_Form
),
));
$this->fields['relation_type0'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('This issue'),
'initial' => current($this->relation_types),
'widget_attrs' => array('size' => 15),
));
$this->fields['relation_issue0'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => null,
'initial' => '',
'widget_attrs' => array('size' => 10),
));
/*
* get predefined tags for issues from current project
*
@@ -235,6 +253,63 @@ class IDF_Form_IssueCreate extends Pluf_Form
return $this->cleaned_data['status'];
}
// this method is not called from Pluf_Form directly, but shared for
// among all similar fields
function clean_relation_type($value)
{
$relation_type = trim($value);
if (empty($relation_type))
return '';
$found = false;
foreach ($this->relation_types as $type) {
if ($type == $relation_type) {
$found = true;
break;
}
}
if (!$found) {
throw new Pluf_Form_Invalid(__('You provided an invalid relation type.'));
}
return $relation_type;
}
function clean_relation_type0()
{
return $this->clean_relation_type($this->cleaned_data['relation_type0']);
}
// this method is not called from Pluf_Form directly, but shared for
// among all similar fields
function clean_relation_issue($value)
{
$issues = trim($value);
if (empty($issues))
return '';
$issue_ids = preg_split('/\s*,\s*/', $issues, -1, PREG_SPLIT_NO_EMPTY);
foreach ($issue_ids as $issue_id) {
if (!ctype_digit($issue_id) || (int)$issue_id < 1) {
throw new Pluf_Form_Invalid(sprintf(
__('The value "%s" is not a valid issue id.'), $issue_id
));
}
$issue = new IDF_Issue($issue_id);
if ($issue->id != $issue_id || $issue->project != $this->project->id) {
throw new Pluf_Form_Invalid(sprintf(
__('The issue "%s" does not exist.'), $issue_id
));
}
}
return implode(', ', $issue_ids);
}
function clean_relation_issue0()
{
return $this->clean_relation_issue($this->cleaned_data['relation_issue0']);
}
/**
* Clean the attachments post failure.
*/
@@ -298,6 +373,30 @@ class IDF_Form_IssueCreate extends Pluf_Form
foreach ($tags as $tag) {
$issue->setAssoc($tag);
}
// add relations (if any)
if (!empty($this->cleaned_data['relation_type0'])) {
$verb = $this->cleaned_data['relation_type0'];
$other_verb = $this->relation_types[$verb];
$related_issues = preg_split('/\s*,\s*/', $this->cleaned_data['relation_issue0'], -1, PREG_SPLIT_NO_EMPTY);
foreach ($related_issues as $related_issue_id) {
$related_issue = new IDF_Issue($related_issue_id);
$rel = new IDF_IssueRelation();
$rel->issue = $issue;
$rel->verb = $verb;
$rel->other_issue = $related_issue;
$rel->submitter = $this->user;
$rel->create();
$other_rel = new IDF_IssueRelation();
$other_rel->issue = $related_issue;
$other_rel->verb = $other_verb;
$other_rel->other_issue = $issue;
$other_rel->submitter = $this->user;
$other_rel->create();
}
}
// add the first comment
$comment = new IDF_IssueComment();
$comment->issue = $issue;

View File

@@ -72,12 +72,29 @@ Performance = Performance issue
Usability = Affects program usability
Maintainability = Hinders future changes';
const init_one_max = 'Type, Priority, Milestone';
// ATTENTION: if you change something here, change the values below as well!
const init_relations = 'is related to
blocks, is blocked by
duplicates, is duplicated by';
// These are actually all noop's, but we have no other chance to
// tell IDF's translation mechanism to mark the strings as translatable
// FIXME: IDF should get a internal translation system for strings like
// that, that can also be easily expanded by users
private function noop()
{
__('is related to');
__('blocks');
__('is blocked by');
__('duplicates');
__('is duplicated by');
}
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 to the reporter to provide certain information'),
'label' => __('Define an issue template to hint the reporter to provide certain information'),
'initial' => self::init_template,
'widget_attrs' => array('rows' => 7,
'cols' => 75),
@@ -118,6 +135,15 @@ Maintainability = Hinders future changes';
'widget_attrs' => array('size' => 60),
));
$this->fields['issue_relations'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Issue relations'),
'initial' => self::init_relations,
'help_text' => __('You can define bidirectional relations like "is related to" or "blocks, is blocked by".'),
'widget_attrs' => array('rows' => 7,
'cols' => 75),
'widget' => 'Pluf_Form_Widget_TextareaInput',
));
}
}

View File

@@ -39,6 +39,7 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate
or $this->user->hasPerm('IDF.project-member', $this->project)) {
$this->show_full = true;
}
$this->relation_types = $this->project->getRelationsFromConfig();
if ($this->show_full) {
$this->fields['summary'] = new Pluf_Form_Field_Varchar(
array('required' => true,
@@ -102,6 +103,52 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate
'size' => 15,
),
));
$idx = 0;
// note: clean_relation_type0 and clean_relation_issue0 already
// exist in the base class
$this->fields['relation_type'.$idx] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('This issue'),
'initial' => current($this->relation_types),
'widget_attrs' => array('size' => 15),
));
$this->fields['relation_issue'.$idx] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => null,
'initial' => '',
'widget_attrs' => array('size' => 10),
));
++$idx;
$relatedIssues = $this->issue->getGroupedRelatedIssues(array(), true);
foreach ($relatedIssues as $verb => $ids) {
$this->fields['relation_type'.$idx] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('This issue'),
'initial' => $verb,
'widget_attrs' => array('size' => 15),
));
$m = 'clean_relation_type'.$idx;
$this->$m = create_function('$form', '
return $form->clean_relation_type($form->cleaned_data["relation_type'.$idx.'"]);
');
$this->fields['relation_issue'.$idx] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => null,
'initial' => implode(', ', $ids),
'widget_attrs' => array('size' => 10),
));
$m = 'clean_relation_issue'.$idx;
$this->$m = create_function('$form', '
return $form->clean_relation_issue($form->cleaned_data["relation_issue'.$idx.'"]);
');
++$idx;
}
$tags = $this->issue->get_tags_list();
for ($i=1;$i<7;$i++) {
$initial = '';
@@ -155,6 +202,51 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate
public function clean()
{
$this->cleaned_data = parent::clean();
// normalize the user's input by removing dublettes and by combining
// ids from identical verbs in different input fields into one array
$normRelatedIssues = array();
for ($idx = 0; isset($this->cleaned_data['relation_type'.$idx]); ++$idx) {
$verb = $this->cleaned_data['relation_type'.$idx];
if (empty($verb))
continue;
$ids = preg_split('/\s*,\s*/', $this->cleaned_data['relation_issue'.$idx],
-1, PREG_SPLIT_NO_EMPTY);
if (count($ids) == 0)
continue;
if (!array_key_exists($verb, $normRelatedIssues))
$normRelatedIssues[$verb] = array();
foreach ($ids as $id) {
if (!in_array($id, $normRelatedIssues[$verb]))
$normRelatedIssues[$verb][] = $id;
}
}
// now look at any added / removed ids
$added = $removed = array();
$relatedIssues = $this->issue->getGroupedRelatedIssues(array(), true);
$added = array_diff_key($normRelatedIssues, $relatedIssues);
$removed = array_diff_key($relatedIssues, $normRelatedIssues);
$keysToLookAt = array_keys(
array_intersect_key($relatedIssues, $normRelatedIssues)
);
foreach ($keysToLookAt as $key) {
$a = array_diff($normRelatedIssues[$key], $relatedIssues[$key]);
if (count($a) > 0)
$added[$key] = $a;
$r = array_diff($relatedIssues[$key], $normRelatedIssues[$key]);
if (count($r) > 0)
$removed[$key] = $r;
}
// cache the added / removed data, so we do not have to
// calculate that again
$this->cleaned_data['_added_issue_relations'] = $added;
$this->cleaned_data['_removed_issue_relations'] = $removed;
// As soon as we know that at least one change was done, we
// return the cleaned data and do not go further.
if (strlen(trim($this->cleaned_data['content']))) {
@@ -214,6 +306,11 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate
return $this->cleaned_data;
}
}
if (count($this->cleaned_data['_added_issue_relations']) != 0 ||
count($this->cleaned_data['_removed_issue_relations']) != 0) {
return $this->cleaned_data;
}
}
// no changes!
throw new Pluf_Form_Invalid(__('No changes were entered.'));
@@ -255,20 +352,22 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate
foreach ($tags as $tag) {
if (!Pluf_Model_InArray($tag, $oldtags)) {
if (!isset($changes['lb'])) $changes['lb'] = array();
if (!isset($changes['lb']['add'])) $changes['lb']['add'] = array();
if ($tag->class != 'Other') {
$changes['lb'][] = (string) $tag; //new tag
$changes['lb']['add'][] = (string) $tag; //new tag
} else {
$changes['lb'][] = (string) $tag->name;
$changes['lb']['add'][] = (string) $tag->name;
}
}
}
foreach ($oldtags as $tag) {
if (!Pluf_Model_InArray($tag, $tags)) {
if (!isset($changes['lb'])) $changes['lb'] = array();
if (!isset($changes['lb']['rem'])) $changes['lb']['rem'] = array();
if ($tag->class != 'Other') {
$changes['lb'][] = '-'.(string) $tag; //new tag
$changes['lb']['rem'][] = (string) $tag; //new tag
} else {
$changes['lb'][] = '-'.(string) $tag->name;
$changes['lb']['rem'][] = (string) $tag->name;
}
}
}
@@ -286,6 +385,47 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate
or ((!is_null($owner) and !is_null($this->issue->get_owner())) and $owner->id != $this->issue->get_owner()->id)) {
$changes['ow'] = (is_null($owner)) ? '---' : $owner->login;
}
// Issue relations - additions
foreach ($this->cleaned_data['_added_issue_relations'] as $verb => $ids) {
$other_verb = $this->relation_types[$verb];
foreach ($ids as $id) {
$related_issue = new IDF_Issue($id);
$rel = new IDF_IssueRelation();
$rel->issue = $this->issue;
$rel->verb = $verb;
$rel->other_issue = $related_issue;
$rel->submitter = $this->user;
$rel->create();
$other_rel = new IDF_IssueRelation();
$other_rel->issue = $related_issue;
$other_rel->verb = $other_verb;
$other_rel->other_issue = $this->issue;
$other_rel->submitter = $this->user;
$other_rel->create();
}
if (!isset($changes['rel'])) $changes['rel'] = array();
if (!isset($changes['rel']['add'])) $changes['rel']['add'] = array();
$changes['rel']['add'][] = $verb.' '.implode(', ', $ids);
}
// Issue relations - removals
foreach ($this->cleaned_data['_removed_issue_relations'] as $verb => $ids) {
foreach ($ids as $id) {
$db = &Pluf::db();
$table = Pluf::factory('IDF_IssueRelation')->getSqlTable();
$sql = new Pluf_SQL('verb=%s AND (
(issue=%s AND other_issue=%s) OR
(other_issue=%s AND issue=%s))',
array($verb,
$this->issue->id, $id,
$this->issue->id, $id));
$db->execute('DELETE FROM '.$table.' WHERE '.$sql->gen());
}
if (!isset($changes['rel'])) $changes['rel'] = array();
if (!isset($changes['rel']['rem'])) $changes['rel']['rem'] = array();
$changes['rel']['rem'][] = $verb.' '.implode(', ', $ids);
}
// Update the issue
$this->issue->batchAssoc('IDF_Tag', $tagids);
$this->issue->summary = trim($this->cleaned_data['summary']);

View File

@@ -169,6 +169,24 @@ class IDF_Issue extends Pluf_Model
}
}
function getGroupedRelatedIssues($opts = array(), $idsOnly = false)
{
$rels = $this->get_related_issues_list(array_merge($opts, array(
'view' => 'with_other_issue',
)));
$res = array();
foreach ($rels as $rel) {
$verb = $rel->verb;
if (!array_key_exists($verb, $res)) {
$res[$verb] = array();
}
$res[$verb][] = $idsOnly ? $rel->other_issue : $rel;
}
return $res;
}
/**
* Returns an HTML fragment used to display this issue in the
* timeline.

View File

@@ -155,10 +155,19 @@ class IDF_IssueComment extends Pluf_Model
$out .= __('Owner:'); break;
case 'lb':
$out .= __('Labels:'); break;
case 'rel':
$out .= __('Relations:'); break;
}
$out .= '</strong>&nbsp;';
if ($w == 'lb') {
$out .= Pluf_esc(implode(', ', $v));
if ($w == 'lb' || $w == 'rel') {
foreach ($v as $t => $ls) {
foreach ($ls as $l) {
if ($t == 'rem') $out .= '<s>';
$out .= Pluf_esc($l);
if ($t == 'rem') $out .= '</s>';
$out .= ' ';
}
}
} else {
$out .= Pluf_esc($v);
}

100
src/IDF/IssueRelation.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2008-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 ***** */
/**
* A relation of one issue to another
*/
class IDF_IssueRelation extends Pluf_Model
{
public $_model = __CLASS__;
function init()
{
$this->_a['table'] = 'idf_issuerelations';
$this->_a['model'] = __CLASS__;
$this->_a['cols'] = array(
// It is mandatory to have an "id" column.
'id' =>
array(
'type' => 'Pluf_DB_Field_Sequence',
'blank' => true,
),
'issue' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Issue',
'blank' => false,
'verbose' => __('issue'),
'relate_name' => 'related_issues',
),
'verb' =>
array(
'type' => 'Pluf_DB_Field_Text',
'blank' => false,
'verbose' => __('verb'),
),
'other_issue' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Issue',
'blank' => false,
'verbose' => __('other issue'),
'relate_name' => 'related_other_issues',
),
'submitter' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
'blank' => false,
'verbose' => __('submitter'),
),
'creation_dtime' =>
array(
'type' => 'Pluf_DB_Field_Datetime',
'blank' => true,
'verbose' => __('creation date'),
),
);
$this->_a['idx'] = array(
'creation_dtime_idx' =>
array(
'col' => 'creation_dtime',
'type' => 'normal',
),
);
$issuetbl = $this->_con->pfx.'idf_issues';
$this->_a['views'] = array(
'with_other_issue' => array(
'join' => 'INNER JOIN '.$issuetbl.' ON other_issue='.$issuetbl.'.id',
'select' => $this->getSelect().', summary',
'props' => array('summary' => 'other_summary'),
));
}
function preSave($create=false)
{
if ($this->id == '') {
$this->creation_dtime = gmdate('Y-m-d H:i:s');
}
}
}

View File

@@ -88,6 +88,7 @@ class IDF_Middleware
'showuser' => 'IDF_Template_ShowUser',
'ashowuser' => 'IDF_Template_AssignShowUser',
'appversion' => 'IDF_Template_AppVersion',
'upload' => 'IDF_Template_Tag_UploadUrl',
));
$params['modifiers'] = array_merge($params['modifiers'],
array(

View File

@@ -0,0 +1,90 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2008-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 ***** */
/**
* Add the new IDF_IssueRelation model.
*
*/
function IDF_Migrations_17AddIssueRelations_up($params=null)
{
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);
$schema->model = new IDF_IssueRelation();
$schema->createTables();
// change the serialization format for added / removed labels in IDF_IssueComment
$comments = Pluf::factory('IDF_IssueComment')->getList();
foreach ($comments as $comment) {
if (!isset($comment->changes['lb'])) continue;
$changes = $comment->changes;
$adds = $removals = array();
foreach ($comment->changes['lb'] as $lb) {
if (substr($lb, 0, 1) == '-')
$removals[] = substr($lb, 1);
else
$adds[] = $lb;
}
$changes['lb'] = array();
if (count($adds) > 0)
$changes['lb']['add'] = $adds;
if (count($removals) > 0)
$changes['lb']['rem'] = $removals;
$comment->changes = $changes;
$comment->update();
}
}
function IDF_Migrations_17AddIssueRelations_down($params=null)
{
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);
$schema->model = new IDF_IssueRelation();
$schema->dropTables();
// change the serialization format for added / removed labels in IDF_IssueComment
$comments = Pluf::factory('IDF_IssueComment')->getList();
foreach ($comments as $comment) {
$changes = $comment->changes;
if (empty($changes))
continue;
if (isset($changes['lb'])) {
$labels = array();
foreach ($changes['lb'] as $type => $lbs) {
if (!is_array($lbs)) {
$labels[] = $lbs;
continue;
}
foreach ($lbs as $lb) {
$labels[] = ($type == 'rem' ? '-' : '') . $lb;
}
}
$changes['lb'] = $labels;
}
// while we're at it, remove any 'rel' changes
unset($changes['rel']);
$comment->changes = $changes;
$comment->update();
}
}

View File

@@ -54,6 +54,7 @@ function IDF_Migrations_Backup_run($folder, $name=null)
'IDF_Queue',
'IDF_Gconf',
'IDF_EmailAddress',
'IDF_IssueRelation',
);
$db = Pluf::db();
// Now, for each table, we dump the content in json, this is a
@@ -100,6 +101,7 @@ function IDF_Migrations_Backup_restore($folder, $name)
'IDF_Queue',
'IDF_Gconf',
'IDF_EmailAddress',
'IDF_IssueRelation',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);

View File

@@ -51,6 +51,7 @@ function IDF_Migrations_Install_setup($params=null)
'IDF_Queue',
'IDF_Gconf',
'IDF_EmailAddress',
'IDF_IssueRelation',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);
@@ -109,6 +110,7 @@ function IDF_Migrations_Install_teardown($params=null)
'IDF_Commit',
'IDF_Project',
'IDF_EmailAddress',
'IDF_IssueRelation',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);

View File

@@ -59,6 +59,16 @@ class IDF_Plugin_SyncGit_Cron
$out .= sprintf($template, $cmd, $key->login, $content)."\n";
}
}
$out = "# indefero start" . PHP_EOL . $out . "# indefero end" . PHP_EOL;
// We update only the part of the file between IDF_START / IDF_END comment
$original_keys = file_get_contents($authorized_keys);
if (strstr($original_keys, "# indefero start") && strstr($original_keys, "# indefero end")) {
$out = preg_replace('/(#\sindefero\sstart).+(#\sindefero\send\s\s?)/isU',
$out, $original_keys);
} else {
$out .= $original_keys;
}
file_put_contents($authorized_keys, $out, LOCK_EX);
}

View File

@@ -27,6 +27,18 @@
*/
class IDF_Plugin_SyncMonotone
{
private $old_err_rep = 0;
public function __construct()
{
$this->old_err_rep = error_reporting(0);
}
public function __destruct()
{
error_reporting($this->old_err_rep);
}
/**
* Entry point of the plugin.
*/
@@ -80,24 +92,33 @@ class IDF_Plugin_SyncMonotone
return;
}
// This guard cleans up on any kind of error, and here is how it works:
// As long as the guard is not committed, it keeps a reference to
// the given project. When the guard is destroyed and the reference
// is still present, it deletes the object. The deletion indirectly
// also calls into this plugin again, as the project delete hook
// will be called, that removes any changes we've made during the
// process.
$projectGuard = new IDF_Plugin_SyncMonotone_ModelGuard($project);
$projecttempl = Pluf::f('mtn_repositories', false);
if ($projecttempl === false) {
throw new IDF_Scm_Exception(
__('"mtn_repositories" must be defined in your configuration file.')
$this->_diagnoseProblem(
__('"mtn_repositories" must be defined in your configuration file')
);
}
$usher_config = Pluf::f('mtn_usher_conf', false);
if (!$usher_config || !is_writable($usher_config)) {
throw new IDF_Scm_Exception(
__('"mtn_usher_conf" does not exist or is not writable.')
$this->_diagnoseProblem(
__('"mtn_usher_conf" does not exist or is not writable')
);
}
$mtnpostpush = realpath(dirname(__FILE__) . '/../../../scripts/mtn-post-push');
if (!file_exists($mtnpostpush)) {
throw new IDF_Scm_Exception(sprintf(
__('Could not find mtn-post-push script "%s".'), $mtnpostpush
$this->_diagnoseProblem(sprintf(
__('Could not find mtn-post-push script "%s"'), $mtnpostpush
));
}
@@ -110,13 +131,12 @@ class IDF_Plugin_SyncMonotone
'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',
);
// enable remote command execution of read-only commands
// only for public projects
if (!$project->private) {
// this is linked and not copied to be able to update
// the list of read-only commands on upgrades
@@ -131,8 +151,8 @@ class IDF_Plugin_SyncMonotone
}
foreach ($confdir_contents as $content) {
if (!file_exists($confdir.$content)) {
throw new IDF_Scm_Exception(sprintf(
__('The configuration file %s is missing.'), $content
$this->_diagnoseProblem(sprintf(
__('The configuration file "%s" is missing'), $content
));
}
}
@@ -140,14 +160,15 @@ class IDF_Plugin_SyncMonotone
$shortname = $project->shortname;
$projectpath = sprintf($projecttempl, $shortname);
if (file_exists($projectpath)) {
throw new IDF_Scm_Exception(sprintf(
__('The project path %s already exists.'), $projectpath
$this->_diagnoseProblem(sprintf(
__('The project path "%s" already exists'), $projectpath
));
}
if (!mkdir($projectpath)) {
throw new IDF_Scm_Exception(sprintf(
__('The project path %s could not be created.'), $projectpath
if (!@mkdir($projectpath)) {
$this->_diagnoseProblem(sprintf(
__('The project path "%s" could not be created'),
$projectpath
));
}
@@ -156,7 +177,7 @@ class IDF_Plugin_SyncMonotone
//
$dbfile = $projectpath.'/database.mtn';
$cmd = sprintf('db init -d %s', escapeshellarg($dbfile));
self::_mtn_exec($cmd);
$this->_mtn_exec($cmd);
//
// step 2) create a server key
@@ -175,16 +196,17 @@ class IDF_Plugin_SyncMonotone
escapeshellarg($projectpath),
escapeshellarg($serverkey)
);
self::_mtn_exec($cmd);
$this->_mtn_exec($cmd);
//
// step 3) create a client key, and save it in IDF
//
$keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
if (!file_exists($keydir)) {
if (!mkdir($keydir)) {
throw new IDF_Scm_Exception(sprintf(
__('The key directory %s could not be created.'), $keydir
if (!@mkdir($keydir)) {
$this->_diagnoseProblem(sprintf(
__('The key directory "%s" could not be created'),
$keydir
));
}
}
@@ -194,14 +216,14 @@ class IDF_Plugin_SyncMonotone
escapeshellarg($keydir),
escapeshellarg($clientkey_name)
);
$keyinfo = self::_mtn_exec($cmd);
$keyinfo = $this->_mtn_exec($cmd);
$parsed_keyinfo = array();
try {
$parsed_keyinfo = IDF_Scm_Monotone_BasicIO::parse($keyinfo);
}
catch (Exception $e) {
throw new IDF_Scm_Exception(sprintf(
$this->_diagnoseProblem(sprintf(
__('Could not parse key information: %s'), $e->getMessage()
));
}
@@ -219,13 +241,13 @@ class IDF_Plugin_SyncMonotone
escapeshellarg($keydir),
escapeshellarg($clientkey_hash)
);
$clientkey_pubdata = self::_mtn_exec($cmd);
$clientkey_pubdata = $this->_mtn_exec($cmd);
$cmd = sprintf('au put_public_key --db=%s %s',
escapeshellarg($dbfile),
escapeshellarg($clientkey_pubdata)
);
self::_mtn_exec($cmd);
$this->_mtn_exec($cmd);
//
// step 4) setup the configuration
@@ -238,18 +260,20 @@ class IDF_Plugin_SyncMonotone
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
if (!@mkdir($filepath)) {
$this->_diagnoseProblem(sprintf(
__('Could not create configuration directory "%s"'),
$filepath
));
}
continue;
}
if (substr($content, -3) != '.in') {
if (!symlink($confdir.$content, $filepath)) {
IDF_Scm_Exception(sprintf(
__('Could not create symlink "%s"'), $filepath
if (!@symlink($confdir.$content, $filepath)) {
$this->_diagnoseProblem(sprintf(
__('Could not create symlink for configuration file "%s"'),
$filepath
));
}
continue;
@@ -264,9 +288,10 @@ class IDF_Plugin_SyncMonotone
// 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 configuration file "%s"'), $filepath
if (@file_put_contents($filepath, $filecontents, LOCK_EX) === false) {
$this->_diagnoseProblem(sprintf(
__('Could not write configuration file "%s"'),
$filepath
));
}
}
@@ -280,7 +305,7 @@ class IDF_Plugin_SyncMonotone
$parsed_config = IDF_Scm_Monotone_BasicIO::parse($usher_rc);
}
catch (Exception $e) {
throw new IDF_Scm_Exception(sprintf(
$this->_diagnoseProblem(sprintf(
__('Could not parse usher configuration in "%s": %s'),
$usher_config, $e->getMessage()
));
@@ -291,7 +316,7 @@ class IDF_Plugin_SyncMonotone
foreach ($stanzas as $stanza_line) {
if ($stanza_line['key'] == 'server' &&
$stanza_line['values'][0] == $shortname) {
throw new IDF_Scm_Exception(sprintf(
$this->_diagnoseProblem(sprintf(
__('usher configuration already contains a server '.
'entry named "%s"'),
$shortname
@@ -315,9 +340,10 @@ class IDF_Plugin_SyncMonotone
// FIXME: more sanity - what happens on failing writes? we do not
// have a backup copy of usher.conf around...
if (file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write usher configuration file "%s"'), $usher_config
if (@file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) {
$this->_diagnoseProblem(sprintf(
__('Could not write usher configuration file "%s"'),
$usher_config
));
}
@@ -325,6 +351,9 @@ class IDF_Plugin_SyncMonotone
// step 6) reload usher to pick up the new configuration
//
IDF_Scm_Monotone_Usher::reload();
// commit the guard, so the newly created project is not deleted
$projectGuard->commit();
}
/**
@@ -345,8 +374,8 @@ class IDF_Plugin_SyncMonotone
$mtn = IDF_Scm_Monotone::factory($project);
$stdio = $mtn->getStdio();
$projectpath = self::_get_project_path($project);
$auth_ids = self::_get_authorized_user_ids($project);
$projectpath = $this->_get_project_path($project);
$auth_ids = $this->_get_authorized_user_ids($project);
$key_ids = array();
foreach ($auth_ids as $auth_id) {
$sql = new Pluf_SQL('user=%s', array($auth_id));
@@ -361,9 +390,10 @@ class IDF_Plugin_SyncMonotone
$write_permissions = implode("\n", $key_ids);
$rcfile = $projectpath.'/write-permissions';
if (file_put_contents($rcfile, $write_permissions, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write write-permissions file "%s"'), $rcfile
if (@file_put_contents($rcfile, $write_permissions, LOCK_EX) === false) {
$this->_diagnoseProblem(sprintf(
__('Could not write write-permissions file "%s"'),
$rcfile
));
}
@@ -382,11 +412,13 @@ class IDF_Plugin_SyncMonotone
array('key' => 'allow', 'values' => array('*')),
);
}
$read_permissions = IDF_Scm_Monotone_BasicIO::compile(array($stanza));
$rcfile = $projectpath.'/read-permissions';
if (file_put_contents($rcfile, $read_permissions, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write read-permissions file "%s"'), $rcfile
if (@file_put_contents($rcfile, $read_permissions, LOCK_EX) === false) {
$this->_diagnoseProblem(sprintf(
__('Could not write read-permissions file "%s"'),
$rcfile
));
}
@@ -401,16 +433,16 @@ class IDF_Plugin_SyncMonotone
$serverRestartRequired = false;
if ($project->private && file_exists($projectfile) && is_link($projectfile)) {
if (!unlink($projectfile)) {
IDF_Scm_Exception(sprintf(
if (!@unlink($projectfile)) {
$this->_diagnoseProblem(sprintf(
__('Could not remove symlink "%s"'), $projectfile
));
}
$serverRestartRequired = true;
} else
if (!$project->private && !file_exists($projectfile)) {
if (!symlink($templatefile, $projectfile)) {
throw new IDF_Scm_Exception(sprintf(
if (!@symlink($templatefile, $projectfile)) {
$this->_diagnoseProblem(sprintf(
__('Could not create symlink "%s"'), $projectfile
));
}
@@ -422,6 +454,9 @@ class IDF_Plugin_SyncMonotone
// seems to be ignored when the server should be started
// again immediately afterwards
IDF_Scm_Monotone_Usher::killServer($project->shortname);
// give usher some time to cool down, otherwise it might hang
// (see https://code.monotone.ca/p/contrib/issues/175/)
sleep(2);
IDF_Scm_Monotone_Usher::startServer($project->shortname);
}
}
@@ -443,8 +478,8 @@ class IDF_Plugin_SyncMonotone
$usher_config = Pluf::f('mtn_usher_conf', false);
if (!$usher_config || !is_writable($usher_config)) {
throw new IDF_Scm_Exception(
__('"mtn_usher_conf" does not exist or is not writable.')
$this->_diagnoseProblem(
__('"mtn_usher_conf" does not exist or is not writable')
);
}
@@ -453,16 +488,16 @@ class IDF_Plugin_SyncMonotone
$projecttempl = Pluf::f('mtn_repositories', false);
if ($projecttempl === false) {
throw new IDF_Scm_Exception(
__('"mtn_repositories" must be defined in your configuration file.')
$this->_diagnoseProblem(
__('"mtn_repositories" must be defined in your configuration file')
);
}
$projectpath = sprintf($projecttempl, $shortname);
if (file_exists($projectpath)) {
if (!self::_delete_recursive($projectpath)) {
throw new IDF_Scm_Exception(sprintf(
__('One or more paths underknees %s could not be deleted.'), $projectpath
if (!$this->_delete_recursive($projectpath)) {
$this->_diagnoseProblem(sprintf(
__('One or more paths underneath %s could not be deleted'), $projectpath
));
}
}
@@ -473,8 +508,9 @@ class IDF_Plugin_SyncMonotone
if ($keyname && $keyhash &&
file_exists($keydir .'/'. $keyname . '.' . $keyhash)) {
if (!@unlink($keydir .'/'. $keyname . '.' . $keyhash)) {
throw new IDF_Scm_Exception(sprintf(
__('Could not delete client private key %s'), $keyname
$this->_diagnoseProblem(sprintf(
__('Could not delete client private key "%s"'),
$keyname
));
}
}
@@ -485,7 +521,7 @@ class IDF_Plugin_SyncMonotone
$parsed_config = IDF_Scm_Monotone_BasicIO::parse($usher_rc);
}
catch (Exception $e) {
throw new IDF_Scm_Exception(sprintf(
$this->_diagnoseProblem(sprintf(
__('Could not parse usher configuration in "%s": %s'),
$usher_config, $e->getMessage()
));
@@ -505,9 +541,10 @@ class IDF_Plugin_SyncMonotone
// FIXME: more sanity - what happens on failing writes? we do not
// have a backup copy of usher.conf around...
if (file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write usher configuration file "%s"'), $usher_config
if (@file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) {
$this->_diagnoseProblem(sprintf(
__('Could not write usher configuration file "%s"'),
$usher_config
));
}
@@ -528,6 +565,8 @@ class IDF_Plugin_SyncMonotone
return;
}
$keyGuard = new IDF_Plugin_SyncMonotone_ModelGuard($key);
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
$conf = new IDF_Conf();
$conf->setProject($project);
@@ -535,8 +574,8 @@ class IDF_Plugin_SyncMonotone
if ($scm != 'mtn')
continue;
$projectpath = self::_get_project_path($project);
$auth_ids = self::_get_authorized_user_ids($project);
$projectpath = $this->_get_project_path($project);
$auth_ids = $this->_get_authorized_user_ids($project);
if (!in_array($key->user, $auth_ids))
continue;
@@ -556,7 +595,7 @@ class IDF_Plugin_SyncMonotone
$parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms);
}
catch (Exception $e) {
throw new IDF_Scm_Exception(sprintf(
$this->_diagnoseProblem(sprintf(
__('Could not parse read-permissions for project "%s": %s'),
$shortname, $e->getMessage()
));
@@ -598,10 +637,11 @@ class IDF_Plugin_SyncMonotone
$read_perms = IDF_Scm_Monotone_BasicIO::compile($parsed_read_perms);
if (file_put_contents($projectpath.'/read-permissions',
if (@file_put_contents($projectpath.'/read-permissions',
$read_perms, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write read-permissions for project "%s"'), $shortname
$this->_diagnoseProblem(sprintf(
__('Could not write read-permissions for project "%s"'),
$shortname
));
}
}
@@ -611,9 +651,9 @@ class IDF_Plugin_SyncMonotone
if (!in_array('*', $lines) && !in_array($mtn_key_id, $lines)) {
$lines[] = $mtn_key_id;
}
if (file_put_contents($projectpath.'/write-permissions',
if (@file_put_contents($projectpath.'/write-permissions',
implode("\n", $lines) . "\n", LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
$this->_diagnoseProblem(sprintf(
__('Could not write write-permissions file for project "%s"'),
$shortname
));
@@ -623,6 +663,8 @@ class IDF_Plugin_SyncMonotone
$stdio = $mtn->getStdio();
$stdio->exec(array('put_public_key', $key->content));
}
$keyGuard->commit();
}
/**
@@ -651,8 +693,8 @@ class IDF_Plugin_SyncMonotone
if ($scm != 'mtn')
continue;
$projectpath = self::_get_project_path($project);
$auth_ids = self::_get_authorized_user_ids($project);
$projectpath = $this->_get_project_path($project);
$auth_ids = $this->_get_authorized_user_ids($project);
if (!in_array($key->user, $auth_ids))
continue;
@@ -672,7 +714,7 @@ class IDF_Plugin_SyncMonotone
$parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms);
}
catch (Exception $e) {
throw new IDF_Scm_Exception(sprintf(
$this->_diagnoseProblem(sprintf(
__('Could not parse read-permissions for project "%s": %s'),
$shortname, $e->getMessage()
));
@@ -693,10 +735,11 @@ class IDF_Plugin_SyncMonotone
$read_perms = IDF_Scm_Monotone_BasicIO::compile($parsed_read_perms);
if (file_put_contents($projectpath.'/read-permissions',
if (@file_put_contents($projectpath.'/read-permissions',
$read_perms, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write read-permissions for project "%s"'), $shortname
$this->_diagnoseProblem(sprintf(
__('Could not write read-permissions for project "%s"'),
$shortname
));
}
}
@@ -711,9 +754,9 @@ class IDF_Plugin_SyncMonotone
continue;
}
}
if (file_put_contents($projectpath.'/write-permissions',
if (@file_put_contents($projectpath.'/write-permissions',
implode("\n", $lines) . "\n", LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
$this->_diagnoseProblem(sprintf(
__('Could not write write-permissions file for project "%s"'),
$shortname
));
@@ -762,7 +805,43 @@ class IDF_Plugin_SyncMonotone
));
}
private static function _get_authorized_user_ids($project)
private function _get_project_path($project)
{
$projecttempl = Pluf::f('mtn_repositories', false);
if ($projecttempl === false) {
$this->_diagnoseProblem(
__('"mtn_repositories" must be defined in your configuration file.')
);
}
$projectpath = sprintf($projecttempl, $project->shortname);
if (!file_exists($projectpath)) {
$this->_diagnoseProblem(sprintf(
__('The project path %s does not exists.'), $projectpath
));
}
return $projectpath;
}
private function _mtn_exec($cmd)
{
$fullcmd = sprintf('%s %s %s',
Pluf::f('idf_exec_cmd_prefix', ''),
Pluf::f('mtn_path', 'mtn'),
$cmd
);
$output = $return = null;
exec($fullcmd, $output, $return);
if ($return != 0) {
$this->_diagnoseProblem(sprintf(
__('The command "%s" could not be executed.'), $cmd
));
}
return implode("\n", $output);
}
private function _get_authorized_user_ids($project)
{
$mem = $project->getMembershipData();
$members = array_merge((array)$mem['members'],
@@ -775,43 +854,7 @@ class IDF_Plugin_SyncMonotone
return $userids;
}
private static function _get_project_path($project)
{
$projecttempl = Pluf::f('mtn_repositories', false);
if ($projecttempl === false) {
throw new IDF_Scm_Exception(
__('"mtn_repositories" must be defined in your configuration file.')
);
}
$projectpath = sprintf($projecttempl, $project->shortname);
if (!file_exists($projectpath)) {
throw new IDF_Scm_Exception(sprintf(
__('The project path %s does not exists.'), $projectpath
));
}
return $projectpath;
}
private static function _mtn_exec($cmd)
{
$fullcmd = sprintf('%s %s %s',
Pluf::f('idf_exec_cmd_prefix', ''),
Pluf::f('mtn_path', 'mtn'),
$cmd
);
$output = $return = null;
exec($fullcmd, $output, $return);
if ($return != 0) {
throw new IDF_Scm_Exception(sprintf(
__('The command "%s" could not be executed.'), $cmd
));
}
return implode("\n", $output);
}
private static function _delete_recursive($path)
private function _delete_recursive($path)
{
if (is_file($path) || is_link($path)) {
return @unlink($path);
@@ -821,10 +864,48 @@ class IDF_Plugin_SyncMonotone
$scan = glob(rtrim($path, '/') . '/*');
$status = 0;
foreach ($scan as $subpath) {
$status |= self::_delete_recursive($subpath);
$status |= $this->_delete_recursive($subpath);
}
$status |= rmdir($path);
$status |= @rmdir($path);
return $status;
}
}
private function _diagnoseProblem($msg)
{
$system_err = error_get_last();
if (!empty($system_err)) {
$msg .= ': '.$system_err['message'];
}
error_reporting($this->old_err_rep);
throw new IDF_Scm_Exception($msg);
}
}
/**
* A simple helper class that deletes the model instance if
* it is not committed
*/
class IDF_Plugin_SyncMonotone_ModelGuard
{
private $model;
public function __construct(Pluf_Model $m)
{
$this->model = $m;
}
public function __destruct()
{
if ($this->model == null)
return;
$this->model->delete();
}
public function commit()
{
$this->model = null;
}
}

View File

@@ -133,6 +133,46 @@ class IDF_Project extends Pluf_Model
return $projects[0];
}
/**
* Returns the number of open/closed issues.
*
* @param string Status ('open'), 'closed'
* @param IDF_Tag Subfilter with a label (null)
* @return int Count
*/
public function getIssueCountByOwner($status='open')
{
switch ($status) {
case 'open':
$tags = implode(',', $this->getTagIdsByStatus('open'));
break;
case 'closed':
default:
$tags = implode(',', $this->getTagIdsByStatus('closed'));
break;
}
$sqlIssueTable = Pluf::factory('IDF_Issue')->getSqlTable();
$query = "SELECT uid AS id,COUNT(uid) AS nb
FROM (
SELECT COALESCE(owner, -1) AS uid
FROM $sqlIssueTable
WHERE status IN ($tags)
) AS ff
GROUP BY uid";
$db = Pluf::db();
$dbData = $db->select($query);
$ownerStatistics = array();
foreach ($dbData as $k => $v) {
$key = ($v['id'] === '-1') ? null : $v['id'];
$ownerStatistics[$key] = (int)$v['nb'];
}
arsort($ownerStatistics);
return $ownerStatistics;
}
/**
* Returns the number of open/closed issues.
*
@@ -233,6 +273,29 @@ class IDF_Project extends Pluf_Model
return $tags;
}
/**
* Returns a list of relations which are available in this project as
* associative array. Each key-value pair marks a set of orthogonal
* relations. To ease processing, each of these pairs is included twice
* in the array, once as key1 => key2 and once as key2 => key1.
*
* @return array List of relation names
*/
public function getRelationsFromConfig()
{
$conf = $this->getConf();
$rel = $conf->getVal('issue_relations', IDF_Form_IssueTrackingConf::init_relations);
$relations = array();
foreach (preg_split("/\015\012|\015|\012/", $rel, -1, PREG_SPLIT_NO_EMPTY) as $s) {
$verbs = preg_split("/\s*,\s*/", $s, 2);
if (count($verbs) == 1)
$relations += array($verbs[0] => $verbs[0]);
else
$relations += array($verbs[0] => $verbs[1], $verbs[1] => $verbs[0]);
}
return $relations;
}
/**
* Return membership data.
*

View File

@@ -88,22 +88,36 @@ class IDF_Scm
}
/**
* Run exec and log some information.
* Runs the given command and log some information.
*
* A previous version used plain exec(), but this should not be used
* for various reasons, one being that this command does not preserve
* trailing whitespace, which is essential for proper diff parsing.
*
* @param $caller Calling method
* @param $cmd Command to run
* @param &$out Array of output
* @param &$return Return value
* @return string Last line of the command
*/
public static function exec($caller, $cmd, &$out=null, &$return=null)
{
$return = -1;
Pluf_Log::stime('timer');
$ret = exec($cmd, $out, $return);
$fp = popen($cmd, 'r');
$buf = '';
if ($fp !== false) {
while (!feof($fp)) {
$buf .= fread($fp, 1024);
}
$return = pclose($fp);
}
$out = preg_split('/\r\n|\r|\n/', $buf);
$elem = count($out);
if ($elem > 0 && $out[$elem-1] === '')
unset($out[$elem-1]);
Pluf_Log::perf(array($caller, $cmd, Pluf_Log::etime('timer', 'total_exec')));
Pluf_Log::debug(array($caller, $cmd, $out));
Pluf_Log::inc('exec_calls');
return $ret;
}
/**
@@ -325,7 +339,8 @@ class IDF_Scm
* stdClass object {
* 'additions' => array('path/to/file', 'path/to/directory', ...),
* 'deletions' => array('path/to/file', 'path/to/directory', ...),
* 'renames' => array('old/path/to/file' => 'new/path/to/file', ...)
* 'renames' => array('old/path/to/file' => 'new/path/to/file', ...),
* 'copies' => array('path/to/source' => 'path/to/target', ...),
* 'patches' => array('path/to/file', ...),
* 'properties' => array('path/to/file' => array(
* 'propname' => 'propvalue', 'deletedprop' => null, ...)

View File

@@ -67,6 +67,7 @@ class IDF_Scm_Git extends IDF_Scm
'additions' => array(),
'deletions' => array(),
'renames' => array(),
'copies' => array(),
'patches' => array(),
'properties' => array(),
);
@@ -218,13 +219,13 @@ class IDF_Scm_Git extends IDF_Scm
$cmd, $return,
implode("\n", $out)));
}
rsort($out);
$res = array();
foreach ($out as $b) {
$elts = explode(' ', $b, 2);
$tag = substr(trim($elts[1]), 10); // Remove refs/tags/ prefix
$res[$tag] = '';
}
krsort($res);
$this->cache['tags'] = $res;
return $res;
@@ -345,6 +346,14 @@ class IDF_Scm_Git extends IDF_Scm
if (!preg_match('/<(.*)>/', $author, $match)) {
return null;
}
// FIXME: newer git versions know a i18n.commitencoding setting which
// leads to another header, "encoding", with which we _could_ try to
// decode the string into utf8. Unfortunately this does not always
// work, especially not in older repos, so we would then still have
// to supply some fallback.
if (!mb_check_encoding($match[1], 'UTF-8')) {
return null;
}
$sql = new Pluf_SQL('login=%s', array($match[1]));
$users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen()));
if ($users->count() > 0) {
@@ -637,7 +646,11 @@ class IDF_Scm_Git extends IDF_Scm
$c['full_message'] = IDF_Commit::toUTF8($c['full_message']);
$c['title'] = IDF_Commit::toUTF8($c['title']);
if (isset($c['parents'])) {
$c['parents'] = explode(' ', trim($c['parents']));
$c['parents'] = preg_split('/ /', trim($c['parents']), -1, PREG_SPLIT_NO_EMPTY);
} else {
// this is actually an error state because we should _always_
// be able to parse the parents line with every git version
$c['parents'] = null;
}
$res[] = (object) $c;
return $res;

View File

@@ -59,7 +59,7 @@ class IDF_Scm_Mercurial_LogStyle
. "\n"
. 'file_del = "{file_del}\0"'
. "\n"
. 'file_copy = "{name}\0{source}\0"'
. 'file_copy = "{source}\0{name}\0"'
. "\n";
} else {
throw new IDF_Scm_Exception('invalid type ' . $type);
@@ -457,24 +457,32 @@ class IDF_Scm_Mercurial extends IDF_Scm
'patches' => preg_split('/\0/', $log->file_mods, -1, PREG_SPLIT_NO_EMPTY),
// hg has no support for built-in attributes, so this keeps empty
'properties' => array(),
// this is filled below
// these two are filled below
'copies' => array(),
'renames' => array(),
);
$file_copies = preg_split('/\0/', $log->file_copies, -1, PREG_SPLIT_NO_EMPTY);
// FIXME: copies are only treated as renames if they have an add _and_
// an drop, otherwise they're just treated as adds
// copies are treated as renames if they have an add _and_ a drop;
// only if they only have an add, but no drop, they're treated as copies
for ($i=0; $i<count($file_copies); $i+=2) {
$new = $file_copies[$i];
$old = $file_copies[$i+1];
$newidx = array_search($new, $return->additions);
$oldidx = array_search($old, $return->deletions);
if ($newidx !== false && $oldidx !== false) {
$return->renames[$old] = $new;
unset($return->additions[$newidx]);
unset($return->deletions[$oldidx]);
$src = $file_copies[$i];
$trg = $file_copies[$i+1];
$srcidx = array_search($src, $return->deletions);
$trgidx = array_search($trg, $return->additions);
if ($srcidx !== false && $trgidx !== false) {
$return->renames[$src] = $trg;
unset($return->deletions[$srcidx]);
unset($return->additions[$trgidx]);
continue;
}
if ($srcidx === false && $trgidx !== false) {
$return->copies[$src] = $trg;
unset($return->additions[$trgidx]);
continue;
}
// file sutures (counter-operation to copy) not supported
}
return $return;
@@ -531,6 +539,7 @@ class IDF_Scm_Mercurial extends IDF_Scm
if ($line == "\0") {
$headers_processed = false;
if (count($c) > 0) {
if (array_key_exists('full_message', $c))
$c['full_message'] = trim($c['full_message']);
$res[] = (object) $c;
}
@@ -547,10 +556,8 @@ class IDF_Scm_Mercurial extends IDF_Scm
$c['commit'] = $match[2];
$c['tree'] = $c['commit'];
$c['full_message'] = '';
} elseif ($match[1] == 'user') {
} elseif ($match[1] == 'author') {
$c['author'] = $match[2];
} elseif ($match[1] == 'summary') {
$c['title'] = $match[2];
} elseif ($match[1] == 'branch') {
$c['branch'] = empty($match[2]) ? 'default' : $match[2];
} elseif ($match[1] == 'parents') {

View File

@@ -31,10 +31,10 @@ class IDF_Scm_Monotone extends IDF_Scm
/** the minimum supported interface version */
public static $MIN_INTERFACE_VERSION = 13.0;
private $stdio;
private static $instances = array();
private $stdio;
/**
* Constructor
*/
@@ -609,6 +609,7 @@ class IDF_Scm_Monotone extends IDF_Scm
'additions' => array(),
'deletions' => array(),
'renames' => array(),
'copies' => array(),
'patches' => array(),
'properties' => array(),
);
@@ -698,6 +699,29 @@ class IDF_Scm_Monotone extends IDF_Scm
return (object) $res;
}
/**
* @see IDF_Scm::getProperties()
*/
public function getProperties($rev, $path='')
{
$out = $this->stdio->exec(array('interface_version'));
// support for querying file attributes of committed revisions
// was added for mtn 1.1 (interface version 13.1)
if (floatval($out) < 13.1)
return array();
$out = $this->stdio->exec(array('get_attributes', $path), array('r' => $rev));
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
$res = array();
foreach ($stanzas as $stanza) {
$line = $stanza[0];
$res[$line['values'][0]] = $line['values'][1];
}
return $res;
}
/**
* @see IDF_Scm::getExtraProperties
*/

View File

@@ -48,7 +48,7 @@ class IDF_Scm_Monotone_Usher
if ($conn == 'none')
return array();
return preg_split('/[ ]/', $conn);
return preg_split('/[ ]/', $conn, -1, PREG_SPLIT_NO_EMPTY);
}
/**
@@ -73,10 +73,10 @@ class IDF_Scm_Monotone_Usher
if ($conn == 'none')
return array();
$single_conns = preg_split('/[ ]/', $conn);
$single_conns = preg_split('/[ ]/', $conn, -1, PREG_SPLIT_NO_EMPTY);
$ret = array();
foreach ($single_conns as $conn) {
preg_match('/\((\w+)\)([^:]+):(\d+)/', $conn, $matches);
preg_match('/\(([^)]+)\)([^:]+):(\d+)/', $conn, $matches);
$ret[$matches[1]][] = (object)array(
'server' => $matches[1],
'address' => $matches[2],

View File

@@ -33,7 +33,6 @@
*/
class IDF_Scm_Svn extends IDF_Scm
{
public $username = '';
public $password = '';
private $assoc = array('dir' => 'tree',
@@ -48,11 +47,7 @@ class IDF_Scm_Svn extends IDF_Scm
public function isAvailable()
{
$cmd = sprintf(Pluf::f('svn_path', 'svn').' info --no-auth-cache --xml --username=%s --password=%s %s',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('info', '--xml'), $this->repo);
$xmlInfo = self::shell_exec('IDF_Scm_Svn::isAvailable', $cmd);
try {
@@ -163,12 +158,7 @@ class IDF_Scm_Svn extends IDF_Scm
return IDF_Scm::REVISION_VALID;
}
$cmd = sprintf(Pluf::f('svn_path', 'svn').' info --no-auth-cache --username=%s --password=%s %s@%s',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo),
escapeshellarg($rev));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('info'), $this->repo, $rev);
self::exec('IDF_Scm_Svn::validateRevision', $cmd, $out, $ret);
if ($ret == 0)
@@ -176,7 +166,6 @@ class IDF_Scm_Svn extends IDF_Scm
return IDF_Scm::REVISION_INVALID;
}
/**
* Test a given object hash.
*
@@ -191,12 +180,9 @@ class IDF_Scm_Svn extends IDF_Scm
}
// Else, test the path on revision
$cmd = sprintf(Pluf::f('svn_path', 'svn').' info --no-auth-cache --xml --username=%s --password=%s %s@%s',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo.'/'.self::smartEncode($path)),
escapeshellarg($rev));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('info', '--xml'),
$this->repo.'/'.self::smartEncode($path),
$rev);
$xmlInfo = self::shell_exec('IDF_Scm_Svn::testHash', $cmd);
// If exception is thrown, return false
@@ -218,12 +204,9 @@ class IDF_Scm_Svn extends IDF_Scm
public function getTree($commit, $folder='/', $branch=null)
{
$cmd = sprintf(Pluf::f('svn_path', 'svn').' ls --no-auth-cache --xml --username=%s --password=%s %s@%s',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo.'/'.self::smartEncode($folder)),
escapeshellarg($commit));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('ls', '--xml'),
$this->repo.'/'.self::smartEncode($folder),
$commit);
$xml = simplexml_load_string(self::shell_exec('IDF_Scm_Svn::getTree', $cmd));
$res = array();
$folder = (strlen($folder) and ($folder != '/')) ? $folder.'/' : '';
@@ -248,7 +231,6 @@ class IDF_Scm_Svn extends IDF_Scm
return $res;
}
/**
* Get the commit message of a revision revision.
*
@@ -260,12 +242,7 @@ class IDF_Scm_Svn extends IDF_Scm
if (isset($this->cache['commitmess'][$rev])) {
return $this->cache['commitmess'][$rev];
}
$cmd = sprintf(Pluf::f('svn_path', 'svn').' log --no-auth-cache --xml --limit 1 --username=%s --password=%s %s@%s',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo),
escapeshellarg($rev));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('log', '--xml', '--limit', '1'), $this->repo, $rev);
try {
$xml = simplexml_load_string(self::shell_exec('IDF_Scm_Svn::getCommitMessage', $cmd));
$this->cache['commitmess'][$rev] = (string) $xml->logentry->msg;
@@ -281,12 +258,8 @@ class IDF_Scm_Svn extends IDF_Scm
if ($rev == null) {
$rev = 'HEAD';
}
$cmd = sprintf(Pluf::f('svn_path', 'svn').' info --no-auth-cache --xml --username=%s --password=%s %s@%s',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo.'/'.self::smartEncode($filename)),
escapeshellarg($rev));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('info', '--xml'),
$this->repo.'/'.self::smartEncode($filename), $rev);
$xml = simplexml_load_string(self::shell_exec('IDF_Scm_Svn::getPathInfo', $cmd));
if (!isset($xml->entry)) {
return false;
@@ -308,12 +281,9 @@ class IDF_Scm_Svn extends IDF_Scm
public function getFile($def, $cmd_only=false)
{
$cmd = sprintf(Pluf::f('svn_path', 'svn').' cat --no-auth-cache --username=%s --password=%s %s@%s',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo.'/'.self::smartEncode($def->fullpath)),
escapeshellarg($def->rev));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('cat'),
$this->repo.'/'.self::smartEncode($def->fullpath),
$def->rev);
return ($cmd_only) ?
$cmd : self::shell_exec('IDF_Scm_Svn::getFile', $cmd);
}
@@ -329,11 +299,7 @@ class IDF_Scm_Svn extends IDF_Scm
return $this->cache['branches'];
}
$res = array();
$cmd = sprintf(Pluf::f('svn_path', 'svn').' ls --no-auth-cache --username=%s --password=%s %s@HEAD',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo.'/branches'));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('ls'), $this->repo.'/branches', 'HEAD');
self::exec('IDF_Scm_Svn::getBranches', $cmd, $out, $ret);
if ($ret == 0) {
foreach ($out as $entry) {
@@ -344,11 +310,8 @@ class IDF_Scm_Svn extends IDF_Scm
}
}
ksort($res);
$cmd = sprintf(Pluf::f('svn_path', 'svn').' info --no-auth-cache --username=%s --password=%s %s@HEAD',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo.'/trunk'));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('info'), $this->repo.'/trunk', 'HEAD');
self::exec('IDF_Scm_Svn::getBranches', $cmd, $out, $ret);
if ($ret == 0) {
$res = array('trunk' => 'trunk') + $res;
@@ -368,11 +331,7 @@ class IDF_Scm_Svn extends IDF_Scm
return $this->cache['tags'];
}
$res = array();
$cmd = sprintf(Pluf::f('svn_path', 'svn').' ls --no-auth-cache --username=%s --password=%s %s@HEAD',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo.'/tags'));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('ls'), $this->repo.'/tags', 'HEAD');
self::exec('IDF_Scm_Svn::getTags', $cmd, $out, $ret);
if ($ret == 0) {
foreach ($out as $entry) {
@@ -412,7 +371,6 @@ class IDF_Scm_Svn extends IDF_Scm
return array();
}
/**
* Get commit details.
*
@@ -426,12 +384,8 @@ class IDF_Scm_Svn extends IDF_Scm
return false;
}
$res = array();
$cmd = sprintf(Pluf::f('svn_path', 'svn').' log --no-auth-cache --xml --limit 1 -v --username=%s --password=%s %s@%s',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo),
escapeshellarg($commit));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('log', '--xml', '--limit', '1', '-v'),
$this->repo, $commit);
$xmlRes = self::shell_exec('IDF_Scm_Svn::getCommit', $cmd);
$xml = simplexml_load_string($xmlRes);
$res['author'] = (string) $xml->logentry->author;
@@ -473,15 +427,87 @@ class IDF_Scm_Svn extends IDF_Scm
private function getDiff($rev='HEAD')
{
$res = array();
$cmd = sprintf(Pluf::f('svn_path', 'svn').' diff --no-auth-cache -c %s --username=%s --password=%s %s',
escapeshellarg($rev),
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('diff', '-c', $rev), $this->repo);
return self::shell_exec('IDF_Scm_Svn::getDiff', $cmd);
}
/**
* @see IDF_Scm::getChanges()
*/
public function getChanges($commit)
{
if ($this->validateRevision($commit) != IDF_Scm::REVISION_VALID) {
return null;
}
$cmd = $this->svnCmd(array('log', '--xml', '-v'), $this->repo, $commit);
$out = array();
$out = self::shell_exec('IDF_Scm_Svn::getChanges', $cmd);
$xml = simplexml_load_string($out);
if (count($xml) == 0) {
return null;
}
$entry = current($xml);
$return = (object) array(
'additions' => array(),
'deletions' => array(),
'patches' => array(),
// while SVN has support for attributes, we cannot see their changes
// in the log's XML unfortunately
'properties' => array(),
'copies' => array(),
'renames' => array(),
);
foreach ($entry->paths->path as $p) {
$path = (string) $p;
foreach ($p->attributes() as $k => $v) {
$key = (string) $k;
$val = (string) $v;
if ($key != 'action')
continue;
if ($val == 'M')
$return->patches[] = $path;
else if ($val == 'A')
$return->additions[] = $path;
else if ($val == 'D')
$return->deletions[] = $path;
}
}
// copies are treated as renames if they have an add _and_ a drop;
// only if they only have an add, but no drop, they're treated as copies
foreach ($entry->paths->path as $p) {
$trg = (string) $p;
$src = null;
foreach ($p->attributes() as $k => $v) {
if ((string) $k == 'copyfrom-path') {
$src = (string) $v;
break;
}
}
if ($src == null)
continue;
$srcidx = array_search($src, $return->deletions);
$trgidx = array_search($trg, $return->additions);
if ($srcidx !== false && $trgidx !== false) {
$return->renames[$src] = $trg;
unset($return->deletions[$srcidx]);
unset($return->additions[$trgidx]);
continue;
}
if ($srcidx === false && $trgidx !== false) {
$return->copies[$src] = $trg;
unset($return->additions[$trgidx]);
continue;
}
// file sutures (counter-operation to copy) not supported
}
return $return;
}
/**
* Get latest changes.
@@ -491,20 +517,15 @@ class IDF_Scm_Svn extends IDF_Scm
*
* @return array Changes.
*/
public function getChangeLog($branch=null, $n=10)
public function getChangeLog($rev=null, $n=10)
{
if ($branch != 'HEAD' and !preg_match('/^\d+$/', $branch)) {
if ($rev != 'HEAD' and !preg_match('/^\d+$/', $rev)) {
// we accept only revisions or HEAD
$branch = 'HEAD';
$rev = 'HEAD';
}
$res = array();
$cmd = sprintf(Pluf::f('svn_path', 'svn').' log --no-auth-cache --xml -v --limit %s --username=%s --password=%s %s@%s',
escapeshellarg($n),
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo),
escapeshellarg($branch));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('log', '--xml', '-v', '--limit', $n),
$this->repo.'@'.$rev);
$xmlRes = self::shell_exec('IDF_Scm_Svn::getChangeLog', $cmd);
$xml = simplexml_load_string($xmlRes);
foreach ($xml->logentry as $entry) {
@@ -520,7 +541,6 @@ class IDF_Scm_Svn extends IDF_Scm
return $res;
}
/**
* Get additionnals properties on path and revision
*
@@ -531,12 +551,8 @@ class IDF_Scm_Svn extends IDF_Scm
public function getProperties($rev, $path='')
{
$res = array();
$cmd = sprintf(Pluf::f('svn_path', 'svn').' proplist --no-auth-cache --xml --username=%s --password=%s %s@%s',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo.'/'.self::smartEncode($path)),
escapeshellarg($rev));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('proplist', '--xml'),
$this->repo.'/'.self::smartEncode($path), $rev);
$xmlProps = self::shell_exec('IDF_Scm_Svn::getProperties', $cmd);
$props = simplexml_load_string($xmlProps);
@@ -554,7 +570,6 @@ class IDF_Scm_Svn extends IDF_Scm
return $res;
}
/**
* Get a specific additionnal property on path and revision
*
@@ -566,20 +581,14 @@ class IDF_Scm_Svn extends IDF_Scm
private function getProperty($property, $rev, $path='')
{
$res = array();
$cmd = sprintf(Pluf::f('svn_path', 'svn').' propget --no-auth-cache --xml %s --username=%s --password=%s %s@%s',
escapeshellarg($property),
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo.'/'.self::smartEncode($path)),
escapeshellarg($rev));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('propget', $property, '--xml'),
$this->repo.'/'.self::smartEncode($path), $rev);
$xmlProp = self::shell_exec('IDF_Scm_Svn::getProperty', $cmd);
$prop = simplexml_load_string($xmlProp);
return (string) $prop->target->property;
}
/**
* Get the number of the last commit in the repository.
*
@@ -590,16 +599,38 @@ class IDF_Scm_Svn extends IDF_Scm
public function getLastCommit($rev='HEAD')
{
$xmlInfo = '';
$cmd = sprintf(Pluf::f('svn_path', 'svn').' info --no-auth-cache --xml --username=%s --password=%s %s@%s',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo),
escapeshellarg($rev));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$cmd = $this->svnCmd(array('info', '--xml'), $this->repo, $rev);
$xmlInfo = self::shell_exec('IDF_Scm_Svn::getLastCommit', $cmd);
$xml = simplexml_load_string($xmlInfo);
return (string) $xml->entry->commit['revision'];
}
private function svnCmd($args = array(), $repoarg = null, $revarg = null)
{
$cmdline = array();
$cmdline[] = Pluf::f('idf_exec_cmd_prefix', '');
$cmdline[] = Pluf::f('svn_path', 'svn');
$cmdline[] = '--no-auth-cache';
$cmdline[] = '--username='.escapeshellarg($this->username);
$cmdline[] = '--password='.escapeshellarg($this->password);
foreach ($args as $arg) {
$cmdline[] = escapeshellarg($arg);
}
if ($repoarg != null) {
if ($revarg != null) {
$repoarg .= '@'.$revarg;
}
$cmdline[] = escapeshellarg($repoarg);
}
if ($revarg != null) {
$cmdline[] = '--revision='.escapeshellarg($revarg);
}
return implode(' ', $cmdline);
}
}

View File

@@ -58,7 +58,7 @@ class IDF_Template_IssueComment extends Pluf_Template_Tag
implode('|', $nouns);
$text = IDF_Template_safePregReplace('#((?:'.$prefix.')(?:\s+r?))([0-9a-f]{1,40}((?:\s+and|\s+or|,)\s+r?[0-9a-f]{1,40})*)\b#i',
array($this, 'callbackCommits'), $text);
$text = IDF_Template_safePregReplace('=(src:)([^\s@#,\(\)\\\\]+(?:(\\\\)[\s@#][^\s@#,\(\)\\\\]+){0,})+(?:\@([^\s#,]+))(?:#(\d+))?=im',
$text = IDF_Template_safePregReplace('=(src:)([^\s@#,\(\)\\\\]+(?:(\\\\)[\s@#][^\s@#,\(\)\\\\]+){0,})+(?:\@([^\s#,]+))?(?:#(\d+))?=im',
array($this, 'callbackSource'), $text);
}
if ($wordwrap) $text = Pluf_Text::wrapHtml($text, 69, "\n");

View File

@@ -90,40 +90,69 @@ class IDF_Template_MarkdownPrefilter extends Pluf_Text_HTML_Filter
);
public $allowed = array(
'a' => array('href', 'title', 'rel'),
'abbr' => array('title'),
'address' => array(),
'b' => array(),
'blockquote' => array(),
'br' => array(),
'caption' => array(),
'code' => array(),
'dd' => array(),
'del' => array('cite', 'class', 'datetime', 'dir', 'id', 'title'),
'div' => array('align', 'class'),
'dl' => array(),
'dt' => array(),
'em' => array(),
'h1' => array('id'),
'h2' => array('id'),
'h3' => array('id'),
'h4' => array('id'),
'h5' => array('id'),
'h6' => array('id'),
'hr' => array(),
'i' => array(),
'img' => array('src', 'class', 'alt', 'height', 'width', 'style'),
'ins' => array('cite', 'class', 'datetime', 'dir', 'id', 'title'),
'li' => array(),
'ol' => array(),
'p' => array('align', 'class'),
'pre' => array(),
'strong' => array(),
'table' => array('summary'),
'td' => array('style'),
'th' => array(),
'tr' => array(),
'ul' => array(),
'a' => array('class', 'dir', 'id', 'style', 'title',
'href', 'hreflang', 'rel'),
'abbr' => array('class', 'dir', 'id', 'style', 'title'),
'address' => array('class', 'dir', 'id', 'style', 'title'),
'b' => array('class', 'dir', 'id', 'style', 'title'),
'blockquote' => array('class', 'dir', 'id', 'style', 'title',
'cite'),
'br' => array('class', 'id', 'style', 'title'),
'caption' => array('class', 'dir', 'id', 'style', 'title',
'align'), // deprecated attribute),
'code' => array('class', 'dir', 'id', 'style', 'title'),
'dd' => array('class', 'dir', 'id', 'style', 'title'),
'del' => array('class', 'dir', 'id', 'style', 'title',
'cite', 'datetime'),
'div' => array('class', 'dir', 'id', 'style', 'title',
'align'), // deprecated attribute
'dl' => array('class', 'dir', 'id', 'style', 'title'),
'dt' => array('class', 'dir', 'id', 'style', 'title'),
'em' => array('class', 'dir', 'id', 'style', 'title'),
'font' => array('class', 'dir', 'id', 'style', 'title', // deprecated element
'color', 'face', 'size'), // deprecated attribute
'h1' => array('class', 'dir', 'id', 'style', 'title',
'align'), // deprecated attribute
'h2' => array('class', 'dir', 'id', 'style', 'title',
'align'), // deprecated attribute
'h3' => array('class', 'dir', 'id', 'style', 'title',
'align'), // deprecated attribute
'h4' => array('class', 'dir', 'id', 'style', 'title',
'align'), // deprecated attribute
'h5' => array('class', 'dir', 'id', 'style', 'title',
'align'), // deprecated attribute
'h6' => array('class', 'dir', 'id', 'style', 'title',
'align'), // deprecated attribute
'hr' => array('class', 'dir', 'id', 'style', 'title',
'align', 'noshade', 'size', 'width'), // deprecated attribute
'i' => array('class', 'dir', 'id', 'style', 'title'),
'img' => array('class', 'dir', 'id', 'style', 'title',
'src', 'alt', 'height', 'width'),
'ins' => array('class', 'dir', 'id', 'style', 'title',
'cite', 'datetime'),
'li' => array('class', 'dir', 'id', 'style', 'title',
'type'), // deprecated attribute
'ol' => array('class', 'dir', 'id', 'style', 'title',
'type'), // deprecated attribute
'p' => array('class', 'dir', 'id', 'style', 'title',
'align'), // deprecated attribute
'pre' => array('class', 'dir', 'id', 'style', 'title',
'width'), // deprecated attribute
'strong' => array('class', 'dir', 'id', 'style', 'title'),
'table' => array('class', 'dir', 'id', 'style', 'title',
'border', 'cellpadding', 'cellspacing', 'frame', 'rules', 'summary', 'width',
'align', 'bgcolor'), // deprecated attribute
'td' => array('class', 'dir', 'id', 'style', 'title',
'align', 'colspan', 'headers', 'rowspan', 'scope', 'valign',
'bgcolor', 'height', 'nowrap', 'width'), // deprecated attribute
'th' => array('class', 'dir', 'id', 'style', 'title',
'align', 'colspan', 'rowspan', 'scope', 'valign',
'bgcolor', 'height', 'nowrap', 'width'), // deprecated attribute
'tr' => array('class', 'dir', 'id', 'style', 'title',
'align', 'valign',
'bgcolor'), // deprecated attribute
'ul' => array('class', 'dir', 'id', 'style', 'title',
'type'), // deprecated attribute
);
// tags which should always be self-closing (e.g. "<img />")
public $no_close = array(

View File

@@ -0,0 +1,35 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 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 ***** */
class IDF_Template_Tag_UploadUrl extends Pluf_Template_Tag
{
function start($file='')
{
echo IDF_Template_Tag_UploadUrl::url($file);
}
public static function url($file='')
{
return Pluf::f('url_upload', Pluf_Template_Tag_MediaUrl::url() . '/upload') . $file;
}
}

View File

@@ -150,7 +150,7 @@ class IDF_Upload extends Pluf_Model
if ($this->id == '') {
$this->creation_dtime = gmdate('Y-m-d H:i:s');
$this->modif_dtime = gmdate('Y-m-d H:i:s');
$this->md5 = md5_file (Pluf::f('upload_path') . '/' . $this->get_project()->shortname . '/files/' . $this->file);
$this->md5 = md5_file ($this->getFullPath());
}
}
@@ -167,6 +167,11 @@ class IDF_Upload extends Pluf_Model
return Pluf::f('url_upload').'/'.$project->shortname.'/files/'.$this->file;
}
function getFullPath()
{
return(Pluf::f('upload_path').'/'.$this->get_project()->shortname.'/files/'.$this->file);
}
/**
* We drop the information from the timeline.
*/

View File

@@ -202,7 +202,11 @@ class IDF_Views_Download
$prj->inOr404($upload);
$upload->downloads += 1;
$upload->update();
return new Pluf_HTTP_Response_Redirect($upload->getAbsoluteUrl($prj));
$path = $upload->getFullPath();
$mime = IDF_FileUtil::getMimeType($path);
$render = new Pluf_HTTP_Response_File($path, $mime[0]);
$render->headers["Content-MD5"] = $upload->md5;
return $render;
}
/**

View File

@@ -77,6 +77,81 @@ class IDF_Views_Issue
$params, $request);
}
/**
* View the issue summary.
* TODO Add thoses data in cache, and process it only after an issue update
*/
public $summary_precond = array('IDF_Precondition::accessIssues');
public function summary($request, $match)
{
$tagStatistics = array();
$ownerStatistics = array();
$status = array();
$isTrackerEmpty = false;
$prj = $request->project;
$opened = $prj->getIssueCountByStatus('open');
$closed = $prj->getIssueCountByStatus('closed');
// Check if the tracker is empty
if ($opened === 0 && $closed === 0) {
$isTrackerEmpty = true;
} else {
if ($opened > 0 || $closed > 0) {
// Issue status statistics
$status['Open'] = array($opened, (int)(100 * $opened / ($opened + $closed)));
$status['Closed'] = array($closed, (int)(100 * $closed / ($opened + $closed)));
}
if ($opened > 0) {
// Issue owner statistics
$owners = $prj->getIssueCountByOwner('open');
foreach ($owners as $user => $nb) {
if ($user === '') {
$key = __('Not assigned');
$login = null;
} else {
$obj = Pluf::factory('Pluf_User')->getOne(array('filter'=>'id='.$user));
$key = $obj->first_name . ' ' . $obj->last_name;
$login = $obj->login;
}
$ownerStatistics[$key] = array($nb, (int)(100 * $nb / $opened), $login);
}
// Issue class tag statistics
$tags = $prj->getTagCloud();
foreach ($tags as $t) {
$tagStatistics[$t->class][$t->name] = array($t->nb_use, $t->id);
}
foreach($tagStatistics as $k => $v) {
$nbIssueInClass = 0;
foreach ($v as $val) {
$nbIssueInClass += $val[0];
}
foreach ($v as $kk => $vv) {
$tagStatistics[$k][$kk] = array($vv[0], (int)(100 * $vv[0] / $nbIssueInClass), $vv[1]);
}
}
// Sort
krsort($tagStatistics);
arsort($ownerStatistics);
}
}
$title = sprintf(__('Summary of tracked issues in %s.'), (string) $prj);
return Pluf_Shortcuts_RenderToResponse('idf/issues/summary.html',
array('page_title' => $title,
'trackerEmpty' => $isTrackerEmpty,
'project' => $prj,
'tagStatistics' => $tagStatistics,
'ownerStatistics' => $ownerStatistics,
'status' => $status,
),
$request);
}
/**
* View the issues watch list of a given user.
* Limited to a specified project
@@ -240,42 +315,55 @@ class IDF_Views_Issue
*
* Only open issues are shown.
*/
public $myIssues_precond = array('IDF_Precondition::accessIssues',
'Pluf_Precondition::loginRequired');
public function myIssues($request, $match)
public $userIssues_precond = array('IDF_Precondition::accessIssues');
public function userIssues($request, $match)
{
$prj = $request->project;
$sql = new Pluf_SQL('login=%s', array($match[2]));
$user = Pluf::factory('Pluf_User')->getOne(array('filter' => $sql->gen()));
if ($user === null) {
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::index',
array($prj->shortname));
return new Pluf_HTTP_Response_Redirect($url);
}
$otags = $prj->getTagIdsByStatus('open');
$ctags = $prj->getTagIdsByStatus('closed');
if (count($otags) == 0) $otags[] = 0;
if (count($ctags) == 0) $ctags[] = 0;
switch ($match[2]) {
switch ($match[3]) {
case 'submit':
$title = sprintf(__('My Submitted %s Issues'), (string) $prj);
$f_sql = new Pluf_SQL('project=%s AND submitter=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $request->user->id));
$titleFormat = __('%s %s Submitted %s Issues');
$f_sql = new Pluf_SQL('project=%s AND submitter=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $user->id));
break;
case 'submitclosed':
$title = sprintf(__('My Closed Submitted %s Issues'), (string) $prj);
$f_sql = new Pluf_SQL('project=%s AND submitter=%s AND status IN ('.implode(', ', $ctags).')', array($prj->id, $request->user->id));
$titleFormat = __('%s %s Closed Submitted %s Issues');
$f_sql = new Pluf_SQL('project=%s AND submitter=%s AND status IN ('.implode(', ', $ctags).')', array($prj->id, $user->id));
break;
case 'ownerclosed':
$title = sprintf(__('My Closed Working %s Issues'), (string) $prj);
$f_sql = new Pluf_SQL('project=%s AND owner=%s AND status IN ('.implode(', ', $ctags).')', array($prj->id, $request->user->id));
$titleFormat = __('%s %s Closed Working %s Issues');
$f_sql = new Pluf_SQL('project=%s AND owner=%s AND status IN ('.implode(', ', $ctags).')', array($prj->id, $user->id));
break;
default:
$title = sprintf(__('My Working %s Issues'), (string) $prj);
$f_sql = new Pluf_SQL('project=%s AND owner=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $request->user->id));
$titleFormat = __('%s %s Working %s Issues');
$f_sql = new Pluf_SQL('project=%s AND owner=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $user->id));
break;
}
$title = sprintf($titleFormat,
$user->first_name,
$user->last_name,
(string) $prj);
// Get stats about the issues
$sql = new Pluf_SQL('project=%s AND submitter=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $request->user->id));
$sql = new Pluf_SQL('project=%s AND submitter=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $user->id));
$nb_submit = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
$sql = new Pluf_SQL('project=%s AND owner=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $request->user->id));
$sql = new Pluf_SQL('project=%s AND owner=%s AND status IN ('.implode(', ', $otags).')', array($prj->id, $user->id));
$nb_owner = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
// Closed issues
$sql = new Pluf_SQL('project=%s AND submitter=%s AND status IN ('.implode(', ', $ctags).')', array($prj->id, $request->user->id));
$sql = new Pluf_SQL('project=%s AND submitter=%s AND status IN ('.implode(', ', $ctags).')', array($prj->id, $user->id));
$nb_submit_closed = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
$sql = new Pluf_SQL('project=%s AND owner=%s AND status IN ('.implode(', ', $ctags).')', array($prj->id, $request->user->id));
$sql = new Pluf_SQL('project=%s AND owner=%s AND status IN ('.implode(', ', $ctags).')', array($prj->id, $user->id));
$nb_owner_closed = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
// Paginator to paginate the issues
@@ -286,7 +374,7 @@ class IDF_Views_Issue
'current_user' => $request->user);
$pag->summary = __('This table shows the open issues.');
$pag->forced_where = $f_sql;
$pag->action = array('IDF_Views_Issue::myIssues', array($prj->shortname, $match[2]));
$pag->action = array('IDF_Views_Issue::userIssues', array($prj->shortname, $match[2]));
$pag->sort_order = array('modif_dtime', 'ASC'); // will be reverted
$pag->sort_reverse_order = array('modif_dtime');
$pag->sort_link_title = true;
@@ -301,9 +389,10 @@ class IDF_Views_Issue
$pag->items_per_page = 10;
$pag->no_results_text = __('No issues were found.');
$pag->setFromRequest($request);
return Pluf_Shortcuts_RenderToResponse('idf/issues/my-issues.html',
return Pluf_Shortcuts_RenderToResponse('idf/issues/userIssues.html',
array('project' => $prj,
'page_title' => $title,
'login' => $user->login,
'nb_submit' => $nb_submit,
'nb_owner' => $nb_owner,
'nb_submit_closed' => $nb_submit_closed,
@@ -345,6 +434,7 @@ class IDF_Views_Issue
'form' => $form,
'page_title' => $title,
'preview' => $preview,
'issue' => new IDF_Issue(),
),
self::autoCompleteArrays($prj)
);
@@ -403,6 +493,8 @@ class IDF_Views_Issue
$issue = Pluf_Shortcuts_GetObjectOr404('IDF_Issue', $match[2]);
$prj->inOr404($issue);
$comments = $issue->get_comments_list(array('order' => 'id ASC'));
$related_issues = $issue->getGroupedRelatedIssues(array('order' => 'other_issue ASC'));
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
array($prj->shortname, $issue->id));
$title = Pluf_Template::markSafe(sprintf(__('Issue <a href="%s">%d</a>: %s'), $url, $issue->id, $issue->summary));
@@ -470,7 +562,8 @@ class IDF_Views_Issue
'preview' => $preview,
'interested' => $interested->count(),
'previous_issue_id' => $previous_issue_id,
'next_issue_id' => $next_issue_id
'next_issue_id' => $next_issue_id,
'related_issues' => $related_issues,
),
$arrays),
$request);
@@ -643,6 +736,79 @@ class IDF_Views_Issue
$request);
}
/**
* Renders a JSON string containing completed issue information
* based on the queried / partial string
*/
public $autoCompleteIssueList_precond = array('IDF_Precondition::accessIssues');
public function autoCompleteIssueList($request, $match)
{
$prj = $request->project;
$issue_id = !empty($match[2]) ? intval($match[2]) : 0;
$query = trim($request->REQUEST['q']);
$limit = !empty($request->REQUEST['limit']) ? intval($request->REQUEST['limit']) : 0;
$limit = max(10, $limit);
$issues = array();
// empty search, return the most recently updated issues
if (empty($query)) {
$sql = new Pluf_SQL('project=%s', array($prj->id));
$tmp = Pluf::factory('IDF_Issue')->getList(array(
'filter' => $sql->gen(),
'order' => 'modif_dtime DESC'
));
$issues += $tmp->getArrayCopy();
}
else {
// ID-based search
if (is_numeric($query)) {
$sql = 'project=%s AND CAST(id AS VARCHAR) LIKE %s';
// MySQL can't cast to VARCHAR and a CAST to CHAR converts
// the whole number, not just the first digit
if (strtolower(Pluf::f('db_engine')) == 'mysql') {
$sql = 'project=%s AND CAST(id AS CHAR) LIKE %s';
}
$sql = new Pluf_SQL($sql, array($prj->id, $query.'%'));
$tmp = Pluf::factory('IDF_Issue')->getList(array(
'filter' => $sql->gen(),
'order' => 'id ASC'
));
$issues += $tmp->getArrayCopy();
}
// text-based search
$res = new Pluf_Search_ResultSet(
IDF_Search::mySearch($query, $prj, 'IDF_Issue')
);
foreach ($res as $issue)
$issues[] = $issue;
}
// Autocomplete from jQuery UI works with JSON, this old one still
// expects a parsable string; since we'd need to bump jQuery beyond
// 1.2.6 for this to use as well, we're trying to cope with the old format.
// see http://www.learningjquery.com/2010/06/autocomplete-migration-guide
$out = '';
$ids = array();
foreach ($issues as $issue)
{
if ($issue->id == $issue_id)
continue;
if (in_array($issue->id, $ids))
continue;
if (--$limit < 0)
break;
$out .= str_replace('|', '&#124;', $issue->summary) .'|'.$issue->id."\n";
$ids[] = $issue->id;
}
return new Pluf_HTTP_Response($out);
}
/**
* Star/Unstar an issue.
*/
@@ -715,6 +881,15 @@ class IDF_Views_Issue
}
$auto['auto_owner'] = substr($auto['auto_owner'], 0, -2);
unset($auto['_auto_owner']);
// Get issue relations
$r = $project->getRelationsFromConfig();
$auto['auto_relation_types'] = '';
foreach ($r as $rt) {
$esc = Pluf_esc($rt);
$auto['auto_relation_types'] .= sprintf('{ name: "%s", to: "%s" }, ',
$esc, $esc);
}
$auto['auto_relation_types'] = substr($auto['auto_relation_types'], 0, -2);
return $auto;
}
}

View File

@@ -344,7 +344,8 @@ class IDF_Views_Project
$params = array();
$keys = array('labels_issue_template',
'labels_issue_open', 'labels_issue_closed',
'labels_issue_predefined', 'labels_issue_one_max');
'labels_issue_predefined', 'labels_issue_one_max',
'issue_relations');
foreach ($keys as $key) {
$_val = $conf->getVal($key, false);
if ($_val !== false) {

View File

@@ -66,7 +66,8 @@ class IDF_Views_Source
'commit' => $commit,
'branches' => $branches,
);
return Pluf_Shortcuts_RenderToResponse('idf/source/invalid_revision.html',
$scmConf = $request->conf->getVal('scm', 'git');
return Pluf_Shortcuts_RenderToResponse('idf/source/'.$scmConf.'/invalid_revision.html',
$params, $request);
}
@@ -307,12 +308,7 @@ class IDF_Views_Source
$cobject->diff = null;
$diff->parse();
$scmConf = $request->conf->getVal('scm', 'git');
try {
$changes = $scm->getChanges($commit);
} catch (Exception $e) {
// getChanges is not yes supported by this backend.
$changes = array();
}
$branches = $scm->getBranches();
$in_branches = $scm->inBranches($cobject->commit, '');
$tags = $scm->getTags();

View File

@@ -118,6 +118,11 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/$#',
'model' => 'IDF_Views_Issue',
'method' => 'index');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/summary/$#',
'base' => $base,
'model' => 'IDF_Views_Issue',
'method' => 'summary');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/search/$#',
'base' => $base,
'model' => 'IDF_Views_Issue',
@@ -148,10 +153,10 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/create/$#',
'model' => 'IDF_Views_Issue',
'method' => 'create');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/my/(\w+)/$#',
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/(.*)/(\w+)/$#',
'base' => $base,
'model' => 'IDF_Views_Issue',
'method' => 'myIssues');
'method' => 'userIssues');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/attachment/(\d+)/(.*)$#',
'base' => $base,
@@ -173,6 +178,11 @@ $ctl[] = array('regex' => '#^/watchlist/(\w+)$#',
'model' => 'IDF_Views_Issue',
'method' => 'forgeWatchList');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/autocomplete/(\d*)$#',
'base' => $base,
'model' => 'IDF_Views_Issue',
'method' => 'autoCompleteIssueList');
// ---------- SCM ----------------------------------------
$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/help/$#',

View File

@@ -130,7 +130,7 @@ msgstr "Entferntes Subversion-Depot"
#: IDF/Form/Admin/ProjectCreate.php:92 IDF/Form/SourceConf.php:40
msgid "Repository username"
msgstr "Depot-Nutzername"
msgstr "Depot-Benutzername"
#: IDF/Form/Admin/ProjectCreate.php:99 IDF/Form/SourceConf.php:47
msgid "Repository password"
@@ -156,11 +156,11 @@ msgstr "Projekt-Eigentümer"
#: IDF/Form/Admin/ProjectCreate.php:123 IDF/Form/Admin/ProjectUpdate.php:76
#: IDF/Form/MembersConf.php:54 IDF/Form/TabsConf.php:52
msgid "Project members"
msgstr "Projekt-Mitglieder"
msgstr "Projektmitglieder"
#: IDF/Form/Admin/ProjectCreate.php:136
msgid "Project template"
msgstr "Projekt-Vorlage"
msgstr "Projektvorlage"
#: IDF/Form/Admin/ProjectCreate.php:138
msgid ""
@@ -168,7 +168,7 @@ msgid ""
"general configuration will be taken from the template project."
msgstr ""
"Benutze das vorgegebene Projekt, um ein neues zu initialisieren. "
"Zugriffsrechte und allgemeine Konfiguration werden von der Projekt-Vorlage "
"Zugriffsrechte und allgemeine Konfiguration werden von der Projektvorlage "
"übernommen."
#: IDF/Form/Admin/ProjectCreate.php:185
@@ -350,7 +350,7 @@ msgid ""
"The password must be hard for other people to guess, but easy for the user "
"to remember."
msgstr ""
"Das Passwort sollte für andere Leute schwer zu erraten, aber für den Nutzer "
"Das Passwort sollte für andere Leute schwer zu erraten, aber für den Benutzer "
"einfach zu erinnern sein."
#: IDF/Form/Admin/UserUpdate.php:89
@@ -366,7 +366,7 @@ msgstr "Beschreibung"
#: IDF/Form/Admin/UserUpdate.php:109 IDF/Form/UserAccount.php:110
msgid "Twitter username"
msgstr "Twitter-Nutzername"
msgstr "Twitter-Benutzername"
#: IDF/Form/Admin/UserUpdate.php:119 IDF/Form/UserAccount.php:120
msgid "Public email address"
@@ -403,7 +403,7 @@ msgstr "Stab"
#: IDF/Form/Admin/UserUpdate.php:164
msgid "If you give staff rights to a user, you really need to trust them."
msgstr ""
"Wenn Du einen Nutzer zum Stab hinzufügst, solltest Du ihm wirklich trauen."
"Wenn Du einen Benutzer zum Stab hinzufügst, solltest Du ihm wirklich trauen."
#: IDF/Form/Admin/UserUpdate.php:172 IDF/Views/Admin.php:213
msgid "Active"
@@ -414,8 +414,8 @@ msgid ""
"If the user is not getting the confirmation email or is abusing the system, "
"you can directly enable or disable their account here."
msgstr ""
"Wenn der Nutzer keine Bestätigungs-E-Mail erhält oder das System "
"missbraucht, kannst Du sein Konto hier direkt aktivieren oder deaktivieren."
"Wenn der Benutzer keine Bestätigungs-E-Mail erhält oder das System "
"missbraucht, kannst Du sein Benutzerkonto hier direkt aktivieren oder deaktivieren."
#: IDF/Form/Admin/UserUpdate.php:274
msgid "--- is not a valid first name."
@@ -425,7 +425,7 @@ msgstr "--- ist kein gültiger Vorname"
msgid ""
"A user with this email already exists, please provide another email address."
msgstr ""
"Ein Nutzer mit dieser E-Mail-Adresse existiert bereits, bitte gib eine "
"Ein Benutzer mit dieser E-Mail-Adresse existiert bereits, bitte gib eine "
"andere E-Mail-Adresse an."
#: IDF/Form/Admin/UserUpdate.php:301 IDF/Form/UserAccount.php:389
@@ -678,7 +678,7 @@ msgstr ""
#: IDF/Form/Register.php:148
msgid "Confirm the creation of your account."
msgstr "Bestätige die Erstellung deines Accounts."
msgstr "Bestätige die Erstellung Deines Benutzerkontos."
#: IDF/Form/RegisterConfirmation.php:40 IDF/Form/RegisterInputKey.php:36
msgid "Your confirmation key"
@@ -705,7 +705,7 @@ msgid ""
"This account has already been confirmed. Maybe should you try to recover "
"your password using the help link."
msgstr ""
"Dieses Konto wurde bereits bestätigt. Vielleicht möchtest Du versuchen, Dein "
"Dieses Benutzerkonto wurde bereits bestätigt. Vielleicht möchtest Du versuchen, Dein "
"Passwort über den Hilfe-Link wiederherzustellen?"
#: IDF/Form/ReviewCreate.php:74
@@ -1204,7 +1204,7 @@ msgstr ""
"Quellcode.<br />\n"
"Wenn Du den Zugriff auf den Quellcode beschränkst, wird kein anonymer "
"Zugriff<br />\n"
"angeboten und die Nutzer müssen sich mit ihrem Passwort oder "
"angeboten und die Benutzer müssen sich mit ihrem Passwort oder "
"öffentlichen<br />\n"
"Schlüssel authentifizieren."
@@ -1241,7 +1241,7 @@ msgid ""
"\" will default to authorized users only."
msgstr ""
"Wenn Du ein Projekt als privat markierst, haben nur Projektmitglieder und "
"Administratoren zusammen mit den von Dir extra authorisierten Nutzern "
"Administratoren zusammen mit den von Dir extra authorisierten Benutzern "
"Zugriff darauf. Du kannst weiterhin zusätzliche Zugriffsrechte für bestimmte "
"Projektfunktionen vergeben, aber die Einstellungen \"Für alle offen\" und "
"\"Angemeldete Benutzer\" werden standardmäßig nur authorisierte Benutzer "
@@ -1287,7 +1287,7 @@ msgid ""
"<a href=\"%%url%%\">Sign in or create your account</a> to create issues or "
"add comments"
msgstr ""
"<a href=\"%%url%%\">Melde Dich an oder lege ein Konto an</a>, um Tickets "
"<a href=\"%%url%%\">Melde Dich an oder lege ein Benutzerkonto an</a>, um Tickets "
"oder Kommentare hinzuzufügen"
#: IDF/gettexttemplates/idf/base-full.html.php:4
@@ -1587,7 +1587,7 @@ msgid ""
"You need to create an account on <a href=\"http://en.gravatar.com/"
"\">Gravatar</a>, this takes about 5 minutes and is free."
msgstr ""
"Du musst Dir einen Account auf <a href=\"http://de.gravatar.com/\">Gravatar</"
"Du musst Dir ein Benutzerkonto auf <a href=\"http://de.gravatar.com/\">Gravatar.com</"
"a> erstellen, es dauert nur 5 Minuten und ist kostenfrei."
#: IDF/gettexttemplates/idf/faq.html.php:10
@@ -1632,7 +1632,7 @@ msgstr "<kbd>Umschalt+h</kbd>: Diese Hilfeseite."
#: IDF/gettexttemplates/idf/faq.html.php:18
msgid "If you are in a project, you have the following shortcuts:"
msgstr ""
"Befindest Du Dich in einem Projekt, kannst du die folgenden Tastaturkürzel "
"Befindest Du Dich in einem Projekt, kannst Du die folgenden Tastaturkürzel "
"benutzen:"
#: IDF/gettexttemplates/idf/faq.html.php:19
@@ -1874,28 +1874,28 @@ msgstr "Es wird eine Bestätigung verlangt."
#: IDF/gettexttemplates/idf/gadmin/users/base.html.php:3
#: IDF/Views/Admin.php:201
msgid "User List"
msgstr "Nutzerliste"
msgstr "Benutzerliste"
#: IDF/gettexttemplates/idf/gadmin/users/base.html.php:4
#: IDF/gettexttemplates/idf/gadmin/users/update.html.php:13
msgid "Update User"
msgstr "Nutzer aktualisieren"
msgstr "Benutzer aktualisieren"
#: IDF/gettexttemplates/idf/gadmin/users/base.html.php:5
#: IDF/gettexttemplates/idf/gadmin/users/create.html.php:4
msgid "Create User"
msgstr "Nutzer anlegen"
msgstr "Benutzer anlegen"
#: IDF/gettexttemplates/idf/gadmin/users/create.html.php:3
msgid "The form contains some errors. Please correct them to create the user."
msgstr ""
"Die Eingabemaske enthält einige Fehler. Bitte korrigiere diese, um den "
"Nutzer zu erstellen."
"Benutzer zu erstellen."
#: IDF/gettexttemplates/idf/gadmin/users/create.html.php:6
msgid "The user password will be sent by email to the user."
msgstr ""
"Das Passwort des Nutzers wird an die E-Mail-Adresse des Nutzers versandt."
"Das Passwort des Benutzers wird an die E-Mail-Adresse des Benutzers versandt."
#: IDF/gettexttemplates/idf/gadmin/users/createuser-email.txt.php:3
#, php-format
@@ -1916,10 +1916,11 @@ msgid ""
msgstr ""
"Hallo %%user%%,\n"
"\n"
"Der Administrator %%admin%% hat ein neues Konto für Dich\n"
"auf der Forge angelegt.\n"
"Der Administrator %%admin%% hat ein neues Benutzerkonto\n"
"für Dich auf der Forge angelegt.\n"
"\n"
"Hier sind die Zugangsdaten, mit denen Du auf Dein Konto zugreifen kannst:\n"
"Hier sind die Zugangsdaten, mit denen Du auf Dein Benutzerkonto\n"
"zugreifen kannst:\n"
"\n"
" Adresse: %%url%%\n"
" Anmeldename: %%user.login%%\n"
@@ -1931,17 +1932,17 @@ msgstr ""
#: IDF/gettexttemplates/idf/gadmin/users/index.html.php:3
#, php-format
msgid "See <a href=\"%%url%%\">not validated users</a>."
msgstr "Zeige <a href=\"%%url%%\">nicht validierte Nutzer</a>."
msgstr "Zeige <a href=\"%%url%%\">nicht validierte Benutzer</a>."
#: IDF/gettexttemplates/idf/gadmin/users/index.html.php:4
msgid "<p>You have here an overview of the users registered in the forge.</p>"
msgstr ""
"<p>Hier siehst Du eine Übersicht der Nutzer, die sich in der Forge "
"<p>Hier siehst Du eine Übersicht der Benutzer, die sich in der Forge "
"registriert haben."
#: IDF/gettexttemplates/idf/gadmin/users/index.html.php:5
msgid "Number of users:"
msgstr "Anzahl der Nutzer:"
msgstr "Anzahl der Benutzer:"
#: IDF/gettexttemplates/idf/gadmin/users/update.html.php:3
msgid ""
@@ -1949,7 +1950,7 @@ msgid ""
"need to ensure that you are providing a valid email\n"
"address"
msgstr ""
"Wenn Du die E-Mail-Adresse eines Nutzers änderst,\n"
"Wenn Du die E-Mail-Adresse eines Benutzers änderst,\n"
"musst Du sicherstellen, eine gültige, neue E-Mail-Adresse\n"
"als Ersatz bereitzustellen."
@@ -1959,14 +1960,14 @@ msgid ""
"able to create new projects and update other non staff users.\n"
msgstr ""
"Wenn Du dem Benutzer Stab-Rechte erteilst, kann er\n"
"neue Projekte anlegen und andere Nutzer, die nicht zum Stab\n"
"neue Projekte anlegen und andere Benutzer, die nicht zum Stab\n"
"gehören, aktualisieren.\n"
#: IDF/gettexttemplates/idf/gadmin/users/update.html.php:9
msgid "The form contains some errors. Please correct them to update the user."
msgstr ""
"Die Eingabemaske enthält einige Fehler. Bitte korrigiere diese, um den "
"Nutzer zu aktualisieren."
"Benutzer zu aktualisieren."
#: IDF/gettexttemplates/idf/gadmin/users/update.html.php:10
#: IDF/gettexttemplates/idf/register/confirmation.html.php:4
@@ -2449,7 +2450,7 @@ msgid ""
"is still valid and more work is needed to fully fix it."
msgstr ""
"Dieses Ticket wurde als geschlossen markiert. Füge nur dann einen Kommentar "
"hinzu, wenn du denkst, dass das geschilderte Problem noch nicht ganz "
"hinzu, wenn Du denkst, dass das geschilderte Problem noch nicht ganz "
"beseitigt wurde."
#: IDF/gettexttemplates/idf/issues/view.html.orig.php:8
@@ -2531,7 +2532,7 @@ msgid ""
"If you don't have an account yet, you can create one <a href=\"%%url%%"
"\">here</a>."
msgstr ""
"Wenn Du noch kein Konto hast, kannst Du <a href=\"%%url%%\">hier</a> ein "
"Wenn Du noch kein Benutzerkonto hast, kannst Du <a href=\"%%url%%\">hier</a> ein "
"neues erstellen."
#: IDF/gettexttemplates/idf/login_form.html.php:4
@@ -2560,7 +2561,7 @@ msgstr "Willkommen."
#: IDF/gettexttemplates/idf/login_form.html.php:10
msgid "It takes less than a minute to create your account."
msgstr "Es dauert weniger als eine Minute, um Dein Konto zu erstellen."
msgstr "Es dauert weniger als eine Minute, um Dein Benutzerkonto zu erstellen."
#: IDF/gettexttemplates/idf/main-menu.html.php:3
#, php-format
@@ -2576,7 +2577,7 @@ msgstr "Abmelden"
#: IDF/gettexttemplates/idf/main-menu.html.php:5
msgid "Sign in or create your account"
msgstr "Anmelden oder Konto erstellen"
msgstr "Anmelden oder Benutzerkonto erstellen"
#: IDF/gettexttemplates/idf/main-menu.html.php:8 IDF/Views/Admin.php:42
msgid "Forge Management"
@@ -2671,7 +2672,7 @@ msgid ""
msgstr ""
"Hallo,\n"
"\n"
"Du hast die Anlage eines Kontos angeforder, um an dem Leben\n"
"Du hast die Anlage eines Benutzerkontos angefordert, um an dem Leben\n"
"eines Softwareprojektes teilhaben zu können.\n"
"\n"
"Um Dein Konto zu bestätigen, folge bitte dem folgenden Link:\n"
@@ -2688,7 +2689,7 @@ msgstr ""
"\n"
"Wenn Du nicht mehr länger an diesem Software-Projekt\n"
"interessiert bist oder wenn Du Dich nicht daran erinnern\n"
"kannst, dieses Konto erstellen zu wollen, entschuldige\n"
"kannst, dieses Benutzerkonto erstellen zu wollen, entschuldige\n"
"und ignoriere einfach diese E-Mail.\n"
"\n"
"Mit freundlichen Grüßen,\n"
@@ -2716,7 +2717,7 @@ msgid ""
"strong> to log in afterwards."
msgstr ""
"Dies ist der letzte Schritt, aber bitte <strong>stelle sicher, dass Du "
"Cookies aktiviert hast</strong>, um Dich im folgenden anzumelden."
"Cookies aktiviert hast</strong>, um Dich im Folgenden anzumelden."
#: IDF/gettexttemplates/idf/register/index.html.php:3
#: IDF/gettexttemplates/idf/register/index.html~.php:3
@@ -2736,7 +2737,7 @@ msgid ""
"login name and password."
msgstr ""
"Wenn Du einfach nur Deine Anmeldedaten vergessen hast, gibt es keinen Grund, "
"ein neues Konto zu erstellen. Gehe einfach <a href=\"%%url%%\">hier her</a>, "
"ein neues Benutzerkonto zu erstellen. Gehe einfach <a href=\"%%url%%\">hier her</a>, "
"um Deinen Anmeldenamen und Dein Passwort wiederherzustellen."
#: IDF/gettexttemplates/idf/register/index.html.php:5
@@ -2748,7 +2749,7 @@ msgid ""
"you have troubles, you can <a href=\"%%url%%\">let us know about your issues "
"at anytime</a>!"
msgstr ""
"Durch Dein Konto bist Du in der Lage, an dem Leben aller Softwareprojekte, "
"Durch Dein Benutzerkonto bist Du in der Lage, an dem Leben aller Softwareprojekte, "
"die hierüber verwaltet werden, teilzuhaben. Das Teilnehmen an einem "
"Softwareprojekt muss Spass machen, deshalb <a href=\"%%url%%\">lass uns "
"jederzeit von Deinen Problemen wissen</a>, solltest Du welche haben."
@@ -3209,13 +3210,13 @@ msgstr "Zweige filtern"
#: IDF/gettexttemplates/idf/source/mercurial/branch_tag_list.html.php:5
#: IDF/gettexttemplates/idf/source/mtn/branch_tag_list.html.php:5
msgid "Tags"
msgstr "Marken"
msgstr "Tags"
#: IDF/gettexttemplates/idf/source/git/branch_tag_list.html.php:6
#: IDF/gettexttemplates/idf/source/mercurial/branch_tag_list.html.php:6
#: IDF/gettexttemplates/idf/source/mtn/branch_tag_list.html.php:6
msgid "filter tags"
msgstr "Marken filtern"
msgstr "Tags filtern"
#: IDF/gettexttemplates/idf/source/git/file.html.php:3
#: IDF/gettexttemplates/idf/source/git/tree.html.php:3
@@ -3231,7 +3232,7 @@ msgid ""
"%%cobject.date%%."
msgstr ""
"Quellcode bei Revision <a class=\"mono\" href=\"%%url%%\">%%commit%%</a> "
"erzeugt am %%cobject.date%%."
"erzeugt %%cobject.date%%."
#: IDF/gettexttemplates/idf/source/git/file.html.php:4
#: IDF/gettexttemplates/idf/source/git/tree.html.php:4
@@ -3409,8 +3410,8 @@ msgstr "Revision:"
#: IDF/gettexttemplates/idf/source/svn/commit.html.php:4
#: IDF/gettexttemplates/idf/source/svn/file.html.php:11
#: IDF/gettexttemplates/idf/source/svn/tree.html.php:16
msgid "Go to revision"
msgstr "Zu Revision gehen:"
msgid "Switch"
msgstr "Wechseln"
#: IDF/gettexttemplates/idf/source/svn/file.html.php:6
#: IDF/gettexttemplates/idf/source/svn/tree.html.php:11
@@ -3442,7 +3443,7 @@ msgstr "Zweige:"
#: IDF/gettexttemplates/idf/source/svn/tree.html.php:18
msgid "Tags:"
msgstr "Marken:"
msgstr "Tags:"
#: IDF/gettexttemplates/idf/user/changeemail-email.txt.php:3
#, php-format
@@ -3491,7 +3492,7 @@ msgstr ""
#: IDF/gettexttemplates/idf/user/changeemail.html.php:4
msgid "Confirm Your New Email Address"
msgstr "Bestätige deine neue Email-Adresse"
msgstr "Bestätige Deine neue Email-Adresse"
#: IDF/gettexttemplates/idf/user/changeemail.html.php:7
#: IDF/gettexttemplates/idf/user/passrecovery-inputkey.html.php:7
@@ -3508,7 +3509,7 @@ msgstr ""
#: IDF/gettexttemplates/idf/user/dashboard.html.php:3
#, php-format
msgid "<a href=\"%%url%%\">Update your account</a>."
msgstr "<a href=\"%%url%%\">Aktualisiere Dein Konto</a>."
msgstr "<a href=\"%%url%%\">Aktualisiere Dein Benutzerkonto</a>."
#: IDF/gettexttemplates/idf/user/dashboard.html.php:4
#, php-format
@@ -3578,7 +3579,7 @@ msgid ""
"If possible, use your real name. By using your real name, people will have "
"more trust in your comments and remarks."
msgstr ""
"Wenn möglich, nutze Deinen realen Namen. Dadurch wird deinen Kommentaren und "
"Wenn möglich, nutze Deinen realen Namen. Dadurch wird Deinen Kommentaren und "
"Bemerkungen mehr Vertrauen geschenkt."
#: IDF/gettexttemplates/idf/user/myaccount.html.php:19
@@ -3646,7 +3647,7 @@ msgstr ""
"Hallo %%user%%,\n"
"\n"
"Du hast Dein Passwort verloren und möchtest es wiederherstellen.\n"
"Um ein neues Passwort für Dein Konto anzugeben, musst Du lediglich\n"
"Um ein neues Passwort für Dein Benutzerkonto anzugeben, musst Du lediglich\n"
"dem folgenden Link folgen. Sie führt zu einer einfachen Eingabemaske,\n"
"mit der Du ein neues Passwort setzen kannst.\n"
"\n"
@@ -3801,7 +3802,7 @@ msgstr ""
"a> mit der <a href=\"%%eurl%%\"><em>Extra</em>-Erweiterung</a> verwendet "
"werden.</p>\n"
"<p>Website-Adresses werden automatisch verlinkt und Du kannst außerdem zu "
"anderen Dokumentations-Seiten durch die Benutzung von doppelten, eckige "
"anderen Dokumentations-Seiten durch die Benutzung von doppelten, eckigen "
"Klammern verlinken, etwa so: [[AndereSeite]].</p>\n"
"<p>Um direkt die Inhalte einer Datei aus dem Depot einzubinden, umklammere "
"den Pfad zur Datei mit dreifachen, eckigen Klammern: [[[Pfad/zu/Datei.txt]]]."
@@ -4388,7 +4389,7 @@ msgstr "Administrator"
#: IDF/Views/Admin.php:214
msgid "Last Login"
msgstr "Letzter Login"
msgstr "Letztes Login"
#: IDF/Views/Admin.php:221
msgid "No users were found."
@@ -4954,7 +4955,7 @@ msgstr ""
#: IDF/Views.php:284
msgid "Here to Help You!"
msgstr "Hier, um dir zu helfen!"
msgstr "Hier, um Dir zu helfen!"
#: IDF/Views.php:300
msgid "InDefero API (Application Programming Interface)"

View File

@@ -45,6 +45,7 @@ $m['IDF_Scm_Cache_Git'] = array('relate_to' => array('IDF_Project'));
$m['IDF_UserData'] = array('relate_to' => array('Pluf_User'));
$m['IDF_EmailAddress'] = array('relate_to' => array('Pluf_User'));
$m['IDF_IssueRelation'] = array('relate_to' => array('IDF_Issue', 'Pluf_User'));
Pluf_Signal::connect('Pluf_Template_Compiler::construct_template_tags_modifiers',
array('IDF_Middleware', 'updateTemplateTagsModifiers'));

View File

@@ -35,6 +35,13 @@
</td>
</tr>
<tr>
<td colspan="2"><strong>{$form.f.issue_relations.labelTag}:</strong><br />
{if $form.f.issue_relations.errors}{$form.f.issue_relations.fieldErrors}{/if}
{$form.f.issue_relations|unsafe}<br />
<span class="helptext">{$form.f.issue_relations.help_text}</span>
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="{trans 'Save Changes'}" name="submit" />
</td>

View File

@@ -70,7 +70,6 @@
<div id="ft">{block foot}{/block}</div>
</div>
{include 'idf/js-hotkeys.html'}
{include 'idf/list-filter.html'}
{block javascript}{/block}
{if $project}
<script type="text/javascript" charset="utf-8">{literal}

View File

@@ -49,7 +49,7 @@
</div>
</div>
</div>
<div class="yui-b context">{block context}{/block}</div>
<div class="yui-b context" id="context">{block context}{/block}</div>
</div>
<div id="ft">{block foot}{/block}</div>
</div>

View File

@@ -66,7 +66,7 @@
</div>
</div>
</div>
<div class="yui-b context">{block context}{/block}</div>
<div class="yui-b context" id="context">{block context}{/block}</div>
</div>
<div id="ft">{block foot}{/block}</div>
</div>
@@ -84,6 +84,21 @@ $(document).ready(function(){
else if (frag.length > 3 && frag.substring(0, 3) == '#ic') {
$(frag).addClass("issue-comment-focus");
}
var contextTop = $('div#context').position().top;
var contextFixEnabled = true;
$(window).scroll(function() {
if (!contextFixEnabled || $(window).scrollTop() < contextTop)
$('div#context').css('position', 'relative');
else
$('div#context').css('position', 'fixed');
});
$(window).resize(function() {
contextFixEnabled =
$('div#context').offset().top + $('div#context').height() <
$(window).height();
});
$(window).resize();
});
//]]>{/literal}
</script>{/if}

View File

@@ -8,12 +8,18 @@
<th>{trans "address"}</th>
<th>{trans "port"}</th>
</tr>
{if count($connections) == 0}
<tr>
<td colspan="2" align="center">{trans 'No connections found.'}</td>
</tr>
{else}
{foreach $connections as $connection}
<tr>
<td>{$connection.address}</td>
<td>{$connection.port}</td>
</tr>
{/foreach}
{/if}
</table>
{/block}

View File

@@ -9,6 +9,11 @@
<th>{trans "status"}</th>
<th>{trans "action"}</th>
</tr>
{if count($servers) == 0}
<tr>
<td colspan="3" align="center">{trans 'No monotone servers configured.'}</td>
</tr>
{else}
{foreach $servers as $server}
<tr>
<td>{$server.name}</td>
@@ -31,6 +36,7 @@
{/if}
</tr>
{/foreach}
{/if}
</table>
{/block}

View File

@@ -2,8 +2,9 @@
{block tabissues} class="active"{/block}
{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>
<a {if $inSummaryIssues}class="active" {/if}href="{url 'IDF_Views_Issue::summary', array($project.shortname)}">{trans 'Summary'}</a>
| <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::userIssues', array($project.shortname, $user.login, '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" />

View File

@@ -63,6 +63,15 @@
</td>
</tr>
<tr>
<th>{$form.f.relation_type0.labelTag}:</th>
<td>
{if $form.f.relation_type0.errors}{$form.f.relation_type0.fieldErrors}{/if}
{if $form.f.relation_issue0.errors}{$form.f.relation_issue0.fieldErrors}{/if}
{$form.f.relation_type0|unsafe}
{$form.f.relation_issue0|unsafe}
</td>
</tr>
<tr>
<th>{$form.f.label1.labelTag}:</th>
<td>
{if $form.f.label1.errors}{$form.f.label1.fieldErrors}{/if}{$form.f.label1|unsafe}

View File

@@ -14,8 +14,18 @@
</ul>{/if}
{if $c.changes}
{foreach $c.changes as $w => $v}
<strong>{if $w == 'su'}{trans 'Summary:'}{/if}{if $w == 'st'}{trans 'Status:'}{/if}{if $w == 'ow'}{trans 'Owner:'}{/if}{if $w == 'lb'}{trans 'Labels:'}{/if}</strong> {if $w == 'lb'}{assign $l = implode(', ', $v)}{$l}{else}{$v}{/if}<br />
<strong>{if $w == 'su'}{trans 'Summary:'}{/if}{if $w == 'st'}{trans 'Status:'}{/if}{if $w == 'ow'}{trans 'Owner:'}{/if}{if $w == 'lb'}{trans 'Labels:'}{/if}{if $w == 'rel'}{trans 'Relations:'}{/if}</strong>
{if $w == 'lb' or $w == 'rel'}
{foreach $v as $t => $ls}
{foreach $ls as $l}
{if $t == 'rem'}<s>{/if}{$l}{if $t == 'rem'}</s>{/if}
{/foreach}
{/foreach}
{else}
{$v}
{/if}<br />
{/foreach}
{/if}
</div></content>
</entry>

View File

@@ -16,7 +16,7 @@
{if strlen($c.content) > 0}{$c.content|safe}{/if}{if $c.changedIssue()}
{foreach $c.changes as $w => $v}
{if $w == 'su'}{trans 'Summary:'}{/if}{if $w == 'st'}{trans 'Status:'}{/if}{if $w == 'ow'}{trans 'Owner:'}{/if}{if $w == 'lb'}{trans 'Labels:'}{/if} {if $w == 'lb'}{assign $l = implode(', ', $v)}{$l}{else}{$v}{/if}{/foreach}{/if}{assign $attachments = $c.get_attachment_list()}{if $attachments.count() > 0}
{if $w == 'su'}{trans 'Summary:'}{/if}{if $w == 'st'}{trans 'Status:'}{/if}{if $w == 'ow'}{trans 'Owner:'}{/if}{if $w == 'lb'}{trans 'Labels:'}{/if}{if $w == 'rel'}{trans 'Relations:'}{/if} {if $w == 'lb' or $w == 'rel'}{foreach $v as $t => $ls}{foreach $ls as $l}{if $t == 'rem'}-{/if}{$l} {/foreach}{/foreach}{else}{$v}{/if}{/foreach}{/if}{assign $attachments = $c.get_attachment_list()}{if $attachments.count() > 0}
{trans 'Attachments:'}{foreach $attachments as $a}
- {$a.filename|safe} - {$a.filesize|ssize}

View File

@@ -7,6 +7,8 @@
var auto_labels = [{/literal}{$auto_labels|safe}{literal}];
var auto_status = [{/literal}{$auto_status|safe}{literal}];
var auto_owner = [{/literal}{$auto_owner|safe}{literal}];
var auto_relation_types = [{/literal}{$auto_relation_types|safe}{literal}];
var j=0;
for (j=1;j<7;j=j+1) {
$("#id_label"+j).autocomplete(auto_labels, {
@@ -49,7 +51,39 @@
return row.to;
}
});
for (var idx = 0; ; ++idx) {
if ($("#id_relation_type" + idx).length == 0)
break;
$("#id_relation_type" + idx).autocomplete(auto_relation_types, {
minChars: 0,
width: 310,
matchContains: true,
max: 50,
highlightItem: false,
formatItem: function(row, i, max, term) {
return row.to.replace(new RegExp("(" + term + ")", "gi"), "<strong>$1</strong>") + " <span style='font-size: 80%;'>" + row.name + "</span>";
},
formatResult: function(row) {
return row.to;
}
});
$("#id_relation_issue" + idx).autocomplete("{/literal}{url 'IDF_Views_Issue::autoCompleteIssueList', array($project.shortname, $issue.id)}{literal}", {
minChars: 0,
width: 310,
matchContains: true,
max: 10,
multiple: true,
delay: 500,
highlightItem: false,
formatItem: function(row, i, max, term) {
return row[1].replace(new RegExp("(" + term + ")", "gi"), "<strong>$1</strong>") + " <span style='font-size: 80%;'>" + row[0] + "</span>";
},
formatResult: function(row) {
return row[1];
}
});
}
});
{/literal} //-->
</script>

View File

@@ -0,0 +1,100 @@
{extends "idf/issues/base.html"}
{block docclass}yui-t2{assign $inSummaryIssues=true}{/block}
{block body}
{if $trackerEmpty}
{aurl 'create_url', 'IDF_Views_Issue::create', array($project.shortname)}
<p>{blocktrans}The issue tracker is empty.<br />You can create your first issue <a href="{$create_url}">here</a>.{/blocktrans}</p>
{else}
<div class='issue-summary'>
{foreach $tagStatistics as $key => $class}
<div>
<h2>{blocktrans}Unresolved: By {$key}{/blocktrans}</h2>
<table class='issue-summary'>
<tbody>
{foreach $class as $key => $value}
<tr>
<td class="name"><a href="{url 'IDF_Views_Issue::listLabel', array($project.shortname, $value[2], 'open')}">{$key}</a></td>
<td class="count">{$value[0]}</td>
<td class="graph">
<table class='graph'>
<tbody><tr>
<td style="width:{$value[1] * 0.8 + 1}%" class="graph-color" valign="center">
<div class="colour-bar"></div>
</td>
<td class="graph-percent">{$value[1]}%</td>
</tr>
</tbody>
</table>
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
{/foreach}
</div>
<div class='issue-summary'>
{if $status}
<div>
<h2>{blocktrans}Status Summary{/blocktrans}</h2>
<table class='issue-summary'>
<tbody>
{foreach $status as $key => $value}
<tr>
<td class="name"><a href="{url 'IDF_Views_Issue::listStatus', array($project.shortname, $key)}">{$key}</a></td>
<td class="count">{$value[0]}</td>
<td class="graph">
<table class='graph'>
<tbody><tr>
<td style="width:{$value[1] * 0.8 + 1}%" class="graph-color" valign="center">
<div class="colour-bar"></div>
</td>
<td class="graph-percent">{$value[1]}%</td>
</tr>
</tbody>
</table>
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
{/if}
{if $ownerStatistics}
<div>
<h2>{blocktrans}Unresolved: By Assignee{/blocktrans}</h2>
<table class='issue-summary'>
<tbody>
{foreach $ownerStatistics as $key => $value}
<tr>
<td class="name">
{if !empty($value[2])}
{aurl 'url', 'IDF_Views_Issue::userIssues', array($project.shortname, $value[2], 'owner')}
<a href="{$url}">{$key}</a>
{else}{$key}{/if}
</td>
<td class="count">{$value[0]}</td>
<td class="graph">
<table class='graph'>
<tbody><tr>
<td style="width:{$value[1] * 0.8 + 1}%" class="graph-color" valign="center">
<div class="colour-bar"></div>
</td>
<td class="graph-percent">{$value[1]}%</td>
</tr>
</tbody>
</table>
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
{/if}
</div>
{/if}
{/block}

View File

@@ -1,5 +1,5 @@
{extends "idf/issues/base.html"}
{block docclass}yui-t2{assign $inMyIssues = true}{/block}
{block docclass}yui-t2{if $user.login == $login}{assign $inMyIssues = true}{/if}{/block}
{block body}
{$issues.render}
{if !$user.isAnonymous()}
@@ -8,10 +8,10 @@
{/block}
{block context}
{aurl 'owner_url', 'IDF_Views_Issue::myIssues', array($project.shortname, 'owner')}
{aurl 'submit_url', 'IDF_Views_Issue::myIssues', array($project.shortname, 'submit')}
{aurl 'owner_closed_url', 'IDF_Views_Issue::myIssues', array($project.shortname, 'ownerclosed')}
{aurl 'submit_closed_url', 'IDF_Views_Issue::myIssues', array($project.shortname, 'submitclosed')}
{aurl 'owner_url', 'IDF_Views_Issue::userIssues', array($project.shortname, $login, 'owner')}
{aurl 'submit_url', 'IDF_Views_Issue::userIssues', array($project.shortname, $login, 'submit')}
{aurl 'owner_closed_url', 'IDF_Views_Issue::userIssues', array($project.shortname, $login, 'ownerclosed')}
{aurl 'submit_closed_url', 'IDF_Views_Issue::userIssues', array($project.shortname, $login, 'submitclosed')}
<p><strong>{trans 'Submitted issues:'}</strong> <a href="{$submit_url}">{$nb_submit}</a>
{if $nb_submit_closed}<br /><span class="helptext">{blocktrans $nb_submit_closed}See the <a href="{$submit_closed_url}">{$nb_submit_closed} closed</a>.{plural}See the <a href="{$submit_closed_url}">{$nb_submit_closed} closed</a>.{/blocktrans}</span>{/if}</p>
{if $nb_owner > 0}

View File

@@ -17,7 +17,7 @@
{assign $submitter_data = $c.get_submitter_data()}
<div class="issue-comment{if $i == 0} issue-comment-first{/if}{if $i == ($nc-1)} issue-comment-last{/if}" id="ic{$c.id}">
{if $submitter_data.avatar != ''}
<img style="float:right; position: relative; max-height: 60px; max-width: 60px;" src="{media}/upload/avatars/{$submitter_data.avatar}" alt=" " />
<img style="float:right; position: relative; max-height: 60px; max-width: 60px;" src="{upload}/avatars/{$submitter_data.avatar}" alt=" " />
{else}
<img style="float:right; position: relative; max-height: 60px; max-width: 60px;" src="http://www.gravatar.com/avatar/{$submitter.email|md5}.jpg?s=60&amp;d={media}/idf/img/spacer.gif" alt=" " />
{/if}
@@ -40,7 +40,16 @@
{if $i> 0 and $c.changedIssue()}
<div class="issue-changes">
{foreach $c.changes as $w => $v}
<strong>{if $w == 'su'}{trans 'Summary:'}{/if}{if $w == 'st'}{trans 'Status:'}{/if}{if $w == 'ow'}{trans 'Owner:'}{/if}{if $w == 'lb'}{trans 'Labels:'}{/if}</strong> {if $w == 'lb'}{assign $l = implode(', ', $v)}{$l}{else}{$v}{/if}<br />
<strong>{if $w == 'su'}{trans 'Summary:'}{/if}{if $w == 'st'}{trans 'Status:'}{/if}{if $w == 'ow'}{trans 'Owner:'}{/if}{if $w == 'lb'}{trans 'Labels:'}{/if}{if $w == 'rel'}{trans 'Relations:'}{/if}</strong>
{if $w == 'lb' or $w == 'rel'}
{foreach $v as $t => $ls}
{foreach $ls as $l}
{if $t == 'rem'}<s>{/if}{$l}{if $t == 'rem'}</s>{/if}
{/foreach}
{/foreach}
{else}
{$v}
{/if}<br />
{/foreach}
</div>
{/if}
@@ -120,6 +129,23 @@
</td>
</tr>
<tr>
<th>{$form.f.relation_type0.labelTag}:</th>
<td>
{assign $prevField}
{foreach $form as $field}
{if strpos($field.name, 'relation_type') === 0}
{$field|unsafe}
{assign $prevField = $field}
{/if}
{if strpos($field.name, 'relation_issue') === 0}
{$field|unsafe}<br />
{if $prevField.errors}{$prevField.fieldErrors}{/if}
{if $field.errors}{$field.fieldErrors}{/if}
{/if}
{/foreach}
</td>
</tr>
<tr>
<th>{$form.f.label1.labelTag}:</th>
<td>
{if $form.f.label1.errors}{$form.f.label1.fieldErrors}{/if}{$form.f.label1|unsafe}
@@ -161,6 +187,21 @@
<span class="label"><a href="{$url}" class="label"><strong>{$tag.class}:</strong>{$tag.name}</a></span><br />
{/foreach}
</p>{/if}
{if count($related_issues) > 0}
{foreach $related_issues as $verb => $rel_issues}
<p>
<strong>{blocktrans}This issue {$verb}{/blocktrans}</strong><br />
{foreach $rel_issues as $rel_issue}
<span class="label">
<a href="{url 'IDF_Views_Issue::view', array($project.shortname, $rel_issue.other_issue)}"
class="label" title="{$rel_issue.other_summary}">
<strong>{$rel_issue.other_issue}</strong> - {$rel_issue.other_summary|shorten:30}
</a>
</span><br />
{/foreach}
</p>
{/foreach}
{/if}
</div>
{/block}
{block javascript}{if $form}{include 'idf/issues/js-autocomplete.html'}

View File

@@ -10,8 +10,8 @@
{if $hasWikiAccess}{hotkey 'Shift+o', 'IDF_Views_Wiki::index', array($project.shortname)}{/if}
{if $hasSourceAccess}{hotkey 'Shift+s', 'IDF_Views_Source::treeBase', array($project.shortname, $project.getScmRoot())}{/if}
{if $hasIssuesAccess and !$user.isAnonymous()}
{hotkey 'Shift+m', 'IDF_Views_Issue::myIssues', array($project.shortname, 'submit')}
{hotkey 'Shift+w', 'IDF_Views_Issue::myIssues', array($project.shortname, 'owner')}
{hotkey 'Shift+m', 'IDF_Views_Issue::userIssues', array($project.shortname, $user.login, 'submit')}
{hotkey 'Shift+w', 'IDF_Views_Issue::userIssues', array($project.shortname, $user.login, 'owner')}
{/if}{/if} //-->
</script>

View File

@@ -4,7 +4,7 @@
<div id="sub-tabs">
<a {if $inOpenReviews}class="active" {/if}href="{url 'IDF_Views_Review::index', array($project.shortname)}">{trans 'Open Reviews'}</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::userIssues', array($project.shortname, $user.login, 'submit')}">{trans 'My Issues'}</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

@@ -109,7 +109,7 @@ to propose more contributions</strong>.
{foreach $comments as $c}{ashowuser 'submitter', $c.get_submitter(), $request}{assign $submitter = $c.get_submitter()}{assign $submitter_data = $c.get_submitter_data()}
<div class="issue-comment{if $i == 1} issue-comment-first{/if}{if $i == ($nc)} issue-comment-last{/if}" id="ic{$c.id}">
{if $submitter_data.avatar != ''}
<img style="float:right; position: relative; max-height: 60px; max-width: 60px;" src="{media}/upload/avatars/{$submitter_data.avatar}" alt=" " />
<img style="float:right; position: relative; max-height: 60px; max-width: 60px;" src="{upload}/avatars/{$submitter_data.avatar}" alt=" " />
{else}
<img style="float:right; position: relative;" src="http://www.gravatar.com/avatar/{$submitter.email|md5}.jpg?s=60&amp;d={media}/idf/img/spacer.gif" alt=" " />
{/if}

View File

@@ -34,6 +34,9 @@
{foreach $changes.renames as $oldname => $newname}
<tr><td><span class="scm-action renamed" title="{trans 'renamed'}">R</span></td><td><a href="{url 'IDF_Views_Source::tree', array($project.shortname, $commit, $newname)}">{$oldname} &rarr; {$newname}</a></td></tr>
{/foreach}
{foreach $changes.copies as $srcname => $destname}
<tr><td><span class="scm-action copied" title="{trans 'copied'}">C</span></td><td><a href="{url 'IDF_Views_Source::tree', array($project.shortname, $commit, $destname)}">{$srcname} &rarr; {$destname}</a></td></tr>
{/foreach}
{foreach $changes.additions as $filename}
<tr><td><span class="scm-action added" title="{trans 'added'}">A</span></td><td><a href="{url 'IDF_Views_Source::tree', array($project.shortname, $commit, $filename)}">{$filename}</a>{if !empty($diff.files[$filename])} (<a href="#diff-{$filename|md5}">{trans 'full'}</a>){/if}</td></tr>
{/foreach}

View File

@@ -0,0 +1,2 @@
{extends "idf/source/invalid_revision.html"}

View File

@@ -5,6 +5,7 @@
<p>{blocktrans}The branch or revision <b>{$commit}</b> is not valid or does not exist
in this repository.{/blocktrans}</p>
{if count($branches) > 0}
<p>{blocktrans}The following list shows all available branches:{/blocktrans}</p>
<ul>
{foreach $branches as $branch => $path}
@@ -14,6 +15,7 @@ in this repository.{/blocktrans}</p>
</li>
{/foreach}
</ul>
{/if}
{if $isOwner or $isMember}
{aurl 'url', 'IDF_Views_Source::help', array($project.shortname)}

View File

@@ -0,0 +1,2 @@
{extends "idf/source/invalid_revision.html"}

View File

@@ -5,12 +5,25 @@
<h2 class="top"><a href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $commit)}">{trans 'Root'}</a><span class="sep">/</span>{if $breadcrumb}{$breadcrumb|safe}{/if}</h2>
<table class="code" summary=" ">
{if !$tree_in and !$tags_in}
{if (!$tree_in and !$tags_in) or $props}
{aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $commit)}
<tfoot>
<tr><th colspan="2">{blocktrans}Source at commit <a class="mono" href="{$url}">{$commit}</a> created {$cobject.date|dateago}.{/blocktrans}<br/>
<span class="smaller">{blocktrans}By {$cobject.author|strip_tags|trim}, {$cobject.title}{/blocktrans}</span>
{if $props}
<tr><th colspan="2">
<ul>
{foreach $props as $prop => $val}
<li>{blocktrans}Property <strong>{$prop}</strong> set to <em>{$val}</em>{/blocktrans}</li>
{/foreach}
</ul>
</th></tr>
{/if}
{if !$tree_in and !$tags_in}
<tr>
<th colspan="2">{blocktrans}Source at commit <a class="mono" href="{$url}">{$commit}</a> created {$cobject.date|dateago}.{/blocktrans}<br/>
<span class="smaller">{blocktrans}By {$cobject.author|strip_tags|trim}, {$cobject.title}{/blocktrans}</span>
</th>
</tr>
{/if}
</tfoot>
{/if}
<tbody>

View File

@@ -0,0 +1,2 @@
{extends "idf/source/invalid_revision.html"}

View File

@@ -11,14 +11,27 @@
<th>{trans 'Message'}</th>
<th>{trans 'Size'}</th>
</tr>
</thead>{if !$tree_in and !$tags_in}
</thead>
{if (!$tree_in and !$tags_in) or $props}
{aurl 'url', 'IDF_Views_Source::commit', array($project.shortname, $commit)}
<tfoot>
{if $props}
<tr><th colspan="5">
<ul>
{foreach $props as $prop => $val}
<li>{blocktrans}Property <strong>{$prop}</strong> set to <em>{$val}</em>{/blocktrans}</li>
{/foreach}
</ul>
</th></tr>
{/if}
{if !$tree_in and !$tags_in}
<tr><th colspan="5">{blocktrans}Source at commit <a class="mono" href="{$url}">{$commit}</a> created {$cobject.date|dateago}.{/blocktrans}<br/>
<span class="smaller">{blocktrans}By {$cobject.author|strip_tags|trim}, {$cobject.title}{/blocktrans}</span>
</th></tr>
{/if}
{/if}
</tfoot>
{/if}<tbody>
<tbody>
{if $base}
<tr>
<td>&nbsp;</td>

View File

@@ -4,7 +4,7 @@
<p><strong>{trans 'Revision:'}</strong> {$commit}</p>
<p>
<input accesskey="4" type="text" value="{$commit}" name="rev" size="5" />
<input type="submit" name="s" value="{trans 'Go to revision'}" />
<input type="submit" name="s" value="{trans 'Switch'}" />
</p>
</form>
{/block}

View File

@@ -4,7 +4,7 @@
<p><strong>{trans 'Revision:'}</strong> {$commit}</p>
<p>
<input accesskey="4" type="text" value="{$commit}" name="rev" size="5"/>
<input type="submit" name="s" value="{trans 'Go to revision'}"/>
<input type="submit" name="s" value="{trans 'Switch'}"/>
</p>
</form>
{/block}

View File

@@ -12,7 +12,7 @@
<tr><th colspan="2">
<ul>
{foreach $props as $prop => $val}
<li>{trans 'Property'} <strong>{$prop}</strong> {trans 'set to:'} <em>{$val}</em></li>
<li>{blocktrans}Property <strong>{$prop}</strong> set to <em>{$val}</em>{/blocktrans}</li>
{/foreach}
</ul>
</th></tr>
@@ -38,7 +38,7 @@
<p>
<input accesskey="4" type="text" value="{$commit}" name="rev" size="5" />
<input type="hidden" name="sourcefile" value="{$base}"/>
<input type="submit" name="s" value="{trans 'Go to revision'}" /></p>
<input type="submit" name="s" value="{trans 'Switch'}" /></p>
</form>
{/block}

View File

@@ -0,0 +1,30 @@
{extends "idf/source/base.html"}
{block docclass}yui-t2{assign $inError=true}{/block}
{block body}
<p>{blocktrans}The revision <b>{$commit}</b> is not valid or does not exist
in this repository.{/blocktrans}</p>
{if count($branches) > 0}
<p>{blocktrans}The following list shows all available branches:{/blocktrans}</p>
<ul>
{foreach $branches as $branch => $path}
{if $path}{aurl 'url', 'IDF_Views_Source::tree', array($project.shortname, 'HEAD', $path)}
{else}{aurl 'url', 'IDF_Views_Source::treeBase', array($project.shortname, 'HEAD')}{/if}
<li class="label">
<a href="{$url}" class="label">{$branch}</a>
</li>
{/foreach}
</ul>
{/if}
{if $isOwner or $isMember}
{aurl 'url', 'IDF_Views_Source::help', array($project.shortname)}
<p>{blocktrans}If this is a new repository, the reason for this error
could be that you have not committed and / or pushed any change so far.
In this case please take a look at the <a href="{$url}">Help</a> page
how to access your repository.{/blocktrans}</p>
{/if}
{/block}

View File

@@ -19,7 +19,7 @@
<tr><th colspan="6">
<ul>
{foreach $props as $prop => $val}
<li>{trans 'Property'} <strong>{$prop}</strong> {trans 'set to:'} <em>{$val}</em></li>
<li>{blocktrans}Property <strong>{$prop}</strong> set to <em>{$val}</em>{/blocktrans}</li>
{/foreach}
</ul>
</th></tr>
@@ -66,7 +66,7 @@
<p>
<input accesskey="4" type="text" value="{$commit}" name="rev" size="5" />
<input type="hidden" name="sourcefile" value="{$base}"/>
<input type="submit" name="s" value="{trans 'Go to revision'}" /></p>
<input type="submit" name="s" value="{trans 'Switch'}" /></p>
</form>
<p><strong>{trans 'Branches:'}</strong><br />
{foreach $branches as $branch => $path}

View File

@@ -3,7 +3,7 @@
<table class="form" summary="">
<tr>
<th style="text-align: right">{if $user_data.avatar != ''}
<img style="max-height: 60px; max-width: 60px;" src="{media}/upload/avatars/{$user_data.avatar}" alt=" " />
<img style="max-height: 60px; max-width: 60px;" src="{upload}/avatars/{$user_data.avatar}" alt=" " />
{else}
<img src="http://www.gravatar.com/avatar/{$member.email|md5}.jpg?s=60&amp;d={media}/idf/img/spacer.gif" alt=" " />
{/if}

90
test/IDF/ProjectTest.php Normal file
View File

@@ -0,0 +1,90 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2008-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 ***** */
class IDF_ProjectTest extends PHPUnit_Framework_TestCase
{
public function testGetIssueCountByOwner()
{
// Add users
$user1 = new Pluf_User();
$user1->login = 'user1';
$user1->create();
$user2 = new Pluf_User();
$user2->login = 'user2';
$user2->create();
// Add a project
$prj = new IDF_Project();
$prj->create();
$tag = $prj->getTagIdsByStatus('open');
// First test with no issue
$stats = $prj->getIssueCountByOwner();
$this->assertEquals($stats, array());
// Add some issues
$issue1 = new IDF_Issue();
$issue1->project = $prj;
$issue1->submitter = $user1;
$issue1->owner = $user1;
$issue1->status = new IDF_Tag($tag[0]);
$issue1->create();
$issue2 = new IDF_Issue();
$issue2->project = $prj;
$issue2->submitter = $user2;
$issue2->owner = $user1;
$issue2->status = new IDF_Tag($tag[0]);
$issue2->create();
$issue3 = new IDF_Issue();
$issue3->project = $prj;
$issue3->submitter = $user2;
$issue3->status = new IDF_Tag($tag[0]);
$issue3->create();
$issue4 = new IDF_Issue();
$issue4->project = $prj;
$issue4->submitter = $user2;
$issue4->owner = $user2;
$issue4->status = new IDF_Tag($tag[0]);
$issue4->create();
// 2nd test
$stats = $prj->getIssueCountByOwner();
$expected = array(0 => 1,
$user2->id => 1,
$user1->id => 2);
$this->assertEquals($stats, $expected);
// Clean DB
$issue4->delete();
$issue3->delete();
$issue2->delete();
$issue1->delete();
$prj->delete();
$user2->delete();
$user1->delete();
}
}

View File

@@ -625,6 +625,7 @@ END;
'additions' => array('new_dir', 'new_dir/new_file'),
'deletions' => array('old_dir', 'old_dir/old_file'),
'renames' => array('dir_with_old_name' => 'new_dir/dir_with_new_name'),
'copies' => array(), // this is always empty
'patches' => array('existing_file'),
'properties' => array(
'new_dir/dir_with_new_name' => array(
@@ -714,6 +715,31 @@ END;
$this->assertEquals('', $commit->diff);
}
public function testGetProperties()
{
$rev = "2345678901234567890123456789012345678901";
$instance = $this->createMock();
$instance->getStdio()->setExpectedOutput(array('interface_version'), array(), '13.1');
$stdio =<<<END
attr "foo" "bar"
state "unchanged"
attr "some new
line" "and more <weird>-
nesses"
END;
$instance->getStdio()->setExpectedOutput(array('get_attributes', 'foo'), array('r' => $rev), $stdio);
$res = $instance->getProperties($rev, 'foo');
$this->assertEquals(2, count($res));
$this->assertEquals(array(
'foo' => 'bar',
"some new\nline" => "and more <weird>-\nnesses"
), $res);
}
public function testGetExtraProperties()
{
$instance = $this->createMock();

55
test/IDF/Scm/SvnTest.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 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 ***** */
class IDF_Scm_SvnTest extends PHPUnit_Framework_TestCase
{
private $proj = null;
public function setUp()
{
$this->proj = new IDF_Project();
$this->proj->id = 1;
$this->proj->name = $this->proj->shortname = 'test';
$this->proj->create();
}
public function tearDown()
{
$this->proj->delete();
}
public function createMock($reponame)
{
$repourl = 'file://'.DATADIR.'/'.__CLASS__.'/'.$reponame;
$instance = new IDF_Scm_Svn($repourl, $this->proj);
return $instance;
}
public function testAccessHistoryOfRenamedAndDeletedFiles()
{
$instance = $this->createMock(__FUNCTION__);
$this->assertEquals('new-file', $instance->getPathInfo('new-file', 1)->fullpath);
$this->assertEquals('alternate-name', $instance->getPathInfo('alternate-name', 2)->fullpath);
}
}

View File

@@ -0,0 +1,2 @@
4
layout sharded 1000

View File

@@ -0,0 +1,5 @@
K 8
svn:date
V 27
2011-08-12T17:06:15.429110Z
END

View File

@@ -0,0 +1,13 @@
K 10
svn:author
V 7
patrick
K 8
svn:date
V 27
2011-08-12T17:07:33.111035Z
K 7
svn:log
V 12
added a file
END

View File

@@ -0,0 +1,13 @@
K 10
svn:author
V 7
patrick
K 8
svn:date
V 27
2011-08-12T17:07:55.894416Z
K 7
svn:log
V 14
renamed a file
END

View File

@@ -0,0 +1,13 @@
K 10
svn:author
V 7
patrick
K 8
svn:date
V 27
2011-08-12T17:08:12.646051Z
K 7
svn:log
V 14
deleted a file
END

View File

@@ -0,0 +1,11 @@
PLAIN
END
ENDREP
id: 0.0.r0/17
type: dir
count: 0
text: 0 0 4 4 2d2977d1c96f487abe4a1e202dd03b4e
cpath: /
17 107

View File

@@ -0,0 +1,28 @@
DELTA
SVNENDREP
id: 0-1.0.r1/17
type: file
count: 0
text: 1 0 4 0 d41d8cd98f00b204e9800998ecf8427e da39a3ee5e6b4b0d3255bfef95601890afd80709 0-0/_2
cpath: /new-file
copyroot: 0 /
PLAIN
K 8
new-file
V 16
file 0-1.0.r1/17
END
ENDREP
id: 0.0.r1/232
type: dir
pred: 0.0.r0/17
count: 1
text: 1 180 39 39 4a0c4e617d69f8a0bf11161d1e1b09a6
cpath: /
copyroot: 0 /
_0.0.t0-0 add-file true false /new-file
232 357

View File

@@ -0,0 +1,29 @@
id: 0-1.0-2.r2/0
type: file
pred: 0-1.0.r1/17
count: 1
text: 1 0 4 0 d41d8cd98f00b204e9800998ecf8427e da39a3ee5e6b4b0d3255bfef95601890afd80709 0-0/_2
cpath: /alternate-name
copyfrom: 1 /new-file
PLAIN
K 14
alternate-name
V 17
file 0-1.0-2.r2/0
END
ENDREP
id: 0.0.r2/256
type: dir
pred: 0.0.r1/232
count: 2
text: 2 196 47 47 518568bcb7e6fe3165235a9f9993f5e1
cpath: /
copyroot: 0 /
0-1.0.r1/17 delete-file false false /new-file
0-1._0.t1-1 add-file false false /alternate-name
1 /new-file
256 382

View File

@@ -0,0 +1,15 @@
PLAIN
END
ENDREP
id: 0.0.r3/17
type: dir
pred: 0.0.r2/256
count: 3
text: 3 0 4 4 2d2977d1c96f487abe4a1e202dd03b4e
cpath: /
copyroot: 0 /
0-1.0-2.r2/0 delete-file false false /alternate-name
17 138

View File

@@ -0,0 +1 @@
5d5eaf70-0fe3-4d07-ada0-45c336a5bfad

View File

@@ -86,6 +86,14 @@ a.soft:visited {
div.context {
padding-left: 1em;
position: relative;
top: 0;
}
div.context h3 {
font-size: 13px;
font-weight: bold;
margin: 10px 0 5px;
}
/**
@@ -127,9 +135,9 @@ ul.errorlist {
div.user-messages {
border: 1px solid rgb(229, 225, 169);
background-color: #fffde3;
margin-bottom: 1em;
margin-bottom: 2em;
margin-left: -1px;
width: 90%;
width: 100%;
}
div.theterms {
@@ -275,7 +283,7 @@ div.issue-changes-timeline {
div.issue-prev-next {
float: right;
margin-top: -25px;
margin-top: -1.5em;
}
div.issue-submit-info {
@@ -928,14 +936,6 @@ ol > li {
/**
* List expander for tag and branch view
*/
.context {}
.context h3 {
font-size: 13px;
font-weight: bold;
margin: 10px 0 5px;
}
.context > .expander {
position: relative;
overflow: hidden;
@@ -1058,6 +1058,10 @@ span.scm-action.renamed {
background-color: purple;
}
span.scm-action.copied {
background-color: orchid;
}
span.scm-action.property-changed {
background-color: blue;
}
@@ -1100,3 +1104,53 @@ div.p-list-private {
right: -3px;
position: relative;
}
/*
* Issue summary
*/
div.issue-summary {
float: left;
width: 50%;
}
div.issue-summary > div {
margin-right: 3em;
padding-top: 1em;
}
div.issue-summary h2 {
border-bottom: 1px solid #A5E26A;
}
table.issue-summary {
width: 100%;
}
table.issue-summary tr td {
border: 0;
padding: .1em .005em;
}
table.issue-summary td.graph {
width: 60%;
}
table.issue-summary td.count {
text-align: right;
padding-right: .5em;
}
table.graph {
width: 100%;
margin: 0;
padding: 0;
}
table.issue-summary td.graph-color {
background: #3C78B5;
}
table.issue-summary td.graph-percent {
padding-left: 1em;
}