Compare commits

...

275 Commits
v1.0 ... master

Author SHA1 Message Date
William MARTIN
f88e529b85 Update the form validation of IDF_Form_ReviewFileComment.
Now we can add a general comment without per file comment.

Fix issue 660
2011-04-06 13:07:19 +02:00
Thomas Keller
f8b49c805a -e is needed for bash's echo (and probably others as well) 2011-02-22 16:25:33 +01:00
Thomas Keller
7ac254169c Merge branch 'develop' of projects.ceondo.com:indefero 2011-02-22 16:08:01 +01:00
William MARTIN
f95ff57db9 Remove --width option for msgmerge 2011-02-22 14:34:58 +01:00
William MARTIN
c2207452bd Update the POT generation in Makefile
Really delete the older POT file to avoid merge with old entry
2011-02-22 14:26:09 +01:00
William MARTIN
a28604ae4f Ignore backup file 2011-02-22 11:43:01 +01:00
William MARTIN
4e53b7d178 Create a Makefile for common task
- Generate tarball from GIT, using git archive
- Updating POT file
- Updating PO file
2011-02-22 11:32:59 +01:00
Thomas Keller
2b7de0b7a4 Merge branch 'develop' of projects.ceondo.com:indefero 2011-02-21 10:44:48 +01:00
William MARTIN
f590b1c5f8 First part of the spanish translate by Mika
Signed-off-by: William MARTIN <william.martin@lcpc.fr>
2011-02-21 09:44:42 +01:00
Thomas Keller
bae73f266c German translation finalized. 2011-02-14 00:05:37 +01:00
Thomas Keller
b00dbfaeaa Typos fixed. 2011-02-13 01:19:17 +01:00
Thomas Keller
e2e1e50890 14 left, but too much for tonight. I fixed a couple of f'ups and
added a short translation intro on top with the proper copyrights.
2011-02-13 01:12:42 +01:00
Thomas Keller
423548dc2c Grammar. 2011-02-13 00:24:18 +01:00
Thomas Keller
545117eeae Punctation fixes and phrase changes. 2011-02-12 01:08:16 +01:00
Thomas Keller
0b0392a274 Another large chunk for the German translation has been finished.
Many fixes also went in.
2011-02-12 00:57:14 +01:00
Thomas Keller
0c0236c766 Russian and German translation updated. 2011-02-11 01:54:48 +01:00
Thomas Keller
067d88136e Updated ru translation from indefero (thanks Denis Kot!) 2011-02-10 01:40:57 +01:00
Thomas Keller
d0cd0bb9bf Update the German translation a bit - still lots of work left. 2011-02-10 01:38:50 +01:00
Thomas Keller
69ecb1a049 Add a proper Plural-Forms setting (Chinese doesn't have a plural)
and remove a wrong translation (I'm unable to find out where the %s
should have been put), so again, 'msgfmt -c' is happy now.
2011-02-09 01:30:33 +01:00
Thomas Keller
e50f2f2b5f Fix a couple of fatal errors reported by 'msgfmt -c'. 2011-02-09 01:24:36 +01:00
Thomas Keller
905a81a715 'zh' alone is actually not a valid locale, but 'zh_CN' is. 2011-02-09 01:17:08 +01:00
Thomas Keller
4d0d41ef02 Updated idf.pot and all translations with the latest strings. 2011-02-09 01:08:39 +01:00
Thomas Keller
15e7adaefb Add the PHP command to extract the i18n contents from Pluf's template files 2011-02-09 01:02:58 +01:00
Thomas Keller
fea67af4df Add a command to merge pot changes into the individual po files 2011-02-09 00:48:20 +01:00
Thomas Keller
86832e4a36 Started on the overhaul of the German translation:
- The previous translator mixed "Du" and "Sie", I stumbled upon
  "Du" at first and decided to stick with that.
- The poedit extra headers have been removed; a proper plural
  configuration has been added.
- Many strings have been misspelled and were thus fixed, though
  there is still lots of work left.
2011-02-09 00:39:03 +01:00
Baptiste Durand-Bret
c6ffc47c24 Fixed issue 611, updated the French translations. 2011-02-04 13:56:04 +01:00
Loïc d'Anterroches
40881bb4e5 Merge branch 'develop' 2011-02-02 10:35:15 +01:00
Loïc d'Anterroches
7cedd4af7a Fixed to not work on a bad key. 2011-02-02 10:34:48 +01:00
Loïc d'Anterroches
6832e45b1f Merge branch 'develop' 2011-02-01 21:02:44 +01:00
Loïc d'Anterroches
e31f10e648 Fixed error in the cron job if a key has a bad format. 2011-02-01 20:58:24 +01:00
Patrick Georgi
336faa4503 Monotone's diff parsing conflicts with Subversion's - make sure we
do not let mtn recognize svn's underline that marks a new patch.
2011-01-30 00:14:35 +01:00
Patrick Georgi
fbd1ebc294 Add the parent property to a subversion commit. 2011-01-30 00:05:09 +01:00
Thomas Keller
7bcfb806b0 Create valid HTML for the branch and tag list headings (no block
elements inside headings) and also use a lower heading, h3 and not h1
for the subheadings.
Reduce the amount of code duplication with include's for each VCS.
2011-01-27 14:12:42 +01:00
Thomas Keller
2908e28999 Vertical-align properly. 2011-01-26 02:40:28 +01:00
Thomas Keller
a188e1b275 Use the proper unicode arrow code point instead of "->". 2011-01-26 02:37:34 +01:00
Thomas Keller
52d76cd146 Make the change list in the commit view a little nicer by styling the
individual change types with colored markers and apply a little help
hint to each of them.
2011-01-26 02:27:08 +01:00
Thomas Keller
4eb34044ff Apply the context area changes to git and hg scm views as well
(partially resolves issue 601)
2011-01-26 01:55:58 +01:00
Thomas Keller
ccb1bd33d4 Add a clear button to the tag / branch filter input fields; simplify
the jQuery code a bit.
2011-01-26 01:37:29 +01:00
Loïc d'Anterroches
ba14aec7a3 Merge branch 'develop' 2011-01-25 09:58:25 +01:00
Loïc d'Anterroches
41fb1bf13c Fixed the table of content to work with all the browsers. 2011-01-25 09:57:44 +01:00
Thomas Keller
ddb05e68a3 Implement IDF_Scm::getExtraProperties 2011-01-24 15:11:12 +01:00
Loïc d'Anterroches
b544792f36 Merge branch 'develop' 2011-01-24 14:22:17 +01:00
Loïc d'Anterroches
a069661b4a Merge branch 'develop' of projects.ceondo.com:indefero into develop 2011-01-24 14:21:44 +01:00
Loïc d'Anterroches
07c94b97f9 Merge branch 'feature-merge-history' into develop 2011-01-24 14:20:49 +01:00
Loïc d'Anterroches
116a7e34db Added the display of the commit parents in the changelog view.
The old commits will not have the parents displayed as already cached in the DB, but the new will get them.
2011-01-24 14:20:33 +01:00
Thomas Keller
f4fc2342d5 Refer to monotone 0.99.1 or later, 0.99 is a bit buggy wrt automation 2011-01-24 12:57:22 +01:00
Loïc d'Anterroches
f7228ef2ec Merge branch 'develop' 2011-01-24 12:06:05 +01:00
Loïc d'Anterroches
207eb1322d Fixed to correctly store utf-8 string in the git cache. 2011-01-24 11:53:24 +01:00
Loïc d'Anterroches
390c9b5048 Added to collect associated data for the commits in one SQL query. 2011-01-23 17:12:19 +01:00
Loïc d'Anterroches
58804923ef Cosmetic fix to prevent wrapping of the date. 2011-01-23 17:08:07 +01:00
Thomas Keller
296091e977 Fetch parent revisions for monotone commits as well. 2011-01-20 23:45:21 +01:00
Thomas Keller
77ba17eb61 Remove commented-out, unneeded manual includes 2011-01-20 23:40:21 +01:00
Loïc d'Anterroches
a489cb15b5 Added the display of the merge history in a commit view. 2011-01-20 10:25:41 +01:00
Loïc d'Anterroches
b0bebb44e6 Merge branch 'develop' of projects.ceondo.com:indefero into develop 2011-01-20 09:48:57 +01:00
Thomas Keller
74baebde96 I changed my mind: remote automate access should be prevented for
private projects and we should also take care that the symlink that
enables it is dynamically created / removed when the private flag
changes for a project.
2011-01-18 15:43:35 +01:00
Thomas Keller
caac979263 Don't throw an exception in case we do not find a branch cert tacked on
a revision from which we want to start our log from, but simply ignore
that and go over to the next revision. Note that this is also a bit
fragile and should probably be converted to plain mtn au log usage.
2011-01-17 11:57:39 +01:00
Thomas Keller
bbc9bd6ef4 Forgot to add the JS filter file. 2011-01-17 01:26:32 +01:00
Thomas Keller
d445a65788 Beef up the branch and tag lists as per issue 601 - currently
only for the monotone plugin's source view.
2011-01-17 01:20:33 +01:00
Thomas Keller
cdebac0b13 The commit object's diff member changed from "changes" to "diff"
so the "dowload the corresponding diff file" link was broken.
2011-01-15 01:02:22 +01:00
Loïc d'Anterroches
651617b8bf Merge branch 'develop' 2011-01-10 10:56:23 +01:00
Loïc d'Anterroches
0e6eb9059a Added an event log to track the post update info. 2011-01-10 10:56:09 +01:00
Loïc d'Anterroches
22d6453d56 Merge branch 'develop' 2011-01-09 13:47:30 +01:00
Loïc d'Anterroches
a7b62a30ff Correctly mark a Subversion repository as empty when at revision 0. 2011-01-09 13:47:07 +01:00
Loïc d'Anterroches
d5e6d355ab Merge branch 'develop' 2011-01-09 11:27:02 +01:00
Loïc d'Anterroches
431e199c1c Fixed to correctly provide the origin url. 2011-01-09 11:26:17 +01:00
Loïc d'Anterroches
3884bcdd01 Merge branch 'develop' 2011-01-09 11:03:02 +01:00
Loïc d'Anterroches
fa974eb8dd Fixed the detection of the paths to the commit hooks.
The symlinks to the post commit/post update scripts are absolute path symlinks, so a simple readlink is used. The  is relative, so a bit of shell skill is used to resolve the path to the absolute path.
2011-01-09 11:02:27 +01:00
Loïc d'Anterroches
023a3ce879 Merge branch 'develop' 2011-01-08 21:35:20 +01:00
Loïc d'Anterroches
c67e61cbaa Fixed the watch list to not crash when empty and support PostgreSQL. 2011-01-08 21:34:51 +01:00
Loïc d'Anterroches
cdeefb43a5 Added a logging of an event. 2011-01-08 21:33:51 +01:00
William MARTIN
93af6a68bd Add table of contents on wiki pages 2011-01-07 22:33:18 +01:00
William MARTIN
146e956432 Add a favicon
Fix issue 594
2011-01-07 11:17:14 +01:00
William MARTIN
afa91188d8 Enhancement of the view of an issue.
Add link previous and after to quickly jump to another issue.
Those links are pointing to issue with the same status (open/closed).

With little trick on url we can do the same thing for browse "my issue" and "my watchlist".
2011-01-06 22:38:38 +01:00
William MARTIN
c2a9a60aa7 Naming conventions correction 2011-01-06 14:30:39 +01:00
William MARTIN
d654c95689 Fix issue 553 : Git escape too much character in a UTF-8 shell
Add an option to configure the git core.quotepath option
2011-01-06 14:03:07 +01:00
William MARTIN
439f1fefe2 Fix issue 588 : Redirect connected user to the anonymous url if they don't have register a SSH Key for GIT 2011-01-06 11:13:29 +01:00
William MARTIN
4245617c6f Fix issue 546 : Add the irc protocol in the markdown prefilter 2011-01-06 11:01:35 +01:00
William MARTIN
5635cdcac7 Remove the use of GROUP_CONCAT in SQL request. 2011-01-06 10:26:29 +01:00
Thomas Keller
dd05a58c8c Major configuration changes for SyncMonotone - we're now using a predefined
configuration tree as template for a new project and copy / symlink that on
project creation. To make this process a little more configurable, two new
configuration options, 'mtn_confdir' and 'mtn_confdir_extra', have been
added which allow the forge admin to adapt the directory structure and its
default hooks to his likings for all new projects. (More on that in
doc/syncmonotone.mdtext).
The 'mtn_remote_auth' configuration option was removed, because setting this
to false would have not worked for setups which did not allow write access
to remote automate commands for anonymous users and opening this would have
meant a huge security hole. Instead, for every project which is created a
corresponding client key is created as well which is used as authentication
in the IDF source frontend.
Finally the monolithic monotonerc file has been split up into individual,
easily configurable lua files which are linked / copied underknees hooks.d/
and which do not conflict with each other (for example by overwriting certain
main notification hooks).
2011-01-06 01:44:41 +01:00
William MARTIN
a437da6a4c Fix sort option of forge watchlist 2011-01-05 17:51:34 +01:00
William MARTIN
0334e88625 Merge branch 'develop' of git://projects.ceondo.com/indefero into develop 2011-01-05 17:21:35 +01:00
William MARTIN
05b081e6d4 Merge branch 'watchlist' into develop 2011-01-05 17:17:53 +01:00
William MARTIN
c7c39c6fa1 Implementation of the watch-list viewer
Fix issue 589
2011-01-05 17:02:06 +01:00
Thomas Keller
0a7c2bca85 Merge branch 'develop' of projects.ceondo.com:indefero into develop 2011-01-05 15:35:49 +01:00
Thomas Keller
fd7a53a854 According to the base64 standard, zero, one or two fill bytes ("=")
might pop up at the end, so always expecting "==" is plainly wrong
(originates from 0897c860, fixes issue 592)
2011-01-05 15:34:01 +01:00
William MARTIN
9ccbcea743 Fixed issue 557, better CSS to render UL/OL. 2011-01-05 12:07:57 +01:00
Loïc d'Anterroches
4b75a55639 Merge branch 'develop' 2011-01-05 10:30:00 +01:00
Loïc d'Anterroches
90b9279c3a Fixed not to try to include a document from a non available repository. 2011-01-05 10:28:25 +01:00
Thomas Keller
80be99890a Add a title to the star when viewing an issue (thanks to William Martin) 2011-01-04 16:51:33 +01:00
Thomas Keller
d4929622bf Sort directories before files in source views. Thanks to William Martin!
(closes issue 573)
2011-01-03 15:10:42 +01:00
Loïc d'Anterroches
7059150ac8 Merge branch 'develop' 2010-12-23 17:26:53 +01:00
Loïc d'Anterroches
777937b70c Fixed apparence of the description of the labels for the default ticket labels. 2010-12-23 17:26:34 +01:00
Loïc d'Anterroches
b4b53d3e22 Merge branch 'develop' 2010-12-23 11:57:48 +01:00
Loïc d'Anterroches
97bc383bc0 Updated to display changes only if the backend supports this function. 2010-12-23 11:57:39 +01:00
Loïc d'Anterroches
dbd088458e Merge branch 'develop' 2010-12-23 11:32:09 +01:00
Loïc d'Anterroches
0739b856e0 Changed to sort the list of projects by name as more natural than by short name. 2010-12-23 11:31:52 +01:00
Loïc d'Anterroches
81acb276d8 Merge branch 'develop' 2010-12-23 11:18:07 +01:00
Thomas Keller
80e965a904 Forgot to add the model filter parameter to the timeline action which
basically rendered the complete navigation useless.
2010-12-08 02:28:56 +01:00
Thomas Keller
39c29dbe10 Started on issue 544, extended commit details
* Scm.php: new SCM method "getChanges" which returns all available
  change information grouped by type
* Monotone.php: implement getChanges via get_revision
* <other scms>: rename "changes" member for getCommit to "diff" which
  matches better
* Source.php: query the commit's changes and set them in the template
* commit.html: render the changes, type-by-type. Link to the tree or
  the individual diff if applicable
* styles.css: some initial style sheet work
2010-12-08 01:48:26 +01:00
Thomas Keller
51c42a65c5 Project owners can now enter multiple email addresses for each notification type.
Each email must be separated by a comma from the other. (Based on a patch from
Pierre Marechal, fixes issue 372).
2010-12-06 00:11:59 +01:00
Thomas Keller
784c9718eb Implemented an extended user profile based on a patch from Jethro Carr (issue 510).
Changes with respect to the original patch:
- use Gconf instead of separate table / data scheme
- better form validation for URLs and emails
- no htmlentity-encoded contents in the database (pluf automatically safe-encodes
  stuff before it writes out contents into templates)
- add visual separators in the form views to have a distinct view of basic
  (important) data and other data which are only displayed in the public profile
- give a hint about the maximum display size of 60x60 px^2 and use max-width and
  max-height in the templates to avoid nasty distortions by the browser
- use target=_blank and rel=nofollow on the twitter and website links in the profile
- some whitespace / formatting / code style fixes
2010-12-05 01:22:32 +01:00
Thomas Keller
874b5aa7e9 monotone changelog: do not add the parents of revisions which have not attached
the branch certificates in question to the horizont, otherwise we end up going
up the whole history for some feature branch as soon as we hit the base branch
of a project.
2010-12-02 23:23:38 +01:00
Thomas Keller
bf28a24b72 French translation updated (closes issue 574) 2010-12-02 22:44:35 +01:00
Thomas Keller
dffeb1f9d5 * move common file-specific functionality out of IDF_Views_Source into new
IDF_FileUtil and change all occurrences accordingly
* cache /etc/mime.types (or whatever is configured) per request in a static
variable in IDF_FileUtil
* always link directly to the download of attached files in the issues view
and place an additional "view" link only for those attachments which we
recognize as text with our weak criteria (closes issue 575)
2010-12-02 01:50:01 +01:00
Thomas Keller
6d7d7ebbfa copy'n'paste from some console window sucks sometimes 2010-12-02 01:48:34 +01:00
Thomas Keller
5b41fe3167 I promise I keep my fingers off git stuff now (a fix for issue 572) 2010-12-01 11:19:13 +01:00
Thomas Keller
28f36dc7b0 Replace the usage of readlink's -f option (which is only available in
GNUs coreutils) by something more sophisticated which is compatible
with both, Linux and the BSDs (fixes issue 526)
2010-11-30 00:24:33 +01:00
Thomas Keller
dc2881ad02 Expand the revision and source linking automatisms to support the following
rev 12345
 added in abc12
 removed in abc12
 src:README@abc12
 src:README, src:COPYING#12
and many more schemes.
2010-11-29 21:50:40 +01:00
Thomas Keller
653299f4d8 Put a proper link around the "Home" menu entry. 2010-11-25 02:02:38 +01:00
Thomas Keller
704850f5c6 Add Chinese localization (thanks go to Jerry! fixes issue 521) 2010-11-25 00:20:10 +01:00
Thomas Keller
1548d4184e Suppress a notice in case the git log line does not contain
a title (fixes issue 520)
2010-11-25 00:13:47 +01:00
Thomas Keller
abc8b8f4ab Another f'up - isValidRevision() is gone, replaced by validateRevision().
Unit tests would really, really help us here...
2010-11-17 23:18:06 +01:00
Thomas Keller
08ef485ca9 Return the proper validation value if "HEAD" is the to-be-validated revision
(fixes issue 568)
2010-11-17 19:10:51 +01:00
Thomas Keller
35e670a1ab The data structure is already a stdClass object, so add a property,
not a key (partially fixes issue 568)
2010-11-17 19:08:41 +01:00
Loïc d'Anterroches
deb1ea4d2b Merge branch 'develop' 2010-11-17 09:45:22 +01:00
Loïc d'Anterroches
3aac4d528a Merge branch 'develop' of projects.ceondo.com:indefero into develop 2010-11-17 09:33:10 +01:00
Thomas Keller
70e8d12420 Output the branch a particular commit is on; this is easy for
mercurial and monotone, but slightly harder till impossible to
do properly for git and svn. Please review and eventually adapt
the code to make it work better (partially fixes issue 450)
2010-11-17 01:53:17 +01:00
Thomas Keller
ad15b13f7e Add a little Q&A section with details how to handle suspended branches and default branch name changes. 2010-11-17 01:02:43 +01:00
Thomas Keller
b4bc6abace Make the monotone master branch editable on forge level at least to
make it possible to switch the default project branch (partially
fixes issue 560)
2010-11-17 00:47:55 +01:00
Thomas Keller
e6f255bc56 Code style; wider input for monotone branch name 2010-11-17 00:47:52 +01:00
Thomas Keller
470a961a80 Add syntax highlighter support for h, hh and hpp (fixes issue 567) 2010-11-17 00:06:18 +01:00
Thomas Keller
d08fee129b fixed two notices which lead to corrupt zip archives 2010-11-09 00:11:27 +00:00
Thomas Keller
8993d2988b Pick initial issue type and priority from the particular first item of the predefined issue labels list (thanks Jakub Vitak, closes issue 556) 2010-10-31 23:18:35 +00:00
Thomas Keller
e776fc0713 Switch doctype from html 4.01 strict to xhtml 1.0 transitional, we
largely ignored the former with xml-like constructs either
(closes issue 511).
2010-10-31 18:26:31 +01:00
Thomas Keller
c0ccdc768a add a couple of file extensions which are supported by our in-tree version of prettyfy (closes issue 490) 2010-10-31 18:18:20 +01:00
Thomas Keller
039ae01cfa Use an even smarter way of including additional resources for monotone
by using a wildcard dir matcher and document this accordingly.
2010-10-30 22:09:55 +00:00
Thomas Keller
612d00ade2 Merge branch 'develop' of projects.ceondo.com:indefero into develop 2010-10-30 21:58:57 +00:00
Thomas Keller
fe001abd26 Rework the way IDF's SCM interface provides downloadable snapshots.
Instead of returning a command which gets executed and which should
pass through / stream its output data to the client, we're just
returning an instance of Pluf_HTTP_Response. This is needed, because
some SCMs, most noticable monotone, have no locally executable command
to provide a snapshot archive (and probably never will for our kind
of setup).

We therefor added a little BSD-licensed class "ZipArchive" which allows
the creation of pkzip-compatible archives on the fly by letting it eat
the file contents directly feed from the (remote) stdio instance.
Download performance is ok and lies between 15K/s and 110K/s, but at
least we do no longer block the browser while we pre-generate the zip
file server-side.

Thanks to Patrick Georgi for all his work!
2010-10-30 21:52:40 +00:00
Thomas Keller
b800ffcc1f * add a section which explains the security concept and explains
the possible remote command execution feature
* add a section which explains how additional hooks can be
  configured for notification purposes
2010-10-28 20:05:42 +02:00
Thomas Keller
8a55952204 * use the built-in push_hook_functions to register the netsync hooks,
this way additional hooks which need these notifications as well
  do not override earlier hooks
* optionally include an additional hooks.lua file at the very end
  in which custom hooks can be defined
2010-10-28 19:45:45 +02:00
Loïc d'Anterroches
8b2363fd6f Merge branch 'develop' of projects.ceondo.com:indefero into develop 2010-10-22 16:25:37 +02:00
Loïc d'Anterroches
e7a0d9d497 Added the .pas extension as supported text extension for the syntax highlighter. 2010-10-22 16:11:39 +02:00
Thomas Keller
c807c4b734 Add *.pas ([object] pascal) to the list of supported source extensions 2010-10-22 16:11:04 +02:00
Thomas Keller
0af51d90ba More CSS tweaks for the project list dropdown 2010-10-16 01:42:34 +02:00
Thomas Keller
972df3b231 Merge branch 'develop' of git://projects.ceondo.com/indefero into develop 2010-10-16 01:19:17 +02:00
Thomas Keller
1887e9effd Ensure that the project list popup doesn't exceed the page height when
many projects are listed - instead make it scrollable.
2010-10-15 12:35:50 +00:00
Thomas Keller
7e10524f92 Add a popup menu in the main menu which allows to quickly jump between projects.
* refactor out the common menu code from several base*.html classes into main-menu.html
  and put the raw links into a unordered list (which makes it easier to create dynamic
  menus as we cannot insert block items into inline items in strict mode)
* query the list of available projects on every request and set them for every template
  in Middleware.php
* make the popup menu pretty
2010-10-15 00:40:09 +00:00
Thomas Keller
c3ff90c4f8 The signal name was wrong, therefor mtn syncs never updated the IDF timeline. 2010-10-14 19:26:09 +00:00
Thomas Keller
2c4f2d3037 $tempfoo was of course not set - lets use a static path prefix here for simplicity 2010-10-14 19:25:25 +00:00
Thomas Keller
07aec736f5 Add --timestamp and --ticker=dot as default options for newly created
servers, which make it easier to follow the logs of individual servers.
2010-10-14 18:46:08 +00:00
Thomas Keller
b30bdc9833 While for allowed upload extensions the user was hinted to the
need of having to start the list with a space, here it was actually
needed code-wise. I rewrote the code so the space was not needed
and the documentation follows the actual behaviour.
2010-10-14 12:13:38 +00:00
Thomas Keller
d171a249c5 Its untrue that the list of extra allowed extensions for uploaded files
has to start with a space - its even a little quirky since the code
expands that to a regex like "/\.(|ext1|ext2...)$/", so in theory files
which end up with a dot and without an extension would be allowed by this.
For now we just fix the comment and indentation of the default
configuration option.
2010-10-14 12:13:21 +00:00
Thomas Keller
d994e0efb0 Remove the CSS3 pre-line rule, which is recognized by modern browsers
and leads to collapsing of multiple consecutive whitespaces
(closes issue 528).
2010-10-09 12:42:05 +00:00
Thomas Keller
0379b862ec Document 'idf_strong_key_check' (closes issue 516) 2010-10-09 12:21:20 +00:00
Thomas Keller
5af2ab4d97 Make the timeline view and RSS feeds filterable by model (closes issue 543). 2010-10-09 11:53:01 +00:00
Thomas Keller
b518385962 Introduce a per-project issue template to hint a reporter to provide
certain information in his issue report (closes issue 540).
2010-10-09 10:40:30 +00:00
Thomas Keller
d25bc74d71 If no branch certificates are attached to a revision, we do not get
an empty array back from _getCerts(), but no entry for 'branch' at all.
2010-10-09 10:09:51 +00:00
Thomas Keller
5641173a04 Comment in hg_repositories by default, so it matches the defaults of the other SCMs 2010-10-07 19:21:05 +02:00
Thomas Keller
806e69b858 Don't let sync git fail on ssh keys with no comment field (references:
issue 531 and issue 545)
2010-10-07 11:56:52 +00:00
Thomas Keller
a29a2a0fa4 The connection list view could never work with this messy backend. 2010-10-07 01:05:15 +00:00
Thomas Keller
4951498c0b Ignore pseudo diff stanzas which mention binary files. 2010-10-06 21:37:53 +00:00
Thomas Keller
97ea828532 Use a persistent cache through Pluf_Cache to speed up cert queries. 2010-10-04 15:42:21 +00:00
Thomas Keller
d539eaf64b - _getLastChangeFor(): drop that, no longer needed
- getTree(), getPathInfo(): use the new extended manifest format
  and save the calls to query file sizes from contents as well
  as the calls to determine the revision in which a file changed
  at last
2010-10-04 15:22:57 +00:00
Thomas Keller
90edbf0d8b Tweak the basicio parser so that it properly handles multi-value
lines with hashes (lines like symbol [hash] [hash] are still not
handled, but aren't outputted from any command either as of now).
2010-10-04 15:20:53 +00:00
Thomas Keller
0c575ccc74 If a symbol is printed without a value list at the very end of a
basic_io dump, we might access a non-existing character position.
This has been fixed and the string length calculation is now only
done once.
2010-10-03 22:23:08 +00:00
Thomas Keller
eebdc5ad12 IDF_Scm_Monotone::getCommit() separate the first line of a commit from the
rest and write the rest in full_message - just like we do it for log and
everything else. This is ugly, really ugly, because it assumes something
on the format of a commit message, which might not be true at all for
some project, but this is something Loic has to decide (see also issue 491
and issue 535)
2010-09-28 21:37:26 +00:00
Thomas Keller
617589f41b Reorganize and expand the help of the monotone plugin.
Make the commentary in idf.php-dist less verbose.
2010-09-17 03:11:36 +02:00
Thomas Keller
50638c768f Ensure that the SyncMonotone plugin does not throw around errors
in case of a local (non-usher) monotone setup.
2010-09-17 02:36:48 +02:00
Thomas Keller
29b8bf8a4e Some revisions might not carry a branch cert (yet), because they're
part of another branch whose certs haven't been pushed into the server
yet, so we need to skip these revisions while going back in time
for the changelog. The initial revision however must carry a branch
cert, otherwise we have nothing to "follow".
2010-09-15 08:46:10 +00:00
Thomas Keller
42936cc51d Mark the submenu item "Open Issues" active when its active
(partially resolves issue 536)
2010-09-14 23:14:29 +00:00
Thomas Keller
b138548a10 * check if the project actually uses mtn as scm for the
membershipsUpdated signal
* code cleanup and simplification
2010-09-14 22:58:34 +00:00
Thomas Keller
7d5ba6248e Merge branch 'develop' of projects.ceondo.com:indefero into develop 2010-09-14 22:34:57 +00:00
Thomas Keller
36a58dcae2 * update the permissions on IDF_Project::membershipsUpdated - listen
to the same signal also for the initial setup, since the memberships
haven't been added at the time the create signal is thrown
* my array references goo was slightly stupid (the usage of foreach
is of course hazardous in cases like this)
* always insert a trailing new line in write-permissions and skip
read-in newlines from being processed
2010-09-14 22:30:28 +00:00
Thomas Keller
2106a5fbdc Merge branch 'develop' of projects.ceondo.com:indefero 2010-09-14 14:23:18 +02:00
Thomas Keller
0897c8608f Allow the upload of SSH keys without the optional comment field
(fixes issue 531 - thanks William!)
2010-09-14 14:22:34 +02:00
Thomas Keller
a32d6d8265 * its late - put_public_key of course needs a specific database
* do not throw around exceptions if a key which should be removed is not found in the database
2010-09-13 01:13:49 +00:00
Thomas Keller
0f9f337e66 * configure whether or not to set remote client authentication for IDF -> remote_stdio
* hook into IDF_Project::preDelete, IDF_Key::postSave and IDF_Key::preDelete
* this is all not quite finished, but a big leap forward to completion
2010-09-13 00:53:24 +00:00
Thomas Keller
bb13722a2f bump copyright year 2010-09-13 00:51:45 +00:00
Thomas Keller
77cdbefe0c Added getter for the stdio instance 2010-09-13 00:50:16 +00:00
Thomas Keller
f68bba1292 Be more careful when parsing value lists - in case we process the last
line of a stanza which does _not_ close with a newline, we're accessing
a not existing string index.
2010-09-12 23:18:58 +00:00
Thomas Keller
37d0ccc728 partially resolve issue 492 (at least for the monotone tree view) 2010-09-11 00:28:31 +02:00
Thomas Keller
7557a73014 While the dateAgo code internally doesn't seem to care about the argument
(unless its not "withal"), its better to fix the spelling here anyways.
2010-09-11 00:21:30 +02:00
Loïc d'Anterroches
f3f00dd182 Fixed ticket 489, improve the Markdown and wiki syntax documentation. 2010-09-02 14:46:15 +02:00
Loïc d'Anterroches
a2297decfd Fixed ticket 486, start to explain how to contribute. 2010-09-02 14:39:09 +02:00
Thomas Keller
07b2b2f305 Merge branch 'develop' of projects.ceondo.com:indefero into develop 2010-09-02 12:26:52 +00:00
Thomas Keller
85df9e5ab2 move IDF_View_Source_Precondition into a separate class file 2010-09-02 12:22:59 +00:00
Loïc d'Anterroches
439014b0b1 Fixed ticket 479, project editing inconsistency. 2010-09-02 14:16:41 +02:00
Thomas Keller
f6fc5ae466 Merge branch 'develop' of projects.ceondo.com:indefero 2010-09-01 13:18:10 +00:00
Loïc d'Anterroches
132c4f6c89 Fixed ticket 481, problem with registration link. 2010-09-01 15:17:24 +02:00
Thomas Keller
21cdf60c31 Introduce a more subtle concept of validity when it comes to revision
indentifiers in IDF - the SCM function isValidRevision has been replaced
by a validateRevision() method which returns one of three states,
valid, invalid or ambiguous.
The source view can then act accordingly and display disambiguate view
for the latter, so the user can select for which revision he actually
wants to execute the requested action. Also, invalid revisions now lead
to another separate view, telling the user that it is invalid / does
not exist and pointing him optionally to the help page where he can read
further how to access his repository to push the first changes into.
(partially resolves issue 525)
2010-09-01 13:13:52 +00:00
Brenda Wallace
b4f8cf8c50 Corrected grammar in instructions. 2010-09-01 15:06:02 +02:00
Loïc d'Anterroches
f4dbabe8de Merge branch 'master' of git://projects.ceondo.com/indefero 2010-09-01 14:51:33 +02:00
Matthew Dawson
c5c7ebff04 Improved the wiki links to have better descripting links. 2010-09-01 14:50:04 +02:00
Thomas Keller
5d263e78e0 its dateago:"without", not dateago:"wihtout" 2010-08-31 21:17:09 +00:00
Thomas Keller
5f4e1da0c8 while there _should_ be really a custom application exception,
currently there is no such one
2010-08-31 21:07:31 +00:00
Thomas Keller
187365db76 IDF_Scm::isValidRevision() only ever takes one argument 2010-08-30 21:50:25 +00:00
Thomas Keller
e26a5c8cdf waiting usher instances should be stoppable as well 2010-08-30 21:29:42 +00:00
Thomas Keller
a384c60937 fix a PHP notice 2010-08-30 20:14:03 +00:00
Thomas Keller
6b4abac08e - PHP doesn't like the $var = <init> kind of initialization and
won't return variable references properly, so revert that change
  again
- since we're requiring 0.99 now, we also have to use au generate_key
  instead of au genkey
2010-08-30 13:46:05 +00:00
Thomas Keller
e789263068 - raise required mtn version to 0.99
- add '--key=' to the default mtn arguments in idf.php-dist
2010-08-30 12:58:52 +00:00
Thomas Keller
82aaf43d5d - initial work on a mtn-post-push script which updates IDF's timeline
when new revisions arrive. this still needs some more tests, but
  its a start.
- refactor out the monotonerc template from SyncMonotone.php and
  place it in a separate template file (access control hooks are
  still missing from there)
2010-08-30 11:20:47 +00:00
Thomas Keller
adae73080c simplify and harden the configuration file writeouts, didn't knew
file_put_contents was around for so long already...
2010-08-30 09:00:20 +02:00
Thomas Keller
b648e6f7a7 - now that we have to configure usher's configuration file anyway,
we can skip the explicit configuration of its host and admin
  password, as we can directly read that from the configuration file
  itself
- expand the SyncMonotone plugin a bit to where this actually becomes
  useful, i.e.  create an accompanying key for each created database
  and also add some initial database-specific configuration
- update the config docs in idf.php-dist to reflect the changes and
  add more details about the inner workings of the SyncMonotone plugin
2010-08-29 23:01:25 +00:00
Thomas Keller
194dcad0e3 Don't check if the main branch is empty, but let the view code later
handle the case where a selector doesn't resolve to at least one
revision gracefully (see also issue 525).
2010-08-29 22:59:12 +00:00
Thomas Keller
af3df142d4 First attempt on a monotone plugin which creates a new database upon
project creation and adds the new server to the running usher instance.
If everything goes well, the usher instance is told to reload its
configuration, so the new server / database is picked up and started
automatically.
2010-08-28 23:10:08 +00:00
Thomas Keller
f2a9518b5c single quoted newlines of course will not work here... stupid me 2010-08-28 22:47:37 +00:00
Thomas Keller
593240b420 implement IDF_Scm_Monotone_BasicIO::compile() 2010-08-28 22:03:47 +00:00
Thomas Keller
8aae0f29d4 refactor the basicio parser into a separate utility class (we soon need a compiler part as well) 2010-08-28 14:06:17 +00:00
Loïc d'Anterroches
14d07a22e2 Merge branch 'mnt-support' 2010-08-27 08:59:09 +02:00
Loïc d'Anterroches
3eb0247b37 Fixed to correctly have a type text input. 2010-08-27 08:57:54 +02:00
Thomas Keller
7f32a5679d * removed type field in IDF_Key on request of Loic and automatically detect
and validate raw key data
* reworked the parseMonotoneKeyData() function to parse ssh and monotone keys
* tweak help texts and exception strings
2010-08-24 23:30:12 +02:00
Thomas Keller
a442fd588e Merge branch 'master' of github.com:tommyd3mdi/indefero-monotone 2010-08-24 22:57:52 +02:00
Thomas Keller
e7ce32fc26 Merge branch 'master' of git://projects.ceondo.com/indefero 2010-08-24 22:45:28 +02:00
Thomas Keller
f0a606e5cf * its IDF_Key, not IDF_Keys 2010-08-23 09:45:04 +02:00
Thomas Keller
0c4846c986 Adapted monotone's README file and moved it under doc/ 2010-08-13 11:42:14 +02:00
Thomas Keller
31e81118dd Follow IDFs coding standards and tweak opening curly braces as well as
quoting where possible
2010-08-13 11:21:07 +02:00
Thomas Keller
e47d51d14c Add the possibility to save mtn public keys per user
* src/IDF/Key.php: new column "type" which is either "ssh" or "mtn";
  utility functions to query the mtn key name and id as well as
  all available key types for the current IDF installation
* src/IDF/Migrations/16KeyType.php: needed migration script
* src/IDF/Plugin/SyncGit/Cron.php: ensure only SSH keys are handled
* adapt forms and templates accordingly
2010-08-11 23:48:09 +02:00
Thomas Keller
ce436cc6ec ensure the usher section is active when we browse it 2010-08-11 23:43:15 +02:00
Loïc d'Anterroches
3d1ac97dc3 Correctly uses the private/description from the template project. 2010-08-11 14:50:32 +02:00
Loïc d'Anterroches
7a2065c687 Fixed to correctly use the default values if the template project was not fully updated. 2010-08-11 14:42:06 +02:00
Loïc d'Anterroches
9e6c7dad88 Fixed to use the private status and the description from the template when creating a project. 2010-08-11 14:12:03 +02:00
Loïc d'Anterroches
4f23ea4dd5 Added the ability to create a new project with another one as template. 2010-08-11 13:58:41 +02:00
Thomas Keller
f25dbd8872 Merge branch 'master' of git://projects.ceondo.com/indefero 2010-08-07 23:42:13 +02:00
Thomas Keller
de036920a4 * src/IDF/Middlewre.php: add a global template variable
"usherConfigured" to denote whether we render links to the usher
control functions in the forge administration
* src/IDF/Scm/Monotone.php: moved IDF_Scm_Monotone_Stdio into
separate file src/IDF/Scm/Monotone/Stdio.php
* src/IDF/Scm/Usher.php: new class to query and modify the state
of a running usher instance
* src/IDF/Views/Admin.php: add actions to query the list of
configured servers, edit their status, view their open connections
and control the state of the usher as a whole
* src/IDF/conf/idf.php-dist: optional usher configuration added;
mail address of monotone-users added; spelling changes
* src/IDF/conf/urls.php: added needed URLs for usher actions
* src/IDF/templates/idf/gadmin/base.html: usher links
2010-08-07 23:28:13 +02:00
Mehdi Kabab
061c806588 Fixed issue 509, indefero wiki do not support strikethrough. 2010-07-25 13:37:05 +02:00
Thomas Keller
780267978d basic test setup works; added tests for isAvailable(), getBranches(),
getTags(), inTags(), inBranches(), getTree() and isValidRevision()
2010-06-30 00:42:09 +02:00
Thomas Keller
0ad7f47885 first, incomplete version of a basic monotone test driver 2010-06-29 16:00:53 +02:00
Thomas Keller
d2f0bac907 * Monotone.php: get inTags() and inBranches() correct - they need to
return a list with selectors as keys, otherwise the main menu won't
  link the active revision, but instead use the main branch
* changelog.html, tree.html: shorten the branch / tag output a bit
  (still ugly to have a fixed output like this, though), link tags
  by their selector, no longer by their revision ID. We loose some
  flexibility here, since tags could actually mark different
  revisions, which are now ignored
2010-06-29 09:59:49 +02:00
Thomas Keller
e46a6fa171 link from the changelog view to the changelog view again when another
revision is selected
2010-06-28 11:46:48 +02:00
Thomas Keller
7cbc690890 use date instead of gmdate, monotone's dates are already UTC 2010-06-28 08:47:39 +02:00
Thomas Keller
a46fd28dae print the mtn master branch configuration in bold, because it is required
when monotone is selected as SCM
2010-06-26 00:18:52 +02:00
Thomas Keller
8decc383aa bugfix: do not validate the custom branch field in the project creation
formular if we should set the project up with another SCM
2010-06-26 00:07:56 +02:00
Thomas Keller
f3268b3d37 uh, we should really share one mtn instance per project and request... 2010-06-24 02:27:38 +02:00
Thomas Keller
4fdf248cb6 Merge branch 'master' of github.com:tommyd3mdi/indefero-monotone 2010-06-24 02:07:44 +02:00
Thomas Keller
1fd1e63043 * idf.php-dist: rework the documentation of the configuration options and
add two scenarios, one with a single, global database and one with
  multiple project databases and usher as proxy; add an option for additional
  command line options for the mtn process; remove the protocol type
  configuration option - we're handling this implicit with the new
  mtn_remote_url configuration
* Monotone.php: add the IDF user for ssh:// URIs; add support for remote_stdio
  and rework the command line stitching
2010-06-24 01:58:41 +02:00
Thomas Keller
5ce324f35f * clear the process environment and set LANG properly so we get english error codes and still keep utf8 compliant
* rename _read to _readStdout and introduce a generic _readStderr which reads from the other pipe on failure
2010-06-24 00:10:41 +02:00
Thomas Keller
7303e9dd58 return the correct file name for dropped files from monotone's diff output 2010-06-23 15:58:07 +02:00
Thomas Keller
e57fc18bcb add a README file which contains the first steps and some hints
for the monotone server configuration as well as some indefero
API critique
2010-06-23 01:27:12 +02:00
Thomas Keller
9a8148079d prevent endless redirection if a requested branch does not exist, i.e. has no matching revisions 2010-06-23 00:22:10 +02:00
Thomas Keller
24762adecc improve error handling and reporting of the stdio process 2010-06-21 23:24:47 +02:00
Thomas Keller
2ee665ac96 Merge branch 'master' of git://projects.ceondo.com/indefero 2010-06-21 23:23:26 +02:00
Denis
2e1a91622e Updated the Russian translations. 2010-06-17 20:03:52 +02:00
mainiak
4687355351 Updated the Czech translations. 2010-06-15 11:21:06 +02:00
Loïc d'Anterroches
38dd610319 Updated the German translations. 2010-06-15 11:18:13 +02:00
mainiak
a4408de74d Updated the Czech translations. 2010-05-27 20:20:53 +02:00
Loic d'Anterroches
7a952215aa Added a series of hooks to trigger backup jobs when files are uploaded/deleted. 2010-05-21 11:29:36 +02:00
Loic d'Anterroches
982b330739 Added the droping of the tags in the predelete to prevent some integrity issues. 2010-05-21 11:28:48 +02:00
Loic d'Anterroches
598451f470 Fixed to get the scripts as executable by default. 2010-05-20 12:23:34 +02:00
Loic d'Anterroches
3e9229be5f Updated to set the subversion hooks at creation of the repository. 2010-05-20 10:42:51 +02:00
Loic d'Anterroches
ad6148cae7 Fixed issue 403, make the calcul of project's disk used space optionnal. 2010-05-19 10:55:50 +02:00
Loic d'Anterroches
2aebc0e099 Fixed to correctly populate the queue when no notifications are defined. 2010-05-18 15:10:43 +02:00
Loic d'Anterroches
06022bf378 Added the type of scm in the queue payload. 2010-05-18 14:31:01 +02:00
Vladimir Solomatin
430c9cb00e Fixed issue 467, exception when deleting an orphan git repo. 2010-05-17 19:40:26 +02:00
Loic d'Anterroches
89780b0317 Fixed ticket 466, Mac OS-friendly source viewing / syntax highlighting. 2010-05-17 12:53:17 +02:00
Loic d'Anterroches
c534894995 Added caching of the database, attachments and uploaded files to avoid calculating them each time. 2010-05-17 12:17:02 +02:00
Loic d'Anterroches
a91ce1600f Added a global configuration registry. 2010-05-17 10:29:00 +02:00
Loic d'Anterroches
145f6d0d1d Added a note to help with some Subversion errors when restarting Apache as root. 2010-05-17 10:02:56 +02:00
Loic d'Anterroches
04e7d7c99b Merge branch 'master' of git://projects.ceondo.com/indefero 2010-05-12 09:19:57 +02:00
mainiak
7ed69c7f4a Improved the Czech translations 2010-05-12 09:19:36 +02:00
Mehdi Kabab
8f914c44a1 Fixed issue 459, variables not defined for exec in PHP 5.3. 2010-05-11 10:13:38 +02:00
Loic d'Anterroches
28ce82c6f6 Improved to sort the Git tags by reverse chronological order. 2010-05-11 09:41:22 +02:00
Loic d'Anterroches
641a3b24a5 Fixed error in the queue addition. 2010-05-11 09:23:15 +02:00
Loic d'Anterroches
0d61866b89 Added initial work on the Czech translation. 2010-05-10 12:55:58 +02:00
Loic d'Anterroches
c1a477e7d0 Do nothing on webhook without an url. 2010-05-10 10:43:47 +02:00
Loic d'Anterroches
692d2e53b2 Changed the header name of the hmac to be generic. 2010-05-10 10:21:22 +02:00
Loic d'Anterroches
47acc73451 Added the webhooks. 2010-05-10 10:11:27 +02:00
Loic d'Anterroches
2f22d48dd0 Added the queue system to handle the webhooks and asynchronous events. 2010-05-06 10:27:08 +02:00
Thomas Keller
e0b0a732b4 phpdoc improved; remove _getMasterBranch() method and implement the specific code directly in getMainBranch() 2010-05-02 01:31:30 +02:00
Thomas Keller
59ad0f5b11 * idf.php-dist: improve the document of the various mtn-related configure options; introduce an option to configure the protocole - separate from the url option, which we now name mtn_remote_host
* IDF_Project: optionally give getSourceAccessUrl() a commit argument, so a particular VCS module can determine a subset of revisions to pull for the specific revision which is browsed
* IDF_Scm_*: add the argument null'd for all VCS; implement a branch lookup for monotone
* tree.html: display the correct branch to clone under each revision tree
2010-05-02 00:56:04 +02:00
Thomas Keller
3b53ceedcd * Monotone.php (IDF_Scm_Monotone): basic_io values need to be unescaped; implement getChangeLog()
* Monotone.php (IDF_Scm_Monotone_Stdio): add support for multiple, equally named options
* Source.php, commit.html: split-off the global commit template (which had some separate code already for SVN) and adapt the left blocks for mtn to shorten branch and tag names just like we do everywhere else
2010-05-01 01:05:54 +02:00
Thomas Keller
15a2bd90b3 Add support for monotone's diff header 2010-05-01 00:56:48 +02:00
Thomas Keller
c49a8204e0 Properly activate the correct branches / tags for the currently viewed revision 2010-04-30 02:38:45 +02:00
Thomas Keller
601e894935 Use the branch / tag name shortener in two other templates as well. 2010-04-30 02:11:40 +02:00
Thomas Keller
445c90fefe Create a separate class which handles command streaming over mtn automate stdio. Use that everywhere instead of the direct system calls. 2010-04-30 02:03:58 +02:00
Thomas Keller
b7ced5fa69 Do not shorten the changelog in the SCM model - thats a task for the view. 2010-04-29 23:38:28 +02:00
Thomas Keller
995f1a13c3 Add a new view modifier which allows the shortening of long strings such as branch or tag names. Use that in the tree view and display the full name in a title tag. 2010-04-29 23:35:57 +02:00
Thomas Keller
cf22909722 * isAvailable(): check monotone's interface version and mark the interface as available if it matches (we might see later on if this alone is actually a good idea especially if we browse an empty database...)
* _getCerts(): implement a cert cache and make multiple cert values easily available
* getCommit(), getCommitLarge(), getFile(), getPathInfo(), testHash(): implement
* getTags(): save the first found revision id for a tag as key in the associative array to make tags actually browsable
2010-04-29 01:44:34 +02:00
Thomas Keller
02603fd8fd disable archive generation for now, this is not possible with monotone as it is implemented now in IDF 2010-04-29 01:42:50 +02:00
Thomas Keller
af4f5aaeb0 make the path to the monotone executable configurable 2010-04-29 01:37:28 +02:00
Thomas Keller
f8012c37d1 from monotone 0.48 onwards the setup command creates its own internal database 2010-04-28 01:12:29 +02:00
Thomas Keller
5954cd0ad1 default to the master branch for the head / tip / main revision 2010-04-28 00:14:19 +02:00
Thomas Keller
5ef6e6c08f mtn still needs a local db and project setup if there is nothing beside an empty database remotely 2010-04-28 00:13:42 +02:00
Thomas Keller
94a5464155 * idf.php-dist: no need to configure a branch prefix any longer now that the project owner can define the master branch name
* Monotone.php: change accordingly to use the configured master branch name and fallback to all branches ("*") if noone is found
2010-04-27 23:28:52 +02:00
Thomas Keller
18ba8d0ac5 Some more files from the initial work 2010-04-27 00:02:47 +02:00
Thomas Keller
9fd4334dec Start on monotone support for indefero. The SCM backend is about 30% done, interesting pieces like getTree() are not finished yet. 2010-04-26 23:56:25 +02:00
169 changed files with 44761 additions and 15704 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
*~
tmp
src/IDF/conf/idf.php
src/IDF/conf/idf.test.php
@ -6,3 +7,5 @@ www/media/upload
src/IDF/gettexttemplates
indefero-*.zip
src/IDF/conf/path.php
.tx/config
src/IDF/locale/idf.pot.bak

View File

@ -23,6 +23,8 @@ Much appreciated contributors:
Ludovic Bellière
Brian Armstrong
Raphaël Emourgeon
Jakub Viták
Vladimir Solomatin
And all the nice users who spent time reporting issues and promoting
the project. The project could not live without them.

115
CONTRIBUTE.mdtext Normal file
View File

@ -0,0 +1,115 @@
[Indefero][idf] is not only a software you can use either hosted for
you or hosted by you, but also a free software you can contribute to.
Here you will get how to contribute and what to contribute.
[idf]: http://www.indefero.net
# Quick Way on How to Contribute
Simple contribution:
1. Open a ticket with your idea. You can directly propose a patch if
you have it.
2. Wait for it to be checked by the devs or meet us on the #indefero
channel on [FreeNode][freenode].
Bigger contribution:
1. Fork Indefero where you want (fork from the develop branch).
2. Code your change and document it.
3. Open a ticket with a pull request and talk about it on IRC.
# The General Contribution Workflow for Regular Contributors
1. Fork Indefero from the **develop** branch.
2. Request a pull request if you do not have write access on the repository.
3. Merge your changes without fast forward in develop. This keeps track of
the history of the changes and makes understanding what is going on easy.
4. Merge your changes with fast forward **only if a single commit**.
Indefero is composed of two main branches:
1. **master**: this is the shipped branch, only a select number of people
can push into it.
2. **develop**: this is the development branch, all the people having write
access to the repository are welcomed to push in.
**Note:** The branching model we use is [explained in details here][bmi]. You
**must** understand it to really contribute to the code base in an
efficient way.
[bmi]: http://nvie.com/git-model "A successful Git branching model"
# What to Contribute
Contribution is easy, you can contribute in a lot of different fields,
contributions small or big are always appreciated. Here is an example
list of what you can do:
- Install InDefero on your system and report the problem you had.
- Find the bad English and typos and propose corrections.
- Help with the translation effort.
- Find little bugs or usability problems and provide ideas on how to fix them.
- Register to the [discussion group][group] and help new users.
- Come and chat on IRC #indefero on the [FreeNode][freenode] servers.
- Find ways to improve the design while keeping it **beautifully simple**.
- Write a blog post about the project, what you think is good or bad.
- Translate InDefero for the sake of the community.
- Or maybe really hack into the code.
As you can see, the real hacking into the code is just a small part of the work, so even if you are not a coder you can do a lot.
[group]: http://groups.google.com/group/indefero-users
[freenode]: http://freenode.net/
## I am a simple user
Thanks a lot! Really! As a project leader, I consider **you** as
**the most important person in the success of the project**. So do not
worry, I will really listen to your needs and make you love this
project.
What you can do to help:
- Use the software and each time you find something a bit annoying in your daily use, report a bug. Usability issues are high priority issues.
- Find typos, grammar mistakes, etc. and report a bug.
- Write about InDefero on your blog/website.
- Read the issues submitted by the users and provide answers if you have them.
- ...
## I am a designer
A lot of things to do for you:
- Check the design and find the flaws in it. Is the space well used, does it look really nice and is it also functional for the first users?
- Do we have good support of all the major browsers?
- ...
## I am a coder
Checkout the code and have fun, but keep in mind that your results
must be simple to use. Do not worry about the beautiful part, the
designers can work on that.
## I am a security guy
Please, do try to break it, if you find a problem, come on IRC or
contact the developers to get the issue fixed as soon as
possible. Please, be nice, do not release the issue in the wild
without first talking to us.
## I am a translator
We currently use (transifex)[http://trac.transifex.org] to help our
users to translate indefero. You don't have to use it, but it's an
easy way to do the job. You can visit the indefero page at transifex
here : http://www.transifex.net/projects/p/indefero/c/indefero/
Please understand that your changes will not be commited instantly,
but are sent to the maintainers e-mails before. Then, your changes
will not be in the main repository until da-loic push the changes. In
that way, try to do big changes with less submissions.

View File

@ -212,4 +212,14 @@ If you access a Subversion server with a self-signed certificate, you
may have problems as your certificate is not trusted, check the
[procedure provided here][svnfix] to solve the problem.
[svnfix]: http://projects.ceondo.com/p/indefero/issues/319/#ic1358
[svnfix]: http://projects.ceondo.com/p/indefero/issues/319/#ic1358
## If the registration links are not working
If You have standard instalaction of PHP ie in Debian, php.ini sets
mbstring.func_overload to value "2" for overloading str*
functions. You need to prevent the overload as it does not make sense
anyway (magic in the background is bad!).
See the [corresponding ticket][reglink].
[reglink]: http://projects.ceondo.com/p/indefero/issues/481/

110
Makefile Normal file
View File

@ -0,0 +1,110 @@
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
# Installation of external tools : transifex-client
# sudo apt-get install python-setuptools
# sudo easy_install -U transifex-client
PLUF_PATH=$(shell php -r "require_once('src/IDF/conf/path.php'); echo PLUF_PATH;")
all help:
@echo "Rules for generate tarball :"
@for b in `git branch | sed "s/^. //g"`; do \
echo -e "\t"$$b"_tarball - Generate a zip archive of the "$$b" branch."; \
done
@echo -e "\nRules for internationnalization :";
@echo -e "\tpot-update - Update the POT file from HTML template and PHP source, then merge it with PO file"
@echo -e "\tpot-push - Send the POT file on transifex server"
@echo -e "\tpo-update - Merge POT file into PO file. POT is not regenerated."
@echo -e "\tpo-push - Send the all PO file on transifex server"
@echo -e "\tpo-pull - Get all PO file from transifex server"
#
# Internationnalization rule, POT & PO file manipulation
#
.PHONY: pot-update po-update
pot-update:
# Backup pot file
@if [ -e src/IDF/locale/idf.pot ]; then \
mv -f src/IDF/locale/idf.pot src/IDF/locale/idf.pot.bak; \
fi
touch src/IDF/locale/idf.pot;
# Extract string
@cd src; php $(PLUF_PATH)/extracttemplates.php IDF/conf/idf.php IDF/gettexttemplates
@cd src; for phpfile in `find . -iname "*.php"`; do \
echo "Parsing file : "$$phpfile; \
xgettext -o idf.pot -p ./IDF/locale/ --from-code=UTF-8 -j --keyword --keyword=__ --keyword=_n:1,2 -L PHP $$phpfile ; \
done
# Remove tmp folder
rm -Rf src/IDF/gettexttemplates
# Update PO
@make po-update
po-update:
@for pofile in `ls src/IDF/locale/*/idf.po`; do \
echo "Updating file : "$$pofile; \
msgmerge -v -U $$pofile src/IDF/locale/idf.pot; \
echo ; \
done
#
# Transifex
#
.PHONY: check-tx-config
check-tx-config:
@if [ ! -e .tx/config ]; then \
mkdir -p .tx; \
touch .tx/config; \
echo "[main]" >> .tx/config; \
echo "host = http://www.transifex.net" >> .tx/config; \
echo "" >> .tx/config; \
echo "[indefero.idfpot]" >> .tx/config; \
echo "file_filter = src/IDF/locale/<lang>/idf.po" >> .tx/config; \
echo "source_file = src/IDF/locale/idf.pot" >> .tx/config; \
echo "source_lang = en" >> .tx/config; \
fi
@if [ ! -e $(HOME)/.transifexrc ]; then \
touch $(HOME)/.transifexrc; \
echo "[http://www.transifex.net]" >> $(HOME)/.transifexrc; \
echo "username = " >> $(HOME)/.transifexrc; \
echo "token = " >> $(HOME)/.transifexrc; \
echo "password = " >> $(HOME)/.transifexrc; \
echo "hostname = http://www.transifex.net" >> $(HOME)/.transifexrc; \
echo "You must edit the file ~/.transifexrc to setup your transifex account (login & password) !"; \
exit 1; \
fi
pot-push: check-tx-config
@tx push -s
po-push: check-tx-config
@tx push -t
po-pull: check-tx-config
@tx pull -a
#
# Generic rule to build a tarball of indefero for a specified branch
# ex: make master_tarball
# make dev_tarball
#
%_tarball:
@git archive --format=zip --prefix="indefero/" $(@:_tarball=) > indefero-$(@:_tarball=)-`git log $(@:_tarball=) -n 1 --pretty=format:%H`.zip

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

272
doc/syncmonotone.mdtext Normal file
View File

@ -0,0 +1,272 @@
# Plugin SyncMonotone by Thomas Keller (me@thomaskeller.biz)
The SyncMonotone plugin allow the direct creation and synchronisation of
monotone repositories with the InDefero database. It has been built to
work together with monotone's "super server" usher, which is used to control
several repositories at once, acts as proxy and single entrance.
## Prerequisites
* a unixoid operating system
* monotone >= 0.99.1
* for a proxy setup with usher:
* boost headers (for usher compilation)
* a current version of usher
* a daemonizer, like supervise
## Installation of monotone
If you install monotone from a distribution package, ensure you do not
install and / or activate the server component. We just need a plain
client installation which usually consists only of the `mtn` binary and
a few docs.
If you install monotone from source (<http://monotone.ca/downloads.php>),
please follow the `INSTALL` document which comes with the software.
It contains detailed instructions, including all needed dependencies.
## Choose your indefero setup
The monotone plugin can be used in several different ways:
1. One database for everything. This is the easiest setup and of possible
use in case you do not want indefero to manage the access to your project.
Your `idf.php` should look like this:
$ cat idf.php
...
$cfg['mtn_path'] = 'mtn';
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles');
$cfg['mtn_repositories'] = '/home/monotone/all_projects.mtn';
$cfg['mtn_remote_url'] = 'ssh://monotone@my.server.com:~all_projects.mtn';
$cfg['mtn_db_access'] = 'local';
...
Pro:
* easy to setup and to manage
Con:
* you need to give committers SSH access to your machine
* database lock problem: the database from which
indefero reads its data might be locked in case a user
syncs at the very moment via SSH
2. One database for every project. Similar to the above setup, but this
time you use the '%s' placeholder which is replaced with the short name
of the indefero project:
$ cat idf.php
...
$cfg['mtn_path'] = 'mtn';
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles');
$cfg['mtn_repositories'] = '/home/monotone/%s.mtn';
$cfg['mtn_remote_url'] = 'ssh://monotone@my.server.com:~%s.mtn';
$cfg['mtn_db_access'] = 'local';
...
The same pro's and con's apply. Additionally you have to be careful about
not giving people physical read/write access of another project's database.
Furthermore, if you do not want to use `ssh`, but `netsync` transport,
each project's database must be served over a separate port.
3. One database for every project, all managed with usher. This is the
recommended setup for a mid-size forge setup. The remaining part of this
document will describe the process to set this up in detail.
Pro:
* access rights can be granted per project and are automatically
managed by indefero, just like the user's public monotone keys
* no database locking issues
* one public server running on the one well-known port
Con:
* harder to setup
## Installation and configuration of usher
1. Clone usher's monotone repository:
$ mtn clone "mtn://monotone.ca?net.venge.monotone.contrib.usher"
2. Compile usher:
$ autoreconf -i
$ ./configure && make
$ sudo make install
This installs the usher binary in $prefix/bin.
3. Create a new usher user:
$ adduser --system --disabled-login --home /var/lib/usher usher
4. Create the basic usher setup:
$ cd /var/lib/usher
$ mkdir projects logs
$ cat > usher.conf
userpass "admin" "<secret-password>"
adminaddr "127.0.0.1:12345"
logdir "log"
^D
$ chmod 600 usher.conf
Your indefero www user needs later write access to `usher.conf` and
`projects/`. There are two ways of setting this up:
* Make the usher user the web user, for example via Apache's `suexec`
* Use acls, like this:
$ setfacl -m u:www:rw usher.conf
$ setfacl -m d:u:www:rwx projects/
5. Wrap a daemonizer around usher, for example supervise from daemontools
(<http://cr.yp.to/damontools.html>):
$ cat > run
#!/bin/sh
cd /var/lib/usher
exec 2>&1
exec \
setuidgid usher \
usher usher.conf
^D
The service can now be started through supervise:
$ supervise /var/lib/usher
## Configuration of indefero
Based on the above setup, the configuration in `src/IDF/conf/idf.php` should
look like this:
$ cat idf.php
...
$cfg['mtn_path'] = 'mtn';
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles');
$cfg['mtn_repositories'] = '/var/lib/usher/projects/%s/';
$cfg['mtn_remote_url'] = 'mtn://my.server.com/%s';
$cfg['mtn_db_access'] = 'remote';
$cfg['mtn_usher_conf'] = '/var/lib/usher/usher.conf';
...
The `%s` placeholders are automatically replaced by the name of the
indefero project. The plugin assumes that every project is separated
by a distinct server name in the monotone URL (hence the use of `/%s`),
so if a user calls
$ mtn sync mtn://my.server.com/project1
then the database / repository of the indefero `project1` is used.
Note that 'mtn_remote_url' is also used as internal URI to query the data
for indefero's source view, so it *must* be a valid host!
Usher also allows the identification of a project repository by hostname,
which would allow an URL template like `mtn://%s.my.server.com`, however
the plugin does not write out the configuration which is needed for this
yet.
For even more advanced setups, usher can also be used to forward sync
requests to other remote servers for load balancing, please consult the
README file for more information.
## Security and remote access
Indefero distinguishs between public and private projects and so does
the monotone plugin.
Public projects can be pulled by everybody and pushed by team members
or additional invited people. Remote command execution is enabled, but
only for read-only commands.
Remote commands can be helpful for a user or a 3rd party tool (like
[mtn-browse](http://mtn-browse.sourceforge.net) or
[guitone](http://guitone.thomaskeller.biz)) to browse the database
contents remotely without having to pull everything in first instance.
Private projects on the other hand can only be synced by team members
or additional invited people. Remote command execution is disabled
by default. If you want to enable that, simply put the keys of the users
you want to give access to in your project's `remote-automate-permissions`
file. In the future this plugin might handle this file just as it handles
`read-permissions` and `write-permissions`.
## Notifications
If you have successfully set up your monotone instance, you probably want
to notify 3rd party systems for incoming changes or simply mirror them
somewhere else for backup purposes. The monotone source tree already comes
with [many example scripts and hooks](http://code.monotone.ca/p/monotone/source/tree/h:net.venge.monotone/contrib)
which serve these purposes, after only little additional configuration.
The usher/indefero-controlled setup automatically looks for *.lua files
in a directory called `hooks.d` right under the project's base directory
(configured via $cfg['mtn_repositories']) and this is the ideal place to
put or link these additional lua sources.
## Custom project configurations and templates
If a new project is created in IDF, the SyncMonotone plugin creates a new
configuration tree for the project into the project's configuration directory,
determined by `$cfg['mtn_repositories']`. IDF ships with the minimum set of
files for this configuration tree and sets up everything automatically for you.
Even more, most of the configuration files from the newly created tree are only
symlinked to the original configuration directory which is configurable via
`$cfg['mtn_confdir']` and defaults to `src/IDF/Plugin/SyncMonotone/`. This has
the advantage that your standard IDF setup automatically receives updates to
existing (symlinked) configuration files as soon as you update to a newer
version.
You could, however, also choose to place the directory tree somewhere else
and adapt the contents of the individual files yourself, so these changes get
automatically applied to all new projects you create. You could even go so far
and add new files to the tree and let them be processed automatically just
as the basic files! All you need to do is to copy your files and / or directories
underknees your `$cfg['mtn_confdir']` and add their relative paths to
`$cfg['mtn_confdir_extra']`.
By convention, all entries which end with a slash are considered directories,
so mkdir(1) is issued for these entries, all files which do not end up with
".in" are considered to be static script files which are just symlinked from
the basic configuration dir and all entries ending on ".in" are considered
configuration files or templates, which are copied over to the project's
configuration tree and which get some basic project-specific values replaced.
The following placeholders are currently recognized and replaced for these files:
* %%PROJECT%% - the name of the created project
* %%MTNPOSTPUSH%% - the absolute path to the `mtn-post-push` script
* %%MTNCLIENTKEY%% - the public key hash of the key which is used by IDF
to authenticate remote stdio access
Thats it - I hope you find it useful :)
## Q&A
### After I created a new project, IDF throws an exception and tells me that it couldn't save the membership data with a cryptic error message. Whats wrong?
Multiple issues could cause that. If you've set up usher, make sure the usher
can fork your database at all and look out for specific errors in the log file
of your project. If you stumble upon permission issues, ensure that the user
who runs the usher can access all files in your project's configuration directory,
including symlinked files.
### I pushed a branch to my server, but it does not show up in IDF. Whats wrong?
Check if the heads of your branch are not suspended, i.e. do not carry a
`suspend` certificate. This usually hides the branch and all of its history
from monotone's eyes and therefor also from indefero. You can either choose
to "unsuspend" the branch simply by committing and pushing another head or
by letting monotone ignore all suspend certs. For the latter, its usually
enough to add `--ignore-suspend-certs` to the list of options in `$cfg['mtn_opts']`.
### I want to display another default branch when I click the "Source" tab. How can I do that?
Let the forge admin know the new master branch for your project. He is able
to change that quickly. Depending on the backend / server setup this might
also require some changes in the usher configuration, but only if usher
recognizes and proxies your database on a branch name level.

View File

@ -79,3 +79,12 @@ the following configuration variables:
* **idf_plugin_syncsvn_access_public ('r')**: Anonymous access.
* **idf_plugin_syncsvn_access_private ('')**: Anonymous access in the case of a private project.
## svn: Can't open file '/root/.subversion/servers': Permission denied error
If you get the error:
svn: Can't open file '/root/.subversion/servers': Permission denied
Check the [fix available](http://projects.ceondo.com/p/indefero/issues/458/)

View File

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

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -1,10 +1,13 @@
#!/bin/sh
last="$1"
new="$2"
PLUF_PATH=`php -r "require_once('./IDF/conf/path.php'); echo PLUF_PATH;"`
echo "php $PLUF_PATH/extracttemplates.php ./IDF/conf/idf.php ./IDF/gettexttemplates"
echo "xgettext -o idf.pot -p ./IDF/locale --force-po --from-code=UTF-8 --keyword --keyword=__ --keyword=_n:1,2 -L PHP ./IDF/*.php"
echo "find ./ -iname \"*.php\" -exec xgettext -o idf.pot -p ./IDF/locale/ --from-code=UTF-8 -j --keyword --keyword=__ --keyword=_n:1,2 -L PHP {} \;"
echo 'for pofile in `ls ./IDF/locale/*/idf.po`; do msgmerge -U $pofile ./IDF/locale/idf.pot; done'
echo "# git tag v$new"
echo "git archive --format=zip --prefix=indefero-$new/ v$new > indefero-$new.zip"
echo "git log --no-merges v$new ^v$last > ChangeLog-$new"
echo "git shortlog --no-merges v$new ^v$last > ShortLog"
echo "git diff --stat --summary -M v$last v$new > diffstat-$new"
echo "git diff --stat --summary -M v$last v$new > diffstat-$new"

0
scripts/SyncMercurial.sh Normal file → Executable file
View File

View File

@ -17,8 +17,8 @@
# git$ ln -s /home/www/indefero/scripts/git-post-update post-update
#
SCRIPTDIR=$(dirname $(readlink -f $0))
FULL_GIT_DIR=$(readlink -f $GIT_DIR)
SCRIPTDIR=$(dirname $(readlink $0))
FULL_GIT_DIR=$(cd "$GIT_DIR" && /bin/pwd || "$GIT_DIR")
PHP_POST_UPDATE=$SCRIPTDIR/gitpostupdate.php
echo php $PHP_POST_UPDATE $FULL_GIT_DIR | at now > /dev/null 2>&1

View File

@ -53,6 +53,7 @@ Pluf_Dispatcher::loadControllers(Pluf::f('idf_views'));
*/
$params = array('git_dir' => $argv[1],
'env' => array_merge($_ENV, $_SERVER));
Pluf_Log::event(array('gitpostupdate.php', 'Send run signal.', $params));
Pluf_Signal::send('gitpostupdate.php::run', 'gitpostupdate.php', $params);

22
scripts/mtn-post-push Executable file
View File

@ -0,0 +1,22 @@
#!/bin/sh
#
# This hook informs IDF that new revisions arrived in the database
# of the specified project.
#
# This hook is normally installed automatically at the creation of your
# repository if you have everything configured correctly. If you want
# to enable it later, you need to call it into your monotonerc file
# from the hook "note_netsync_end". (See chapter "Event Notifications
# and Triggers" on <http://monotone.ca/docs/Hooks.html#Hooks>.)
#
dir=$(dirname "$0")
res=$(cd "$dir" && /bin/pwd || "$dir")
SCRIPTDIR="$res/$(readlink $0)"
PHP_POST_PUSH=$SCRIPTDIR/mtnpostpush.php
TMPFILE=$(mktemp /tmp/mtn-post-push.XXXXXX) || exit 1
while read rev; do echo $rev >> $TMPFILE; done
echo php $PHP_POST_PUSH "$1" \< $TMPFILE \&\& rm -f $TMPFILE |\
at now > /dev/null 2>&1

63
scripts/mtnpostpush.php Normal file
View File

@ -0,0 +1,63 @@
<?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-2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* This script will send the notifications after a push in your
* repository.
*/
require dirname(__FILE__).'/../src/IDF/conf/path.php';
require 'Pluf.php';
Pluf::start(dirname(__FILE__).'/../src/IDF/conf/idf.php');
Pluf_Dispatcher::loadControllers(Pluf::f('idf_views'));
/**
* [signal]
*
* mtnpostpush.php::run
*
* [sender]
*
* mtnpostpush.php
*
* [description]
*
* This signal allows an application to perform a set of tasks
* after a push to a monotone repository.
*
* [parameters]
*
* array('project' => 'name-of-the-project',
* 'revisions' => array('123abc...', '456def...', ...));
*
*/
fwrite(STDERR, "waiting for revisions on STDIN...\n");
$stdin = file_get_contents('php://stdin');
$params = array('project' => $argv[1],
'revisions' => explode("\n", chop($stdin)));
Pluf_Signal::send('mtnpostpush.php::run', 'mtnpostpush.php', $params);

69
scripts/queuecron.php Normal file
View File

@ -0,0 +1,69 @@
<?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 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 ***** */
/**
* This script process the queue of items.
*
* At the moment the queue is only used for the webhooks, but it would
* be good in the future to use it for indexing and email
* notifications.
*
*/
require dirname(__FILE__).'/../src/IDF/conf/path.php';
require 'Pluf.php';
Pluf::start(dirname(__FILE__).'/../src/IDF/conf/idf.php');
Pluf_Dispatcher::loadControllers(Pluf::f('idf_views'));
#;*/ ::
$lock_file = Pluf::f('idf_queuecron_lock',
Pluf::f('tmp_folder', '/tmp').'/queuecron.lock');
if (file_exists($lock_file)) {
Pluf_Log::event(array('queuecron.php', 'skip'));
return;
}
file_put_contents($lock_file, time(), LOCK_EX);
/**
* [signal]
*
* queuecron.php::run
*
* [sender]
*
* queuecron.php
*
* [description]
*
* This signal allows an application to perform a set of tasks when
* the queue cron job is run. This is done usually every 5 minutes.
*
* [parameters]
*
* array()
*
*/
$params = array();
Pluf_Signal::send('queuecron.php::run', 'queuecron.php', $params);
unlink($lock_file);

View File

@ -18,7 +18,7 @@
# www$ ln -s /home/www/indefero/scripts/svn-post-commit post-commit
#
SCRIPTDIR=$(dirname $(readlink -f $0))
SCRIPTDIR=$(dirname $(readlink $0))
PHP_POST_COMMIT=$SCRIPTDIR/svnpostcommit.php
echo php $PHP_POST_COMMIT "$1" "$2" | at now > /dev/null 2>&1

29
scripts/svn-post-revprop-change Executable file
View File

@ -0,0 +1,29 @@
#!/bin/sh
#
# This hook does only one thing:
#
# 1. It calls the svnpostrevpropchange.php script with the current repository
# and revision as argument. The svnpostrevpropchange.php script will then
# trigger the 'svnpostrevpropchange.php::run' event with the repository
# path, revision, username, property name and action as arguments together
# with merged $_ENV and $_SERVER array.
#
# This hook is normally installed automatically at the creation of your
# repository if you have everything configured correctly. If you want
# to enable it later, you need to symlink it as "post-revprop-change" in your
# $REPOSITORY/hooks folder. It needs to be executable.
#
# www$ chmod +x /home/www/indefero/scripts/svn-post-revprop-change
# www$ cd /home/svn/repositories/project/hooks
# www$ ln -s /home/www/indefero/scripts/svn-post-revprop-change post-revprop-change
#
SCRIPTDIR=$(dirname $(readlink $0))
PHP_POST_REVPROP=$SCRIPTDIR/svnpostrevpropchange.php
echo php $PHP_POST_REVPROP "$1" "$2" "$3" "$4" "$5" | at now > /dev/null 2>&1
REPOS="$1"
REV="$2"
USER="$3"
PROPNAME="$4"
ACTION="$5"

View File

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

View File

@ -0,0 +1,70 @@
<?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-2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* This script allows you to hook into the post-revprop-change action
* of your subversion repository. I am using it to perform near real
* time backup of the repositories on indefero.net.
*/
require dirname(__FILE__).'/../src/IDF/conf/path.php';
require 'Pluf.php';
Pluf::start(dirname(__FILE__).'/../src/IDF/conf/idf.php');
Pluf_Dispatcher::loadControllers(Pluf::f('idf_views'));
/**
* [signal]
*
* svnpostrevpropchange.php::run
*
* [sender]
*
* svnpostrevpropchange.php
*
* [description]
*
* This signal allows an application to perform a set of tasks on a
* post property revision change of a subversion repository.
*
* [parameters]
*
* array('repo_dir' => '/path/to/subversion/repository',
* 'revision' => 1234,
* 'user' => 'username',
* 'propname' => 'changed-property',
* 'action' => 'the action M, A or D',
* 'env' => array_merge($_ENV, $_SERVER));
*
*/
$params = array('repo_dir' => $argv[1],
'revision' => $argv[2],
'user' => $argv[3],
'propname' => $argv[4],
'action' => $argv[5],
'env' => array_merge($_ENV, $_SERVER));
Pluf_Signal::send('svnpostrevpropchange.php::run', 'svnpostrevpropchange.php',
$params);

View File

@ -34,6 +34,7 @@ Pluf::loadFunction('Pluf_Template_dateAgo');
class IDF_Commit extends Pluf_Model
{
public $_model = __CLASS__;
public $extra = null; /**< Extra data as IDF_Gconf object */
function init()
{
@ -44,9 +45,9 @@ class IDF_Commit extends Pluf_Model
'id' =>
array(
'type' => 'Pluf_DB_Field_Sequence',
'blank' => true,
'blank' => true,
),
'project' =>
'project' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Project',
@ -54,7 +55,7 @@ class IDF_Commit extends Pluf_Model
'verbose' => __('project'),
'relate_name' => 'commits',
),
'author' =>
'author' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
@ -118,7 +119,7 @@ class IDF_Commit extends Pluf_Model
{
IDF_Search::index($this);
if ($create) {
IDF_Timeline::insert($this, $this->get_project(),
IDF_Timeline::insert($this, $this->get_project(),
$this->get_author(), $this->creation_dtime);
}
}
@ -127,6 +128,7 @@ class IDF_Commit extends Pluf_Model
{
IDF_Timeline::remove($this);
IDF_Search::remove($this);
IDF_Gconf::dropForModel($this);
}
/**
@ -142,6 +144,10 @@ class IDF_Commit extends Pluf_Model
array($project->id, $change->commit));
$r = Pluf::factory('IDF_Commit')->getList(array('filter'=>$sql->gen()));
if ($r->count() > 0) {
$r[0]->extra = new IDF_Gconf();
$r[0]->extra->serialize = true;
$r[0]->extra->setModel($r[0]);
$r[0]->extra->initCache();
return $r[0];
}
if (!isset($change->full_message)) {
@ -154,9 +160,16 @@ class IDF_Commit extends Pluf_Model
$commit->summary = self::toUTF8($change->title);
$commit->fullmessage = self::toUTF8($change->full_message);
$commit->author = $scm->findAuthor($change->author);
$commit->origauthor = $change->author;
$commit->origauthor = self::toUTF8($change->author);
$commit->creation_dtime = $change->date;
$commit->create();
$extra = $scm->getExtraProperties($change);
$commit->extra = new IDF_Gconf();
$commit->extra->serialize = true; // As we can store arrays
$commit->extra->setModel($commit);
foreach ($extra as $key => $val) {
$commit->extra->setVal($key, $val);
}
$commit->notify($project->getConf());
return $commit;
}
@ -200,13 +213,13 @@ class IDF_Commit extends Pluf_Model
* Returns the timeline fragment for the commit.
*
*
* @param Pluf_HTTP_Request
* @param Pluf_HTTP_Request
* @return Pluf_Template_SafeString
*/
public function timelineFragment($request)
{
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::commit',
array($request->project->shortname,
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::commit',
array($request->project->shortname,
$this->scm_id));
$out = '<tr class="log"><td><a href="'.$url.'">'.
Pluf_esc(Pluf_Template_dateAgo($this->creation_dtime, 'without')).
@ -222,24 +235,24 @@ class IDF_Commit extends Pluf_Model
</tr>
<tr class="extra">
<td colspan="2">
<div class="helptext right">'.sprintf(__('Commit&nbsp;%s, by %s'), '<a href="'.$url.'" class="mono">'.$this->scm_id.'</a>', $user).'</div></td></tr>';
<div class="helptext right">'.sprintf(__('Commit&nbsp;%s, by %s'), '<a href="'.$url.'" class="mono">'.$this->scm_id.'</a>', $user).'</div></td></tr>';
return Pluf_Template::markSafe($out);
}
/**
* Returns the feed fragment for the commit.
*
* @param Pluf_HTTP_Request
* @param Pluf_HTTP_Request
* @return Pluf_Template_SafeString
*/
public function feedFragment($request)
{
$url = Pluf::f('url_base')
.Pluf_HTTP_URL_urlForView('IDF_Views_Source::commit',
array($request->project->shortname,
.Pluf_HTTP_URL_urlForView('IDF_Views_Source::commit',
array($request->project->shortname,
$this->scm_id));
$date = Pluf_Date::gmDateToGmString($this->creation_dtime);
$author = ($this->get_author()) ?
$author = ($this->get_author()) ?
$this->get_author() : $this->origauthor;
$cproject = $this->get_project();
$context = new Pluf_Template_Context_Request(
@ -264,12 +277,41 @@ class IDF_Commit extends Pluf_Model
*/
public function notify($conf, $create=true)
{
// Now we add to the queue, soon we will push everything in
// the queue, including email notifications and indexing.
// Even if the url is empty, we add to the queue as some
// plugins may want to do something with this information in
// an asynchronous way.
$project = $this->get_project();
$scm = $project->getConf()->getVal('scm', 'git');
$url = str_replace(array('%p', '%r'),
array($project->shortname, $this->scm_id),
$conf->getVal('webhook_url', ''));
$payload = array('to_send' => array(
'project' => $project->shortname,
'rev' => $this->scm_id,
'scm' => $scm,
'summary' => $this->summary,
'fullmessage' => $this->fullmessage,
'author' => $this->origauthor,
'creation_date' => $this->creation_dtime,
),
'project_id' => $project->id,
'authkey' => $project->getPostCommitHookKey(),
'url' => $url,
);
$item = new IDF_Queue();
$item->type = 'new_commit';
$item->payload = $payload;
$item->create();
if ('' == $conf->getVal('source_notification_email', '')) {
return;
}
$current_locale = Pluf_Translation::getLocale();
$langs = Pluf::f('languages', array('en'));
Pluf_Translation::loadSetLocale($langs[0]);
Pluf_Translation::loadSetLocale($langs[0]);
$context = new Pluf_Template_Context(
array(
@ -280,13 +322,16 @@ class IDF_Commit extends Pluf_Model
);
$tmpl = new Pluf_Template('idf/source/commit-created-email.txt');
$text_email = $tmpl->render($context);
$email = new Pluf_Mail(Pluf::f('from_email'),
$conf->getVal('source_notification_email'),
sprintf(__('New Commit %s - %s (%s)'),
$this->scm_id, $this->summary,
$this->get_project()->shortname));
$email->addTextMessage($text_email);
$email->sendMail();
$addresses = explode(',', $conf->getVal('source_notification_email'));
foreach ($addresses as $address) {
$email = new Pluf_Mail(Pluf::f('from_email'),
$address,
sprintf(__('New Commit %s - %s (%s)'),
$this->scm_id, $this->summary,
$this->get_project()->shortname));
$email->addTextMessage($text_email);
$email->sendMail();
}
Pluf_Translation::loadSetLocale($current_locale);
}
}

View File

@ -51,7 +51,7 @@ class IDF_Diff
$i = 0; // Used to skip the end of a git patch with --\nversion number
foreach ($this->lines as $line) {
$i++;
if (0 === strpos($line, '--') and isset($this->lines[$i])
if (0 === strpos($line, '--') and isset($this->lines[$i])
and preg_match('/^\d+\.\d+\.\d+\.\d+$/', $this->lines[$i])) {
break;
}
@ -71,6 +71,28 @@ class IDF_Diff
$current_chunk = 0;
$indiff = true;
continue;
} else if (!$indiff && 0 === strpos($line, '=========')) {
// ignore pseudo stanzas with a hint of a binary file
if (preg_match("/^# (.+) is binary/", $this->lines[$i]))
continue;
// by default always use the new name of a possibly renamed file
$current_file = self::getMtnFile($this->lines[$i+1]);
// mtn 0.48 and newer set /dev/null as file path for dropped files
// so we display the old name here
if ($current_file == "/dev/null") {
$current_file = self::getMtnFile($this->lines[$i]);
}
if ($current_file == "/dev/null") {
throw new Exception(
"could not determine path from diff"
);
}
$files[$current_file] = array();
$files[$current_file]['chunks'] = array();
$files[$current_file]['chunks_def'] = array();
$current_chunk = 0;
$indiff = true;
continue;
} else if (0 === strpos($line, 'Index: ')) {
$current_file = self::getSvnFile($line);
$files[$current_file] = array();
@ -133,6 +155,12 @@ class IDF_Diff
return substr(trim($line), 7);
}
public static function getMtnFile($line)
{
preg_match("/^[+-]{3} ([^\t]+)/", $line, $m);
return $m[1];
}
/**
* Return the html version of a parsed diff.
*/
@ -141,8 +169,8 @@ class IDF_Diff
$out = '';
foreach ($this->files as $filename=>$file) {
$pretty = '';
$fileinfo = IDF_Views_Source::getMimeType($filename);
if (IDF_Views_Source::isSupportedExtension($fileinfo[2])) {
$fileinfo = IDF_FileUtil::getMimeType($filename);
if (IDF_FileUtil::isSupportedExtension($fileinfo[2])) {
$pretty = ' prettyprint';
}
$out .= "\n".'<table class="diff" summary="">'."\n";
@ -215,14 +243,14 @@ class IDF_Diff
* @param int Number of lines before/after the chunk to be displayed (10)
* @return Pluf_Template_SafeString The table body
*/
public function fileCompare($orig, $chunks, $filename, $context=10)
public function fileCompare($orig, $chunks, $filename, $context=10)
{
$orig_lines = preg_split("/\015\012|\015|\012/", $orig);
$new_chunks = $this->mergeChunks($orig_lines, $chunks, $context);
return $this->renderCompared($new_chunks, $filename);
}
public function mergeChunks($orig_lines, $chunks, $context=10)
public function mergeChunks($orig_lines, $chunks, $context=10)
{
$spans = array();
$new_chunks = array();
@ -250,7 +278,7 @@ class IDF_Diff
for ($lc=$spans[$i][0];$lc<$chunk[0][0];$lc++) {
$exists = false;
foreach ($chunk_lines as $line) {
if ($lc == $line[0]
if ($lc == $line[0]
or ($chunk[0][1]-$chunk[0][0]+$lc) == $line[1]) {
$exists = true;
break;
@ -259,7 +287,7 @@ class IDF_Diff
if (!$exists) {
$orig = isset($orig_lines[$lc-1]) ? $orig_lines[$lc-1] : '';
$n_chunk[] = array(
$lc,
$lc,
$chunk[0][1]-$chunk[0][0]+$lc,
$orig
);
@ -283,7 +311,7 @@ class IDF_Diff
}
if (!$exists) {
$n_chunk[] = array(
$lc,
$lc,
$lline[1]-$lline[0]+$lc,
$orig_lines[$lc-1]
);
@ -305,7 +333,7 @@ class IDF_Diff
foreach ($chunk as $line) {
if ($line[0] > $lline[0] or empty($line[0])) {
$nnew_chunks[$i-1][] = $line;
}
}
}
} else {
$nnew_chunks[] = $chunk;
@ -322,9 +350,9 @@ class IDF_Diff
public function renderCompared($chunks, $filename)
{
$fileinfo = IDF_Views_Source::getMimeType($filename);
$fileinfo = IDF_FileUtil::getMimeType($filename);
$pretty = '';
if (IDF_Views_Source::isSupportedExtension($fileinfo[2])) {
if (IDF_FileUtil::isSupportedExtension($fileinfo[2])) {
$pretty = ' prettyprint';
}
$out = '';

165
src/IDF/FileUtil.php Normal file
View File

@ -0,0 +1,165 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* File utilities.
*
*/
class IDF_FileUtil
{
/**
* Extension supported by the syntax highlighter.
*/
public static $supportedExtenstions = array(
'ascx', 'ashx', 'asmx', 'aspx', 'browser', 'bsh', 'c', 'cl', 'cc',
'config', 'cpp', 'cs', 'csh', 'csproj', 'css', 'cv', 'cyc', 'el', 'fs',
'h', 'hh', 'hpp', 'hs', 'html', 'html', 'java', 'js', 'lisp', 'master',
'pas', 'perl', 'php', 'pl', 'pm', 'py', 'rb', 'scm', 'sh', 'sitemap',
'skin', 'sln', 'svc', 'vala', 'vb', 'vbproj', 'vbs', 'wsdl', 'xhtml',
'xml', 'xsd', 'xsl', 'xslt');
/**
* Test if an extension is supported by the syntax highlighter.
*
* @param string The extension to test
* @return bool
*/
public static function isSupportedExtension($extension)
{
return in_array($extension, self::$supportedExtenstions);
}
/**
* Returns a HTML snippet with a line-by-line pre-rendered table
* for the given source content
*
* @param array file information as returned by getMimeType or getMimeTypeFromContent
* @param string the content of the file
* @return string
*/
public static function highLight($fileinfo, $content)
{
$pretty = '';
if (self::isSupportedExtension($fileinfo[2])) {
$pretty = ' prettyprint';
}
$table = array();
$i = 1;
foreach (preg_split("/\015\012|\015|\012/", $content) as $line) {
$table[] = '<tr class="c-line"><td class="code-lc" id="L'.$i.'"><a href="#L'.$i.'">'.$i.'</a></td>'
.'<td class="code mono'.$pretty.'">'.IDF_Diff::padLine(Pluf_esc($line)).'</td></tr>';
$i++;
}
return Pluf_Template::markSafe(implode("\n", $table));
}
/**
* Find the mime type of a file.
*
* Use /etc/mime.types to find the type.
*
* @param string Filename/Filepath
* @param array Mime type found or 'application/octet-stream', basename, extension
*/
public static function getMimeType($file)
{
static $mimes = null;
if ($mimes == null) {
$mimes = array();
$src = Pluf::f('idf_mimetypes_db', '/etc/mime.types');
$filecontent = @file_get_contents($src);
if ($filecontent !== false) {
$mimes = preg_split("/\015\012|\015|\012/", $filecontent);
}
}
$info = pathinfo($file);
if (isset($info['extension'])) {
foreach ($mimes as $mime) {
if ('#' != substr($mime, 0, 1)) {
$elts = preg_split('/ |\t/', $mime, -1, PREG_SPLIT_NO_EMPTY);
if (in_array($info['extension'], $elts)) {
return array($elts[0], $info['basename'], $info['extension']);
}
}
}
} else {
// we consider that if no extension and base name is all
// uppercase, then we have a text file.
if ($info['basename'] == strtoupper($info['basename'])) {
return array('text/plain', $info['basename'], 'txt');
}
$info['extension'] = 'bin';
}
return array('application/octet-stream', $info['basename'], $info['extension']);
}
/**
* Find the mime type of a file using the fileinfo class.
*
* @param string Filename/Filepath
* @param string File content
* @return array Mime type found or 'application/octet-stream', basename, extension
*/
public static function getMimeTypeFromContent($file, $filedata)
{
$info = pathinfo($file);
$res = array('application/octet-stream',
$info['basename'],
isset($info['extension']) ? $info['extension'] : 'bin');
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME);
$mime = finfo_buffer($finfo, $filedata);
finfo_close($finfo);
if ($mime) {
$res[0] = $mime;
}
if (!isset($info['extension']) && $mime) {
$res[2] = (0 === strpos($mime, 'text/')) ? 'txt' : 'bin';
} elseif (!isset($info['extension'])) {
$res[2] = 'bin';
}
}
return $res;
}
/**
* Find if a given mime type is a text file.
* This uses the output of the self::getMimeType function.
*
* @param array (Mime type, file name, extension)
* @return bool Is text
*/
public static function isText($fileinfo)
{
if (0 === strpos($fileinfo[0], 'text/')) {
return true;
}
$ext = 'mdtext php-dist h gitignore diff patch';
$extra_ext = trim(Pluf::f('idf_extra_text_ext', ''));
if (!empty($extra_ext))
$ext .= ' ' . $extra_ext;
$ext = array_merge(self::$supportedExtenstions, explode(' ' , $ext));
return (in_array($fileinfo[2], $ext));
}
}

View File

@ -38,6 +38,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
'git' => __('git'),
'svn' => __('Subversion'),
'mercurial' => __('mercurial'),
'mtn' => __('monotone'),
);
foreach (Pluf::f('allowed_scm', array()) as $key => $class) {
$choices[$options[$key]] = $key;
@ -63,6 +64,14 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
'help_text' => __('It must be unique for each project and composed only of letters, digits and dash (-) like "my-project".'),
));
$this->fields['shortdesc'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Short description'),
'help_text' => __('A one line description of the project.'),
'initial' => '',
'widget_attrs' => array('size' => '35'),
));
$this->fields['scm'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Repository type'),
@ -92,6 +101,14 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
'widget' => 'Pluf_Form_Widget_PasswordInput',
));
$this->fields['mtn_master_branch'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Master branch'),
'initial' => '',
'widget_attrs' => array('size' => '35'),
'help_text' => __('This should be a world-wide unique identifier for your project. A reverse DNS notation like "com.my-domain.my-project" is a good idea.'),
));
$this->fields['owners'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Project owners'),
@ -109,6 +126,20 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
'cols' => 40),
'widget' => 'Pluf_Form_Widget_TextareaInput',
));
$projects = array('--' => '--');
foreach (Pluf::factory('IDF_Project')->getList(array('order' => 'name ASC')) as $proj) {
$projects[$proj->name] = $proj->shortname;
}
$this->fields['template'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Project template'),
'initial' => '--',
'help_text' => __('Use the given project to initialize the new project. Access rights and general configuration will be taken from the template project.'),
'widget' => 'Pluf_Form_Widget_SelectInput',
'widget_attrs' => array('choices' => $projects),
));
/**
* [signal]
*
@ -156,6 +187,34 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
return $url;
}
public function clean_mtn_master_branch()
{
// do not validate, but empty the field if a different
// SCM should be used
if ($this->cleaned_data['scm'] != 'mtn')
return '';
$mtn_master_branch = mb_strtolower($this->cleaned_data['mtn_master_branch']);
if (!preg_match('/^([\w\d]+([-][\w\d]+)*)(\.[\w\d]+([-][\w\d]+)*)*$/',
$mtn_master_branch)) {
throw new Pluf_Form_Invalid(__(
'The master branch is empty or contains illegal characters, '.
'please use only letters, digits, dashs and dots as separators.'
));
}
$sql = new Pluf_SQL('vkey=%s AND vdesc=%s',
array('mtn_master_branch', $mtn_master_branch));
$l = Pluf::factory('IDF_Conf')->getList(array('filter'=>$sql->gen()));
if ($l->count() > 0) {
throw new Pluf_Form_Invalid(__(
'This master branch is already used. Please select another one.'
));
}
return $mtn_master_branch;
}
public function clean_shortname()
{
$shortname = mb_strtolower($this->cleaned_data['shortname']);
@ -184,6 +243,11 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
$this->cleaned_data[$key] = '';
}
}
if ($this->cleaned_data['scm'] != 'mtn') {
$this->cleaned_data['mtn_master_branch'] = '';
}
/**
* [signal]
*
@ -217,24 +281,88 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form
$project = new IDF_Project();
$project->name = $this->cleaned_data['name'];
$project->shortname = $this->cleaned_data['shortname'];
$project->private = $this->cleaned_data['private_project'];
$project->description = __('Click on the Project Management tab to set the description of your project.');
$project->shortdesc = $this->cleaned_data['shortdesc'];
if ($this->cleaned_data['template'] != '--') {
// Find the template project
$sql = new Pluf_SQL('shortname=%s',
array($this->cleaned_data['template']));
$tmpl = Pluf::factory('IDF_Project')->getOne(array('filter' => $sql->gen()));
$project->private = $tmpl->private;
$project->description = $tmpl->description;
} else {
$project->private = $this->cleaned_data['private_project'];
$project->description = __('Click on the Project Management tab to set the description of your project.');
}
$project->create();
$conf = new IDF_Conf();
$conf->setProject($project);
$keys = array('scm', 'svn_remote_url',
'svn_username', 'svn_password');
$keys = array('scm', 'svn_remote_url', 'svn_username',
'svn_password', 'mtn_master_branch');
foreach ($keys as $key) {
$this->cleaned_data[$key] = (!empty($this->cleaned_data[$key])) ?
$this->cleaned_data[$key] = (!empty($this->cleaned_data[$key])) ?
$this->cleaned_data[$key] : '';
$conf->setVal($key, $this->cleaned_data[$key]);
}
if ($this->cleaned_data['template'] != '--') {
$tmplconf = new IDF_Conf();
$tmplconf->setProject($tmpl);
// We need to get all the configuration variables we want from
// the old project and put them into the new project.
$props = array(
'labels_download_predefined' => IDF_Form_UploadConf::init_predefined,
'labels_download_one_max' => IDF_Form_UploadConf::init_one_max,
'labels_wiki_predefined' => IDF_Form_WikiConf::init_predefined,
'labels_wiki_one_max' => IDF_Form_WikiConf::init_one_max,
'labels_issue_template' => IDF_Form_IssueTrackingConf::init_template,
'labels_issue_open' => IDF_Form_IssueTrackingConf::init_open,
'labels_issue_closed' => IDF_Form_IssueTrackingConf::init_closed,
'labels_issue_predefined' => IDF_Form_IssueTrackingConf::init_predefined,
'labels_issue_one_max' => IDF_Form_IssueTrackingConf::init_one_max,
'webhook_url' => '',
'downloads_access_rights' => 'all',
'review_access_rights' => 'all',
'wiki_access_rights' => 'all',
'source_access_rights' => 'all',
'issues_access_rights' => 'all',
'downloads_notification_email' => '',
'review_notification_email' => '',
'wiki_notification_email' => '',
'source_notification_email' => '',
'issues_notification_email' => '',
);
foreach ($props as $prop => $def) {
$conf->setVal($prop, $tmplconf->getVal($prop, $def));
}
}
$project->created();
IDF_Form_MembersConf::updateMemberships($project,
$this->cleaned_data);
if ($this->cleaned_data['template'] == '--') {
IDF_Form_MembersConf::updateMemberships($project,
$this->cleaned_data);
} else {
// Get the membership of the template $tmpl
IDF_Form_MembersConf::updateMemberships($project,
$tmpl->getMembershipData('string'));
}
$project->membershipsUpdated();
return $project;
}
/**
* Check that the template project exists.
*/
public function clean_template()
{
if ($this->cleaned_data['template'] == '--') {
return $this->cleaned_data['template'];
}
$sql = new Pluf_SQL('shortname=%s', array($this->cleaned_data['template']));
if (Pluf::factory('IDF_Project')->getOne(array('filter' => $sql->gen())) == null) {
throw new Pluf_Form_Invalid(__('This project is not available.'));
}
return $this->cleaned_data['template'];
}
}

View File

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

View File

@ -77,22 +77,20 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form
'initial' => '',
'widget' => 'Pluf_Form_Widget_SelectInput',
'widget_attrs' => array(
'choices' =>
'choices' =>
Pluf_L10n::getInstalledLanguages()
),
));
$this->fields['ssh_key'] = new Pluf_Form_Field_Varchar(
$this->fields['public_key'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Add a public SSH key'),
'label' => __('Add a public key'),
'initial' => '',
'widget_attrs' => array('rows' => 3,
'cols' => 40),
'widget' => 'Pluf_Form_Widget_TextareaInput',
'help_text' => __('Be careful to provide the public key and not the private key!')
'help_text' => __('Paste a SSH or monotone public key. Be careful to not provide your private key here!')
));
}
/**
@ -137,11 +135,11 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form
$params = array('user' => $user);
Pluf_Signal::send('Pluf_User::passwordUpdated',
'IDF_Form_Admin_UserCreate', $params);
// Create the ssh key as needed
if ('' !== $this->cleaned_data['ssh_key']) {
// Create the public key as needed
if ('' !== $this->cleaned_data['public_key']) {
$key = new IDF_Key();
$key->user = $user;
$key->content = $this->cleaned_data['ssh_key'];
$key->content = $this->cleaned_data['public_key'];
$key->create();
}
// Send an email to the user with the password
@ -162,16 +160,11 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form
return $user;
}
function clean_ssh_key()
{
return IDF_Form_UserAccount::checkSshKey($this->cleaned_data['ssh_key']);
}
function clean_last_name()
{
$last_name = trim($this->cleaned_data['last_name']);
if ($last_name == mb_strtoupper($last_name)) {
return mb_convert_case(mb_strtolower($last_name),
return mb_convert_case(mb_strtolower($last_name),
MB_CASE_TITLE, 'UTF-8');
}
return $last_name;
@ -181,7 +174,7 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form
{
$first_name = trim($this->cleaned_data['first_name']);
if ($first_name == mb_strtoupper($first_name)) {
return mb_convert_case(mb_strtolower($first_name),
return mb_convert_case(mb_strtolower($first_name),
MB_CASE_TITLE, 'UTF-8');
}
return $first_name;
@ -211,4 +204,12 @@ class IDF_Form_Admin_UserCreate extends Pluf_Form
}
return $this->cleaned_data['login'];
}
public function clean_public_key()
{
$this->cleaned_data['public_key'] =
IDF_Form_UserAccount::checkPublicKey($this->cleaned_data['public_key']);
return $this->cleaned_data['public_key'];
}
}

View File

@ -24,13 +24,15 @@
/**
* Update user's details.
*/
class IDF_Form_Admin_UserUpdate extends Pluf_Form
class IDF_Form_Admin_UserUpdate extends Pluf_Form
{
public $user = null;
public function initFields($extra=array())
{
$this->user = $extra['user'];
$user_data = IDF_UserData::factory($this->user);
$this->fields['first_name'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('First name'),
@ -66,7 +68,7 @@ class IDF_Form_Admin_UserUpdate extends Pluf_Form
'initial' => $this->user->language,
'widget' => 'Pluf_Form_Widget_SelectInput',
'widget_attrs' => array(
'choices' =>
'choices' =>
Pluf_L10n::getInstalledLanguages()
),
));
@ -93,6 +95,66 @@ class IDF_Form_Admin_UserUpdate extends Pluf_Form
),
));
$this->fields['description'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Description'),
'initial' => $user_data->description,
'widget_attrs' => array('rows' => 3,
'cols' => 40),
'widget' => 'Pluf_Form_Widget_TextareaInput',
));
$this->fields['twitter'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Twitter username'),
'initial' => $user_data->twitter,
'widget_attrs' => array(
'maxlength' => 50,
'size' => 15,
),
));
$this->fields['public_email'] = new Pluf_Form_Field_Email(
array('required' => false,
'label' => __('Public email address'),
'initial' => $user_data->public_email,
'widget_attrs' => array(
'maxlength' => 50,
'size' => 15,
),
));
$this->fields['website'] = new Pluf_Form_Field_Url(
array('required' => false,
'label' => __('Website URL'),
'initial' => $user_data->website,
'widget_attrs' => array(
'maxlength' => 50,
'size' => 15,
),
));
$this->fields['custom_avatar'] = new Pluf_Form_Field_File(
array('required' => false,
'label' => __('Upload custom avatar'),
'initial' => '',
'max_size' => Pluf::f('max_upload_size', 2097152),
'move_function_params' => array('upload_path' => Pluf::f('upload_path').'/avatars',
'upload_path_create' => true,
'upload_overwrite' => true,
'file_name' => 'user_'.$this->user->id.'_%s'),
'help_text' => __('An image file with a width and height not larger than 60 pixels (bigger images are scaled down).'),
));
$this->fields['remove_custom_avatar'] = new Pluf_Form_Field_Boolean(
array('required' => false,
'label' => __('Remove custom avatar'),
'initial' => false,
'widget' => 'Pluf_Form_Widget_CheckboxInput',
'widget_attrs' => array(),
'help_text' => __('Tick this to delete the custom avatar.'),
));
if ($extra['request']->user->administrator) {
$this->fields['staff'] = new Pluf_Form_Field_Boolean(
array('required' => false,
@ -136,8 +198,37 @@ class IDF_Form_Admin_UserUpdate extends Pluf_Form
$update_pass = true;
}
$this->user->setFromFormData($this->cleaned_data);
if ($commit) {
$this->user->update();
// FIXME: go the extra mile and check the input lengths for
// all fields here!
// FIXME: this is all doubled in UserAccount!
$user_data = IDF_UserData::factory($this->user);
// Add or remove avatar - we need to do this here because every
// single setter directly leads to a save in the database
if ($user_data->avatar != '' &&
($this->cleaned_data['remove_custom_avatar'] == 1 ||
$this->cleaned_data['custom_avatar'] != '')) {
$avatar_path = Pluf::f('upload_path').'/avatars/'.basename($user_data->avatar);
if (basename($avatar_path) != '' && is_file($avatar_path)) {
unlink($avatar_path);
}
$user_data->avatar = '';
}
if ($this->cleaned_data['custom_avatar'] != '') {
$user_data->avatar = $this->cleaned_data['custom_avatar'];
}
$user_data->description = $this->cleaned_data['description'];
$user_data->twitter = $this->cleaned_data['twitter'];
$user_data->public_email = $this->cleaned_data['public_email'];
$user_data->website = $this->cleaned_data['website'];
if ($update_pass) {
/**
* [signal]
@ -170,7 +261,7 @@ class IDF_Form_Admin_UserUpdate extends Pluf_Form
{
$last_name = trim($this->cleaned_data['last_name']);
if ($last_name == mb_strtoupper($last_name)) {
return mb_convert_case(mb_strtolower($last_name),
return mb_convert_case(mb_strtolower($last_name),
MB_CASE_TITLE, 'UTF-8');
}
return $last_name;
@ -183,7 +274,7 @@ class IDF_Form_Admin_UserUpdate extends Pluf_Form
throw new Pluf_Form_Invalid(__('--- is not a valid first name.'));
}
if ($first_name == mb_strtoupper($first_name)) {
$first_name = mb_convert_case(mb_strtolower($first_name),
$first_name = mb_convert_case(mb_strtolower($first_name),
MB_CASE_TITLE, 'UTF-8');
}
return $first_name;
@ -201,12 +292,23 @@ class IDF_Form_Admin_UserUpdate extends Pluf_Form
return $email;
}
function clean_custom_avatar()
{
// Just png, jpeg/jpg or gif
if (!preg_match('/\.(png|jpg|jpeg|gif)$/i', $this->cleaned_data['custom_avatar']) &&
$this->cleaned_data['custom_avatar'] != '') {
@unlink(Pluf::f('upload_path').'/avatars/'.$this->cleaned_data['custom_avatar']);
throw new Pluf_Form_Invalid(__('For security reason, you cannot upload a file with this extension.'));
}
return $this->cleaned_data['custom_avatar'];
}
/**
* Check to see if the 2 passwords are the same.
* Check to see if the two passwords are the same.
*/
public function clean()
{
if (!isset($this->errors['password'])
if (!isset($this->errors['password'])
&& !isset($this->errors['password2'])) {
$password1 = $this->cleaned_data['password'];
$password2 = $this->cleaned_data['password2'];

View File

@ -0,0 +1,51 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# Plume Framework is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Plume Framework is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Similar to Pluf_Form_Field_Email, this form field validates one or more
* email addresses separated by a comma
*/
class IDF_Form_Field_EmailList extends Pluf_Form_Field
{
public $widget = 'Pluf_Form_Widget_TextInput';
public function clean($value)
{
parent::clean($value);
if (in_array($value, $this->empty_values)) {
$value = '';
}
if ($value == '') {
return $value;
}
$emails = preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
foreach ($emails as $email) {
if (!Pluf_Utils::isValidEmail($email)) {
throw new Pluf_Form_Invalid(__(
'Please enter one or more valid email addresses.'
));
}
}
return implode(',', $emails);
}
}

View File

@ -45,6 +45,9 @@ class IDF_Form_IssueCreate extends Pluf_Form
or $this->user->hasPerm('IDF.project-member', $this->project)) {
$this->show_full = true;
}
$contentTemplate = $this->project->getConf()->getVal(
'labels_issue_template', IDF_Form_IssueTrackingConf::init_template
);
$this->fields['summary'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Summary'),
@ -57,7 +60,7 @@ class IDF_Form_IssueCreate extends Pluf_Form
$this->fields['content'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Description'),
'initial' => '',
'initial' => $contentTemplate,
'widget' => 'Pluf_Form_Widget_TextareaInput',
'widget_attrs' => array(
'cols' => 58,
@ -105,14 +108,41 @@ class IDF_Form_IssueCreate extends Pluf_Form
'size' => 15,
),
));
/*
* get predefined tags for issues from current project
*
* first Type:<...> and Priority:<...> will be used
*
*/
$predefined = preg_split("/[\r\n]+/", $extra['project']->getConf()->getVal(
'labels_issue_predefined'
));
$predefined_type = 'Type:Defect';
foreach ($predefined as $tag) {
if (strpos($tag, 'Type:') === 0) {
$predefined_type = explode('=', $tag, 2);
$predefined_type = trim($predefined_type[0]);
break;
}
}
$predefined_priority = 'Priority:Medium';
foreach ($predefined as $tag) {
if (strpos($tag, 'Priority:') === 0) {
$predefined_priority = explode('=', $tag, 2);
$predefined_priority = trim($predefined_priority[0]);
break;
}
}
for ($i=1;$i<7;$i++) {
$initial = '';
switch ($i) {
case 1:
$initial = 'Type:Defect';
$initial = $predefined_type;
break;
case 2:
$initial = 'Priority:Medium';
$initial = $predefined_priority;
break;
}
$this->fields['label'.$i] = new Pluf_Form_Field_Varchar(
@ -276,6 +306,7 @@ class IDF_Form_IssueCreate extends Pluf_Form
$comment->create();
// If we have a file, create the IDF_IssueFile and attach
// it to the comment.
$created_files = array();
for ($i=1;$i<4;$i++) {
if ($this->cleaned_data['attachment'.$i]) {
$file = new IDF_IssueFile();
@ -283,8 +314,36 @@ class IDF_Form_IssueCreate extends Pluf_Form
$file->submitter = $this->user;
$file->comment = $comment;
$file->create();
$created_files[] = $file;
}
}
/**
* [signal]
*
* IDF_Issue::create
*
* [sender]
*
* IDF_Form_IssueCreate
*
* [description]
*
* This signal allows an application to perform a set of tasks
* just after the creation of an issue. The comment contains
* the description of the issue.
*
* [parameters]
*
* array('issue' => $issue,
* 'comment' => $comment,
* 'files' => $attached_files);
*
*/
$params = array('issue' => $issue,
'comment' => $comment,
'files' => $created_files);
Pluf_Signal::send('IDF_Issue::create', 'IDF_Form_IssueCreate',
$params);
return $issue;
}

View File

@ -31,6 +31,15 @@ class IDF_Form_IssueTrackingConf extends Pluf_Form
* Defined as constants to easily access the value in the
* IssueUpdate/Create form in the case nothing is in the db yet.
*/
const init_template = 'Steps to reproduce the problem:
1.
2.
3.
Expected result:
Actual result:
';
const init_open = 'New = Issue has not had initial review yet
Accepted = Problem reproduced / Need acknowledged
Started = Work on this issue has begun';
@ -66,6 +75,15 @@ Maintainability = Hinders future changes';
public function initFields($extra=array())
{
$this->fields['labels_issue_template'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Define an issue template to hint the reporter to provide certain information'),
'initial' => self::init_template,
'widget_attrs' => array('rows' => 7,
'cols' => 75),
'widget' => 'Pluf_Form_Widget_TextareaInput',
));
$this->fields['labels_issue_open'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Open issue status values'),
@ -87,6 +105,7 @@ Maintainability = Hinders future changes';
array('required' => true,
'label' => __('Predefined issue labels'),
'initial' => self::init_predefined,
'help_text' => __('The first "Type:" and "Priority:" entries found in this list are automatically chosen as defaults for new issues.'),
'widget_attrs' => array('rows' => 7,
'cols' => 75),
'widget' => 'Pluf_Form_Widget_TextareaInput',
@ -99,8 +118,6 @@ Maintainability = Hinders future changes';
'widget_attrs' => array('size' => 60),
));
}
}

View File

@ -305,6 +305,7 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate
$this->issue->submitter != $this->user->id) {
$this->issue->setAssoc($this->user); // interested user.
}
$attached_files = array();
for ($i=1;$i<4;$i++) {
if ($this->cleaned_data['attachment'.$i]) {
$file = new IDF_IssueFile();
@ -312,8 +313,36 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate
$file->submitter = $this->user;
$file->comment = $comment;
$file->create();
$attached_files[] = $file;
}
}
/**
* [signal]
*
* IDF_Issue::update
*
* [sender]
*
* IDF_Form_IssueUpdate
*
* [description]
*
* This signal allows an application to perform a set of tasks
* just after the update of an issue.
*
* [parameters]
*
* array('issue' => $issue,
* 'comment' => $comment,
* 'files' => $attached_files);
*
*/
$params = array('issue' => $this->issue,
'comment' => $comment,
'files' => $attached_files);
Pluf_Signal::send('IDF_Issue::update', 'IDF_Form_IssueUpdate',
$params);
return $this->issue;
}
}

View File

@ -101,7 +101,7 @@ class IDF_Form_MembersConf extends Pluf_Form
$n = count($bad);
if ($n) {
$badlogins = Pluf_esc(implode(', ', $bad));
throw new Pluf_Form_Invalid(sprintf(_n('The following login is invalid: %s.', 'The following login are invalids: %s.', $n), $badlogins));
throw new Pluf_Form_Invalid(sprintf(_n('The following login is invalid: %s.', 'The following logins are invalid: %s.', $n), $badlogins));
}
return $logins;
}

View File

@ -205,6 +205,30 @@ class IDF_Form_ReviewCreate extends Pluf_Form
$patch->patch = $this->cleaned_data['patch'];
$patch->create();
$patch->notify($this->project->getConf());
/**
* [signal]
*
* IDF_Review::create
*
* [sender]
*
* IDF_Form_ReviewCreate
*
* [description]
*
* This signal allows an application to perform a set of tasks
* just after the creation of a review and the notification.
*
* [parameters]
*
* array('review' => $review,
* 'patch' => $patch);
*
*/
$params = array('review' => $review,
'patch' => $patch);
Pluf_Signal::send('IDF_Review::create', 'IDF_Form_ReviewCreate',
$params);
return $review;
}

View File

@ -52,7 +52,7 @@ class IDF_Form_ReviewFileComment extends Pluf_Form
));
}
$this->fields['content'] = new Pluf_Form_Field_Varchar(
array('required' => true,
array('required' => false,
'label' => __('General comment'),
'initial' => '',
'widget' => 'Pluf_Form_Widget_TextareaInput',
@ -94,24 +94,40 @@ class IDF_Form_ReviewFileComment extends Pluf_Form
*/
public function clean()
{
foreach ($this->files as $filename => $def) {
if (!empty($this->cleaned_data[md5($filename)])) {
return $this->cleaned_data;
$isOk = false;
foreach($this->files as $filename => $def) {
$this->cleaned_data[md5($filename)] = trim($this->cleaned_data[md5($filename)]);
if(!empty($this->cleaned_data[md5($filename)])) {
$isOk = true;
}
}
throw new Pluf_Form_Invalid(__('You need to provide comments on at least one file.'));
if(!empty($this->cleaned_data['content'])) {
$isOk = true;
}
if (!$isOk) {
throw new Pluf_Form_Invalid(__('You need to provide your general comment about the proposal, or comments on at least one file.'));
}
return $this->cleaned_data;
}
function clean_content()
{
$content = trim($this->cleaned_data['content']);
if (!$this->show_full and strlen($content) == 0) {
throw new Pluf_Form_Invalid(__('You need to provide your general comment about the proposal.'));
if(empty($content)) {
if ($this->fields['status']->initial != $this->fields['status']->value) {
return __('The status have been updated.');
}
} else {
return $content;
}
return $content;
throw new Pluf_Form_Invalid(__('This field is required.'));
}
/**
* Save the model in the database.
*

View File

@ -34,7 +34,7 @@ class IDF_Form_SourceConf extends Pluf_Form
public function initFields($extra=array())
{
$this->conf = $extra['conf'];
if ($this->conf->getVal('scm', 'git') == 'svn') {
if ($extra['remote_svn']) {
$this->fields['svn_username'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Repository username'),
@ -49,6 +49,16 @@ class IDF_Form_SourceConf extends Pluf_Form
'widget' => 'Pluf_Form_Widget_PasswordInput',
));
}
Pluf::loadFunction('Pluf_HTTP_URL_urlForView');
$url = Pluf_HTTP_URL_urlForView('idf_faq').'#webhooks';
$this->fields['webhook_url'] = new Pluf_Form_Field_Url(
array('required' => false,
'label' => __('Webhook URL'),
'initial' => $this->conf->getVal('webhook_url', ''),
'help_text' => sprintf(__('Learn more about the <a href="%s">post-commit web hooks</a>.'), $url),
'widget_attrs' => array('size' => 35),
));
}
}

View File

@ -45,7 +45,7 @@ class IDF_Form_TabsConf extends Pluf_Form
array('required' => true,
'label' => $label,
'initial' => $this->conf->getVal($key, 'all'),
'widget_attrs' => array('choices' =>
'widget_attrs' => array('choices' =>
array(
__('Open to all') => 'all',
__('Signed in users') => 'login',
@ -63,10 +63,11 @@ class IDF_Form_TabsConf extends Pluf_Form
'source_notification_email',
'issues_notification_email',);
foreach ($ak as $key) {
$this->fields[$key] = new Pluf_Form_Field_Email(
$this->fields[$key] = new IDF_Form_Field_EmailList(
array('required' => false,
'label' => $key,
'initial' => $this->conf->getVal($key, ''),
'widget_attrs' => array('size' => 40),
));
}

View File

@ -146,6 +146,28 @@ class IDF_Form_UpdateUpload extends Pluf_Form
$this->upload->modif_dtime = gmdate('Y-m-d H:i:s');
$this->upload->update();
$this->upload->batchAssoc('IDF_Tag', $tags);
/**
* [signal]
*
* IDF_Upload::update
*
* [sender]
*
* IDF_Form_UpdateUpload
*
* [description]
*
* This signal allows an application to perform a set of tasks
* just after the update of an uploaded file.
*
* [parameters]
*
* array('upload' => $upload);
*
*/
$params = array('upload' => $this->upload);
Pluf_Signal::send('IDF_Upload::update',
'IDF_Form_UpdateUpload', $params);
return $this->upload;
}
}

View File

@ -176,6 +176,28 @@ class IDF_Form_Upload extends Pluf_Form
}
// Send the notification
$upload->notify($this->project->getConf());
/**
* [signal]
*
* IDF_Upload::create
*
* [sender]
*
* IDF_Form_Upload
*
* [description]
*
* This signal allows an application to perform a set of tasks
* just after the upload of a file and after the notification run.
*
* [parameters]
*
* array('upload' => $upload);
*
*/
$params = array('upload' => $upload);
Pluf_Signal::send('IDF_Upload::create', 'IDF_Form_Upload',
$params);
return $upload;
}
}

View File

@ -33,6 +33,8 @@ class IDF_Form_UserAccount extends Pluf_Form
public function initFields($extra=array())
{
$this->user = $extra['user'];
$user_data = IDF_UserData::factory($this->user);
$this->fields['first_name'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('First name'),
@ -65,7 +67,7 @@ class IDF_Form_UserAccount extends Pluf_Form
'initial' => $this->user->language,
'widget' => 'Pluf_Form_Widget_SelectInput',
'widget_attrs' => array(
'choices' =>
'choices' =>
Pluf_L10n::getInstalledLanguages()
),
));
@ -92,17 +94,75 @@ class IDF_Form_UserAccount extends Pluf_Form
),
));
$this->fields['ssh_key'] = new Pluf_Form_Field_Varchar(
$this->fields['description'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Add a public SSH key'),
'label' => __('Description'),
'initial' => $user_data->description,
'widget_attrs' => array('rows' => 3,
'cols' => 40),
'widget' => 'Pluf_Form_Widget_TextareaInput',
));
$this->fields['twitter'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Twitter username'),
'initial' => $user_data->twitter,
'widget_attrs' => array(
'maxlength' => 50,
'size' => 15,
),
));
$this->fields['public_email'] = new Pluf_Form_Field_Email(
array('required' => false,
'label' => __('Public email address'),
'initial' => $user_data->public_email,
'widget_attrs' => array(
'maxlength' => 50,
'size' => 15,
),
));
$this->fields['website'] = new Pluf_Form_Field_Url(
array('required' => false,
'label' => __('Website URL'),
'initial' => $user_data->website,
'widget_attrs' => array(
'maxlength' => 50,
'size' => 15,
),
));
$this->fields['custom_avatar'] = new Pluf_Form_Field_File(
array('required' => false,
'label' => __('Upload custom avatar'),
'initial' => '',
'max_size' => Pluf::f('max_upload_size', 2097152),
'move_function_params' => array('upload_path' => Pluf::f('upload_path').'/avatars',
'upload_path_create' => true,
'upload_overwrite' => true,
'file_name' => 'user_'.$this->user->id.'_%s'),
'help_text' => __('An image file with a width and height not larger than 60 pixels (bigger images are scaled down).'),
));
$this->fields['remove_custom_avatar'] = new Pluf_Form_Field_Boolean(
array('required' => false,
'label' => __('Remove custom avatar'),
'initial' => false,
'widget' => 'Pluf_Form_Widget_CheckboxInput',
'widget_attrs' => array(),
'help_text' => __('Tick this to delete the custom avatar.'),
));
$this->fields['public_key'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Add a public key'),
'initial' => '',
'widget_attrs' => array('rows' => 3,
'cols' => 40),
'widget' => 'Pluf_Form_Widget_TextareaInput',
'help_text' => __('Be careful to provide your public key and not your private key!')
'help_text' => __('Paste a SSH or monotone public key. Be careful to not provide your private key here!')
));
}
/**
@ -140,7 +200,7 @@ class IDF_Form_UserAccount extends Pluf_Form
'email' => $new_email,
'user'=> $this->user,
)
);
);
$tmpl = new Pluf_Template('idf/user/changeemail-email.txt');
$text_email = $tmpl->render($context);
$email = new Pluf_Mail(Pluf::f('from_email'), $new_email,
@ -151,16 +211,45 @@ class IDF_Form_UserAccount extends Pluf_Form
}
$this->user->setFromFormData($this->cleaned_data);
// Add key as needed.
if ('' !== $this->cleaned_data['ssh_key']) {
if ('' !== $this->cleaned_data['public_key']) {
$key = new IDF_Key();
$key->user = $this->user;
$key->content = $this->cleaned_data['ssh_key'];
$key->content = $this->cleaned_data['public_key'];
if ($commit) {
$key->create();
}
}
if ($commit) {
$this->user->update();
// FIXME: go the extra mile and check the input lengths for
// all fields here!
// FIXME: this is all doubled in admin/UserUpdate!
$user_data = IDF_UserData::factory($this->user);
// Add or remove avatar - we need to do this here because every
// single setter directly leads to a save in the database
if ($user_data->avatar != '' &&
($this->cleaned_data['remove_custom_avatar'] == 1 ||
$this->cleaned_data['custom_avatar'] != '')) {
$avatar_path = Pluf::f('upload_path').'/avatars/'.basename($user_data->avatar);
if (basename($avatar_path) != '' && is_file($avatar_path)) {
unlink($avatar_path);
}
$user_data->avatar = '';
}
if ($this->cleaned_data['custom_avatar'] != '') {
$user_data->avatar = $this->cleaned_data['custom_avatar'];
}
$user_data->description = $this->cleaned_data['description'];
$user_data->twitter = $this->cleaned_data['twitter'];
$user_data->public_email = $this->cleaned_data['public_email'];
$user_data->website = $this->cleaned_data['website'];
if ($update_pass) {
/**
* [signal]
@ -190,7 +279,7 @@ class IDF_Form_UserAccount extends Pluf_Form
}
/**
* Check an ssh key.
* Check arbitrary public keys.
*
* It will throw a Pluf_Form_Invalid exception if it cannot
* validate the key.
@ -199,27 +288,59 @@ class IDF_Form_UserAccount extends Pluf_Form
* @param $user int The user id of the user of the key (0)
* @return string The clean key
*/
public static function checkSshKey($key, $user=0)
public static function checkPublicKey($key, $user=0)
{
$key = trim($key);
if (strlen($key) == 0) {
return '';
}
$key = str_replace(array("\n", "\r"), '', $key);
if (!preg_match('#^ssh\-[a-z]{3}\s(\S+)\s\S+$#', $key, $matches)) {
throw new Pluf_Form_Invalid(__('The format of the key is not valid. It must start with ssh-dss or ssh-rsa, a long string on a single line and at the end a comment.'));
}
if (Pluf::f('idf_strong_key_check', false)) {
$tmpfile = Pluf::f('tmp_folder', '/tmp').'/'.$user.'-key';
file_put_contents($tmpfile, $key, LOCK_EX);
$cmd = Pluf::f('idf_exec_cmd_prefix', '').
'ssh-keygen -l -f '.escapeshellarg($tmpfile).' > /dev/null 2>&1';
exec($cmd, $out, $return);
unlink($tmpfile);
if ($return != 0) {
throw new Pluf_Form_Invalid(__('Please check the key as it does not appears to be a valid key.'));
if (preg_match('#^ssh\-[a-z]{3}\s\S+(\s\S+)?$#', $key)) {
$key = str_replace(array("\n", "\r"), '', $key);
if (Pluf::f('idf_strong_key_check', false)) {
$tmpfile = Pluf::f('tmp_folder', '/tmp').'/'.$user.'-key';
file_put_contents($tmpfile, $key, LOCK_EX);
$cmd = Pluf::f('idf_exec_cmd_prefix', '').
'ssh-keygen -l -f '.escapeshellarg($tmpfile).' > /dev/null 2>&1';
exec($cmd, $out, $return);
unlink($tmpfile);
if ($return != 0) {
throw new Pluf_Form_Invalid(
__('Please check the key as it does not appear '.
'to be a valid SSH public key.')
);
}
}
}
else if (preg_match('#^\[pubkey [^\]]+\]\s*\S+\s*\[end\]$#', $key)) {
if (Pluf::f('idf_strong_key_check', false)) {
// if monotone can read it, it should be valid
$mtn_opts = implode(' ', Pluf::f('mtn_opts', array()));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').
sprintf('%s %s -d :memory: read >/tmp/php-out 2>&1',
Pluf::f('mtn_path', 'mtn'), $mtn_opts);
$fp = popen($cmd, 'w');
fwrite($fp, $key);
$return = pclose($fp);
if ($return != 0) {
throw new Pluf_Form_Invalid(
__('Please check the key as it does not appear '.
'to be a valid monotone public key.')
);
}
}
}
else {
throw new Pluf_Form_Invalid(
__('Public key looks neither like a SSH '.
'nor monotone public key.'));
}
// If $user, then check if not the same key stored
if ($user) {
$ruser = Pluf::factory('Pluf_User', $user);
@ -227,24 +348,33 @@ class IDF_Form_UserAccount extends Pluf_Form
$sql = new Pluf_SQL('content=%s', array($key));
$keys = Pluf::factory('IDF_Key')->getList(array('filter' => $sql->gen()));
if (count($keys) > 0) {
throw new Pluf_Form_Invalid(__('You already have uploaded this SSH key.'));
throw new Pluf_Form_Invalid(
__('You already have uploaded this key.')
);
}
}
}
return $key;
}
function clean_ssh_key()
function clean_custom_avatar()
{
return self::checkSshKey($this->cleaned_data['ssh_key'],
$this->user->id);
// Just png, jpeg/jpg or gif
if (!preg_match('/\.(png|jpg|jpeg|gif)$/i', $this->cleaned_data['custom_avatar']) &&
$this->cleaned_data['custom_avatar'] != '') {
@unlink(Pluf::f('upload_path').'/avatars/'.$this->cleaned_data['custom_avatar']);
throw new Pluf_Form_Invalid(__('For security reason, you cannot upload a file with this extension.'));
}
return $this->cleaned_data['custom_avatar'];
}
function clean_last_name()
{
$last_name = trim($this->cleaned_data['last_name']);
if ($last_name == mb_strtoupper($last_name)) {
return mb_convert_case(mb_strtolower($last_name),
return mb_convert_case(mb_strtolower($last_name),
MB_CASE_TITLE, 'UTF-8');
}
return $last_name;
@ -254,7 +384,7 @@ class IDF_Form_UserAccount extends Pluf_Form
{
$first_name = trim($this->cleaned_data['first_name']);
if ($first_name == mb_strtoupper($first_name)) {
return mb_convert_case(mb_strtolower($first_name),
return mb_convert_case(mb_strtolower($first_name),
MB_CASE_TITLE, 'UTF-8');
}
return $first_name;
@ -264,7 +394,7 @@ class IDF_Form_UserAccount extends Pluf_Form
{
$this->cleaned_data['email'] = mb_strtolower(trim($this->cleaned_data['email']));
$guser = new Pluf_User();
$sql = new Pluf_SQL('email=%s AND id!=%s',
$sql = new Pluf_SQL('email=%s AND id!=%s',
array($this->cleaned_data['email'], $this->user->id));
if ($guser->getCount(array('filter' => $sql->gen())) > 0) {
throw new Pluf_Form_Invalid(sprintf(__('The email "%s" is already used.'), $this->cleaned_data['email']));
@ -272,12 +402,20 @@ class IDF_Form_UserAccount extends Pluf_Form
return $this->cleaned_data['email'];
}
function clean_public_key()
{
$this->cleaned_data['public_key'] =
self::checkPublicKey($this->cleaned_data['public_key'],
$this->user->id);
return $this->cleaned_data['public_key'];
}
/**
* Check to see if the 2 passwords are the same.
* Check to see if the 2 passwords are the same
*/
public function clean()
{
if (!isset($this->errors['password'])
if (!isset($this->errors['password'])
&& !isset($this->errors['password2'])) {
$password1 = $this->cleaned_data['password'];
$password2 = $this->cleaned_data['password2'];
@ -285,6 +423,9 @@ class IDF_Form_UserAccount extends Pluf_Form
throw new Pluf_Form_Invalid(__('The passwords do not match. Please give them again.'));
}
}
return $this->cleaned_data;
}
}

210
src/IDF/Gconf.php Normal file
View File

@ -0,0 +1,210 @@
<?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 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 ***** */
/**
* Configuration of the objects.
*
* It is just storing a list of key/value pairs associated to
* different objects. If you use this table for your model, do not
* forget to drop the corresponding keys in your preDelete call.
*/
class IDF_Gconf extends Pluf_Model
{
public $_model = __CLASS__;
public $datacache = null;
public $dirty = array();
public $f = null;
protected $_mod = null;
/**
* Do we (un)serialize the data when getting/setting them.
*/
public $serialize = false;
function init()
{
$this->_a['table'] = 'idf_gconf';
$this->_a['model'] = __CLASS__;
$this->_a['cols'] = array(
// It is mandatory to have an "id" column.
'id' =>
array(
'type' => 'Pluf_DB_Field_Sequence',
//It is automatically added.
'blank' => true,
),
'model_class' =>
array(
'type' => 'Pluf_DB_Field_Varchar',
'blank' => false,
'size' => 150,
'verbose' => __('model class'),
),
'model_id' =>
array(
'type' => 'Pluf_DB_Field_Integer',
'blank' => false,
'verbose' => __('model id'),
),
'vkey' =>
array(
'type' => 'Pluf_DB_Field_Varchar',
'blank' => false,
'size' => 50,
'verbose' => __('key'),
),
'vdesc' =>
array(
'type' => 'Pluf_DB_Field_Text',
'blank' => false,
'verbose' => __('value'),
),
);
$this->_a['idx'] = array('model_vkey_idx' =>
array(
'col' => 'model_class, model_id, vkey',
'type' => 'unique',
),
);
$this->f = new IDF_Config_DataProxy($this);
}
function setModel($model)
{
$this->datacache = null;
$this->_mod = $model;
}
function initCache()
{
$this->datacache = array();
$this->dirty = array();
$sql = new Pluf_SQL('model_class=%s AND model_id=%s',
array($this->_mod->_model, $this->_mod->id));
foreach ($this->getList(array('filter' => $sql->gen())) as $val) {
$this->datacache[$val->vkey] = ($this->serialize) ? unserialize($val->vdesc) : $val->vdesc;
$this->dirty[$val->vkey] = $val->id;
}
}
/**
* FIXME: This is not efficient when setting a large number of
* values in a loop.
*/
function setVal($key, $value)
{
if (!is_null($this->getVal($key, null))
and $value == $this->getVal($key)) {
return;
}
$svalue = ($this->serialize) ? serialize($value) : $value;
if (isset($this->dirty[$key])) {
// we get to check if deleted by other process + update
$conf = new IDF_Gconf($this->dirty[$key]);
if ($conf->id == $this->dirty[$key]) {
$conf->vdesc = $svalue;
$conf->update();
$this->datacache[$key] = $value;
return;
}
}
// we insert
$conf = new IDF_Gconf();
$conf->model_class = $this->_mod->_model;
$conf->model_id = $this->_mod->id;
$conf->vkey = $key;
$conf->vdesc = $svalue;
$conf->create();
$this->datacache[$key] = $value;
$this->dirty[$key] = $conf->id;
}
function getVal($key, $default='')
{
if ($this->datacache === null) {
$this->initCache();
}
return (isset($this->datacache[$key])) ? $this->datacache[$key] : $default;
}
function delVal($key, $initcache=true)
{
$gconf = new IDF_Gconf();
$sql = new Pluf_SQL('vkey=%s AND model_class=%s AND model_id=%s', array($key, $this->_mod->_model, $this->_mod->id));
foreach ($gconf->getList(array('filter' => $sql->gen())) as $c) {
$c->delete();
}
if ($initcache) {
$this->initCache();
}
}
/**
* Collection selection.
*
* Suppose you have 5 objects with associated meta data in the
* Gconf storage, if you load the data independently for each
* object, you end up with 5 SELECT queries. With 25 objects, 25
* SELECT. You can select with one query all the data and merge in
* the code. It is faster. The collection selection get a
* model_class and a list of ids and returns an id indexed array
* of associative array data. This is for read only access as you
* do not get a series of Gconf objects.
*/
public static function collect($class, $ids)
{
$gconf = new IDF_Gconf();
$stmpl = sprintf('model_class=%%s AND model_id IN (%s)',
implode(',' , $ids));
$sql = new Pluf_SQL($stmpl, array($class));
$out = array_fill_keys($ids, array());
foreach ($gconf->getList(array('filter' => $sql->gen())) as $c) {
$out[$c->model_id][$c->vkey] = $c->vdesc;
}
return $out;
}
/**
* Drop the conf of a model.
*
* If your model is using this table, just add the following line
* in your preDelete() method:
*
* IDF_Gconf::dropForModel($this)
*
* It will take care of the cleaning.
*/
static public function dropForModel($model)
{
$table = Pluf::factory(__CLASS__)->getSqlTable();
$sql = new Pluf_SQL('model_class=%s AND model_id=%s',
array($model->_model, $model->id));
$db = &Pluf::db();
$db->execute('DELETE FROM '.$table.' WHERE '.$sql->gen());
}
static public function dropUser($signal, &$params)
{
self::dropForModel($params['user']);
}
}

View File

@ -42,9 +42,9 @@ class IDF_Issue extends Pluf_Model
'id' =>
array(
'type' => 'Pluf_DB_Field_Sequence',
'blank' => true,
'blank' => true,
),
'project' =>
'project' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Project',
@ -59,7 +59,7 @@ class IDF_Issue extends Pluf_Model
'size' => 250,
'verbose' => __('summary'),
),
'submitter' =>
'submitter' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
@ -67,7 +67,7 @@ class IDF_Issue extends Pluf_Model
'verbose' => __('submitter'),
'relate_name' => 'submitted_issue',
),
'owner' =>
'owner' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
@ -76,7 +76,7 @@ class IDF_Issue extends Pluf_Model
'verbose' => __('owner'),
'relate_name' => 'owned_issue',
),
'interested' =>
'interested' =>
array(
'type' => 'Pluf_DB_Field_Manytomany',
'model' => 'Pluf_User',
@ -86,14 +86,14 @@ class IDF_Issue extends Pluf_Model
),
'tags' =>
array(
'type' => 'Pluf_DB_Field_Manytomany',
'type' => 'Pluf_DB_Field_Manytomany',
'blank' => true,
'model' => 'IDF_Tag',
'verbose' => __('labels'),
),
'status' =>
'status' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'type' => 'Pluf_DB_Field_Foreignkey',
'blank' => false,
'model' => 'IDF_Tag',
'verbose' => __('status'),
@ -111,7 +111,7 @@ class IDF_Issue extends Pluf_Model
'verbose' => __('modification date'),
),
);
$this->_a['idx'] = array(
$this->_a['idx'] = array(
'modif_dtime_idx' =>
array(
'col' => 'modif_dtime',
@ -120,7 +120,7 @@ class IDF_Issue extends Pluf_Model
);
$table = $this->_con->pfx.'idf_issue_idf_tag_assoc';
$this->_a['views'] = array(
'join_tags' =>
'join_tags' =>
array(
'join' => 'LEFT JOIN '.$table
.' ON idf_issue_id=id',
@ -164,7 +164,7 @@ class IDF_Issue extends Pluf_Model
// that the issue as at least one comment in the database when
// doing the indexing.
if ($create) {
IDF_Timeline::insert($this, $this->get_project(),
IDF_Timeline::insert($this, $this->get_project(),
$this->get_submitter());
}
}
@ -177,12 +177,12 @@ class IDF_Issue extends Pluf_Model
* as such create links to other items etc. You can consider that
* if displayed, you can create a link to it.
*
* @param Pluf_HTTP_Request
* @param Pluf_HTTP_Request
* @return Pluf_Template_SafeString
*/
public function timelineFragment($request)
{
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
array($request->project->shortname,
$this->id));
$out = '<tr class="log"><td><a href="'.$url.'">'.
@ -193,14 +193,14 @@ class IDF_Issue extends Pluf_Model
$ic = (in_array($this->status, $request->project->getTagIdsByStatus('closed'))) ? 'issue-c' : 'issue-o';
$out .= sprintf(__('<a href="%1$s" class="%2$s" title="View issue">Issue %3$d</a>, %4$s'), $url, $ic, $this->id, Pluf_esc($this->summary)).'</td>';
$out .= "\n".'<tr class="extra"><td colspan="2">
<div class="helptext right">'.sprintf(__('Creation of <a href="%s" class="%s">issue&nbsp;%d</a>, by %s'), $url, $ic, $this->id, $user).'</div></td></tr>';
<div class="helptext right">'.sprintf(__('Creation of <a href="%s" class="%s">issue&nbsp;%d</a>, by %s'), $url, $ic, $this->id, $user).'</div></td></tr>';
return Pluf_Template::markSafe($out);
}
public function feedFragment($request)
{
$url = Pluf::f('url_base')
.Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
.Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
array($request->project->shortname,
$this->id));
$title = sprintf(__('%s: Issue %d created - %s'),
@ -241,15 +241,17 @@ class IDF_Issue extends Pluf_Model
$prj = $this->get_project();
$to_email = array();
if ('' != $conf->getVal('issues_notification_email', '')) {
$langs = Pluf::f('languages', array('en'));
$to_email[] = array($conf->getVal('issues_notification_email'),
$langs[0]);
$langs = Pluf::f('languages', array('en'));
$addresses = explode(',', $conf->getVal('issues_notification_email'));
foreach ($addresses as $address) {
$to_email[] = array($address, $langs[0]);
}
}
$current_locale = Pluf_Translation::getLocale();
$id = '<'.md5($this->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>';
if ($create) {
if (null != $this->get_owner() and $this->owner != $this->submitter) {
$email_lang = array($this->get_owner()->email,
if (null != $this->get_owner() and $this->owner != $this->submitter) {
$email_lang = array($this->get_owner()->email,
$this->get_owner()->language);
if (!in_array($email_lang, $to_email)) {
$to_email[] = $email_lang;

View File

@ -41,9 +41,9 @@ class IDF_IssueComment extends Pluf_Model
'id' =>
array(
'type' => 'Pluf_DB_Field_Sequence',
'blank' => true,
'blank' => true,
),
'issue' =>
'issue' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Issue',
@ -57,7 +57,7 @@ class IDF_IssueComment extends Pluf_Model
'blank' => false,
'verbose' => __('comment'),
),
'submitter' =>
'submitter' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
@ -79,7 +79,7 @@ class IDF_IssueComment extends Pluf_Model
'verbose' => __('creation date'),
),
);
$this->_a['idx'] = array(
$this->_a['idx'] = array(
'creation_dtime_idx' =>
array(
'col' => 'creation_dtime',
@ -119,7 +119,7 @@ class IDF_IssueComment extends Pluf_Model
$sql = new Pluf_SQL('issue=%s', array($this->issue));
$co = Pluf::factory('IDF_IssueComment')->getList(array('filter'=>$sql->gen()));
if ($co->count() > 1) {
IDF_Timeline::insert($this, $this->get_issue()->get_project(),
IDF_Timeline::insert($this, $this->get_issue()->get_project(),
$this->get_submitter());
}
}
@ -129,7 +129,7 @@ class IDF_IssueComment extends Pluf_Model
public function timelineFragment($request)
{
$issue = $this->get_issue();
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
array($request->project->shortname,
$issue->id));
$url .= '#ic'.$this->id;
@ -168,7 +168,7 @@ class IDF_IssueComment extends Pluf_Model
}
$out .= '</td></tr>';
$out .= "\n".'<tr class="extra"><td colspan="2">
<div class="helptext right">'.sprintf(__('Comment on <a href="%s" class="%s">issue&nbsp;%d</a>, by %s'), $url, $ic, $issue->id, $user).'</div></td></tr>';
<div class="helptext right">'.sprintf(__('Comment on <a href="%s" class="%s">issue&nbsp;%d</a>, by %s'), $url, $ic, $issue->id, $user).'</div></td></tr>';
return Pluf_Template::markSafe($out);
}
@ -176,7 +176,7 @@ class IDF_IssueComment extends Pluf_Model
{
$issue = $this->get_issue();
$url = Pluf::f('url_base')
.Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
.Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
array($request->project->shortname,
$issue->id));
$title = sprintf(__('%s: Comment on issue %d - %s'),
@ -196,4 +196,9 @@ class IDF_IssueComment extends Pluf_Model
$tmpl = new Pluf_Template('idf/issues/feedfragment.xml');
return $tmpl->render($context);
}
public function get_submitter_data()
{
return IDF_UserData::factory($this->get_submitter());
}
}

View File

@ -39,9 +39,9 @@ class IDF_IssueFile extends Pluf_Model
array(
'type' => 'Pluf_DB_Field_Sequence',
//It is automatically added.
'blank' => true,
'blank' => true,
),
'comment' =>
'comment' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_IssueComment',
@ -49,7 +49,7 @@ class IDF_IssueFile extends Pluf_Model
'verbose' => __('comment'),
'relate_name' => 'attachment',
),
'submitter' =>
'submitter' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
@ -63,7 +63,7 @@ class IDF_IssueFile extends Pluf_Model
'size' => 100,
'verbose' => __('file name'),
),
'attachment' =>
'attachment' =>
array(
'type' => 'Pluf_DB_Field_File',
'blank' => false,
@ -76,7 +76,7 @@ class IDF_IssueFile extends Pluf_Model
'verbose' => __('file size'),
'help_text' => 'Size in bytes.',
),
'type' =>
'type' =>
array(
'type' => 'Pluf_DB_Field_Varchar',
'blank' => false,
@ -111,7 +111,7 @@ class IDF_IssueFile extends Pluf_Model
$file = Pluf::f('upload_issue_path').'/'.$this->attachment;
$this->filesize = filesize($file);
// remove .dummy
$this->filename = substr(basename($file), 0, -6);
$this->filename = substr(basename($file), 0, -6);
$img_extensions = array('jpeg', 'jpg', 'png', 'gif');
$info = pathinfo($this->filename);
if (!isset($info['extension'])) $info['extension'] = '';
@ -128,4 +128,10 @@ class IDF_IssueFile extends Pluf_Model
{
@unlink(Pluf::f('upload_issue_path').'/'.$this->attachment);
}
function isText()
{
$info = IDF_FileUtil::getMimeType($this->filename);
return IDF_FileUtil::isText($info);
}
}

View File

@ -22,7 +22,7 @@
# ***** END LICENSE BLOCK ***** */
/**
* Storage of the SSH keys.
* Storage of the public keys (ssh or monotone).
*
*/
class IDF_Key extends Pluf_Model
@ -39,9 +39,9 @@ class IDF_Key extends Pluf_Model
array(
'type' => 'Pluf_DB_Field_Sequence',
//It is automatically added.
'blank' => true,
'blank' => true,
),
'user' =>
'user' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
@ -52,14 +52,14 @@ class IDF_Key extends Pluf_Model
array(
'type' => 'Pluf_DB_Field_Text',
'blank' => false,
'verbose' => __('ssh key'),
'verbose' => __('public key'),
),
);
// WARNING: Not using getSqlTable on the Pluf_User object to
// avoid recursion.
$t_users = $this->_con->pfx.'users';
$t_users = $this->_con->pfx.'users';
$this->_a['views'] = array(
'join_user' =>
'join_user' =>
array(
'join' => 'LEFT JOIN '.$t_users
.' ON '.$t_users.'.id='.$this->_con->qn('user'),
@ -75,6 +75,58 @@ class IDF_Key extends Pluf_Model
return Pluf_Template::markSafe(Pluf_esc(substr($this->content, 0, 25)).' [...] '.Pluf_esc(substr($this->content, -55)));
}
private function parseContent()
{
if (preg_match('#^\[pubkey ([^\]]+)\]\s*(\S+)\s*\[end\]$#', $this->content, $m)) {
return array('mtn', $m[1], $m[2]);
}
else if (preg_match('#^ssh\-[a-z]{3}\s(\S+)(?:\s(\S+))?$#', $this->content, $m)) {
return array('ssh', $m[2], $m[1]);
}
throw new Exception(__('Invalid or unknown key data detected.'));
}
/**
* Returns the type of the public key
*
* @return string 'ssh' or 'mtn'
*/
function getType()
{
list($type, , ) = $this->parseContent();
return $type;
}
/**
* Returns the key name of the key
*
* @return string
*/
function getName()
{
list(, $keyName, ) = $this->parseContent();
return $keyName;
}
/**
* This function should be used to calculate the key id from the
* public key hash for authentication purposes. This avoids clashes
* in case the key name is not unique across the project
*
* And yes, this is actually how monotone itself calculates the key
* id...
*
* @return string
*/
function getMtnId()
{
list($type, $keyName, $keyData) = $this->parseContent();
if ($type != 'mtn')
throw new Exception('key is not a monotone public key');
return sha1($keyName.":".$keyData);
}
function postSave($create=false)
{
/**
@ -89,7 +141,7 @@ class IDF_Key extends Pluf_Model
* [description]
*
* This signal allows an application to perform special
* operations after the saving of a SSH Key.
* operations after the saving of a public Key.
*
* [parameters]
*
@ -127,5 +179,4 @@ class IDF_Key extends Pluf_Model
Pluf_Signal::send('IDF_Key::preDelete',
'IDF_Key', $params);
}
}

View File

@ -92,6 +92,7 @@ class IDF_Middleware
array(
'size' => 'IDF_Views_Source_PrettySize',
'ssize' => 'IDF_Views_Source_PrettySizeSimple',
'shorten' => 'IDF_Views_Source_ShortenString',
));
}
}
@ -104,12 +105,14 @@ function IDF_Middleware_ContextPreProcessor($request)
$c['isAdmin'] = ($request->user->administrator or $request->user->staff);
if (isset($request->project)) {
$c['project'] = $request->project;
$c['isOwner'] = $request->user->hasPerm('IDF.project-owner',
$c['isOwner'] = $request->user->hasPerm('IDF.project-owner',
$request->project);
$c['isMember'] = $request->user->hasPerm('IDF.project-member',
$c['isMember'] = $request->user->hasPerm('IDF.project-member',
$request->project);
$c = array_merge($c, $request->rights);
}
$c['usherConfigured'] = Pluf::f("mtn_usher_conf", null) !== null;
$c['allProjects'] = IDF_Views::getProjects($request->user);
return $c;
}

View File

@ -0,0 +1,53 @@
<?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 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_Queue model.
*
*/
function IDF_Migrations_14Queue_up($params=null)
{
$models = array(
'IDF_Queue',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);
foreach ($models as $model) {
$schema->model = new $model();
$schema->createTables();
}
}
function IDF_Migrations_14Queue_down($params=null)
{
$models = array(
'IDF_Queue',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);
foreach ($models as $model) {
$schema->model = new $model();
$schema->dropTables();
}
}

View File

@ -0,0 +1,53 @@
<?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 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_Gconf model.
*
*/
function IDF_Migrations_15AddGconf_up($params=null)
{
$models = array(
'IDF_Gconf',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);
foreach ($models as $model) {
$schema->model = new $model();
$schema->createTables();
}
}
function IDF_Migrations_15AddGconf_down($params=null)
{
$models = array(
'IDF_Gconf',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);
foreach ($models as $model) {
$schema->model = new $model();
$schema->dropTables();
}
}

View File

@ -51,6 +51,8 @@ function IDF_Migrations_Backup_run($folder, $name=null)
'IDF_Review_FileComment',
'IDF_Key',
'IDF_Scm_Cache_Git',
'IDF_Queue',
'IDF_Gconf',
);
$db = Pluf::db();
// Now, for each table, we dump the content in json, this is a
@ -94,6 +96,8 @@ function IDF_Migrations_Backup_restore($folder, $name)
'IDF_Review_FileComment',
'IDF_Key',
'IDF_Scm_Cache_Git',
'IDF_Queue',
'IDF_Gconf',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);

View File

@ -48,6 +48,8 @@ function IDF_Migrations_Install_setup($params=null)
'IDF_Review_FileComment',
'IDF_Key',
'IDF_Scm_Cache_Git',
'IDF_Queue',
'IDF_Gconf',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);
@ -85,6 +87,8 @@ function IDF_Migrations_Install_teardown($params=null)
$perm = Pluf_Permission::getFromString('IDF.project-authorized-user');
if ($perm) $perm->delete();
$models = array(
'IDF_Gconf',
'IDF_Queue',
'IDF_Scm_Cache_Git',
'IDF_Key',
'IDF_Review_FileComment',

View File

@ -48,13 +48,18 @@ class IDF_Plugin_SyncGit_Cron
$out = '';
$keys = Pluf::factory('IDF_Key')->getList(array('view'=>'join_user'));
foreach ($keys as $key) {
if (strlen($key->content) > 40 // minimal check
and preg_match('/^[a-zA-Z][a-zA-Z0-9_.-]*(@[a-zA-Z][a-zA-Z0-9.-]*)?$/', $key->login)) {
try {
$key_type = $key->getType();
} catch (Exception $e) {
// The key is a bad key, skip it
continue;
}
if ($key_type == 'ssh' and preg_match('/^[a-zA-Z][a-zA-Z0-9_.-]*(@[a-zA-Z][a-zA-Z0-9.-]*)?$/', $key->login)) {
$content = trim(str_replace(array("\n", "\r"), '', $key->content));
$out .= sprintf($template, $cmd, $key->login, $content)."\n";
}
}
file_put_contents($authorized_keys, $out, LOCK_EX);
file_put_contents($authorized_keys, $out, LOCK_EX);
}
/**
@ -99,6 +104,7 @@ class IDF_Plugin_SyncGit_Cron
if (count($orphans)) {
$cmd = Pluf::f('idf_exec_cmd_prefix', '').'rm -rf '.implode(' ', $orphans);
exec($cmd);
clearstatcache();
while (list(, $project) = each($orphans)) {
if (is_dir($project)) {
throw new Exception(sprintf('Cannot remove %s directory.', $project));

View File

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

View File

@ -0,0 +1,830 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* This classes is a plugin which allows to synchronise access rights
* between indefero and monotone usher setups.
*/
class IDF_Plugin_SyncMonotone
{
/**
* Entry point of the plugin.
*/
static public function entry($signal, &$params)
{
$plug = new IDF_Plugin_SyncMonotone();
switch ($signal) {
case 'IDF_Project::created':
$plug->processProjectCreate($params['project']);
break;
case 'IDF_Project::membershipsUpdated':
$plug->processMembershipsUpdated($params['project']);
break;
case 'IDF_Project::preDelete':
$plug->processProjectDelete($params['project']);
break;
case 'IDF_Key::postSave':
$plug->processKeyCreate($params['key']);
break;
case 'IDF_Key::preDelete':
$plug->processKeyDelete($params['key']);
break;
case 'mtnpostpush.php::run':
$plug->processSyncTimeline($params['project']);
break;
}
}
/**
* Initial steps to setup a new monotone project:
*
* 1) run mtn db init to initialize a new database underknees
* 'mtn_repositories'
* 2) create a new server key in the same directory
* 3) create a new client key for IDF and store it in the project conf
* 4) setup the configuration
* 5) add the database as new local server in the usher configuration
* 6) reload the running usher instance so it acknowledges the new server
*
* The initial right setup happens in processMembershipsUpdated()
*
* @param IDF_Project
*/
function processProjectCreate($project)
{
if ($project->getConf()->getVal('scm') != 'mtn') {
return;
}
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
$projecttempl = Pluf::f('mtn_repositories', false);
if ($projecttempl === false) {
throw new IDF_Scm_Exception(
__('"mtn_repositories" must be defined in your configuration file.')
);
}
$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.')
);
}
$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
));
}
// check some static configuration files
$confdir = Pluf::f('mtn_confdir', false);
if ($confdir === false) {
$confdir = dirname(__FILE__).'/SyncMonotone/';
}
$confdir_contents = array(
'monotonerc.in',
'remote-automate-permissions.in',
'hooks.d/',
// this is linked and not copied to be able to update
// the list of read-only commands on upgrades
'hooks.d/indefero_authorize_remote_automate.conf',
'hooks.d/indefero_authorize_remote_automate.lua',
'hooks.d/indefero_post_push.conf.in',
'hooks.d/indefero_post_push.lua',
);
if (!$project->private) {
// this is linked and not copied to be able to update
// the list of read-only commands on upgrades
$confdir_contents[] = 'hooks.d/indefero_authorize_remote_automate.conf';
}
// check whether we should handle additional files in the config directory
$confdir_extra_contents = Pluf::f('mtn_confdir_extra', false);
if ($confdir_extra_contents !== false) {
$confdir_contents =
array_merge($confdir_contents, $confdir_extra_contents);
}
foreach ($confdir_contents as $content) {
if (!file_exists($confdir.$content)) {
throw new IDF_Scm_Exception(sprintf(
__('The configuration file %s is missing.'), $content
));
}
}
$shortname = $project->shortname;
$projectpath = sprintf($projecttempl, $shortname);
if (file_exists($projectpath)) {
throw new IDF_Scm_Exception(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
));
}
//
// step 1) create a new database
//
$dbfile = $projectpath.'/database.mtn';
$cmd = sprintf('db init -d %s', escapeshellarg($dbfile));
self::_mtn_exec($cmd);
//
// step 2) create a server key
//
// try to parse the key's domain part from the remote_url's host
// name, otherwise fall back to the configured Apache server name
$server = $_SERVER['SERVER_NAME'];
$remote_url = Pluf::f('mtn_remote_url');
if (($parsed = parse_url($remote_url)) !== false &&
!empty($parsed['host'])) {
$server = $parsed['host'];
}
$serverkey = $shortname.'-server@'.$server;
$cmd = sprintf('au generate_key --confdir=%s %s ""',
escapeshellarg($projectpath),
escapeshellarg($serverkey)
);
self::_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
));
}
}
$clientkey_name = $shortname.'-client@'.$server;
$cmd = sprintf('au generate_key --keydir=%s %s ""',
escapeshellarg($keydir),
escapeshellarg($clientkey_name)
);
$keyinfo = self::_mtn_exec($cmd);
$parsed_keyinfo = array();
try {
$parsed_keyinfo = IDF_Scm_Monotone_BasicIO::parse($keyinfo);
}
catch (Exception $e) {
throw new IDF_Scm_Exception(sprintf(
__('Could not parse key information: %s'), $e->getMessage()
));
}
$clientkey_hash = $parsed_keyinfo[0][1]['hash'];
$clientkey_file = $keydir . '/' . $clientkey_name . '.' . $clientkey_hash;
$clientkey_data = file_get_contents($clientkey_file);
$project->getConf()->setVal('mtn_client_key_name', $clientkey_name);
$project->getConf()->setVal('mtn_client_key_hash', $clientkey_hash);
$project->getConf()->setVal('mtn_client_key_data', $clientkey_data);
// add the public client key to the server
$cmd = sprintf('au get_public_key --keydir=%s %s',
escapeshellarg($keydir),
escapeshellarg($clientkey_hash)
);
$clientkey_pubdata = self::_mtn_exec($cmd);
$cmd = sprintf('au put_public_key --db=%s %s',
escapeshellarg($dbfile),
escapeshellarg($clientkey_pubdata)
);
self::_mtn_exec($cmd);
//
// step 4) setup the configuration
//
// we assume that all confdir entries ending with a slash mean a
// directory that has to be created, that all files ending on ".in"
// have to be processed and copied in place and that all other files
// just need to be symlinked from the original location
foreach ($confdir_contents as $content) {
$filepath = $projectpath.'/'.$content;
if (substr($content, -1) == '/') {
if (!mkdir($filepath)) {
throw new IDF_Scm_Exception(sprintf(
__('Could not create configuration directory "%s"'), $filepath
));
}
continue;
}
if (substr($content, -3) != '.in') {
if (!symlink($confdir.$content, $filepath)) {
IDF_Scm_Exception(sprintf(
__('Could not create symlink "%s"'), $filepath
));
}
continue;
}
$filecontents = file_get_contents($confdir.'/'.$content);
$filecontents = str_replace(
array('%%MTNPOSTPUSH%%', '%%PROJECT%%', '%%MTNCLIENTKEY%%'),
array($mtnpostpush, $shortname, $clientkey_hash),
$filecontents
);
// 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
));
}
}
//
// step 5) read in and append the usher config with the new server
//
$usher_rc = file_get_contents($usher_config);
$parsed_config = array();
try {
$parsed_config = IDF_Scm_Monotone_BasicIO::parse($usher_rc);
}
catch (Exception $e) {
throw new IDF_Scm_Exception(sprintf(
__('Could not parse usher configuration in "%s": %s'),
$usher_config, $e->getMessage()
));
}
// ensure we haven't configured a server with this name already
foreach ($parsed_config as $stanzas) {
foreach ($stanzas as $stanza_line) {
if ($stanza_line['key'] == 'server' &&
$stanza_line['values'][0] == $shortname) {
throw new IDF_Scm_Exception(sprintf(
__('usher configuration already contains a server '.
'entry named "%s"'),
$shortname
));
}
}
}
$new_server = array(
array('key' => 'server', 'values' => array($shortname)),
array('key' => 'local', 'values' => array(
'--confdir', $projectpath,
'-d', $dbfile,
'--timestamps',
'--ticker=dot'
)),
);
$parsed_config[] = $new_server;
$usher_rc = IDF_Scm_Monotone_BasicIO::compile($parsed_config);
// FIXME: more sanity - what happens on failing writes? we do not
// have a backup copy of usher.conf around...
if (file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write usher configuration file "%s"'), $usher_config
));
}
//
// step 6) reload usher to pick up the new configuration
//
IDF_Scm_Monotone_Usher::reload();
}
/**
* Updates the read / write permissions for the monotone database
*
* @param IDF_Project
*/
public function processMembershipsUpdated($project)
{
if ($project->getConf()->getVal('scm') != 'mtn') {
return;
}
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
$mtn = IDF_Scm_Monotone::factory($project);
$stdio = $mtn->getStdio();
$projectpath = self::_get_project_path($project);
$auth_ids = self::_get_authorized_user_ids($project);
$key_ids = array();
foreach ($auth_ids as $auth_id) {
$sql = new Pluf_SQL('user=%s', array($auth_id));
$keys = Pluf::factory('IDF_Key')->getList(array('filter' => $sql->gen()));
foreach ($keys as $key) {
if ($key->getType() != 'mtn')
continue;
$stdio->exec(array('put_public_key', $key->content));
$key_ids[] = $key->getMtnId();
}
}
$write_permissions = implode("\n", $key_ids);
$rcfile = $projectpath.'/write-permissions';
if (file_put_contents($rcfile, $write_permissions, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write write-permissions file "%s"'), $rcfile
));
}
if ($project->private) {
$stanza = array(
array('key' => 'pattern', 'values' => array('*')),
);
foreach ($key_ids as $key_id)
{
$stanza[] = array('key' => 'allow', 'values' => array($key_id));
}
}
else {
$stanza = array(
array('key' => 'pattern', 'values' => array('*')),
array('key' => 'allow', 'values' => array('*')),
);
}
$read_permissions = IDF_Scm_Monotone_BasicIO::compile(array($stanza));
$rcfile = $projectpath.'/read-permissions';
if (file_put_contents($rcfile, $read_permissions, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write read-permissions file "%s"'), $rcfile
));
}
// link / unlink the read-only automate permissions for the project
$confdir = Pluf::f('mtn_confdir', false);
if ($confdir === false) {
$confdir = dirname(__FILE__).'/SyncMonotone/';
}
$file = 'hooks.d/indefero_authorize_remote_automate.conf';
$projectfile = $projectpath.'/'.$file;
$templatefile = $confdir.'/'.$file;
$serverRestartRequired = false;
if ($project->private && file_exists($projectfile) && is_link($projectfile)) {
if (!unlink($projectfile)) {
IDF_Scm_Exception(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(
__('Could not create symlink "%s"'), $projectfile
));
}
$serverRestartRequired = true;
}
if ($serverRestartRequired) {
// FIXME: we should actually use stopServer() here, but this
// seems to be ignored when the server should be started
// again immediately afterwards
IDF_Scm_Monotone_Usher::killServer($project->shortname);
IDF_Scm_Monotone_Usher::startServer($project->shortname);
}
}
/**
* Clean up after a mtn project was deleted
*
* @param IDF_Project
*/
public function processProjectDelete($project)
{
if ($project->getConf()->getVal('scm') != 'mtn') {
return;
}
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
$usher_config = Pluf::f('mtn_usher_conf', false);
if (!$usher_config || !is_writable($usher_config)) {
throw new IDF_Scm_Exception(
__('"mtn_usher_conf" does not exist or is not writable.')
);
}
$shortname = $project->shortname;
IDF_Scm_Monotone_Usher::killServer($shortname);
$projecttempl = Pluf::f('mtn_repositories', false);
if ($projecttempl === false) {
throw new IDF_Scm_Exception(
__('"mtn_repositories" must be defined in your configuration file.')
);
}
$projectpath = sprintf($projecttempl, $shortname);
if (file_exists($projectpath)) {
if (!self::_delete_recursive($projectpath)) {
throw new IDF_Scm_Exception(sprintf(
__('One or more paths underknees %s could not be deleted.'), $projectpath
));
}
}
$keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
$keyname = $project->getConf()->getVal('mtn_client_key_name', false);
$keyhash = $project->getConf()->getVal('mtn_client_key_hash', false);
if ($keyname && $keyhash &&
file_exists($keydir .'/'. $keyname . '.' . $keyhash)) {
if (!@unlink($keydir .'/'. $keyname . '.' . $keyhash)) {
throw new IDF_Scm_Exception(sprintf(
__('Could not delete client private key %s'), $keyname
));
}
}
$usher_rc = file_get_contents($usher_config);
$parsed_config = array();
try {
$parsed_config = IDF_Scm_Monotone_BasicIO::parse($usher_rc);
}
catch (Exception $e) {
throw new IDF_Scm_Exception(sprintf(
__('Could not parse usher configuration in "%s": %s'),
$usher_config, $e->getMessage()
));
}
foreach ($parsed_config as $idx => $stanzas) {
foreach ($stanzas as $stanza_line) {
if ($stanza_line['key'] == 'server' &&
$stanza_line['values'][0] == $shortname) {
unset($parsed_config[$idx]);
break;
}
}
}
$usher_rc = IDF_Scm_Monotone_BasicIO::compile($parsed_config);
// FIXME: more sanity - what happens on failing writes? we do not
// have a backup copy of usher.conf around...
if (file_put_contents($usher_config, $usher_rc, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write usher configuration file "%s"'), $usher_config
));
}
IDF_Scm_Monotone_Usher::reload();
}
/**
* Adds the (monotone) key to all monotone projects of this forge
* where the user of the key has write access to
*/
public function processKeyCreate($key)
{
if ($key->getType() != 'mtn') {
return;
}
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
$conf = new IDF_Conf();
$conf->setProject($project);
$scm = $conf->getVal('scm', 'mtn');
if ($scm != 'mtn')
continue;
$projectpath = self::_get_project_path($project);
$auth_ids = self::_get_authorized_user_ids($project);
if (!in_array($key->user, $auth_ids))
continue;
$mtn_key_id = $key->getMtnId();
// if the project is not defined as private, all people have
// read access already, so we don't need to write anything
// and we currently do not check if read-permissions really
// contains
// pattern "*"
// allow "*"
// which is the default for non-private projects
if ($project->private == true) {
$read_perms = file_get_contents($projectpath.'/read-permissions');
$parsed_read_perms = array();
try {
$parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms);
}
catch (Exception $e) {
throw new IDF_Scm_Exception(sprintf(
__('Could not parse read-permissions for project "%s": %s'),
$shortname, $e->getMessage()
));
}
$wildcard_section = null;
for ($i=0; $i<count($parsed_read_perms); ++$i) {
foreach ($parsed_read_perms[$i] as $stanza_line) {
if ($stanza_line['key'] == 'pattern' &&
$stanza_line['values'][0] == '*') {
$wildcard_section =& $parsed_read_perms[$i];
break;
}
}
}
if ($wildcard_section == null)
{
$wildcard_section = array(
array('key' => 'pattern', 'values' => array('*'))
);
$parsed_read_perms[] =& $wildcard_section;
}
$key_found = false;
foreach ($wildcard_section as $line)
{
if ($line['key'] == 'allow' && $line['values'][0] == $mtn_key_id) {
$key_found = true;
break;
}
}
if (!$key_found) {
$wildcard_section[] = array(
'key' => 'allow', 'values' => array($mtn_key_id)
);
}
$read_perms = IDF_Scm_Monotone_BasicIO::compile($parsed_read_perms);
if (file_put_contents($projectpath.'/read-permissions',
$read_perms, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write read-permissions for project "%s"'), $shortname
));
}
}
$write_perms = file_get_contents($projectpath.'/write-permissions');
$lines = preg_split("/(\n|\r\n)/", $write_perms, -1, PREG_SPLIT_NO_EMPTY);
if (!in_array('*', $lines) && !in_array($mtn_key_id, $lines)) {
$lines[] = $mtn_key_id;
}
if (file_put_contents($projectpath.'/write-permissions',
implode("\n", $lines) . "\n", LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write write-permissions file for project "%s"'),
$shortname
));
}
$mtn = IDF_Scm_Monotone::factory($project);
$stdio = $mtn->getStdio();
$stdio->exec(array('put_public_key', $key->content));
}
}
/**
* Removes the (monotone) key from all monotone projects of this forge
* where the user of the key has write access to
*/
public function processKeyDelete($key)
{
try {
if ($key->getType() != 'mtn') {
return;
}
} catch (Exception $e) {
// bad key type, skip it.
return;
}
if (Pluf::f('mtn_db_access', 'local') == 'local') {
return;
}
foreach (Pluf::factory('IDF_Project')->getList() as $project) {
$conf = new IDF_Conf();
$conf->setProject($project);
$scm = $conf->getVal('scm', 'mtn');
if ($scm != 'mtn')
continue;
$projectpath = self::_get_project_path($project);
$auth_ids = self::_get_authorized_user_ids($project);
if (!in_array($key->user, $auth_ids))
continue;
$mtn_key_id = $key->getMtnId();
// if the project is not defined as private, all people have
// read access already, so we don't need to write anything
// and we currently do not check if read-permissions really
// contains
// pattern "*"
// allow "*"
// which is the default for non-private projects
if ($project->private) {
$read_perms = file_get_contents($projectpath.'/read-permissions');
$parsed_read_perms = array();
try {
$parsed_read_perms = IDF_Scm_Monotone_BasicIO::parse($read_perms);
}
catch (Exception $e) {
throw new IDF_Scm_Exception(sprintf(
__('Could not parse read-permissions for project "%s": %s'),
$shortname, $e->getMessage()
));
}
// while we add new keys only to an existing wild-card entry
// we remove dropped keys from all sections since the key
// should be simply unavailable for all of them
for ($h=0; $h<count($parsed_read_perms); ++$h) {
for ($i=0; $i<count($parsed_read_perms[$h]); ++$i) {
if ($parsed_read_perms[$h][$i]['key'] == 'allow' &&
$parsed_read_perms[$h][$i]['values'][0] == $mtn_key_id) {
unset($parsed_read_perms[$h][$i]);
continue;
}
}
}
$read_perms = IDF_Scm_Monotone_BasicIO::compile($parsed_read_perms);
if (file_put_contents($projectpath.'/read-permissions',
$read_perms, LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write read-permissions for project "%s"'), $shortname
));
}
}
$write_perms = file_get_contents($projectpath.'/write-permissions');
$lines = preg_split("/(\n|\r\n)/", $write_perms, -1, PREG_SPLIT_NO_EMPTY);
for ($i=0; $i<count($lines); ++$i) {
if ($lines[$i] == $mtn_key_id) {
unset($lines[$i]);
// the key should actually only exist once in the
// file, but we're paranoid
continue;
}
}
if (file_put_contents($projectpath.'/write-permissions',
implode("\n", $lines) . "\n", LOCK_EX) === false) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write write-permissions file for project "%s"'),
$shortname
));
}
$mtn = IDF_Scm_Monotone::factory($project);
$stdio = $mtn->getStdio();
// if the public key did not sign any revisions, drop it from
// the database as well
try {
if (strlen($stdio->exec(array('select', 'k:' . $mtn_key_id))) == 0) {
$stdio->exec(array('drop_public_key', $mtn_key_id));
}
} catch (IDF_Scm_Exception $e) {
if (strpos($e->getMessage(), 'there is no key named') === false)
throw $e;
}
}
}
/**
* Update the timeline after a push
*
*/
public function processSyncTimeline($project_name)
{
try {
$project = IDF_Project::getOr404($project_name);
} catch (Pluf_HTTP_Error404 $e) {
Pluf_Log::event(array(
'IDF_Plugin_SyncMonotone::processSyncTimeline',
'Project not found.',
array($project_name, $params)
));
return false; // Project not found
}
Pluf_Log::debug(array(
'IDF_Plugin_SyncMonotone::processSyncTimeline',
'Project found', $project_name, $project->id
));
IDF_Scm::syncTimeline($project, true);
Pluf_Log::event(array(
'IDF_Plugin_SyncMonotone::processSyncTimeline',
'sync', array($project_name, $project->id)
));
}
private static function _get_authorized_user_ids($project)
{
$mem = $project->getMembershipData();
$members = array_merge((array)$mem['members'],
(array)$mem['owners'],
(array)$mem['authorized']);
$userids = array();
foreach ($members as $member) {
$userids[] = $member->id;
}
return $userids;
}
private static function _get_project_path($project)
{
$projecttempl = Pluf::f('mtn_repositories', false);
if ($projecttempl === false) {
throw new IDF_Scm_Exception(
__('"mtn_repositories" must be defined in your configuration file.')
);
}
$projectpath = sprintf($projecttempl, $project->shortname);
if (!file_exists($projectpath)) {
throw new IDF_Scm_Exception(sprintf(
__('The project path %s does not exists.'), $projectpath
));
}
return $projectpath;
}
private static function _mtn_exec($cmd)
{
$fullcmd = sprintf('%s %s %s',
Pluf::f('idf_exec_cmd_prefix', ''),
Pluf::f('mtn_path', 'mtn'),
$cmd
);
$output = $return = null;
exec($fullcmd, $output, $return);
if ($return != 0) {
throw new IDF_Scm_Exception(sprintf(
__('The command "%s" could not be executed.'), $cmd
));
}
return implode("\n", $output);
}
private static function _delete_recursive($path)
{
if (is_file($path) || is_link($path)) {
return @unlink($path);
}
if (is_dir($path)) {
$scan = glob(rtrim($path, '/') . '/*');
$status = 0;
foreach ($scan as $subpath) {
$status |= self::_delete_recursive($subpath);
}
$status |= rmdir($path);
return $status;
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,58 @@
-- ***** BEGIN LICENSE BLOCK *****
-- This file is part of InDefero, an open source project management application.
-- Copyright (C) 2011 Céondo Ltd and contributors.
--
-- InDefero is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.
--
-- InDefero is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
--
-- ***** END LICENSE BLOCK *****
--
-- let IDF know of new arriving revisions to fill its timeline
--
_idf_revs = {}
push_hook_functions(
{
start =
function (session_id)
_idf_revs[session_id] = {}
return "continue",nil
end,
revision_received =
function (new_id, revision, certs, session_id)
table.insert(_idf_revs[session_id], new_id)
return "continue",nil
end,
["end"] =
function (session_id, ...)
if table.getn(_idf_revs[session_id]) == 0 then
return "continue",nil
end
local pin,pout,pid = spawn_pipe(IDF_push_script, IDF_project);
if pid == -1 then
print("could not execute " .. IDF_push_script)
return "continue",nil
end
for _,r in ipairs(_idf_revs[session_id]) do
pin:write(r .. "\n")
end
pin:close()
wait(pid)
return "continue",nil
end
})

View File

@ -0,0 +1,30 @@
-- ***** BEGIN LICENSE BLOCK *****
-- This file is part of InDefero, an open source project management application.
-- Copyright (C) 2011 Céondo Ltd and contributors.
--
-- InDefero is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.
--
-- InDefero is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
--
-- ***** END LICENSE BLOCK *****
---- Load local hooks if they exist.
-- The way this is supposed to work is that hooks.d can contain symbolic
-- links to lua scripts. These links MUST have the extension .lua
-- If the script needs some configuration, a corresponding file with
-- the extension .conf is the right spot.
----
-- First load the configuration of the hooks, if applicable
includedirpattern(get_confdir() .. "/hooks.d/","*.conf")
-- Then load the hooks themselves
includedirpattern(get_confdir() .. "/hooks.d/","*.lua")

View File

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

View File

@ -87,6 +87,36 @@ class IDF_Plugin_SyncSvn
escapeshellarg($svn_path.'/'.$shortname));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$ll = exec($cmd, $output, $return);
if ($return != 0) {
Pluf_Log::error(array('IDF_Plugin_SyncSvn::processSvnCreate',
'Error',
array('path' => $svn_path.'/'.$shortname,
'output' => $output)));
return;
}
$p = realpath(dirname(__FILE__).'/../../../scripts/svn-post-commit');
exec(sprintf(Pluf::f('idf_exec_cmd_prefix', '').'ln -s %s %s',
escapeshellarg($p),
escapeshellarg($svn_path.'/'.$shortname.'/hooks/post-commit')),
$out, $res);
if ($res != 0) {
Pluf_Log::warn(array('IDF_Plugin_SyncSvn::processSvnCreate',
'post-commit hook creation error.',
$svn_path.'/'.$shortname.'/hooks/post-commit'));
return;
}
$p = realpath(dirname(__FILE__).'/../../../scripts/svn-post-revprop-change');
exec(sprintf(Pluf::f('idf_exec_cmd_prefix', '').'ln -s %s %s',
escapeshellarg($p),
escapeshellarg($svn_path.'/'.$shortname.'/hooks/post-revprop-change')),
$out, $res);
if ($res != 0) {
Pluf_Log::warn(array('IDF_Plugin_SyncSvn::processSvnCreate',
'post-revprop-change hook creation error.',
$svn_path.'/'.$shortname.'/hooks/post-revprop-change'));
return;
}
return ($return == 0);
}

View File

@ -39,7 +39,7 @@ class IDF_Project extends Pluf_Model
*
* @see self::isRestricted
*/
protected $_isRestricted = null;
protected $_isRestricted = null;
function init()
{
@ -52,7 +52,7 @@ class IDF_Project extends Pluf_Model
'id' =>
array(
'type' => 'Pluf_DB_Field_Sequence',
'blank' => true,
'blank' => true,
),
'name' =>
array(
@ -113,7 +113,7 @@ class IDF_Project extends Pluf_Model
return '';
}
function preSave($create=false)
{
if ($this->id == '') {
@ -181,7 +181,7 @@ class IDF_Project extends Pluf_Model
*/
public function getTagIdsByStatus($status='open', $cache_refresh=false)
{
if (!$cache_refresh
if (!$cache_refresh
and isset($this->_extra_cache['getTagIdsByStatus-'.$status])) {
return $this->_extra_cache['getTagIdsByStatus-'.$status];
}
@ -197,7 +197,7 @@ class IDF_Project extends Pluf_Model
break;
}
$tags = array();
foreach ($this->getTagsFromConfig($key, $default, 'Status') as $tag) {
foreach ($this->getTagsFromConfig($key, $default, 'Status') as $tag) {
$tags[] = (int) $tag->id;
}
$this->_extra_cache['getTagIdsByStatus-'.$status] = $tags;
@ -289,9 +289,9 @@ class IDF_Project extends Pluf_Model
if ($fmt == 'objects') {
return new Pluf_Template_ContextVars(array('members' => $members, 'owners' => $owners, 'authorized' => $authorized));
} else {
return array('members' => implode("\n", (array) $members),
return array('members' => implode("\n", (array) $members),
'owners' => implode("\n", (array) $owners),
'authorized' => implode("\n", (array) $authorized),
'authorized' => implode("\n", (array) $authorized),
);
}
}
@ -366,12 +366,13 @@ class IDF_Project extends Pluf_Model
public function getRepositorySize($force=false)
{
$last_eval = $this->getConf()->getVal('repository_size_check_date', 0);
if (!$force and $last_eval > time()-86400) {
if (Pluf::f('idf_no_size_check', false) or
(!$force and $last_eval > time()-172800)) {
return $this->getConf()->getVal('repository_size', -1);
}
$this->getConf()->setVal('repository_size_check_date', time());
$scm = IDF_Scm::get($this);
$this->getConf()->setVal('repository_size', $scm->getRepositorySize());
$this->getConf()->setVal('repository_size_check_date', time());
return $this->getConf()->getVal('repository_size', -1);
}
@ -381,15 +382,16 @@ class IDF_Project extends Pluf_Model
* This will return the right url based on the user.
*
* @param Pluf_User The user (null)
* @param string A specific commit to access
*/
public function getSourceAccessUrl($user=null)
public function getSourceAccessUrl($user=null, $commit=null)
{
$right = $this->getConf()->getVal('source_access_rights', 'all');
if (($user == null or $user->isAnonymous())
if (($user == null or $user->isAnonymous())
and $right == 'all' and !$this->private) {
return $this->getRemoteAccessUrl();
return $this->getRemoteAccessUrl($commit);
}
return $this->getWriteRemoteAccessUrl($user);
return $this->getWriteRemoteAccessUrl($user, $commit);
}
@ -397,15 +399,17 @@ class IDF_Project extends Pluf_Model
* Get the remote access url to the repository.
*
* This will always return the anonymous access url.
*
* @param string A specific commit to access
*/
public function getRemoteAccessUrl()
public function getRemoteAccessUrl($commit=null)
{
$conf = $this->getConf();
$scm = $conf->getVal('scm', 'git');
$scms = Pluf::f('allowed_scm');
Pluf::loadClass($scms[$scm]);
return call_user_func(array($scms[$scm], 'getAnonymousAccessUrl'),
$this);
$this, $commit);
}
/**
@ -414,14 +418,27 @@ class IDF_Project extends Pluf_Model
* Some SCM have a remote access URL to write which is not the
* same as the one to read. For example, you do a checkout with
* git-daemon and push with SSH.
*
* @param string A specific commit to access
*/
public function getWriteRemoteAccessUrl($user)
public function getWriteRemoteAccessUrl($user,$commit=null)
{
$conf = $this->getConf();
$scm = $conf->getVal('scm', 'git');
$scms = Pluf::f('allowed_scm');
return call_user_func(array($scms[$scm], 'getAuthAccessUrl'),
$this, $user);
$this, $user, $commit);
}
/**
* Get the post commit hook key.
*
* The goal is to get something predictable but from which one
* cannot reverse find the secret key.
*/
public function getPostCommitHookKey()
{
return md5($this->id.sha1(Pluf::f('secret_key')).$this->shortname);
}
/**
@ -433,9 +450,10 @@ class IDF_Project extends Pluf_Model
{
$conf = $this->getConf();
$roots = array(
'git' => 'master',
'svn' => 'HEAD',
'mercurial' => 'tip'
'git' => 'master',
'svn' => 'HEAD',
'mercurial' => 'tip',
'mtn' => 'h:'.$conf->getVal('mtn_master_branch', '*'),
);
$scm = $conf->getVal('scm', 'git');
return $roots[$scm];
@ -448,7 +466,7 @@ class IDF_Project extends Pluf_Model
* By convention, all the objects belonging to a project have the
* 'project' property set, so this is easy to check.
*
* @param Pluf_Model
* @param Pluf_Model
*/
public function inOr404($obj)
{
@ -505,7 +523,7 @@ class IDF_Project extends Pluf_Model
*
* [description]
*
* This signal allows an application to update the statistics
* This signal allows an application to update the statistics
* array of a project. For example to add the on disk size
* of the repository if available.
*
@ -617,7 +635,7 @@ class IDF_Project extends Pluf_Model
Pluf_Signal::send('IDF_Project::preDelete',
'IDF_Project', $params);
$what = array('IDF_Upload', 'IDF_Review', 'IDF_Issue',
'IDF_WikiPage', 'IDF_Commit',
'IDF_WikiPage', 'IDF_Commit', 'IDF_Tag',
);
foreach ($what as $m) {
foreach (Pluf::factory($m)->getList(array('filter' => 'project='.(int)$this->id)) as $item) {
@ -649,7 +667,7 @@ class IDF_Project extends Pluf_Model
);
$conf = $this->getConf();
foreach ($tabs as $tab) {
if (!in_array($conf->getVal($tab, 'all'),
if (!in_array($conf->getVal($tab, 'all'),
array('all', 'none'))) {
$this->_isRestricted = true;
return true;

222
src/IDF/Queue.php Normal file
View File

@ -0,0 +1,222 @@
<?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 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
n# 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 ***** */
/**
* Queue system for the management of asynchronous operations.
*
* Anybody can add an item to the queue and any application can
* register itself to process an item from the queue.
*
* An item in the queue is considered as fully processed when all the
* handlers have processed it successfully.
*
* To push a new item in the queue:
*
* <code>
* $item = new IDF_Queue();
* $item->type = 'new_commit';
* $item->payload = array('what', 'ever', array('data'));
* $item->create();
* </code>
*
* To process one item from the queue, you first need to register an
* handler, by adding the following in your relations.php file before
* the return statement or in your config file.
*
* <code>
* Pluf_Signal::connect('IDF_Queue::processItem',
* array('YourApp_Class', 'processItem'));
* </code>
*
* The processItem method will be called with two arguments, the first
* is the name of the signal ('IDF_Queue::processItem') and the second
* is an array with:
*
* <code>
* array('item' => $item,
* 'res' => array('OtherApp_Class::handler' => false,
* 'FooApp_Class::processItem' => true));
* </code>
*
* When you process an item, you need first to check if the type is
* corresponding to what you want to work with, then you need to check
* in 'res' if you have not already processed successfully the item,
* that is the key 'YourApp_Class::processItem' must be set to true,
* and then you can process the item. At the end of your processing,
* you need to modify by reference the 'res' key to add your status.
*
* All the data except for the type is in the payload, this makes the
* queue flexible to manage many different kind of tasks.
*
*/
class IDF_Queue extends Pluf_Model
{
public $_model = __CLASS__;
function init()
{
$this->_a['table'] = 'idf_queue';
$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,
),
'status' =>
array(
'type' => 'Pluf_DB_Field_Integer',
'blank' => false,
'choices' => array(
'pending' => 0,
'in_progress' => 1,
'need_retry' => 2,
'done' => 3,
'error' => 4,
),
'default' => 0,
),
'trials' =>
array(
'type' => 'Pluf_DB_Field_Integer',
'default' => 0,
),
'type' =>
array(
'type' => 'Pluf_DB_Field_Varchar',
'blank' => false,
'size' => 50,
),
'payload' =>
array(
'type' => 'Pluf_DB_Field_Serialized',
'blank' => false,
),
'results' =>
array(
'type' => 'Pluf_DB_Field_Serialized',
'blank' => false,
),
'lasttry_dtime' =>
array(
'type' => 'Pluf_DB_Field_Datetime',
'blank' => true,
),
'creation_dtime' =>
array(
'type' => 'Pluf_DB_Field_Datetime',
'blank' => true,
),
);
}
function preSave($create=false)
{
if ($create) {
$this->creation_dtime = gmdate('Y-m-d H:i:s');
$this->lasttry_dtime = gmdate('Y-m-d H:i:s');
$this->results = array();
$this->trials = 0;
$this->status = 0;
}
}
/**
* The current item is going to be processed.
*/
function processItem()
{
/**
* [signal]
*
* IDF_Queue::processItem
*
* [sender]
*
* IDF_Queue
*
* [description]
*
* This signal allows an application to run an asynchronous
* job. The handler gets the queue item and the results from
* the previous run. If the handler key is not set, then the
* job was not run. If set it can be either true (already done)
* or false (error at last run).
*
* [parameters]
*
* array('item' => $item, 'res' => $res)
*
*/
$params = array('item' => $this, 'res' => $this->results);
Pluf_Signal::send('IDF_Queue::processItem',
'IDF_Queue', $params);
$this->status = 3; // Success
foreach ($params['res'] as $handler=>$ok) {
if (!$ok) {
$this->status = 2; // Set to need retry
$this->trials += 1;
break;
}
}
$this->results = $params['res'];
$this->lasttry_dtime = gmdate('Y-m-d H:i:s');
$this->update();
}
/**
* Parse the queue.
*
* It is a signal handler to just hook itself at the right time in
* the cron job performing the maintainance work.
*
* The processing relies on the fact that no other processing jobs
* must run at the same time. That is, your cron job must use a
* lock file or something like to not run in parallel.
*
* The processing is simple, first get 500 queue items, mark them
* as being processed and for each of them call the processItem()
* method which will trigger another event for processing.
*
* If you are processing more than 500 items per batch, you need
* to switch to a different solution.
*
*/
public static function process($sender, &$params)
{
$where = 'status=0 OR status=2';
$items = Pluf::factory('IDF_Queue')->getList(array('filter'=>$where,
'nb'=> 500));
Pluf_Log::event(array('IDF_Queue::process', $items->count()));
foreach ($items as $item) {
$item->status = 1;
$item->update();
}
foreach ($items as $item) {
$item->status = 1;
$item->processItem();
}
}
}

View File

@ -25,12 +25,12 @@
* A comment set on a review.
*
* A comment is associated to a patch as a review can have many
* patches associated to it.
* patches associated to it.
*
* A comment is also tracking the changes in the review in the same
* way the issue comment is tracking the changes in the issue.
*
*
*
*/
class IDF_Review_Comment extends Pluf_Model
{
@ -45,9 +45,9 @@ class IDF_Review_Comment extends Pluf_Model
'id' =>
array(
'type' => 'Pluf_DB_Field_Sequence',
'blank' => true,
'blank' => true,
),
'patch' =>
'patch' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Review_Patch',
@ -61,7 +61,7 @@ class IDF_Review_Comment extends Pluf_Model
'blank' => true, // if only commented on lines
'verbose' => __('comment'),
),
'submitter' =>
'submitter' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
@ -118,8 +118,8 @@ class IDF_Review_Comment extends Pluf_Model
function postSave($create=false)
{
if ($create) {
IDF_Timeline::insert($this,
$this->get_patch()->get_review()->get_project(),
IDF_Timeline::insert($this,
$this->get_patch()->get_review()->get_project(),
$this->get_submitter());
}
}
@ -127,7 +127,7 @@ class IDF_Review_Comment extends Pluf_Model
public function timelineFragment($request)
{
$review = $this->get_patch()->get_review();
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
array($request->project->shortname,
$review->id));
$out = '<tr class="log"><td><a href="'.$url.'">'.
@ -138,14 +138,14 @@ class IDF_Review_Comment extends Pluf_Model
$ic = (in_array($review->status, $request->project->getTagIdsByStatus('closed'))) ? 'issue-c' : 'issue-o';
$out .= sprintf(__('<a href="%1$s" class="%2$s" title="View review">Review %3$d</a>, %4$s'), $url, $ic, $review->id, Pluf_esc($review->summary)).'</td>';
$out .= "\n".'<tr class="extra"><td colspan="2">
<div class="helptext right">'.sprintf(__('Update of <a href="%s" class="%s">review&nbsp;%d</a>, by %s'), $url, $ic, $review->id, $user).'</div></td></tr>';
<div class="helptext right">'.sprintf(__('Update of <a href="%s" class="%s">review&nbsp;%d</a>, by %s'), $url, $ic, $review->id, $user).'</div></td></tr>';
return Pluf_Template::markSafe($out);
}
public function feedFragment($request)
{
$review = $this->get_patch()->get_review();
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
array($request->project->shortname,
$review->id));
$title = sprintf(__('%s: Updated review %d - %s'),
@ -221,4 +221,9 @@ class IDF_Review_Comment extends Pluf_Model
}
Pluf_Translation::loadSetLocale($current_locale);
}
public function get_submitter_data()
{
return IDF_UserData::factory($this->get_submitter());
}
}

View File

@ -197,14 +197,17 @@ class IDF_Review_Patch extends Pluf_Model
);
$tmpl = new Pluf_Template('idf/review/review-created-email.txt');
$text_email = $tmpl->render($context);
$email = new Pluf_Mail(Pluf::f('from_email'),
$conf->getVal('review_notification_email'),
sprintf(__('New Code Review %s - %s (%s)'),
$this->get_review()->id,
$this->get_review()->summary,
$this->get_review()->get_project()->shortname));
$email->addTextMessage($text_email);
$email->sendMail();
$addresses = explode(';',$conf->getVal('review_notification_email'));
foreach ($addresses as $address) {
$email = new Pluf_Mail(Pluf::f('from_email'),
$address,
sprintf(__('New Code Review %s - %s (%s)'),
$this->get_review()->id,
$this->get_review()->summary,
$this->get_review()->get_project()->shortname));
$email->addTextMessage($text_email);
$email->sendMail();
}
Pluf_Translation::loadSetLocale($current_locale);
}
}

View File

@ -63,7 +63,7 @@ class IDF_Scm
public $project = null;
/**
* Cache storage.
* Cache storage.
*
* It must only be used to store data for the lifetime of the
* object. For example if you need to get the list of branches in
@ -166,13 +166,28 @@ class IDF_Scm
throw new Pluf_Exception_NotImplemented();
}
const REVISION_VALID = 0;
const REVISION_INVALID = 1;
const REVISION_AMBIGUOUS = 2;
/**
* Check if a revision or commit is valid.
* Check if a revision or commit is valid, invalid or ambiguous.
*
* @param string Revision or commit
* @return bool
* @return int One of REVISION_VALID, REVISION_INVALID or REVISION_AMBIGIOUS
*/
public function isValidRevision($rev)
public function validateRevision($rev)
{
throw new Pluf_Exception_NotImplemented();
}
/**
* Returns an array of single commit objects for ambiguous commit identifiers
*
* @param string Ambiguous commit identifier
* @return array of objects
*/
public function disambiguateRevision($commit)
{
throw new Pluf_Exception_NotImplemented();
}
@ -217,7 +232,7 @@ class IDF_Scm
* 'foo-branch' => 'branches/foo-branch',)
* </pre>
*
* @return array Branches
* @return array Branches
*/
public function getBranches()
{
@ -282,7 +297,7 @@ class IDF_Scm
* @param string Revision or commit
* @param string Folder ('/')
* @param string Branch (null)
* @return array
* @return array
*/
public function getTree($rev, $folder='/', $branch=null)
{
@ -301,6 +316,34 @@ class IDF_Scm
throw new Pluf_Exception_NotImplemented();
}
/**
* Returns all recorded changes which lead to the particular commit
* or revision.
*
* Example output:
*
* 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', ...)
* 'patches' => array('path/to/file', ...),
* 'properties' => array('path/to/file' => array(
* 'propname' => 'propvalue', 'deletedprop' => null, ...)
* ),
* ...)
* }
*
* Each member of the returned object is mandatory, but may contain
* an empty array if no changes were recorded.
*
* @param string A commit identifier
* @return object with arrays of individual changes
*/
public function getChanges($commit)
{
throw new Pluf_Exception_NotImplemented();
}
/**
* Get latest changes.
*
@ -378,13 +421,28 @@ class IDF_Scm
}
/**
* Generate the command to create a zip archive at a given commit.
* Given a changelog parsed node, returns extra data.
*
* For example, if the node is a commit object from git, it will a
* stdClass object with the parents array. The extra value could
* then be the parent(s) commit(s).
*
* @param stdClass Commit object/Parse object
* @return array Extra properties
*/
public function getExtraProperties($obj)
{
return array();
}
/**
* Generate a zip archive at a given commit, wrapped in a HTTP response, suitable for pushing to client.
*
* @param string Commit
* @param string Prefix ('repository/')
* @return string Command
* @return Pluf_HTTP_Response The HTTP Response containing the zip archive
*/
public function getArchiveCommand($commit, $prefix='repository/')
public function getArchiveStream($commit, $prefix='repository/')
{
throw new Pluf_Exception_NotImplemented();
}
@ -396,7 +454,7 @@ class IDF_Scm
public static function syncTimeline($project, $force=false)
{
$cache = Pluf_Cache::factory();
$key = 'IDF_Scm:'.$project->shortname.':lastsync';
$key = 'IDF_Scm:'.$project->shortname.':lastsync';
if ($force or null === ($res=$cache->get($key))) {
$scm = IDF_Scm::get($project);
if ($scm->isAvailable()) {

View File

@ -55,7 +55,9 @@ class IDF_Scm_Cache_Git extends Pluf_Model
$cache->project = $this->_project;
$cache->githash = $blob->hash;
$blob->title = IDF_Commit::toUTF8($blob->title);
$cache->content = $blob->date.chr(31).$blob->author.chr(31).$blob->title;
$cache->content = IDF_Commit::toUTF8($blob->date) . chr(31)
. IDF_Commit::toUTF8($blob->author) . chr(31)
. IDF_Commit::toUTF8($blob->title);
$sql = new Pluf_SQL('project=%s AND githash=%s',
array($this->_project->id, $blob->hash));
if (0 == Pluf::factory(__CLASS__)->getCount(array('filter' => $sql->gen()))) {
@ -85,6 +87,8 @@ class IDF_Scm_Cache_Git extends Pluf_Model
array($this->_project->id));
foreach (Pluf::factory(__CLASS__)->getList(array('filter' => $sql->gen())) as $blob) {
$tmp = explode(chr(31), $blob->content, 3);
// sometimes the title might be empty
if (!isset($tmp[2])) $tmp[2] = '';
$res[$blob->githash] = (object) array(
'hash' => $blob->githash,
@ -131,4 +135,4 @@ class IDF_Scm_Cache_Git extends Pluf_Model
),
);
}
}
}

View File

@ -27,13 +27,13 @@
*/
class IDF_Scm_Git extends IDF_Scm
{
public $mediumtree_fmt = 'commit %H%nAuthor: %an <%ae>%nTree: %T%nDate: %ai%n%n%s%n%n%b';
public $mediumtree_fmt = 'commit %H%nAuthor: %an <%ae>%nTree: %T%nParents: %P%nDate: %ai%n%n%s%n%n%b';
/* ============================================== *
* *
* Common Methods Implemented By All The SCMs *
* *
* ============================================== */
* ============================================== */
public function __construct($repo, $project=null)
{
@ -48,7 +48,7 @@ class IDF_Scm_Git extends IDF_Scm
}
$cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk '
.escapeshellarg($this->repo);
$out = explode(' ',
$out = explode(' ',
self::shell_exec('IDF_Scm_Git::getRepositorySize', $cmd),
2);
return (int) $out[0]*1024;
@ -70,13 +70,13 @@ class IDF_Scm_Git extends IDF_Scm
return $this->cache['branches'];
}
$cmd = Pluf::f('idf_exec_cmd_prefix', '')
.sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' branch',
.sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' branch',
escapeshellarg($this->repo));
self::exec('IDF_Scm_Git::getBranches',
self::exec('IDF_Scm_Git::getBranches',
$cmd, $out, $return);
if ($return != 0) {
throw new IDF_Scm_Exception(sprintf($this->error_tpl,
$cmd, $return,
$cmd, $return,
implode("\n", $out)));
}
$res = array();
@ -116,6 +116,14 @@ class IDF_Scm_Git extends IDF_Scm
return $this->_inObject($commit, 'branch');
}
/**
* Will find the parents if available.
*/
public function getExtraProperties($obj)
{
return (isset($obj->parents)) ? array('parents' => $obj->parents) : array();
}
/**
* @see IDF_Scm::getTags()
**/
@ -125,7 +133,7 @@ class IDF_Scm_Git extends IDF_Scm
return $this->cache['tags'];
}
$cmd = Pluf::f('idf_exec_cmd_prefix', '')
.sprintf('GIT_DIR=%s %s tag',
.sprintf('GIT_DIR=%s %s for-each-ref --format="%%(taggerdate:iso)%%(committerdate:iso) %%(objectname) %%(refname)" refs/tags',
escapeshellarg($this->repo),
Pluf::f('git_path', 'git'));
self::exec('IDF_Scm_Git::getTags', $cmd, $out, $return);
@ -134,12 +142,15 @@ class IDF_Scm_Git extends IDF_Scm
$cmd, $return,
implode("\n", $out)));
}
rsort($out);
$res = array();
foreach ($out as $b) {
if (false !== strpos($b, '/')) {
$res[$this->getCommit($b)->commit] = $b;
$elts = explode(' ', $b, 5);
$tag = substr(trim($elts[4]), 10);
if (false !== strpos($tag, '/')) {
$res[$elts[3]] = $b;
} else {
$res[$b] = '';
$res[$tag] = '';
}
}
$this->cache['tags'] = $res;
@ -216,7 +227,7 @@ class IDF_Scm_Git extends IDF_Scm
if ($folder) {
// As we are limiting to a given folder, we need to find
// the tree corresponding to this folder.
$tinfo = $this->getTreeInfo($commit, $folder);
$tinfo = $this->getTreeInfo($commit, $folder);
if (isset($tinfo[0]) and $tinfo[0]->type == 'tree') {
$tree = $tinfo[0]->hash;
} else {
@ -231,7 +242,7 @@ class IDF_Scm_Git extends IDF_Scm
// information as possible.
if ($file->type == 'blob') {
$file->date = $co->date;
$file->log = '----';
$file->log = '----';
$file->author = 'Unknown';
}
$file->fullpath = ($folder) ? $folder.'/'.$file->file : $file->file;
@ -270,13 +281,21 @@ class IDF_Scm_Git extends IDF_Scm
return null;
}
public static function getAnonymousAccessUrl($project)
public static function getAnonymousAccessUrl($project, $commit=null)
{
return sprintf(Pluf::f('git_remote_url'), $project->shortname);
}
public static function getAuthAccessUrl($project, $user)
public static function getAuthAccessUrl($project, $user, $commit=null)
{
// if the user haven't registred a public ssh key,
// he can't use the write url which use the SSH authentification
if ($user != null) {
$keys = $user->get_idf_key_list();
if (count ($keys) == 0)
return self::getAnonymousAccessUrl($project);
}
return sprintf(Pluf::f('git_write_remote_url'), $project->shortname);
}
@ -293,10 +312,12 @@ class IDF_Scm_Git extends IDF_Scm
}
public function isValidRevision($commit)
public function validateRevision($commit)
{
$type = $this->testHash($commit);
return ('commit' == $type || 'tag' == $type);
if ('commit' == $type || 'tag' == $type)
return IDF_Scm::REVISION_VALID;
return IDF_Scm::REVISION_INVALID;
}
/**
@ -320,7 +341,7 @@ class IDF_Scm_Git extends IDF_Scm
/**
* Get the tree info.
*
* @param string Tree hash
* @param string Tree hash
* @param bool Do we recurse in subtrees (true)
* @param string Folder in which we want to get the info ('')
* @return array Array of file information.
@ -332,15 +353,15 @@ class IDF_Scm_Git extends IDF_Scm
}
$cmd_tmpl = 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' ls-tree -l %s %s';
$cmd = Pluf::f('idf_exec_cmd_prefix', '')
.sprintf($cmd_tmpl, escapeshellarg($this->repo),
.sprintf($cmd_tmpl, escapeshellarg($this->repo),
escapeshellarg($tree), escapeshellarg($folder));
$out = array();
$res = array();
self::exec('IDF_Scm_Git::getTreeInfo', $cmd, $out);
foreach ($out as $line) {
list($perm, $type, $hash, $size, $file) = preg_split('/ |\t/', $line, 5, PREG_SPLIT_NO_EMPTY);
$res[] = (object) array('perm' => $perm, 'type' => $type,
'size' => $size, 'hash' => $hash,
$res[] = (object) array('perm' => $perm, 'type' => $type,
'size' => $size, 'hash' => $hash,
'file' => $file);
}
return $res;
@ -356,8 +377,8 @@ class IDF_Scm_Git extends IDF_Scm
public function getPathInfo($totest, $commit='HEAD')
{
$cmd_tmpl = 'GIT_DIR=%s '.Pluf::f('git_path', 'git').' ls-tree -r -t -l %s';
$cmd = sprintf($cmd_tmpl,
escapeshellarg($this->repo),
$cmd = sprintf($cmd_tmpl,
escapeshellarg($this->repo),
escapeshellarg($commit));
$out = array();
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
@ -366,8 +387,8 @@ class IDF_Scm_Git extends IDF_Scm
list($perm, $type, $hash, $size, $file) = preg_split('/ |\t/', $line, 5, PREG_SPLIT_NO_EMPTY);
if ($totest == $file) {
$pathinfo = pathinfo($file);
return (object) array('perm' => $perm, 'type' => $type,
'size' => $size, 'hash' => $hash,
return (object) array('perm' => $perm, 'type' => $type,
'size' => $size, 'hash' => $hash,
'fullpath' => $file,
'file' => $pathinfo['basename']);
}
@ -379,9 +400,9 @@ class IDF_Scm_Git extends IDF_Scm
{
$cmd = sprintf(Pluf::f('idf_exec_cmd_prefix', '').
'GIT_DIR=%s '.Pluf::f('git_path', 'git').' cat-file blob %s',
escapeshellarg($this->repo),
escapeshellarg($this->repo),
escapeshellarg($def->hash));
return ($cmd_only)
return ($cmd_only)
? $cmd : self::shell_exec('IDF_Scm_Git::getFile', $cmd);
}
@ -396,13 +417,13 @@ class IDF_Scm_Git extends IDF_Scm
{
if ($getdiff) {
$cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' show --date=iso --pretty=format:%s %s',
escapeshellarg($this->repo),
"'".$this->mediumtree_fmt."'",
escapeshellarg($this->repo),
"'".$this->mediumtree_fmt."'",
escapeshellarg($commit));
} else {
$cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' log -1 --date=iso --pretty=format:%s %s',
escapeshellarg($this->repo),
"'".$this->mediumtree_fmt."'",
escapeshellarg($this->repo),
"'".$this->mediumtree_fmt."'",
escapeshellarg($commit));
}
$out = array();
@ -426,11 +447,13 @@ class IDF_Scm_Git extends IDF_Scm
}
}
$out = self::parseLog($log);
$out[0]->changes = implode("\n", $change);
$out[0]->diff = implode("\n", $change);
} else {
$out = self::parseLog($out);
$out[0]->changes = '';
$out[0]->diff = '';
}
$out[0]->branch = implode(', ', $this->inBranches($commit, null));
return $out[0];
}
@ -443,8 +466,8 @@ class IDF_Scm_Git extends IDF_Scm
public function isCommitLarge($commit='HEAD')
{
$cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' log --numstat -1 --pretty=format:%s %s',
escapeshellarg($this->repo),
"'commit %H%n'",
escapeshellarg($this->repo),
"'commit %H%n'",
escapeshellarg($commit));
$out = array();
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
@ -477,7 +500,7 @@ class IDF_Scm_Git extends IDF_Scm
if ($n === null) $n = '';
else $n = ' -'.$n;
$cmd = sprintf('GIT_DIR=%s '.Pluf::f('git_path', 'git').' log%s --date=iso --pretty=format:\'%s\' %s',
escapeshellarg($this->repo), $n, $this->mediumtree_fmt,
escapeshellarg($this->repo), $n, $this->mediumtree_fmt,
escapeshellarg($commit));
$out = array();
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
@ -503,6 +526,9 @@ class IDF_Scm_Git extends IDF_Scm
$c['full_message'] = trim($c['full_message']);
$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']));
}
$res[] = (object) $c;
}
$c = array();
@ -538,17 +564,21 @@ class IDF_Scm_Git extends IDF_Scm
$c['full_message'] = !empty($c['full_message']) ? trim($c['full_message']) : '';
$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']));
}
$res[] = (object) $c;
return $res;
}
public function getArchiveCommand($commit, $prefix='repository/')
public function getArchiveStream($commit, $prefix='repository/')
{
return sprintf(Pluf::f('idf_exec_cmd_prefix', '').
$cmd = sprintf(Pluf::f('idf_exec_cmd_prefix', '').
'GIT_DIR=%s '.Pluf::f('git_path', 'git').' archive --format=zip --prefix=%s %s',
escapeshellarg($this->repo),
escapeshellarg($prefix),
escapeshellarg($commit));
return new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
}
/*
@ -683,7 +713,7 @@ class IDF_Scm_Git extends IDF_Scm
/**
* Build the blob info cache.
*
* We build the blob info cache 500 commits at a time.
* We build the blob info cache 500 commits at a time.
*/
public function buildBlobInfoCache()
{
@ -737,7 +767,7 @@ class IDF_Scm_Git extends IDF_Scm
/**
* Cache blob info.
*
*
* Given a series of blob info, cache them.
*
* @param array Blob info
@ -765,7 +795,7 @@ class IDF_Scm_Git extends IDF_Scm
foreach ($data as $rec) {
if (isset($hashes[substr($rec, 0, 40)])) {
$tmp = explode(chr(31), substr($rec, 40), 3);
$res[substr($rec, 0, 40)] =
$res[substr($rec, 0, 40)] =
(object) array('hash' => substr($rec, 0, 40),
'date' => $tmp[0],
'title' => $tmp[2],
@ -777,7 +807,7 @@ class IDF_Scm_Git extends IDF_Scm
/**
* File cache blob info.
*
*
* Given a series of blob info, cache them.
*
* @param array Blob info
@ -792,13 +822,13 @@ class IDF_Scm_Git extends IDF_Scm
}
$data = implode(chr(30), $data).chr(30);
$cache = Pluf::f('tmp_folder').'/IDF_Scm_Git-'.md5($this->repo).'.cache.db';
$fp = fopen($cache, 'ab');
$fp = fopen($cache, 'ab');
if ($fp) {
flock($fp, LOCK_EX);
flock($fp, LOCK_EX);
fwrite($fp, $data, strlen($data));
fclose($fp); // releases the lock too
return true;
}
return false;
}
}
}

View File

@ -37,9 +37,9 @@ class IDF_Scm_Mercurial extends IDF_Scm
{
$cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk '
.escapeshellarg($this->repo);
$out = explode(' ',
$out = explode(' ',
self::shell_exec('IDF_Scm_Mercurial::getRepositorySize',
$cmd),
$cmd),
2);
return (int) $out[0]*1024;
}
@ -77,24 +77,29 @@ class IDF_Scm_Mercurial extends IDF_Scm
return 'tip';
}
public static function getAnonymousAccessUrl($project)
public static function getAnonymousAccessUrl($project, $commit=null)
{
return sprintf(Pluf::f('mercurial_remote_url'), $project->shortname);
}
public static function getAuthAccessUrl($project, $user)
public static function getAuthAccessUrl($project, $user, $commit=null)
{
return sprintf(Pluf::f('mercurial_remote_url'), $project->shortname);
}
public function isValidRevision($rev)
public function validateRevision($rev)
{
$cmd = sprintf(Pluf::f('hg_path', 'hg').' log -R %s -r %s',
escapeshellarg($this->repo),
escapeshellarg($rev));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
self::exec('IDF_Scm_Mercurial::isValidRevision', $cmd, $out, $ret);
return ($ret == 0) && (count($out) > 0);
// FIXME: apparently a given hg revision can also be ambigious -
// handle this case here sometime
if ($ret == 0 && count($out) > 0)
return IDF_Scm::REVISION_VALID;
return IDF_Scm::REVISION_INVALID;
}
/**
@ -109,11 +114,11 @@ class IDF_Scm_Mercurial extends IDF_Scm
$cmd = sprintf(Pluf::f('hg_path', 'hg').' log -R %s -r %s',
escapeshellarg($this->repo),
escapeshellarg($hash));
$ret = 0;
$ret = 0;
$out = array();
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
self::exec('IDF_Scm_Mercurial::testHash', $cmd, $out, $ret);
return ($ret != 0) ? false : 'commit';
return ($ret != 0) ? false : 'commit';
}
public function getTree($commit, $folder='/', $branch=null)
@ -130,7 +135,7 @@ class IDF_Scm_Mercurial extends IDF_Scm
$found = true;
break;
}
}
}
if (!$found) {
throw new Exception(sprintf(__('Folder %1$s not found in commit %2$s.'), $folder, $commit));
}
@ -142,7 +147,7 @@ class IDF_Scm_Mercurial extends IDF_Scm
/**
* Get the tree info.
*
* @param string Tree hash
* @param string Tree hash
* @param bool Do we recurse in subtrees (true)
* @return array Array of file information.
*/
@ -152,7 +157,7 @@ class IDF_Scm_Mercurial extends IDF_Scm
throw new Exception(sprintf(__('Not a valid tree: %s.'), $tree));
}
$cmd_tmpl = Pluf::f('hg_path', 'hg').' manifest -R %s --debug -r %s';
$cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), $tree, ($recurse) ? '' : '');
$cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), $tree, ($recurse) ? '' : '');
$out = array();
$res = array();
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
@ -192,7 +197,7 @@ class IDF_Scm_Mercurial extends IDF_Scm
}
$fullpath = ($folder) ? $folder.'/'.$file : $file;
$efullpath = self::smartEncode($fullpath);
$res[] = (object) array('perm' => $perm, 'type' => $type,
$res[] = (object) array('perm' => $perm, 'type' => $type,
'hash' => $hash, 'fullpath' => $fullpath,
'efullpath' => $efullpath, 'file' => $file);
}
@ -202,7 +207,7 @@ class IDF_Scm_Mercurial extends IDF_Scm
public function getPathInfo($totest, $commit='tip')
{
$cmd_tmpl = Pluf::f('hg_path', 'hg').' manifest -R %s --debug -r %s';
$cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), $commit);
$cmd = sprintf($cmd_tmpl, escapeshellarg($this->repo), $commit);
$out = array();
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
self::exec('IDF_Scm_Mercurial::getPathInfo', $cmd, $out);
@ -219,8 +224,8 @@ class IDF_Scm_Mercurial extends IDF_Scm
$tmp .= $dir[$i];
if ($tmp == $totest) {
$pathinfo = pathinfo($totest);
return (object) array('perm' => '000', 'type' => 'tree',
'hash' => $hash,
return (object) array('perm' => '000', 'type' => 'tree',
'hash' => $hash,
'fullpath' => $totest,
'file' => $pathinfo['basename'],
'commit' => $commit
@ -239,8 +244,8 @@ class IDF_Scm_Mercurial extends IDF_Scm
}
if ($totest == $file) {
$pathinfo = pathinfo($totest);
return (object) array('perm' => $perm, 'type' => $type,
'hash' => $hash,
return (object) array('perm' => $perm, 'type' => $type,
'hash' => $hash,
'fullpath' => $totest,
'file' => $pathinfo['basename'],
'commit' => $commit
@ -249,15 +254,15 @@ class IDF_Scm_Mercurial extends IDF_Scm
}
return false;
}
public function getFile($def, $cmd_only=false)
{
$cmd = sprintf(Pluf::f('hg_path', 'hg').' cat -R %s -r %s %s',
escapeshellarg($this->repo),
escapeshellarg($def->commit),
escapeshellarg($this->repo),
escapeshellarg($def->commit),
escapeshellarg($this->repo.'/'.$def->fullpath));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
return ($cmd_only) ?
return ($cmd_only) ?
$cmd : self::shell_exec('IDF_Scm_Mercurial::getFile', $cmd);
}
@ -272,7 +277,7 @@ class IDF_Scm_Mercurial extends IDF_Scm
return $this->cache['branches'];
}
$out = array();
$cmd = sprintf(Pluf::f('hg_path', 'hg').' branches -R %s',
$cmd = sprintf(Pluf::f('hg_path', 'hg').' branches -R %s',
escapeshellarg($this->repo));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
self::exec('IDF_Scm_Mercurial::getBranches', $cmd, $out);
@ -296,7 +301,7 @@ class IDF_Scm_Mercurial extends IDF_Scm
return $this->cache['tags'];
}
$out = array();
$cmd = sprintf(Pluf::f('hg_path', 'hg').' tags -R %s',
$cmd = sprintf(Pluf::f('hg_path', 'hg').' tags -R %s',
escapeshellarg($this->repo));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
self::exec('IDF_Scm_Mercurial::getTags', $cmd, $out);
@ -311,13 +316,13 @@ class IDF_Scm_Mercurial extends IDF_Scm
public function inBranches($commit, $path)
{
return (in_array($commit, array_keys($this->getBranches())))
return (in_array($commit, array_keys($this->getBranches())))
? array($commit) : array();
}
public function inTags($commit, $path)
{
return (in_array($commit, array_keys($this->getTags())))
return (in_array($commit, array_keys($this->getTags())))
? array($commit) : array();
}
@ -333,9 +338,9 @@ class IDF_Scm_Mercurial extends IDF_Scm
if (!$this->isValidRevision($commit)) {
return false;
}
$tmpl = ($getdiff) ?
$tmpl = ($getdiff) ?
Pluf::f('hg_path', 'hg').' log -p -r %s -R %s' : Pluf::f('hg_path', 'hg').' log -r %s -R %s';
$cmd = sprintf($tmpl,
$cmd = sprintf($tmpl,
escapeshellarg($commit), escapeshellarg($this->repo));
$out = array();
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
@ -354,7 +359,7 @@ class IDF_Scm_Mercurial extends IDF_Scm
}
}
$out = self::parseLog($log, 6);
$out[0]->changes = implode("\n", $change);
$out[0]->diff = implode("\n", $change);
return $out[0];
}
@ -411,7 +416,7 @@ class IDF_Scm_Mercurial extends IDF_Scm
$c['full_message'] = '';
$i=1;
continue;
}
if ($i == $hdrs) {
$c['title'] = trim($line);
@ -424,6 +429,8 @@ class IDF_Scm_Mercurial extends IDF_Scm
$c['author'] = $match[2];
} elseif ($match[1] == 'summary') {
$c['title'] = $match[2];
} elseif ($match[1] == 'branch') {
$c['branch'] = $match[2];
} else {
$c[$match[1]] = trim($match[2]);
}
@ -438,23 +445,25 @@ class IDF_Scm_Mercurial extends IDF_Scm
}
}
$c['tree'] = !empty($c['commit']) ? trim($c['commit']) : '';
$c['branch'] = empty($c['branch']) ? 'default' : $c['branch'];
$c['full_message'] = !empty($c['full_message']) ? trim($c['full_message']) : '';
$res[] = (object) $c;
return $res;
}
/**
* Generate the command to create a zip archive at a given commit.
* Generate a zip archive at a given commit.
*
* @param string Commit
* @param string Prefix ('git-repo-dump')
* @return string Command
* @return Pluf_HTTP_Response The HTTP response containing the zip archive
*/
public function getArchiveCommand($commit, $prefix='')
protected function getArchiveStream($commit, $prefix='')
{
return sprintf(Pluf::f('idf_exec_cmd_prefix', '').
$cmd = sprintf(Pluf::f('idf_exec_cmd_prefix', '').
Pluf::f('hg_path', 'hg').' archive --type=zip -R %s -r %s -',
escapeshellarg($this->repo),
escapeshellarg($commit));
return new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
}
}

798
src/IDF/Scm/Monotone.php Normal file
View File

@ -0,0 +1,798 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Monotone scm class
*
* @author Thomas Keller <me@thomaskeller.biz>
*/
class IDF_Scm_Monotone extends IDF_Scm
{
/** the minimum supported interface version */
public static $MIN_INTERFACE_VERSION = 12.0;
private $stdio;
private static $instances = array();
/**
* @see IDF_Scm::__construct()
*/
public function __construct($project)
{
$this->project = $project;
$this->stdio = new IDF_Scm_Monotone_Stdio($project);
}
/**
* Returns the stdio instance in use
*
* @return IDF_Scm_Monotone_Stdio
*/
public function getStdio()
{
return $this->stdio;
}
/**
* @see IDF_Scm::getRepositorySize()
*/
public function getRepositorySize()
{
// FIXME: this obviously won't work with remote databases - upstream
// needs to implement mtn db info in automate at first
$repo = sprintf(Pluf::f('mtn_repositories'), $this->project->shortname);
if (!file_exists($repo)) {
return 0;
}
$cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk '
.escapeshellarg($repo);
$out = explode(' ',
self::shell_exec('IDF_Scm_Monotone::getRepositorySize', $cmd),
2);
return (int) $out[0]*1024;
}
/**
* @see IDF_Scm::isAvailable()
*/
public function isAvailable()
{
try
{
$out = $this->stdio->exec(array('interface_version'));
return floatval($out) >= self::$MIN_INTERFACE_VERSION;
}
catch (IDF_Scm_Exception $e) {}
return false;
}
/**
* @see IDF_Scm::getBranches()
*/
public function getBranches()
{
if (isset($this->cache['branches'])) {
return $this->cache['branches'];
}
// FIXME: we could / should introduce handling of suspended
// (i.e. dead) branches here by hiding them from the user's eye...
$out = $this->stdio->exec(array('branches'));
// note: we could expand each branch with one of its head revisions
// here, but these would soon become bogus anyway and we cannot
// map multiple head revisions here either, so we just use the
// selector as placeholder
$res = array();
foreach (preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY) as $b) {
$res["h:$b"] = $b;
}
$this->cache['branches'] = $res;
return $res;
}
/**
* monotone has no concept of a "main" branch, so just return
* the configured one. Ensure however that we can select revisions
* with it at all.
*
* @see IDF_Scm::getMainBranch()
*/
public function getMainBranch()
{
$conf = $this->project->getConf();
if (false === ($branch = $conf->getVal('mtn_master_branch', false))
|| empty($branch)) {
$branch = "*";
}
return $branch;
}
/**
* @see IDF_Scm::getArchiveStream
*/
public function getArchiveStream($commit, $prefix='repository/')
{
$revs = $this->_resolveSelector($commit);
// sanity: this should actually not happen, because the
// revision is validated before already
if (count($revs) == 0) {
return new Pluf_HTTP_Response_NotFound();
}
return new IDF_Scm_Monotone_ZipRender($this->stdio, $revs[0]);
}
/**
* expands a selector or a partial revision id to zero, one or
* multiple 40 byte revision ids
*
* @param string $selector
* @return array
*/
private function _resolveSelector($selector)
{
$out = $this->stdio->exec(array('select', $selector));
return preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY);
}
/**
* Queries the certs for a given revision and returns them in an
* associative array array("branch" => array("branch1", ...), ...)
*
* @param string
* @param array
*/
private function _getCerts($rev)
{
$cache = Pluf_Cache::factory();
$cachekey = 'mtn-plugin-certs-for-rev-' . $rev;
$certs = $cache->get($cachekey);
if ($certs === null) {
$out = $this->stdio->exec(array('certs', $rev));
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
$certs = array();
foreach ($stanzas as $stanza) {
$certname = null;
foreach ($stanza as $stanzaline) {
// luckily, name always comes before value
if ($stanzaline['key'] == 'name') {
$certname = $stanzaline['values'][0];
continue;
}
if ($stanzaline['key'] == 'value') {
if (!array_key_exists($certname, $certs)) {
$certs[$certname] = array();
}
$certs[$certname][] = $stanzaline['values'][0];
break;
}
}
}
$cache->set($cachekey, $certs);
}
return $certs;
}
/**
* Returns unique certificate values for the given revs and the specific
* cert name, optionally prefixed with $prefix
*
* @param array
* @param string
* @param string
* @return array
*/
private function _getUniqueCertValuesFor($revs, $certName, $prefix)
{
$certValues = array();
foreach ($revs as $rev) {
$certs = $this->_getCerts($rev);
if (!array_key_exists($certName, $certs))
continue;
foreach ($certs[$certName] as $certValue) {
$certValues[] = "$prefix$certValue";
}
}
return array_unique($certValues);
}
/**
* @see IDF_Scm::inBranches()
*/
public function inBranches($commit, $path)
{
$revs = $this->_resolveSelector($commit);
if (count($revs) == 0) return array();
return $this->_getUniqueCertValuesFor($revs, 'branch', 'h:');
}
/**
* @see IDF_Scm::getTags()
*/
public function getTags()
{
if (isset($this->cache['tags'])) {
return $this->cache['tags'];
}
$out = $this->stdio->exec(array('tags'));
$tags = array();
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
foreach ($stanzas as $stanza) {
$tagname = null;
foreach ($stanza as $stanzaline) {
// revision comes directly after the tag stanza
if ($stanzaline['key'] == 'tag') {
$tagname = $stanzaline['values'][0];
continue;
}
if ($stanzaline['key'] == 'revision') {
// FIXME: warn if multiple revisions have
// equally named tags
if (!array_key_exists("t:$tagname", $tags)) {
$tags["t:$tagname"] = $tagname;
}
break;
}
}
}
$this->cache['tags'] = $tags;
return $tags;
}
/**
* @see IDF_Scm::inTags()
*/
public function inTags($commit, $path)
{
$revs = $this->_resolveSelector($commit);
if (count($revs) == 0) return array();
return $this->_getUniqueCertValuesFor($revs, 'tag', 't:');
}
/**
* Takes a single stanza coming from an extended manifest output
* and converts it into a file structure used by IDF
*
* @param string $forceBasedir If given then the element's path is checked
* to be directly beneath the given directory.
* If not, null is returned and the parsing is
* aborted.
* @return array | null
*/
private function _fillFileEntry(array $manifestEntry, $forceBasedir = null)
{
$fullpath = $manifestEntry[0]['values'][0];
$filename = basename($fullpath);
$dirname = dirname($fullpath);
$dirname = $dirname == '.' ? '' : $dirname;
if ($forceBasedir !== null && $forceBasedir != $dirname) {
return null;
}
$file = array();
$file['file'] = $filename;
$file['fullpath'] = $fullpath;
$file['efullpath'] = self::smartEncode($fullpath);
$wanted_mark = '';
if ($manifestEntry[0]['key'] == 'dir') {
$file['type'] = 'tree';
$file['size'] = 0;
$wanted_mark = 'path_mark';
}
else {
$file['type'] = 'blob';
$file['hash'] = $manifestEntry[1]['hash'];
$size = 0;
foreach ($manifestEntry as $line) {
if ($line['key'] == 'size') {
$size = $line['values'][0];
break;
}
}
$file['size'] = $size;
$wanted_mark = 'content_mark';
}
$rev_mark = null;
foreach ($manifestEntry as $line) {
if ($line['key'] == $wanted_mark) {
$rev_mark = $line['hash'];
break;
}
}
if ($rev_mark !== null) {
$file['rev'] = $rev_mark;
$certs = $this->_getCerts($rev_mark);
// FIXME: this assumes that author, date and changelog are always given
$file['author'] = implode(", ", $certs['author']);
$dates = array();
foreach ($certs['date'] as $date)
$dates[] = date('Y-m-d H:i:s', strtotime($date));
$file['date'] = implode(', ', $dates);
$combinedChangelog = implode("\n---\n", $certs['changelog']);
$split = preg_split("/[\n\r]/", $combinedChangelog, 2);
// FIXME: the complete log message is currently not used in the
// tree view (the same is true for the other SCM implementations)
// but we _should_ really use or at least return that here
// in case we want to do fancy stuff like described in
// issue 492
$file['log'] = $split[0];
}
return $file;
}
/**
* @see IDF_Scm::getTree()
*/
public function getTree($commit, $folder='/', $branch=null)
{
$revs = $this->_resolveSelector($commit);
if (count($revs) == 0) {
return array();
}
$out = $this->stdio->exec(array(
'get_extended_manifest_of', $revs[0]
));
$files = array();
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
$folder = $folder == '/' || empty($folder) ? '' : $folder;
foreach ($stanzas as $stanza) {
if ($stanza[0]['key'] == 'format_version')
continue;
$file = $this->_fillFileEntry($stanza, $folder);
if ($file === null)
continue;
$files[] = (object) $file;
}
return $files;
}
/**
* @see IDF_Scm::findAuthor()
*/
public function findAuthor($author)
{
// We extract anything which looks like an email.
$match = array();
if (!preg_match('/([^ ]+@[^ ]+)/', $author, $match)) {
return null;
}
foreach (array('email', 'login') as $what) {
$sql = new Pluf_SQL($what.'=%s', array($match[1]));
$users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen()));
if ($users->count() > 0) {
return $users[0];
}
}
return null;
}
/**
* @see IDF_Scm::getAnonymousAccessUrl()
*/
public static function getAnonymousAccessUrl($project, $commit = null)
{
$scm = IDF_Scm::get($project);
$branch = $scm->getMainBranch();
if (!empty($commit)) {
$revs = $scm->_resolveSelector($commit);
if (count($revs) > 0) {
$certs = $scm->_getCerts($revs[0]);
// for the very seldom case that a revision
// has no branch certificate
if (!array_key_exists('branch', $certs)) {
$branch = '*';
}
else
{
$branch = $certs['branch'][0];
}
}
}
$remote_url = Pluf::f('mtn_remote_url', '');
if (empty($remote_url)) {
return '';
}
return sprintf($remote_url, $project->shortname).'?'.$branch;
}
/**
* @see IDF_Scm::getAuthAccessUrl()
*/
public static function getAuthAccessUrl($project, $user, $commit = null)
{
$url = self::getAnonymousAccessUrl($project, $commit);
return preg_replace("#^ssh://#", "ssh://$user@", $url);
}
/**
* Returns this object correctly initialized for the project.
*
* @param IDF_Project
* @return IDF_Scm_Monotone
*/
public static function factory($project)
{
if (!array_key_exists($project->shortname, self::$instances)) {
self::$instances[$project->shortname] =
new IDF_Scm_Monotone($project);
}
return self::$instances[$project->shortname];
}
/**
* @see IDF_Scm::validateRevision()
*/
public function validateRevision($commit)
{
$revs = $this->_resolveSelector($commit);
if (count($revs) == 0)
return IDF_Scm::REVISION_INVALID;
if (count($revs) > 1)
return IDF_Scm::REVISION_AMBIGUOUS;
return IDF_Scm::REVISION_VALID;
}
/**
* @see IDF_Scm::disambiguateRevision
*/
public function disambiguateRevision($commit)
{
$revs = $this->_resolveSelector($commit);
$out = array();
foreach ($revs as $rev)
{
$certs = $this->_getCerts($rev);
$log = array();
$log['author'] = implode(', ', $certs['author']);
$log['branch'] = implode(', ', $certs['branch']);
$dates = array();
foreach ($certs['date'] as $date)
$dates[] = date('Y-m-d H:i:s', strtotime($date));
$log['date'] = implode(', ', $dates);
$combinedChangelog = implode("\n---\n", $certs['changelog']);
$split = preg_split("/[\n\r]/", $combinedChangelog, 2);
$log['title'] = $split[0];
$log['full_message'] = (isset($split[1])) ? trim($split[1]) : '';
$log['commit'] = $rev;
$out[] = (object)$log;
}
return $out;
}
/**
* @see IDF_Scm::getPathInfo()
*/
public function getPathInfo($file, $commit = null)
{
if ($commit === null) {
$commit = 'h:' . $this->getMainBranch();
}
$revs = $this->_resolveSelector($commit);
if (count($revs) == 0)
return false;
$out = $this->stdio->exec(array(
'get_extended_manifest_of', $revs[0]
));
$files = array();
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
foreach ($stanzas as $stanza) {
if ($stanza[0]['key'] == 'format_version')
continue;
if ($stanza[0]['values'][0] != $file)
continue;
$file = $this->_fillFileEntry($stanza);
return (object) $file;
}
return false;
}
/**
* @see IDF_Scm::getFile()
*/
public function getFile($def, $cmd_only=false)
{
// this won't work with remote databases
if ($cmd_only) {
throw new Pluf_Exception_NotImplemented();
}
return $this->stdio->exec(array('get_file', $def->hash));
}
/**
* Returns the differences between two revisions as unified diff
*
* @param string The target of the diff
* @param string The source of the diff, if not given, the first
* parent of the target is used
* @return string
*/
private function _getDiff($target, $source = null)
{
if (empty($source)) {
$source = "p:$target";
}
// FIXME: add real support for merge revisions here which have
// two distinct diff sets
$targets = $this->_resolveSelector($target);
$sources = $this->_resolveSelector($source);
if (count($targets) == 0 || count($sources) == 0) {
return '';
}
// if target contains a root revision, we cannot produce a diff
if (empty($sources[0])) {
return '';
}
return $this->stdio->exec(
array('content_diff'),
array('r' => array($sources[0], $targets[0]))
);
}
/**
* @see IDF_Scm::getChanges()
*/
public function getChanges($commit)
{
$revs = $this->_resolveSelector($commit);
if (count($revs) == 0)
return null;
$revision = $revs[0];
$out = $this->stdio->exec(array('get_revision', $revision));
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
$return = (object) array(
'additions' => array(),
'deletions' => array(),
'renames' => array(),
'patches' => array(),
'properties' => array(),
);
foreach ($stanzas as $stanza) {
if ($stanza[0]['key'] == 'format_version' ||
$stanza[0]['key'] == 'old_revision' ||
$stanza[0]['key'] == 'new_manifest')
continue;
if ($stanza[0]['key'] == 'add_file' ||
$stanza[0]['key'] == 'add_dir') {
$return->additions[] = $stanza[0]['values'][0];
continue;
}
if ($stanza[0]['key'] == 'delete') {
$return->deletions[] = $stanza[0]['values'][0];
continue;
}
if ($stanza[0]['key'] == 'rename') {
$return->renames[$stanza[0]['values'][0]] =
$stanza[1]['values'][0];
continue;
}
if ($stanza[0]['key'] == 'patch') {
$return->patches[] = $stanza[0]['values'][0];
continue;
}
if ($stanza[0]['key'] == 'clear' ||
$stanza[0]['key'] == 'set') {
$filename = $stanza[0]['values'][0];
if (!array_key_exists($filename, $return->properties)) {
$return->properties[$filename] = array();
}
$key = $stanza[1]['values'][0];
$value = null;
if (isset($stanza[2])) {
$value = $stanza[2]['values'][0];
}
$return->properties[$filename][$key] = $value;
continue;
}
}
return $return;
}
/**
* @see IDF_Scm::getCommit()
*/
public function getCommit($commit, $getdiff=false)
{
$revs = $this->_resolveSelector($commit);
if (count($revs) == 0)
return array();
$res = array();
$parents = $this->stdio->exec(array('parents', $revs[0]));
$res['parents'] = preg_split("/\n/", $parents, -1, PREG_SPLIT_NO_EMPTY);
$certs = $this->_getCerts($revs[0]);
// FIXME: this assumes that author, date and changelog are always given
$res['author'] = implode(', ', $certs['author']);
$dates = array();
foreach ($certs['date'] as $date)
$dates[] = date('Y-m-d H:i:s', strtotime($date));
$res['date'] = implode(', ', $dates);
$combinedChangelog = implode("\n---\n", $certs['changelog']);
$split = preg_split("/[\n\r]/", $combinedChangelog, 2);
$res['title'] = $split[0];
$res['full_message'] = (isset($split[1])) ? trim($split[1]) : '';
$res['branch'] = implode(', ', $certs['branch']);
$res['commit'] = $revs[0];
$res['diff'] = ($getdiff) ? $this->_getDiff($revs[0]) : '';
return (object) $res;
}
/**
* @see IDF_Scm::getExtraProperties
*/
public function getExtraProperties($obj)
{
return (isset($obj->parents)) ? array('parents' => $obj->parents) : array();
}
/**
* @see IDF_Scm::isCommitLarge()
*/
public function isCommitLarge($commit=null)
{
if (empty($commit)) {
$commit = 'h:'.$this->getMainBranch();
}
$revs = $this->_resolveSelector($commit);
if (count($revs) == 0)
return false;
$out = $this->stdio->exec(array(
'get_revision', $revs[0]
));
$newAndPatchedFiles = 0;
$stanzas = IDF_Scm_Monotone_BasicIO::parse($out);
foreach ($stanzas as $stanza) {
if ($stanza[0]['key'] == 'patch' || $stanza[0]['key'] == 'add_file')
$newAndPatchedFiles++;
}
return $newAndPatchedFiles > 100;
}
/**
* @see IDF_Scm::getChangeLog()
*/
public function getChangeLog($commit=null, $n=10)
{
$horizont = $this->_resolveSelector($commit);
$initialBranches = array();
$logs = array();
while (!empty($horizont) && $n > 0) {
if (count($horizont) > 1) {
$out = $this->stdio->exec(array('toposort') + $horizont);
$horizont = preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY);
}
$rev = array_shift($horizont);
$certs = $this->_getCerts($rev);
// read in the initial branches we should follow
if (count($initialBranches) == 0) {
if (!isset($certs['branch'])) {
// this revision has no branch cert, we cannot start logging
// from this revision
continue;
}
$initialBranches = $certs['branch'];
}
// only add it to our log if it is on one of the initial branches
// ignore revisions without any branch certificate
if (count(array_intersect($initialBranches, (array)@$certs['branch'])) > 0) {
--$n;
$log = array();
$log['author'] = implode(', ', $certs['author']);
$dates = array();
foreach ($certs['date'] as $date)
$dates[] = date('Y-m-d H:i:s', strtotime($date));
$log['date'] = implode(', ', $dates);
$combinedChangelog = implode("\n---\n", $certs['changelog']);
$split = preg_split("/[\n\r]/", $combinedChangelog, 2);
$log['title'] = $split[0];
$log['full_message'] = (isset($split[1])) ? trim($split[1]) : '';
$log['commit'] = $rev;
$logs[] = (object)$log;
$out = $this->stdio->exec(array('parents', $rev));
$horizont += preg_split("/\n/", $out, -1, PREG_SPLIT_NO_EMPTY);
}
}
return $logs;
}
}

View File

@ -0,0 +1,174 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Utility class to parse and compile basic_io stanzas
*
* @author Thomas Keller <me@thomaskeller.biz>
*/
class IDF_Scm_Monotone_BasicIO
{
/**
* Parses monotone's basic_io format
*
* @param string $in
* @return array of arrays
*/
public static function parse($in)
{
$pos = 0;
$stanzas = array();
$length = strlen($in);
while ($pos < $length) {
$stanza = array();
while ($pos < $length) {
if ($in[$pos] == "\n") break;
$stanzaLine = array('key' => '', 'values' => array(), 'hash' => null);
while ($pos < $length) {
$ch = $in[$pos];
if ($ch == '"' || $ch == '[') break;
++$pos;
if ($ch == ' ') continue;
$stanzaLine['key'] .= $ch;
}
// symbol w/o a value list
if ($pos >= $length || $in[$pos] == "\n") break;
if ($in[$pos] == '[') {
unset($stanzaLine['values']);
++$pos; // opening square bracket
$stanzaLine['hash'] = substr($in, $pos, 40);
$pos += 40;
++$pos; // closing square bracket
}
else
{
unset($stanzaLine['hash']);
$valCount = 0;
// if hashs and plain values are encountered in the same
// value list, we add the hash values as simple values as well
while ($in[$pos] == '"' || $in[$pos] == '[') {
$isHashValue = $in[$pos] == '[';
++$pos; // opening quote / bracket
$stanzaLine['values'][$valCount] = '';
while ($pos < $length) {
$ch = $in[$pos]; $pr = $in[$pos-1];
if (($isHashValue && $ch == ']')
||(!$isHashValue && $ch == '"' && $pr != '\\'))
break;
++$pos;
$stanzaLine['values'][$valCount] .= $ch;
}
++$pos; // closing quote
if (!$isHashValue) {
$stanzaLine['values'][$valCount] = str_replace(
array("\\\\", "\\\""),
array("\\", "\""),
$stanzaLine['values'][$valCount]
);
}
if ($pos >= $length)
break;
if ($in[$pos] == ' ') {
++$pos; // space
++$valCount;
}
}
}
$stanza[] = $stanzaLine;
++$pos; // newline
}
$stanzas[] = $stanza;
++$pos; // newline
}
return $stanzas;
}
/**
* Compiles monotone's basicio format
*
* @param array $in Array of arrays
* @return string
*/
public static function compile($in)
{
$out = "";
$first = true;
foreach ((array)$in as $sx => $stanza) {
if ($first)
$first = false;
else
$out .= "\n";
$maxkeylength = 0;
foreach ((array)$stanza as $lx => $line) {
if (!array_key_exists('key', $line)) {
throw new IDF_Scm_Exception(
'"key" not found in basicio stanza '.$sx.', line '.$lx
);
}
$maxkeylength = max($maxkeylength, strlen($line['key']));
}
foreach ((array)$stanza as $lx => $line) {
$out .= str_pad($line['key'], $maxkeylength, ' ', STR_PAD_LEFT);
if (array_key_exists('hash', $line)) {
$out .= ' ['.$line['hash'].']';
} else
if (array_key_exists('values', $line)) {
if (!is_array($line['values']) || count($line['values']) == 0) {
throw new IDF_Scm_Exception(
'"values" must be an array of a size >= 1 '.
'in basicio stanza '.$sx.', line '.$lx
);
}
foreach ($line['values'] as $value) {
$out .= ' "'.str_replace(
array("\\", "\""),
array("\\\\", "\\\""),
$value).'"';
}
}
else
{
throw new IDF_Scm_Exception(
'neither "hash" nor "values" found in basicio '.
'stanza '.$sx.', line '.$lx
);
}
$out .= "\n";
}
}
return $out;
}
}

View File

@ -0,0 +1,403 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Monotone stdio class
*
* Connects to a monotone process and executes commands via its
* stdio interface
*
* @author Thomas Keller <me@thomaskeller.biz>
*/
class IDF_Scm_Monotone_Stdio
{
/** this is the most recent STDIO version. The number is output
at the protocol start. Older versions of monotone (prior 0.47)
do not output it and are therefor incompatible */
public static $SUPPORTED_STDIO_VERSION = 2;
private $project;
private $proc;
private $pipes;
private $oob;
private $cmdnum;
private $lastcmd;
/**
* Constructor - starts the stdio process
*
* @param IDF_Project
*/
public function __construct(IDF_Project $project)
{
$this->project = $project;
$this->start();
}
/**
* Destructor - stops the stdio process
*/
public function __destruct()
{
$this->stop();
}
/**
* Returns a string with additional options which are passed to
* an mtn instance connecting to remote databases
*
* @return string
*/
public function _getAuthOptions()
{
$prjconf = $this->project->getConf();
$name = $prjconf->getVal('mtn_client_key_name', false);
$hash = $prjconf->getVal('mtn_client_key_hash', false);
if (!$name || !$hash) {
throw new IDF_Scm_Exception(sprintf(
__('Monotone client key name or hash not in project conf.')
));
}
$keydir = Pluf::f('tmp_folder').'/mtn-client-keys';
if (!file_exists($keydir)) {
if (!mkdir($keydir)) {
throw new IDF_Scm_Exception(sprintf(
__('The key directory %s could not be created.'), $keydir
));
}
}
// in case somebody cleaned out the cache, we restore the key here
$keyfile = $keydir . '/' . $name .'.'. $hash;
if (!file_exists($keyfile)) {
$data = $prjconf->getVal('mtn_client_key_data');
if (!file_put_contents($keyfile, $data, LOCK_EX)) {
throw new IDF_Scm_Exception(sprintf(
__('Could not write client key "%s"'), $keyfile
));
}
}
return sprintf('--keydir=%s --key=%s ',
escapeshellarg($keydir),
escapeshellarg($hash)
);
}
/**
* Starts the stdio process and resets the command counter
*/
public function start()
{
if (is_resource($this->proc))
$this->stop();
$remote_db_access = Pluf::f('mtn_db_access', 'remote') == 'remote';
$cmd = Pluf::f('idf_exec_cmd_prefix', '') .
Pluf::f('mtn_path', 'mtn') . ' ';
$opts = Pluf::f('mtn_opts', array());
foreach ($opts as $opt) {
$cmd .= sprintf('%s ', escapeshellarg($opt));
}
if ($remote_db_access) {
$cmd .= $this->_getAuthOptions();
$host = sprintf(Pluf::f('mtn_remote_url'), $this->project->shortname);
$cmd .= sprintf('automate remote_stdio %s', escapeshellarg($host));
}
else
{
$repo = sprintf(Pluf::f('mtn_repositories'), $this->project->shortname);
if (!file_exists($repo)) {
throw new IDF_Scm_Exception(
"repository file '$repo' does not exist"
);
}
$cmd .= sprintf('--db %s automate stdio', escapeshellarg($repo));
}
$descriptors = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$env = array('LANG' => 'en_US.UTF-8');
$this->proc = proc_open($cmd, $descriptors, $this->pipes,
null, $env);
if (!is_resource($this->proc)) {
throw new IDF_Scm_Exception('could not start stdio process');
}
$this->_checkVersion();
$this->cmdnum = -1;
}
/**
* Stops the stdio process and closes all pipes
*/
public function stop()
{
if (!is_resource($this->proc))
return;
fclose($this->pipes[0]);
fclose($this->pipes[1]);
fclose($this->pipes[2]);
proc_close($this->proc);
$this->proc = null;
}
/**
* select()'s on stdout and returns true as soon as we got new
* data to read, false if the select() timed out
*
* @return boolean
* @throws IDF_Scm_Exception
*/
private function _waitForReadyRead()
{
if (!is_resource($this->pipes[1]))
return false;
$read = array($this->pipes[1], $this->pipes[2]);
$write = $except = null;
$streamsChanged = stream_select(
$read, $write, $except, 0, 20000
);
if ($streamsChanged === false) {
throw new IDF_Scm_Exception(
'Could not select() on read pipe'
);
}
if ($streamsChanged == 0) {
return false;
}
return true;
}
/**
* Checks the version of the used stdio protocol
*
* @throws IDF_Scm_Exception
*/
private function _checkVersion()
{
$this->_waitForReadyRead();
$version = fgets($this->pipes[1]);
if ($version === false) {
throw new IDF_Scm_Exception(
"Could not determine stdio version, stderr is:\n".
$this->_readStderr()
);
}
if (!preg_match('/^format-version: (\d+)$/', $version, $m) ||
$m[1] != self::$SUPPORTED_STDIO_VERSION)
{
throw new IDF_Scm_Exception(
'stdio format version mismatch, expected "'.
self::$SUPPORTED_STDIO_VERSION.'", got "'.@$m[1].'"'
);
}
fgets($this->pipes[1]);
}
/**
* Writes a command to stdio
*
* @param array
* @param array
* @throws IDF_Scm_Exception
*/
private function _write(array $args, array $options = array())
{
$cmd = '';
if (count($options) > 0) {
$cmd = 'o';
foreach ($options as $k => $vals) {
if (!is_array($vals))
$vals = array($vals);
foreach ($vals as $v) {
$cmd .= strlen((string)$k) . ':' . (string)$k;
$cmd .= strlen((string)$v) . ':' . (string)$v;
}
}
$cmd .= 'e ';
}
$cmd .= 'l';
foreach ($args as $arg) {
$cmd .= strlen((string)$arg) . ':' . (string)$arg;
}
$cmd .= "e\n";
if (!fwrite($this->pipes[0], $cmd)) {
throw new IDF_Scm_Exception("could not write '$cmd' to process");
}
$this->lastcmd = $cmd;
$this->cmdnum++;
}
/**
* Reads all output from stderr and returns it
*
* @return string
*/
private function _readStderr()
{
$err = "";
while (($line = fgets($this->pipes[2])) !== false) {
$err .= $line;
}
return empty($err) ? '<empty>' : $err;
}
/**
* Reads the last output from the stdio process, parses and returns it
*
* @return string
* @throws IDF_Scm_Exception
*/
private function _readStdout()
{
$this->oob = array('w' => array(),
'p' => array(),
't' => array(),
'e' => array());
$output = "";
$errcode = 0;
while (true) {
if (!$this->_waitForReadyRead())
continue;
$data = array(0,"",0);
$idx = 0;
while (true) {
$c = fgetc($this->pipes[1]);
if ($c === false) {
throw new IDF_Scm_Exception(
"No data on stdin, stderr is:\n".
$this->_readStderr()
);
}
if ($c == ':') {
if ($idx == 2)
break;
++$idx;
continue;
}
if (is_numeric($c))
$data[$idx] = $data[$idx] * 10 + $c;
else
$data[$idx] .= $c;
}
// sanity
if ($this->cmdnum != $data[0]) {
throw new IDF_Scm_Exception(
'command numbers out of sync; expected '.
$this->cmdnum .', got '. $data[0]
);
}
$toRead = $data[2];
$buffer = "";
while ($toRead > 0) {
$buffer .= fread($this->pipes[1], $toRead);
$toRead = $data[2] - strlen($buffer);
}
switch ($data[1]) {
case 'w':
case 'p':
case 't':
case 'e':
$this->oob[$data[1]][] = $buffer;
continue;
case 'm':
$output .= $buffer;
continue;
case 'l':
$errcode = $buffer;
break 2;
}
}
if ($errcode != 0) {
throw new IDF_Scm_Exception(
"command '{$this->lastcmd}' returned error code $errcode: ".
implode(' ', $this->oob['e'])
);
}
return $output;
}
/**
* Executes a command over stdio and returns its result
*
* @param array Array of arguments
* @param array Array of options as key-value pairs. Multiple options
* can be defined in sub-arrays, like
* "r" => array("123...", "456...")
* @return string
*/
public function exec(array $args, array $options = array())
{
$this->_write($args, $options);
return $this->_readStdout();
}
/**
* Returns the last out-of-band output for a previously executed
* command as associative array with 'e' (error), 'w' (warning),
* 'p' (progress) and 't' (ticker, unparsed) as keys
*
* @return array
*/
public function getLastOutOfBandOutput()
{
return $this->oob;
}
}

View File

@ -0,0 +1,269 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
require_once(dirname(__FILE__) . "/BasicIO.php");
/**
* Connects with the admininistrative interface of usher,
* the monotone proxy. This class contains only static methods because
* there is really no state to keep between each invocation, as usher
* closes the connection after every command.
*
* @author Thomas Keller <me@thomaskeller.biz>
*/
class IDF_Scm_Monotone_Usher
{
/**
* Without giving a specific state, returns an array of all servers.
* When a state is given, the array contains only servers which are
* in the given state.
*
* @param string $state One of REMOTE, ACTIVE, WAITING, SLEEPING,
* STOPPING, STOPPED, SHUTTINGDOWN or SHUTDOWN
* @return array
*/
public static function getServerList($state = null)
{
$conn = self::_triggerCommand('LIST '.$state);
if ($conn == 'none')
return array();
return preg_split('/[ ]/', $conn);
}
/**
* Returns an array of all open connections to the given server, or to
* any server if no server is specified.
* If there are no connections to list, an empty array is returned.
*
* Example:
* array("server1" => array(
* array("address" => "192.168.1.0", "port" => "13456"),
* ...
* ),
* "server2" => ...
* )
*
* @param string $server
* @return array
*/
public static function getConnectionList($server = null)
{
$conn = self::_triggerCommand('LISTCONNECTIONS '.$server);
if ($conn == 'none')
return array();
$single_conns = preg_split('/[ ]/', $conn);
$ret = array();
foreach ($single_conns as $conn) {
preg_match('/\((\w+)\)([^:]+):(\d+)/', $conn, $matches);
$ret[$matches[1]][] = (object)array(
'server' => $matches[1],
'address' => $matches[2],
'port' => $matches[3],
);
}
if ($server !== null) {
if (array_key_exists($server, $ret))
return $ret[$server];
return array();
}
return $ret;
}
/**
* Get the status of a particular server, or of the usher as a whole if
* no server is specified.
*
* @param string $server
* @return One of REMOTE, SLEEPING, STOPPING, STOPPED for servers or
* ACTIVE, WAITING, SHUTTINGDOWN or SHUTDOWN for usher itself
*/
public static function getStatus($server = null)
{
return self::_triggerCommand('STATUS '.$server);
}
/**
* Looks up the name of the server that would be used for an incoming
* connection having the given host and pattern.
*
* @param string $host Host
* @param string $pattern Branch pattern
* @return server name
* @throws IDF_Scm_Exception
*/
public static function matchServer($host, $pattern)
{
$ret = self::_triggerCommand('MATCH '.$host.' '.$pattern);
if (preg_match('/^OK: (.+)/', $ret, $m))
return $m[1];
preg_match('/^ERROR: (.+)/', $ret, $m);
throw new IDF_Scm_Exception('could not match server: '.$m[1]);
}
/**
* Prevent the given local server from receiving further connections,
* and stop it once all connections are closed. The return value will
* be the new status of that server: ACTIVE local servers will become
* STOPPING, and WAITING and SLEEPING serveres become STOPPED.
* Servers in other states are not affected.
*
* @param string $server
* @return string State of the server after the command
*/
public static function stopServer($server)
{
return self::_triggerCommand("STOP $server");
}
/**
* Allow a STOPPED or STOPPING server to receive connections again.
* The return value is the new status of that server: STOPPING servers
* become ACTIVE, and STOPPED servers become SLEEPING. Servers in other
* states are not affected.
*
* @param string $server
* @return string State of the server after the command
*/
public static function startServer($server)
{
return self::_triggerCommand('START '.$server);
}
/**
* Immediately kill the given local server, dropping any open connections,
* and prevent is from receiving new connections and restarting. The named
* server will immediately change to state STOPPED.
*
* @param string $server
* @return bool True if successful
*/
public static function killServer($server)
{
return self::_triggerCommand('KILL_NOW '.$server) == 'ok';
}
/**
* Do not accept new connections for any servers, local or remote.
*
* @return bool True if successful
*/
public static function shutDown()
{
return self::_triggerCommand('SHUTDOWN') == 'ok';
}
/**
* Begin accepting connections after a SHUTDOWN.
*
* @return bool True if successful
*/
public static function startUp()
{
return self::_triggerCommand('STARTUP') == 'ok';
}
/**
* Reload the config file, the same as sending SIGHUP.
*
* @return bool True if successful (after the configuration was reloaded)
*/
public static function reload()
{
return self::_triggerCommand('RELOAD') == 'ok';
}
private static function _triggerCommand($cmd)
{
$uc = Pluf::f('mtn_usher_conf', false);
if (!$uc || !is_readable($uc)) {
throw new IDF_Scm_Exception(
'"mtn_usher_conf" is not configured or not readable'
);
}
$parsed_config =
IDF_Scm_Monotone_BasicIO::parse(file_get_contents($uc));
$host = $port = $user = $pass = null;
foreach ($parsed_config as $stanza) {
foreach ($stanza as $line) {
if ($line['key'] == 'adminaddr') {
list($host, $port) = explode(":", @$line['values'][0]);
break;
}
if ($line['key'] == 'userpass') {
$user = @$line['values'][0];
$pass = @$line['values'][1];
}
}
}
if (empty($host)) {
throw new IDF_Scm_Exception('usher host is empty');
}
if (!preg_match('/^\d+$/', $port))
{
throw new IDF_Scm_Exception('usher port is invalid');
}
if (empty($user)) {
throw new IDF_Scm_Exception('usher user is empty');
}
if (empty($pass)) {
throw new IDF_Scm_Exception('usher pass is empty');
}
$sock = @fsockopen($host, $port, $errno, $errstr);
if (!$sock) {
throw new IDF_Scm_Exception(
"could not connect to usher: $errstr ($errno)"
);
}
fwrite($sock, 'USERPASS '.$user.' '.$pass."\n");
if (feof($sock)) {
throw new IDF_Scm_Exception(
'usher closed the connection - this should not happen'
);
}
fwrite($sock, $cmd."\n");
$out = '';
while (!feof($sock)) {
$out .= fgets($sock);
}
fclose($sock);
$out = rtrim($out);
if ($out == 'unknown command') {
throw new IDF_Scm_Exception('unknown command: '.$cmd);
}
return $out;
}
}

View File

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

View File

@ -24,7 +24,7 @@
/**
* Subversion backend.
* When a branch is not a branch.
*
*
* Contrary to most other SCMs, Subversion is using folders to manage
* the branches and so what is either the commit or the branch in
* other SCMs is the revision number with Subversion. So, do not be
@ -48,6 +48,25 @@ class IDF_Scm_Svn extends IDF_Scm
public function isAvailable()
{
$cmd = sprintf(Pluf::f('svn_path', 'svn').' info --xml --username=%s --password=%s %s',
escapeshellarg($this->username),
escapeshellarg($this->password),
escapeshellarg($this->repo));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
$xmlInfo = self::shell_exec('IDF_Scm_Svn::isAvailable', $cmd);
try {
$xml = simplexml_load_string($xmlInfo);
}
catch (Exception $e) {
return false;
}
if (!isset($xml->entry->commit['revision'])) {
return false;
}
if (0 == (int)$xml->entry->commit['revision']) {
return false;
}
return true;
}
@ -80,12 +99,13 @@ class IDF_Scm_Svn extends IDF_Scm
* Returns the URL of the subversion repository.
*
* @param IDF_Project
* @param string
* @return string URL
*/
public static function getAnonymousAccessUrl($project)
public static function getAnonymousAccessUrl($project,$commit=null)
{
$conf = $project->getConf();
if (false !== ($url=$conf->getVal('svn_remote_url', false))
if (false !== ($url=$conf->getVal('svn_remote_url', false))
&& !empty($url)) {
// Remote repository
return $url;
@ -97,12 +117,13 @@ class IDF_Scm_Svn extends IDF_Scm
* Returns the URL of the subversion repository.
*
* @param IDF_Project
* @param string
* @return string URL
*/
public static function getAuthAccessUrl($project, $user)
public static function getAuthAccessUrl($project, $user, $commit=null)
{
$conf = $project->getConf();
if (false !== ($url=$conf->getVal('svn_remote_url', false))
if (false !== ($url=$conf->getVal('svn_remote_url', false))
&& !empty($url)) {
// Remote repository
return $url;
@ -120,7 +141,7 @@ class IDF_Scm_Svn extends IDF_Scm
{
$conf = $project->getConf();
// Find the repository
if (false !== ($rep=$conf->getVal('svn_remote_url', false))
if (false !== ($rep=$conf->getVal('svn_remote_url', false))
&& !empty($rep)) {
// Remote repository
$scm = new IDF_Scm_Svn($rep, $project);
@ -136,19 +157,23 @@ class IDF_Scm_Svn extends IDF_Scm
/**
* Subversion revisions are either a number or 'HEAD'.
*/
public function isValidRevision($rev)
public function validateRevision($rev)
{
if ($rev == 'HEAD') {
return true;
return IDF_Scm::REVISION_VALID;
}
$cmd = sprintf(Pluf::f('svn_path', 'svn').' info --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;
self::exec('IDF_Scm_Svn::isValidRevision', $cmd, $out, $ret);
return (0 == $ret);
self::exec('IDF_Scm_Svn::validateRevision', $cmd, $out, $ret);
if ($ret == 0)
return IDF_Scm::REVISION_VALID;
return IDF_Scm::REVISION_INVALID;
}
@ -268,7 +293,7 @@ class IDF_Scm_Svn extends IDF_Scm
$file['type'] = $this->assoc[(string) $entry['kind']];
$pathinfo = pathinfo($filename);
$file['file'] = $pathinfo['basename'];
$file['rev'] = $rev;
$file['rev'] = $rev;
$file['author'] = (string) $entry->author;
$file['date'] = gmdate('Y-m-d H:i:s', strtotime((string) $entry->commit->date));
$file['size'] = (string) $entry->size;
@ -284,12 +309,12 @@ class IDF_Scm_Svn extends IDF_Scm
escapeshellarg($this->repo.'/'.self::smartEncode($def->fullpath)),
escapeshellarg($def->rev));
$cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd;
return ($cmd_only) ?
return ($cmd_only) ?
$cmd : self::shell_exec('IDF_Scm_Svn::getFile', $cmd);
}
/**
* Subversion branches are folder based.
* Subversion branches are folder based.
*
* One need to list the folder to know them.
*/
@ -328,7 +353,7 @@ class IDF_Scm_Svn extends IDF_Scm
}
/**
* Subversion tags are folder based.
* Subversion tags are folder based.
*
* One need to list the folder to know them.
*/
@ -392,7 +417,7 @@ class IDF_Scm_Svn extends IDF_Scm
*/
public function getCommit($commit, $getdiff=false)
{
if (!$this->isValidRevision($commit)) {
if ($this->validateRevision($commit) != IDF_Scm::REVISION_VALID) {
return false;
}
$res = array();
@ -408,8 +433,12 @@ class IDF_Scm_Svn extends IDF_Scm
$res['date'] = gmdate('Y-m-d H:i:s', strtotime((string) $xml->logentry->date));
$res['title'] = (string) $xml->logentry->msg;
$res['commit'] = (string) $xml->logentry['revision'];
$res['changes'] = ($getdiff) ? $this->getDiff($commit) : '';
$res['parents'] = $xml->logentry['revision'] > 1
? array((string) $xml->logentry['revision'] - 1)
: array();
$res['diff'] = ($getdiff) ? $this->getDiff($commit) : '';
$res['tree'] = '';
$res['branch'] = '';
return (object) $res;
}

View File

@ -51,9 +51,13 @@ class IDF_Template_IssueComment extends Pluf_Template_Tag
array($this, 'callbackReviews'), $text);
}
if ($request->rights['hasSourceAccess']) {
$text = preg_replace_callback('#(commits?\s+)([0-9a-f]{1,40}(?:(?:\s+and|\s+or|,)\s+[0-9a-f]{1,40})*)\b#i',
$verbs = array('added', 'fixed', 'reverted', 'changed', 'removed');
$nouns = array('commit', 'commits', 'revision', 'revisions', 'rev', 'revs');
$prefix = implode(' in|', $verbs).' in' . '|'.
implode('|', $nouns);
$text = preg_replace_callback('#((?:'.$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 = preg_replace_callback('#(src:)([^\s\(\)\\\\]+(?:(\\\\)\s+[^\s\(\)\\\\]+){0,})+#im',
$text = preg_replace_callback('=(src:)([^\s@#,\(\)\\\\]+(?:(\\\\)[\s@#][^\s@#,\(\)\\\\]+){0,})+(?:\@([^\s#,]+))(?:#(\d+))?=im',
array($this, 'callbackSource'), $text);
}
if ($wordwrap) $text = Pluf_Text::wrapHtml($text, 69, "\n");
@ -122,15 +126,12 @@ class IDF_Template_IssueComment extends Pluf_Template_Tag
function callbackCommits($m)
{
$keyword = rtrim($m[1]);
if ('commits' === $keyword) {
// Multiple commits like 'commits 6e030e6, a25bfc1 and
// 3c094f8'.
return $m[1].preg_replace_callback('#\b[0-9a-f]{4,40}\b#i', array($this, 'callbackCommit'), $m[2]);
} else if ('commit' === $keyword) {
if (empty($m[3])) {
// Single commit like 'commit 6e030e6'.
return $m[1].call_user_func(array($this, 'callbackCommit'), array($m[2]));
}
return $m[0];
// Multiple commits like 'commits 6e030e6, a25bfc1 and 3c094f8'.
return $m[1].preg_replace_callback('#\b[0-9a-f]{1,40}\b#i', array($this, 'callbackCommit'), $m[2]);
}
/**
@ -189,17 +190,38 @@ class IDF_Template_IssueComment extends Pluf_Template_Tag
function callbackSource($m)
{
if (!$this->scm->isAvailable()) return $m[0];
if (!$this->scm->isAvailable())
return $m[0];
$commit = null;
if (!empty($m[4])) {
if (!$this->scm->getCommit($m[4])) {
return $m[0];
}
$commit = $m[4];
}
$file = $m[2];
if (!empty($m[3])) $file = str_replace($m[3], '', $file);
$request_file_info = $this->scm->getPathInfo($file);
if (!empty($m[3]))
$file = str_replace($m[3], '', $file);
$linktext = $file;
if (!empty($commit))
$linktext .= '@'.$commit;
$request_file_info = $this->scm->getPathInfo($file, $commit);
if (!$request_file_info) {
return $m[0];
}
if ($request_file_info->type != 'tree') {
return $m[1].'<a href="'.Pluf_HTTP_URL_urlForView('IDF_Views_Source::tree', array($this->project->shortname, $this->scm->getMainBranch(), $file)).'">'.$file.'</a>';
if ($request_file_info->type == 'tree') {
return $m[0];
}
return $m[0];
$link = Pluf_HTTP_URL_urlForView('IDF_Views_Source::tree', array(
$this->project->shortname,
$commit == null ? $this->scm->getMainBranch() : $commit,
$file
));
if (!empty($m[5])) {
$link .= '#L'.$m[5];
$linktext .= '#'.$m[5];
}
return $m[1].'<a href="'.$link.'">'.$linktext.'</a>';
}
/**

View File

@ -46,25 +46,32 @@ class IDF_Template_Markdown extends Pluf_Template_Tag
array($this, 'callbackEmbeddedDoc'),
$text);
}
// Replace [Page]([[PageName]]) with corresponding link to the page, with link text being Page.
$text = preg_replace_callback('#\[([^\]]+)\]\(\[\[([A-Za-z0-9\-]+)\]\]\)#im',
array($this, 'callbackWikiPage'),
$text);
// Replace [[PageName]] with corresponding link to the page.
$text = preg_replace_callback('#\[\[([A-Za-z0-9\-]+)\]\]#im',
array($this, 'callbackWikiPage'),
array($this, 'callbackWikiPageNoName'),
$text);
$filter = new IDF_Template_MarkdownPrefilter();
echo $filter->go(Pluf_Text_MarkDown_parse($text));
}
function callbackWikiPageNoName($m)
{
$m[2] = $m[1]; //Set the link text to be the same as the page name.
return $this->callbackWikiPage($m);
}
function callbackWikiPage($m)
{
$sql = new Pluf_SQL('project=%s AND title=%s',
array($this->project->id, $m[1]));
array($this->project->id, $m[2]));
$pages = Pluf::factory('IDF_WikiPage')->getList(array('filter'=>$sql->gen()));
if ($pages->count() != 1 and !$this->request->rights['hasWikiAccess']) {
return $m[0];
}
if ($pages->count() != 1 and $this->request->rights['hasWikiAccess']
and !$this->request->user->isAnonymous()) {
return '<img style="vertical-align: text-bottom;" alt=" " src="'.Pluf::f('url_media').'/idf/img/add.png" /><a href="'.Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::create', array($this->project->shortname), array('name'=>$m[1])).'" title="'.__('Create this documentation page').'">'.$m[1].'</a>';
return '<img style="vertical-align: text-bottom;" alt=" " src="'.Pluf::f('url_media').'/idf/img/add.png" /><a href="'.Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::create', array($this->project->shortname), array('name'=>$m[2])).'" title="'.__('Create this documentation page').'">'.$m[1].'</a>';
}
if (!$this->request->rights['hasWikiAccess'] or $pages->count() == 0) {
return $m[1];
@ -75,6 +82,9 @@ class IDF_Template_Markdown extends Pluf_Template_Tag
function callbackEmbeddedDoc($m)
{
$scm = IDF_Scm::get($this->request->project);
if (!$scm->isAvailable()) {
return $m[0];
}
$view_source = new IDF_Views_Source();
$match = array('dummy', $this->request->project->shortname);
$match[] = (isset($m[2])) ? $m[2] : $scm->getMainBranch();
@ -86,7 +96,7 @@ class IDF_Template_Markdown extends Pluf_Template_Tag
$info = pathinfo($m[1]);
$fileinfo = array($res->headers['Content-Type'], $m[1],
isset($info['extension']) ? $info['extension'] : 'bin');
if (!IDF_Views_Source::isText($fileinfo)) {
if (!IDF_FileUtil::isText($fileinfo)) {
return $m[0];
}
return $res->content;

View File

@ -99,6 +99,7 @@ class IDF_Template_MarkdownPrefilter extends Pluf_Text_HTML_Filter
'caption' => array(),
'code' => array(),
'dd' => array(),
'del' => array('cite', 'class', 'datetime', 'dir', 'id', 'title'),
'div' => array('align', 'class'),
'dl' => array(),
'dt' => array(),
@ -112,6 +113,7 @@ class IDF_Template_MarkdownPrefilter extends Pluf_Text_HTML_Filter
'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'),
@ -151,7 +153,9 @@ class IDF_Template_MarkdownPrefilter extends Pluf_Text_HTML_Filter
'blockquote',
'pre',
'iframe',
'h1', 'h2', 'h3', 'address'
'h1', 'h2', 'h3', 'address',
'del',
'ins',
);
// attributes which should be checked for valid protocols
public $protocol_attributes = array(
@ -164,6 +168,7 @@ class IDF_Template_MarkdownPrefilter extends Pluf_Text_HTML_Filter
'https',
'ftp',
'mailto',
'irc'
);
// tags which should be removed if they contain no content
// (e.g. "<b></b>" or "<b />")
@ -174,5 +179,7 @@ class IDF_Template_MarkdownPrefilter extends Pluf_Text_HTML_Filter
'caption',
'li',
'span',
'del',
'ins',
);
}

View File

@ -0,0 +1,47 @@
<?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 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 ***** */
/**
* Tests some of the FileUtils
*/
class IDF_Tests_TestFileUtil extends UnitTestCase
{
public function __construct()
{
parent::__construct('Test the file utils.');
}
public function testGetMimeType()
{
$files = array(
'whatever.php' => 'application/x-httpd-php',
'whatever.pht' => 'application/x-httpd-php',
'README' => 'text/plain',
);
foreach ($files as $file => $mime) {
$m = IDF_Views_Source::getMimeType($file);
$this->assertEqual($mime, $m[0]);
}
}
}

View File

@ -0,0 +1,239 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
require_once("simpletest/autorun.php");
/**
* Test the monotone class.
*/
class IDF_Tests_TestMonotone extends UnitTestCase
{
private $tmpdir, $dbfile, $mtnInstance;
private function mtnCall($args, $stdin = null, $dir = null)
{
// if you have an SSH agent running for key caching,
// please disable it
$cmdline = array("mtn",
"--confdir", $this->tmpdir,
"--db", $this->dbfile,
"--norc",
"--timestamps");
$cmdline = array_merge($cmdline, $args);
$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("file", "{$this->tmpdir}/mtn-errors", "a")
);
$pipes = array();
$dir = !empty($dir) ? $dir : $this->tmpdir;
$process = proc_open(implode(" ", $cmdline),
$descriptorspec,
$pipes,
$dir);
if (!is_resource($process)) {
throw new Exception("could not create process");
}
if (!empty($stdin)) {
fwrite($pipes[0], $stdin);
fclose($pipes[0]);
}
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$ret = proc_close($process);
if ($ret != 0) {
throw new Exception(
"call ended with a non-zero error code (complete cmdline was: ".
implode(" ", $cmdline).")"
);
}
return $stdout;
}
public function __construct()
{
parent::__construct("Test the monotone class.");
$this->tmpdir = sys_get_temp_dir() . "/mtn-test";
$this->dbfile = "{$this->tmpdir}/test.mtn";
set_include_path(get_include_path() . ":../../../pluf-master/src");
require_once("Pluf.php");
Pluf::start(dirname(__FILE__)."/../conf/idf.php");
// Pluf::f() mocking
$GLOBALS['_PX_config']['mtn_repositories'] = "{$this->tmpdir}/%s.mtn";
}
private static function deleteRecursive($dirname)
{
if (is_dir($dirname))
$dir_handle=opendir($dirname);
while ($file = readdir($dir_handle)) {
if ($file!="." && $file!="..") {
if (!is_dir($dirname."/".$file)) {
unlink ($dirname."/".$file);
continue;
}
self::deleteRecursive($dirname."/".$file);
}
}
closedir($dir_handle);
rmdir($dirname);
return true;
}
public function setUp()
{
if (is_dir($this->tmpdir)) {
self::deleteRecursive($this->tmpdir);
}
mkdir($this->tmpdir);
$this->mtnCall(array("db", "init"));
$this->mtnCall(array("genkey", "test@test.de"), "\n\n");
$workspaceRoot = "{$this->tmpdir}/test-workspace";
mkdir($workspaceRoot);
$this->mtnCall(array("setup", "-b", "testbranch", "."), null, $workspaceRoot);
file_put_contents("$workspaceRoot/foo", "blubber");
$this->mtnCall(array("add", "foo"), null, $workspaceRoot);
$this->mtnCall(array("commit", "-m", "initial"), null, $workspaceRoot);
file_put_contents("$workspaceRoot/bar", "blafoo");
mkdir("$workspaceRoot/subdir");
file_put_contents("$workspaceRoot/subdir/bla", "blabla");
$this->mtnCall(array("add", "-R", "--unknown"), null, $workspaceRoot);
$this->mtnCall(array("commit", "-m", "second"), null, $workspaceRoot);
$rev = $this->mtnCall(array("au", "get_base_revision_id"), null, $workspaceRoot);
$this->mtnCall(array("tag", rtrim($rev), "release-1.0"));
$project = new IDF_Project();
$project->shortname = "test";
$this->mtnInstance = new IDF_Scm_Monotone($project);
}
public function testIsAvailable()
{
$this->assertTrue($this->mtnInstance->isAvailable());
}
public function testGetBranches()
{
$branches = $this->mtnInstance->getBranches();
$this->assertEqual(1, count($branches));
list($key, $value) = each($branches);
$this->assertEqual("h:testbranch", $key);
$this->assertEqual("testbranch", $value);
}
public function testGetTags()
{
$tags = $this->mtnInstance->getTags();
$this->assertEqual(1, count($tags));
list($key, $value) = each($tags);
$this->assertEqual("t:release-1.0", $key);
$this->assertEqual("release-1.0", $value);
}
public function testInBranches()
{
$revOut = $this->mtnCall(array("au", "select", "b:testbranch"));
$revs = preg_split('/\n/', $revOut, -1, PREG_SPLIT_NO_EMPTY);
$branches = $this->mtnInstance->inBranches($revs[0], null);
$this->assertEqual(1, count($branches));
$this->assertEqual("h:testbranch", $branches[0]);
$branches = $this->mtnInstance->inBranches("t:release-1.0", null);
$this->assertEqual(1, count($branches));
$this->assertEqual("h:testbranch", $branches[0]);
}
public function testInTags()
{
$rev = $this->mtnCall(array("au", "select", "t:release-1.0"));
$tags = $this->mtnInstance->inTags(rtrim($rev), null);
$this->assertEqual(1, count($tags));
$this->assertEqual("t:release-1.0", $tags[0]);
// pick the first (root) revisions in this database
$rev = $this->mtnCall(array("au", "roots"));
$tags = $this->mtnInstance->inTags(rtrim($rev), null);
$this->assertEqual(0, count($tags));
}
public function testGetTree()
{
$files = $this->mtnInstance->getTree("t:release-1.0");
$this->assertEqual(3, count($files));
$this->assertEqual("bar", $files[0]->file);
$this->assertEqual("blob", $files[0]->type);
$this->assertEqual(6, $files[0]->size); // "blafoo"
$this->assertEqual("second\n", $files[0]->log);
$this->assertEqual("foo", $files[1]->file);
$this->assertEqual("blob", $files[1]->type);
$this->assertEqual(7, $files[1]->size); // "blubber"
$this->assertEqual("initial\n", $files[1]->log);
$this->assertEqual("subdir", $files[2]->file);
$this->assertEqual("tree", $files[2]->type);
$this->assertEqual(0, $files[2]->size);
$files = $this->mtnInstance->getTree("t:release-1.0", "subdir");
$this->assertEqual(1, count($files));
$this->assertEqual("bla", $files[0]->file);
$this->assertEqual("subdir/bla", $files[0]->fullpath);
$this->assertEqual("blob", $files[0]->type);
$this->assertEqual(6, $files[0]->size); // "blabla"
$this->assertEqual("second\n", $files[0]->log);
}
public function testIsValidRevision()
{
$this->assertTrue($this->mtnInstance->isValidRevision("t:release-1.0"));
$this->assertFalse($this->mtnInstance->isValidRevision("abcdef12345"));
}
}

View File

@ -32,19 +32,6 @@ class IDF_Tests_TestSource extends UnitTestCase
parent::__construct('Test the source class.');
}
public function testGetMimeType()
{
$files = array(
'whatever.php' => 'application/x-httpd-php',
'whatever.pht' => 'application/x-httpd-php',
'README' => 'text/plain',
);
foreach ($files as $file => $mime) {
$m = IDF_Views_Source::getMimeType($file);
$this->assertEqual($mime, $m[0]);
}
}
public function testRegexCommit()
{
$regex = '#^/p/([\-\w]+)/source/tree/([^\/]+)/(.*)$#';
@ -61,4 +48,4 @@ class IDF_Tests_TestSource extends UnitTestCase
$this->assertEqual($res[2], $m[3]);
}
}
}
}

View File

@ -39,9 +39,9 @@ class IDF_Upload extends Pluf_Model
'id' =>
array(
'type' => 'Pluf_DB_Field_Sequence',
'blank' => true,
'blank' => true,
),
'project' =>
'project' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Project',
@ -77,7 +77,7 @@ class IDF_Upload extends Pluf_Model
'default' => 0,
'verbose' => __('file size in bytes'),
),
'submitter' =>
'submitter' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
@ -87,7 +87,7 @@ class IDF_Upload extends Pluf_Model
),
'tags' =>
array(
'type' => 'Pluf_DB_Field_Manytomany',
'type' => 'Pluf_DB_Field_Manytomany',
'blank' => true,
'model' => 'IDF_Tag',
'verbose' => __('labels'),
@ -112,7 +112,7 @@ class IDF_Upload extends Pluf_Model
'verbose' => __('modification date'),
),
);
$this->_a['idx'] = array(
$this->_a['idx'] = array(
'modif_dtime_idx' =>
array(
'col' => 'modif_dtime',
@ -121,7 +121,7 @@ class IDF_Upload extends Pluf_Model
);
$table = $this->_con->pfx.'idf_tag_idf_upload_assoc';
$this->_a['views'] = array(
'join_tags' =>
'join_tags' =>
array(
'join' => 'LEFT JOIN '.$table
.' ON idf_upload_id=id',
@ -150,7 +150,7 @@ class IDF_Upload extends Pluf_Model
function postSave($create=false)
{
if ($create) {
IDF_Timeline::insert($this, $this->get_project(),
IDF_Timeline::insert($this, $this->get_project(),
$this->get_submitter(), $this->creation_dtime);
}
}
@ -173,13 +173,13 @@ class IDF_Upload extends Pluf_Model
* Returns the timeline fragment for the file.
*
*
* @param Pluf_HTTP_Request
* @param Pluf_HTTP_Request
* @return Pluf_Template_SafeString
*/
public function timelineFragment($request)
{
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Download::view',
array($request->project->shortname,
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Download::view',
array($request->project->shortname,
$this->id));
$out = '<tr class="log"><td><a href="'.$url.'">'.
Pluf_esc(Pluf_Template_dateAgo($this->creation_dtime, 'without')).
@ -189,15 +189,15 @@ class IDF_Upload extends Pluf_Model
$out .= sprintf(__('<a href="%1$s" title="View download">Download %2$d</a>, %3$s'), $url, $this->id, Pluf_esc($this->summary)).'</td>';
$out .= '</tr>';
$out .= "\n".'<tr class="extra"><td colspan="2">
<div class="helptext right">'.sprintf(__('Addition of <a href="%s">download&nbsp;%d</a>, by %s'), $url, $this->id, $user).'</div></td></tr>';
<div class="helptext right">'.sprintf(__('Addition of <a href="%s">download&nbsp;%d</a>, by %s'), $url, $this->id, $user).'</div></td></tr>';
return Pluf_Template::markSafe($out);
}
public function feedFragment($request)
{
$url = Pluf::f('url_base')
.Pluf_HTTP_URL_urlForView('IDF_Views_Download::view',
array($request->project->shortname,
.Pluf_HTTP_URL_urlForView('IDF_Views_Download::view',
array($request->project->shortname,
$this->id));
$title = sprintf(__('%s: Download %d added - %s'),
$request->project->name,
@ -227,7 +227,7 @@ class IDF_Upload extends Pluf_Model
}
$current_locale = Pluf_Translation::getLocale();
$langs = Pluf::f('languages', array('en'));
Pluf_Translation::loadSetLocale($langs[0]);
Pluf_Translation::loadSetLocale($langs[0]);
$context = new Pluf_Template_Context(
array('file' => $this,
@ -237,14 +237,16 @@ class IDF_Upload extends Pluf_Model
));
$tmpl = new Pluf_Template('idf/downloads/download-created-email.txt');
$text_email = $tmpl->render($context);
$email = new Pluf_Mail(Pluf::f('from_email'),
$conf->getVal('downloads_notification_email'),
sprintf(__('New download - %s (%s)'),
$this->summary,
$this->get_project()->shortname));
$email->addTextMessage($text_email);
$email->sendMail();
$addresses = explode(',', $conf->getVal('downloads_notification_email'));
foreach ($addresses as $address) {
$email = new Pluf_Mail(Pluf::f('from_email'),
$address,
sprintf(__('New download - %s (%s)'),
$this->summary,
$this->get_project()->shortname));
$email->addTextMessage($text_email);
$email->sendMail();
}
Pluf_Translation::loadSetLocale($current_locale);
}
}

59
src/IDF/UserData.php Normal file
View File

@ -0,0 +1,59 @@
<?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 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 ***** */
/**
* Thin wrapper around the general purpose Gconf data driver
* to model a userdata object as key value store
*/
class IDF_UserData extends IDF_Gconf
{
/** columns for the underlying model for which we do not want to
override __get and __set */
private static $protectedVars =
array('id', 'model_class', 'model_id', 'vkey', 'vdesc');
function __set($key, $value)
{
if (in_array($key, self::$protectedVars))
{
parent::__set($key, $value);
return;
}
$this->setVal($key, $value);
}
function __get($key)
{
if (in_array($key, self::$protectedVars))
return parent::__get($key);
return $this->getVal($key, null);
}
public static function factory($user)
{
$conf = new IDF_UserData();
$conf->setModel((object) array('_model'=>'IDF_UserData', 'id' => $user->id));
$conf->initCache();
return $conf;
}
}

View File

@ -86,7 +86,10 @@ class IDF_Views
$title = __('Create Your Account');
$params = array('request'=>$request);
if ($request->method == 'POST') {
$form = new IDF_Form_Register($request->POST, $params);
$form = new IDF_Form_Register(array_merge(
(array)$request->POST,
(array)$request->FILES
), $params);
if ($form->isValid()) {
$user = $form->save(); // It is sending the confirmation email
$url = Pluf_HTTP_URL_urlForView('IDF_Views::registerInputKey');
@ -314,10 +317,10 @@ class IDF_Views
if ($user->isAnonymous()) {
$sql = sprintf('%s=%s', $db->qn('private'), $false);
return Pluf::factory('IDF_Project')->getList(array('filter'=> $sql,
'order' => 'shortname ASC'));
'order' => 'name ASC'));
}
if ($user->administrator) {
return Pluf::factory('IDF_Project')->getList(array('order' => 'shortname ASC'));
return Pluf::factory('IDF_Project')->getList(array('order' => 'name ASC'));
}
// grab the list of projects where the user is admin, member
// or authorized
@ -338,6 +341,6 @@ class IDF_Views
$sql .= sprintf(' OR id IN (%s)', implode(', ', $ids));
}
return Pluf::factory('IDF_Project')->getList(array('filter' => $sql,
'order' => 'shortname ASC'));
'order' => 'name ASC'));
}
}

View File

@ -66,7 +66,7 @@ class IDF_Views_Admin
'name' => __('Name'),
array('id', 'IDF_Views_Admin_projectSize', __('Repository Size')),
);
$pag->configure($list_display, array(),
$pag->configure($list_display, array(),
array('shortname'));
$pag->extra_classes = array('', '', 'right');
$pag->items_per_page = 25;
@ -118,6 +118,9 @@ class IDF_Views_Admin
/**
* Creation of a project.
*
* A project can use another project as template. In that case,
* everything but the data in the input at creation time is
* reused, including the wiki pages.
*/
public $projectCreate_precond = array('Pluf_Precondition::staffRequired');
public function projectCreate($request, $match)
@ -211,8 +214,8 @@ class IDF_Views_Admin
array('last_login', 'Pluf_Paginator_DateYMDHM', __('Last Login')),
);
$pag->extra_classes = array('', '', 'a-c', 'a-c', 'a-c', 'a-c');
$pag->configure($list_display,
array('login', 'last_name', 'email'),
$pag->configure($list_display,
array('login', 'last_name', 'email'),
array('login', 'last_login'));
$pag->items_per_page = 50;
$pag->no_results_text = __('No users were found.');
@ -225,7 +228,7 @@ class IDF_Views_Admin
),
$request);
}
/**
* Not validated users.
*/
@ -265,7 +268,9 @@ class IDF_Views_Admin
}
if ($request->method == 'POST') {
$form = new IDF_Form_Admin_UserUpdate($request->POST, $params);
$form = new IDF_Form_Admin_UserUpdate(array_merge($request->POST,
$request->FILES),
$params);
if ($form->isValid()) {
$form->save();
$request->user->setMessage(__('The user has been updated.'));
@ -296,7 +301,9 @@ class IDF_Views_Admin
'request' => $request,
);
if ($request->method == 'POST') {
$form = new IDF_Form_Admin_UserCreate($request->POST, $params);
$form = new IDF_Form_Admin_UserCreate(array_merge($request->POST,
$request->FILES),
$params);
if ($form->isValid()) {
$cuser = $form->save();
$request->user->setMessage(sprintf(__('The user %s has been created.'), (string) $cuser));
@ -314,6 +321,148 @@ class IDF_Views_Admin
),
$request);
}
/**
* Usher servers overview
*
*/
public $usher_precond = array('Pluf_Precondition::staffRequired');
public function usher($request, $match)
{
$title = __('Usher management');
$servers = array();
foreach (IDF_Scm_Monotone_Usher::getServerList() as $server) {
$servers[] = (object)array(
"name" => $server,
"status" => IDF_Scm_Monotone_Usher::getStatus($server),
);
}
return Pluf_Shortcuts_RenderToResponse(
'idf/gadmin/usher/index.html',
array(
'page_title' => $title,
'servers' => $servers,
),
$request
);
}
/**
* Usher control
*
*/
public $usherControl_precond = array('Pluf_Precondition::staffRequired');
public function usherControl($request, $match)
{
$title = __('Usher control');
$action = $match[1];
if (!empty($action)) {
if (!in_array($action, array('reload', 'shutdown', 'startup'))) {
throw new Pluf_HTTP_Error404();
}
$msg = null;
if ($action == 'reload') {
IDF_Scm_Monotone_Usher::reload();
$msg = __('Usher configuration has been reloaded');
}
else if ($action == 'shutdown') {
IDF_Scm_Monotone_Usher::shutDown();
$msg = __('Usher has been shut down');
}
else
{
IDF_Scm_Monotone_Usher::startUp();
$msg = __('Usher has been started up');
}
$request->user->setMessage($msg);
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::usherControl', array(''));
return new Pluf_HTTP_Response_Redirect($url);
}
return Pluf_Shortcuts_RenderToResponse(
'idf/gadmin/usher/control.html',
array(
'page_title' => $title,
'status' => IDF_Scm_Monotone_Usher::getStatus(),
),
$request
);
}
/**
* Usher control
*
*/
public $usherServerControl_precond = array('Pluf_Precondition::staffRequired');
public function usherServerControl($request, $match)
{
$server = $match[1];
if (!in_array($server, IDF_Scm_Monotone_Usher::getServerList())) {
throw new Pluf_HTTP_Error404();
}
$action = $match[2];
if (!in_array($action, array('start', 'stop', 'kill'))) {
throw new Pluf_HTTP_Error404();
}
$msg = null;
if ($action == 'start') {
IDF_Scm_Monotone_Usher::startServer($server);
$msg = sprintf(__('The server "%s" has been started'), $server);
}
else if ($action == 'stop') {
IDF_Scm_Monotone_Usher::stopServer($server);
$msg = sprintf(__('The server "%s" has been stopped'), $server);
}
else
{
IDF_Scm_Monotone_Usher::killServer($server);
$msg = sprintf(__('The server "%s" has been killed'), $server);
}
$request->user->setMessage($msg);
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::usher');
return new Pluf_HTTP_Response_Redirect($url);
}
/**
* Open connections for a configured server
*
*/
public $usherServerConnections_precond = array('Pluf_Precondition::staffRequired');
public function usherServerConnections($request, $match)
{
$server = $match[1];
if (!in_array($server, IDF_Scm_Monotone_Usher::getServerList())) {
throw new Pluf_HTTP_Error404();
}
$title = sprintf(__('Open connections for "%s"'), $server);
$connections = IDF_Scm_Monotone_Usher::getConnectionList($server);
if (count($connections) == 0) {
$request->user->setMessage(sprintf(
__('no connections for server "%s"'), $server
));
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Admin::usher');
return new Pluf_HTTP_Response_Redirect($url);
}
return Pluf_Shortcuts_RenderToResponse(
'idf/gadmin/usher/connections.html',
array(
'page_title' => $title,
'server' => $server,
'connections' => $connections,
),
$request
);
}
}
function IDF_Views_Admin_bool($field, $item)
@ -326,7 +475,7 @@ function IDF_Views_Admin_bool($field, $item)
/**
* Display the size of the project.
*
* @param string Field
* @param string Field
* @param IDF_Project
* @return string
*/
@ -344,25 +493,51 @@ function IDF_Views_Admin_projectSize($field, $project)
*
* @return array Associative array with the size of each element
*/
function IDF_Views_Admin_getForgeSize()
function IDF_Views_Admin_getForgeSize($force=false)
{
$conf = new IDF_Gconf();
$conf->setModel((object) array('_model'=>'IDF_Forge', 'id'=> 1));
$res = array();
$res['repositories'] = 0;
foreach (Pluf::factory('IDF_Project')->getList() as $prj) {
$size = $prj->getRepositorySize();
$size = $prj->getRepositorySize($force);
if ($size != -1) {
$res['repositories'] += $size;
}
}
$cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk '
.escapeshellarg(Pluf::f('upload_path'));
$out = explode(' ', shell_exec($cmd), 2);
$res['downloads'] = $out[0]*1024;
$cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk '
.escapeshellarg(Pluf::f('upload_issue_path'));
$out = explode(' ', shell_exec($cmd), 2);
$res['attachments'] = $out[0]*1024;
$res['database'] = IDF_Views_Admin_getForgeDbSize();
$last_eval = $conf->getVal('downloads_size_check_date', 0);
if (Pluf::f('idf_no_size_check', false) or
(!$force and $last_eval > time()-172800)) {
$res['downloads'] = $conf->getVal('downloads_size', 0);
} else {
$conf->setVal('downloads_size_check_date', time());
$cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk '
.escapeshellarg(Pluf::f('upload_path'));
$out = explode(' ', shell_exec($cmd), 2);
$res['downloads'] = $out[0]*1024;
$conf->setVal('downloads_size', $res['downloads']);
}
$last_eval = $conf->getVal('attachments_size_check_date', 0);
if (Pluf::f('idf_no_size_check', false) or
(!$force and $last_eval > time()-172800)) {
$res['attachments'] = $conf->getVal('attachments_size', 0);
} else {
$conf->setVal('attachments_size_check_date', time());
$cmd = Pluf::f('idf_exec_cmd_prefix', '').'du -sk '
.escapeshellarg(Pluf::f('upload_path'));
$out = explode(' ', shell_exec($cmd), 2);
$res['attachments'] = $out[0]*1024;
$conf->setVal('attachments_size', $res['attachments']);
}
$last_eval = $conf->getVal('database_size_check_date', 0);
if (Pluf::f('idf_no_size_check', false) or
(!$force and $last_eval > time()-172800)) {
$res['database'] = $conf->getVal('database_size', 0);
} else {
$conf->setVal('database_size_check_date', time());
$res['database'] = IDF_Views_Admin_getForgeDbSize();
$conf->setVal('database_size', $res['database']);
}
$res['total'] = $res['repositories'] + $res['downloads'] + $res['attachments'] + $res['database'];
return $res;
}
@ -380,8 +555,8 @@ function IDF_Views_Admin_getForgeDbSize()
}
switch (Pluf::f('db_engine')) {
case 'PostgreSQL':
$sql = 'SELECT relname, pg_total_relation_size(CAST(relname AS
TEXT)) AS size FROM pg_class AS pgc, pg_namespace AS pgn
$sql = 'SELECT relname, pg_total_relation_size(CAST(relname AS
TEXT)) AS size FROM pg_class AS pgc, pg_namespace AS pgn
WHERE pg_table_is_visible(pgc.oid) IS TRUE AND relkind = \'r\'
AND pgc.relnamespace = pgn.oid
AND pgn.nspname NOT IN (\'information_schema\', \'pg_catalog\')';

View File

@ -152,6 +152,29 @@ class IDF_Views_Download
if ($request->method == 'POST') {
$fname = $upload->file;
@unlink(Pluf::f('upload_path').'/'.$prj->shortname.'/files/'.$fname);
/**
* [signal]
*
* IDF_Upload::delete
*
* [sender]
*
* IDF_Form_UpdateUpload
*
* [description]
*
* This signal allows an application to perform a set of tasks
* just before the deletion of the corresponding object in the
* database but just after the deletion from the storage.
*
* [parameters]
*
* array('upload' => $upload);
*
*/
$params = array('upload' => $upload);
Pluf_Signal::send('IDF_Upload::delete',
'IDF_Views_Download', $params);
$upload->delete();
$request->user->setMessage(__('The file has been deleted.'));
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Download::index',

View File

@ -79,7 +79,165 @@ class IDF_Views_Issue
}
/**
* View the issues of a given user.
* View the issues watch list of a given user.
* Limited to a specified project
*/
public $watchList_precond = array('IDF_Precondition::accessIssues',
'Pluf_Precondition::loginRequired');
public function watchList($request, $match)
{
$prj = $request->project;
$otags = $prj->getTagIdsByStatus('open');
$ctags = $prj->getTagIdsByStatus('closed');
if (count($otags) == 0) $otags[] = 0;
if (count($ctags) == 0) $ctags[] = 0;
// Get the id list of issue in the user watch list (for all projects !)
$db =& Pluf::db();
$sql_results = $db->select('SELECT idf_issue_id as id FROM '.Pluf::f('db_table_prefix', '').'idf_issue_pluf_user_assoc WHERE pluf_user_id='.$request->user->id);
$issue_ids = array(0);
foreach ($sql_results as $id) {
$issue_ids[] = $id['id'];
}
$issue_ids = implode (',', $issue_ids);
// Count open and close issues
$sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array($prj->id));
$nb_open = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
$sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array($prj->id));
$nb_closed = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
// Generate a filter for the paginator
switch ($match[2]) {
case 'closed':
$title = sprintf(__('Watch List: Closed Issues for %s'), (string) $prj);
$summary = __('This table shows the closed issues in your watch list for %s project.', (string) $prj);
$f_sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array($prj->id));
break;
case 'open':
default:
$title = sprintf(__('Watch List: Open Issues for %s'), (string) $prj);
$summary = __('This table shows the open issues in your watch list for %s project.', (string) $prj);
$f_sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array($prj->id));
break;
}
// Paginator to paginate the issues
$pag = new Pluf_Paginator(new IDF_Issue());
$pag->class = 'recent-issues';
$pag->item_extra_props = array('project_m' => $prj,
'shortname' => $prj->shortname,
'current_user' => $request->user);
$pag->summary = $summary;
$pag->forced_where = $f_sql;
$pag->action = array('IDF_Views_Issue::watchList', array($prj->shortname, $match[1]));
$pag->sort_order = array('modif_dtime', 'ASC'); // will be reverted
$pag->sort_reverse_order = array('modif_dtime');
$pag->sort_link_title = true;
$pag->extra_classes = array('a-c', '', 'a-c', '');
$list_display = array(
'id' => __('Id'),
array('summary', 'IDF_Views_Issue_SummaryAndLabels', __('Summary')),
array('status', 'IDF_Views_Issue_ShowStatus', __('Status')),
array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')),
);
$pag->configure($list_display, array(), array('id', 'status', 'modif_dtime'));
$pag->items_per_page = 10;
$pag->no_results_text = __('No issues were found.');
$pag->setFromRequest($request);
return Pluf_Shortcuts_RenderToResponse('idf/issues/project-watchlist.html',
array('project' => $prj,
'page_title' => $title,
'open' => $nb_open,
'closed' => $nb_closed,
'issues' => $pag,
),
$request);
}
/**
* View the issues watch list of a given user.
* For all projects
*/
public $forgeWatchList_precond = array('Pluf_Precondition::loginRequired');
public function forgeWatchList($request, $match)
{
$otags = array();
$ctags = array();
// Note that this approach does not scale, we will need to add
// a table to cache the meaning of the tags for large forges.
foreach (IDF_Views::getProjects($request->user) as $project) {
$otags = array_merge($otags, $project->getTagIdsByStatus('open'));
}
foreach (IDF_Views::getProjects($request->user) as $project) {
$ctags = array_merge($ctags, $project->getTagIdsByStatus('closed'));
}
if (count($otags) == 0) $otags[] = 0;
if (count($ctags) == 0) $ctags[] = 0;
// Get the id list of issue in the user watch list (for all projects !)
$db =& Pluf::db();
$sql_results = $db->select('SELECT idf_issue_id as id FROM '.Pluf::f('db_table_prefix', '').'idf_issue_pluf_user_assoc WHERE pluf_user_id='.$request->user->id);
$issue_ids = array(0);
foreach ($sql_results as $id) {
$issue_ids[] = $id['id'];
}
$issue_ids = implode (',', $issue_ids);
// Count open and close issues
$sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array());
$nb_open = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
$sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array());
$nb_closed = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen()));
// Generate a filter for the paginator
switch ($match[1]) {
case 'closed':
$title = sprintf(__('Watch List: Closed Issues'));
$summary = __('This table shows the closed issues in your watch list.');
$f_sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array());
break;
case 'open':
default:
$title = sprintf(__('Watch List: Open Issues'));
$summary = __('This table shows the open issues in your watch list.');
$f_sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array());
break;
}
// Paginator to paginate the issues
$pag = new Pluf_Paginator(new IDF_Issue());
$pag->class = 'recent-issues';
$pag->item_extra_props = array('current_user' => $request->user);
$pag->summary = $summary;
$pag->forced_where = $f_sql;
$pag->action = array('IDF_Views_Issue::forgeWatchList', array($match[1]));
$pag->sort_order = array('modif_dtime', 'ASC'); // will be reverted
$pag->sort_reverse_order = array('modif_dtime');
$pag->sort_link_title = true;
$pag->extra_classes = array('a-c', '', 'a-c', 'a-c', 'a-c');
$list_display = array(
'id' => __('Id'),
array('summary', 'IDF_Views_Issue_SummaryAndLabelsUnknownProject', __('Summary')),
array('project', 'Pluf_Paginator_FkToString', __('Project')),
array('status', 'IDF_Views_Issue_ShowStatus', __('Status')),
array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')),
);
$pag->configure($list_display, array(), array('id', 'project', 'status', 'modif_dtime'));
$pag->items_per_page = 10;
$pag->no_results_text = __('No issues were found.');
$pag->setFromRequest($request);
return Pluf_Shortcuts_RenderToResponse('idf/issues/forge-watchlist.html',
array('page_title' => $title,
'open' => $nb_open,
'closed' => $nb_closed,
'issues' => $pag,
),
$request);
}
/**
* View the issues of a given user.
*
* Only open issues are shown.
*/
@ -201,7 +359,7 @@ class IDF_Views_Issue
{
$prj = $request->project;
if (!isset($request->REQUEST['q']) or trim($request->REQUEST['q']) == '') {
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::index',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::index',
array($prj->shortname));
return new Pluf_HTTP_Response_Redirect($url);
}
@ -263,7 +421,7 @@ class IDF_Views_Issue
'issue' => $issue,
);
if ($request->method == 'POST') {
$form = new IDF_Form_IssueUpdate(array_merge($request->POST,
$form = new IDF_Form_IssueUpdate(array_merge($request->POST,
$request->FILES),
$params);
if (!isset($request->POST['preview']) && $form->isValid()) {
@ -279,6 +437,27 @@ class IDF_Views_Issue
$form = new IDF_Form_IssueUpdate(null, $params);
}
}
// Search previous and next issue id
$octags = $prj->getTagIdsByStatus(($closed) ? 'closed' : 'open');
if (count($octags) == 0) $octags[] = 0;
$sql_previous = new Pluf_SQL('project=%s AND status IN ('.implode(', ', $octags).') AND id<%s',
array($prj->id, $match[2])
);
$sql_next = new Pluf_SQL('project=%s AND status IN ('.implode(', ', $octags).') AND id>%s',
array($prj->id, $match[2])
);
$previous_issue = Pluf::factory('IDF_Issue')->getList(array('filter' => $sql_previous->gen(),
'order' => 'id DESC',
'nb' => 1
));
$next_issue = Pluf::factory('IDF_Issue')->getList(array('filter' => $sql_next->gen(),
'order' => 'id ASC',
'nb' => 1
));
$previous_issue_id = (isset($previous_issue[0])) ? $previous_issue[0]->id : 0;
$next_issue_id = (isset($next_issue[0])) ? $next_issue[0]->id : 0;
$arrays = self::autoCompleteArrays($prj);
return Pluf_Shortcuts_RenderToResponse('idf/issues/view.html',
array_merge(
@ -290,7 +469,9 @@ class IDF_Views_Issue
'page_title' => $title,
'closed' => $closed,
'preview' => $preview,
'interested' =>$interested->count(),
'interested' => $interested->count(),
'previous_issue_id' => $previous_issue_id,
'next_issue_id' => $next_issue_id
),
$arrays),
$request);
@ -306,7 +487,7 @@ class IDF_Views_Issue
$prj = $request->project;
$attach = Pluf_Shortcuts_GetObjectOr404('IDF_IssueFile', $match[2]);
$prj->inOr404($attach->get_comment()->get_issue());
$info = IDF_Views_Source::getMimeType($attach->filename);
$info = IDF_FileUtil::getMimeType($attach->filename);
$mime = 'application/octet-stream';
if (strpos($info[0], 'image/') === 0) {
$mime = $info[0];
@ -330,14 +511,14 @@ class IDF_Views_Issue
$prj->inOr404($attach->get_comment()->get_issue());
// If one cannot see the attachement, redirect to the
// getAttachment view.
$info = IDF_Views_Source::getMimeType($attach->filename);
if (!IDF_Views_Source::isText($info)) {
$info = IDF_FileUtil::getMimeType($attach->filename);
if (!IDF_FileUtil::isText($info)) {
return $this->getAttachment($request, $match);
}
// Now we want to look at the file but with links back to the
// issue.
$file = IDF_Views_Source::highLight($info,
file_get_contents(Pluf::f('upload_issue_path').'/'.$attach->attachment));
$file = IDF_FileUtil::highLight($info,
file_get_contents(Pluf::f('upload_issue_path').'/'.$attach->attachment));
$title = sprintf(__('View %s'), $attach->filename);
return Pluf_Shortcuts_RenderToResponse('idf/issues/attachment.html',
array(
@ -406,7 +587,7 @@ class IDF_Views_Issue
{
$prj = $request->project;
$tag = Pluf_Shortcuts_GetObjectOr404('IDF_Tag', $match[2]);
$status = $match[3];
$status = $match[3];
if ($tag->project != $prj->id or !in_array($status, array('open', 'closed'))) {
throw new Pluf_HTTP_Error404();
}
@ -414,7 +595,7 @@ class IDF_Views_Issue
$title = sprintf(__('%1$s Issues with Label %2$s'), (string) $prj,
(string) $tag);
} else {
$title = sprintf(__('%1$s Closed Issues with Label %2$s'),
$title = sprintf(__('%1$s Closed Issues with Label %2$s'),
(string) $prj, (string) $tag);
}
// Get stats about the open/closed issues having this tag.
@ -539,6 +720,17 @@ class IDF_Views_Issue
}
}
/**
* When you access to your forge watch list, issue don't known
* the project shortname.
*/
function IDF_Views_Issue_SummaryAndLabelsUnknownProject($field, $issue, $extra='')
{
$shortname = $issue->get_project()->shortname;
$issue->__set('shortname', $shortname);
return IDF_Views_Issue_SummaryAndLabels ($field, $issue, $extra);
}
/**
* Display the summary of an issue, then on a new line, display the
* list of labels with a link to a view "by label only".
@ -547,11 +739,11 @@ class IDF_Views_Issue
*/
function IDF_Views_Issue_SummaryAndLabels($field, $issue, $extra='')
{
$edit = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
$edit = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
array($issue->shortname, $issue->id));
$tags = array();
foreach ($issue->get_tags_list() as $tag) {
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::listLabel',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::listLabel',
array($issue->shortname, $tag->id, 'open'));
$tags[] = sprintf('<a class="label" href="%s">%s</a>', $url, Pluf_esc((string) $tag));
}
@ -574,4 +766,6 @@ function IDF_Views_Issue_SummaryAndLabels($field, $issue, $extra='')
function IDF_Views_Issue_ShowStatus($field, $issue, $extra='')
{
return Pluf_esc($issue->get_status()->name);
}
}

View File

@ -44,12 +44,12 @@ class IDF_Views_Project
if ($request->rights['hasDownloadsAccess']) {
$tags = IDF_Views_Download::getDownloadTags($prj);
// the first tag is the featured, the last is the deprecated.
$downloads = $tags[0]->get_idf_upload_list();
$downloads = $tags[0]->get_idf_upload_list();
}
$pages = array();
if ($request->rights['hasWikiAccess']) {
$tags = IDF_Views_Wiki::getWikiTags($prj);
$pages = $tags[0]->get_idf_wikipage_list();
$pages = $tags[0]->get_idf_wikipage_list();
}
return Pluf_Shortcuts_RenderToResponse('idf/project/home.html',
array(
@ -61,6 +61,66 @@ class IDF_Views_Project
$request);
}
/**
* Returns an associative array with available model filters
*
* @return array
*/
private static function getAvailableModelFilters()
{
return array(
'all' => __('All Updates'),
'commits' => __('Commits'),
'issues' => __('Issues and Comments'),
'downloads' => __('Downloads'),
'documents' => __('Documents'),
'reviews' => __('Reviews and Patches'),
);
}
/**
* Returns an array of model classes for which the current user
* has rights and which should be used according to his filter
*
* @param object $request
* @param string $model_filter
* @return array
*/
private static function determineModelClasses($request, $model_filter = 'all')
{
$classes = array();
if (true === IDF_Precondition::accessSource($request) &&
($model_filter == 'all' || $model_filter == 'commits')) {
$classes[] = '\'IDF_Commit\'';
// FIXME: this looks like a hack...
IDF_Scm::syncTimeline($request->project);
}
if (true === IDF_Precondition::accessIssues($request) &&
($model_filter == 'all' || $model_filter == 'issues')) {
$classes[] = '\'IDF_Issue\'';
$classes[] = '\'IDF_IssueComment\'';
}
if (true === IDF_Precondition::accessDownloads($request) &&
($model_filter == 'all' || $model_filter == 'downloads')) {
$classes[] = '\'IDF_Upload\'';
}
if (true === IDF_Precondition::accessWiki($request) &&
($model_filter == 'all' || $model_filter == 'documents')) {
$classes[] = '\'IDF_WikiPage\'';
$classes[] = '\'IDF_WikiRevision\'';
}
if (true === IDF_Precondition::accessReview($request) &&
($model_filter == 'all' || $model_filter == 'reviews')) {
$classes[] = '\'IDF_Review_Comment\'';
$classes[] = '\'IDF_Review_Patch\'';
}
if (count($classes) == 0) {
$classes[] = '\'IDF_Dummy\'';
}
return $classes;
}
/**
* Timeline of the project.
*/
@ -68,43 +128,26 @@ class IDF_Views_Project
public function timeline($request, $match)
{
$prj = $request->project;
$title = sprintf(__('%s Updates'), (string) $prj);
$team = $prj->getMembershipData();
$model_filter = @$match[2];
$all_model_filters = self::getAvailableModelFilters();
if (!array_key_exists($model_filter, $all_model_filters)) {
$model_filter = 'all';
}
$title = (string)$prj . ' ' . $all_model_filters[$model_filter];
$pag = new IDF_Timeline_Paginator(new IDF_Timeline());
$pag->class = 'recent-issues';
$pag->item_extra_props = array('request' => $request);
$pag->summary = __('This table shows the project updates.');
// Need to check the rights
$rights = array();
if (true === IDF_Precondition::accessSource($request)) {
$rights[] = '\'IDF_Commit\'';
IDF_Scm::syncTimeline($request->project);
}
if (true === IDF_Precondition::accessIssues($request)) {
$rights[] = '\'IDF_Issue\'';
$rights[] = '\'IDF_IssueComment\'';
}
if (true === IDF_Precondition::accessDownloads($request)) {
$rights[] = '\'IDF_Upload\'';
}
if (true === IDF_Precondition::accessWiki($request)) {
$rights[] = '\'IDF_WikiPage\'';
$rights[] = '\'IDF_WikiRevision\'';
}
if (true === IDF_Precondition::accessReview($request)) {
$rights[] = '\'IDF_Review_Comment\'';
$rights[] = '\'IDF_Review_Patch\'';
}
if (count($rights) == 0) {
$rights[] = '\'IDF_Dummy\'';
}
$sql = sprintf('model_class IN (%s)', implode(', ', $rights));
$pag->forced_where = new Pluf_SQL('project=%s AND '.$sql,
$classes = self::determineModelClasses($request, $model_filter);
$sql = sprintf('model_class IN (%s)', implode(', ', $classes));
$pag->forced_where = new Pluf_SQL('project=%s AND '.$sql,
array($prj->id));
$pag->sort_order = array('creation_dtime', 'ASC');
$pag->sort_reverse_order = array('creation_dtime');
$pag->action = array('IDF_Views_Project::timeline', array($prj->shortname));
$pag->action = array('IDF_Views_Project::timeline', array($prj->shortname, $model_filter));
$list_display = array(
'creation_dtime' => __('Age'),
'id' => __('Change'),
@ -113,32 +156,23 @@ class IDF_Views_Project
$pag->items_per_page = 20;
$pag->no_results_text = __('No changes were found.');
$pag->setFromRequest($request);
$downloads = array();
if ($request->rights['hasDownloadsAccess']) {
$tags = IDF_Views_Download::getDownloadTags($prj);
// the first tag is the featured, the last is the deprecated.
$downloads = $tags[0]->get_idf_upload_list();
}
$pages = array();
if ($request->rights['hasWikiAccess']) {
$tags = IDF_Views_Wiki::getWikiTags($prj);
$pages = $tags[0]->get_idf_wikipage_list();
}
if (!$request->user->isAnonymous() and $prj->isRestricted()) {
$feedurl = Pluf_HTTP_URL_urlForView('idf_project_timeline_feed_auth',
array($prj->shortname,
array($prj->shortname,
$model_filter,
IDF_Precondition::genFeedToken($prj, $request->user)));
} else {
$feedurl = Pluf_HTTP_URL_urlForView('idf_project_timeline_feed',
array($prj->shortname));
array($prj->shortname, $model_filter));
}
return Pluf_Shortcuts_RenderToResponse('idf/project/timeline.html',
array(
'page_title' => $title,
'feedurl' => $feedurl,
'timeline' => $pag,
'team' => $team,
'downloads' => $downloads,
'model_filter' => $model_filter,
'all_model_filters' => $all_model_filters,
),
$request);
@ -156,31 +190,17 @@ class IDF_Views_Project
public function timelineFeed($request, $match)
{
$prj = $request->project;
// Need to check the rights
$rights = array();
if (true === IDF_Precondition::accessSource($request)) {
$rights[] = '\'IDF_Commit\'';
IDF_Scm::syncTimeline($request->project);
$model_filter = @$match[2];
$model_filter = @$match[2];
$all_model_filters = self::getAvailableModelFilters();
if (!array_key_exists($model_filter, $all_model_filters)) {
$model_filter = 'all';
}
if (true === IDF_Precondition::accessIssues($request)) {
$rights[] = '\'IDF_Issue\'';
$rights[] = '\'IDF_IssueComment\'';
}
if (true === IDF_Precondition::accessDownloads($request)) {
$rights[] = '\'IDF_Upload\'';
}
if (true === IDF_Precondition::accessWiki($request)) {
$rights[] = '\'IDF_WikiPage\'';
$rights[] = '\'IDF_WikiRevision\'';
}
if (true === IDF_Precondition::accessReview($request)) {
$rights[] = '\'IDF_Review_Comment\'';
$rights[] = '\'IDF_Review_Patch\'';
}
if (count($rights) == 0) {
$rights[] = '\'IDF_Dummy\'';
}
$sqls = sprintf('model_class IN (%s)', implode(', ', $rights));
$title = $all_model_filters[$model_filter];
$classes = self::determineModelClasses($request, $model_filter);
$sqls = sprintf('model_class IN (%s)', implode(', ', $classes));
$sql = new Pluf_SQL('project=%s AND '.$sqls, array($prj->id));
$params = array(
'filter' => $sql->gen(),
@ -188,7 +208,7 @@ class IDF_Views_Project
'nb' => 20,
);
$items = Pluf::factory('IDF_Timeline')->getList($params);
$set = new Pluf_Model_Set($items,
$set = new Pluf_Model_Set($items,
array('public_dtime' => 'public_dtime'));
$out = array();
foreach ($set as $item) {
@ -203,11 +223,10 @@ class IDF_Views_Project
}
$out = Pluf_Template::markSafe(implode("\n", $out));
$tmpl = new Pluf_Template('idf/index.atom');
$title = __('Updates');
$feedurl = Pluf::f('url_base').Pluf::f('idf_base').$request->query;
$viewurl = Pluf_HTTP_URL_urlForView('IDF_Views_Project::timeline',
array($prj->shortname));
$context = new Pluf_Template_Context_Request($request,
$context = new Pluf_Template_Context_Request($request,
array('body' => $out,
'date' => $date,
'title' => $title,
@ -235,7 +254,7 @@ class IDF_Views_Project
if ($form->isValid()) {
$prj = $form->save();
$request->user->setMessage(__('The project has been updated.'));
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Project::admin',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Project::admin',
array($prj->shortname));
return new Pluf_HTTP_Response_Redirect($url);
}
@ -277,7 +296,8 @@ class IDF_Views_Project
}
} else {
$params = array();
$keys = array('labels_issue_open', 'labels_issue_closed',
$keys = array('labels_issue_template',
'labels_issue_open', 'labels_issue_closed',
'labels_issue_predefined', 'labels_issue_one_max');
foreach ($keys as $key) {
$_val = $conf->getVal($key, false);
@ -445,7 +465,7 @@ class IDF_Views_Project
} else {
$params = array();
$keys = array('downloads_access_rights', 'source_access_rights',
'issues_access_rights', 'review_access_rights',
'issues_access_rights', 'review_access_rights',
'wiki_access_rights',
'downloads_notification_email',
'review_notification_email',
@ -475,50 +495,52 @@ class IDF_Views_Project
/**
* Administrate the source control.
*
* There, the login/password of the subversion remote repo can be
* change together with the webhook url.
*/
public $adminSource_precond = array('IDF_Precondition::projectOwner');
public function adminSource($request, $match)
{
$prj = $request->project;
$title = sprintf(__('%s Source'), (string) $prj);
$form = null;
$remote_svn = false;
if ($request->conf->getVal('scm') == 'svn' and
strlen($request->conf->getVal('svn_remote_url')) > 0) {
$remote_svn = true;
$extra = array(
'conf' => $request->conf,
);
if ($request->method == 'POST') {
$form = new IDF_Form_SourceConf($request->POST, $extra);
if ($form->isValid()) {
foreach ($form->cleaned_data as $key=>$val) {
$request->conf->setVal($key, $val);
}
$request->user->setMessage(__('The project source configuration has been saved.'));
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Project::adminSource',
array($prj->shortname));
return new Pluf_HTTP_Response_Redirect($url);
$remote_svn = ($request->conf->getVal('scm') == 'svn' and
strlen($request->conf->getVal('svn_remote_url')) > 0);
$extra = array(
'conf' => $request->conf,
'remote_svn' => $remote_svn,
);
if ($request->method == 'POST') {
$form = new IDF_Form_SourceConf($request->POST, $extra);
if ($form->isValid()) {
foreach ($form->cleaned_data as $key=>$val) {
$request->conf->setVal($key, $val);
}
} else {
$params = array();
foreach (array('svn_username', 'svn_password') as $key) {
$_val = $request->conf->getVal($key, false);
if ($_val !== false) {
$params[$key] = $_val;
}
}
if (count($params) == 0) {
$params = null; //Nothing in the db, so new form.
}
$form = new IDF_Form_SourceConf($params, $extra);
$request->user->setMessage(__('The project source configuration has been saved.'));
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Project::adminSource',
array($prj->shortname));
return new Pluf_HTTP_Response_Redirect($url);
}
} else {
$params = array();
foreach (array('svn_username', 'svn_password', 'webhook_url') as $key) {
$_val = $request->conf->getVal($key, false);
if ($_val !== false) {
$params[$key] = $_val;
}
}
if (count($params) == 0) {
$params = null; //Nothing in the db, so new form.
}
$form = new IDF_Form_SourceConf($params, $extra);
}
$scm = $request->conf->getVal('scm', 'git');
$options = array(
'git' => __('git'),
'svn' => __('Subversion'),
'mercurial' => __('mercurial'),
'mtn' => __('monotone'),
);
$repository_type = $options[$scm];
return Pluf_Shortcuts_RenderToResponse('idf/admin/source.html',
@ -529,7 +551,8 @@ class IDF_Views_Project
'repository_size' => $prj->getRepositorySize(),
'page_title' => $title,
'form' => $form,
'hookkey' => $prj->getPostCommitHookKey(),
),
$request);
}
}
}

View File

@ -89,10 +89,10 @@ class IDF_Views_Review
));
if ($form->isValid()) {
$review = $form->save();
$urlr = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
$urlr = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
array($prj->shortname, $review->id));
$request->user->setMessage(sprintf(__('The <a href="%s">code review %d</a> has been created.'), $urlr, $review->id));
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::index',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::index',
array($prj->shortname));
return new Pluf_HTTP_Response_Redirect($url);
}
@ -155,10 +155,10 @@ class IDF_Views_Review
if ($form->isValid()) {
$review_comment = $form->save();
$review = $patch->get_review();
$urlr = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
$urlr = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
array($prj->shortname, $review->id));
$request->user->setMessage(sprintf(__('Your <a href="%s">code review %d</a> has been published.'), $urlr, $review->id));
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::index',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::index',
array($prj->shortname));
$review_comment->notify($request->conf);
return new Pluf_HTTP_Response_Redirect($url);
@ -181,7 +181,7 @@ class IDF_Views_Review
foreach ($cts as $ct) {
$reviewers[] = $ct->get_comment()->get_submitter();
}
if (count($def['chunks'])) {
if (count($def['chunks'])) {
$orig_file = ($fileinfo) ? $scm->getFile($fileinfo) : '';
$files[$filename] = array(
$diff->fileCompare($orig_file, $def, $filename),
@ -192,6 +192,7 @@ class IDF_Views_Review
$files[$filename] = array('', $form->f->{md5($filename)}, $cts);
}
}
$reviewers = Pluf_Model_RemoveDuplicates($reviewers);
return Pluf_Shortcuts_RenderToResponse('idf/review/view.html',
array_merge(
@ -219,7 +220,7 @@ class IDF_Views_Review
*/
function IDF_Views_Review_SummaryAndLabels($field, $review, $extra='')
{
$edit = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
$edit = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
array($review->shortname, $review->id));
$tags = array();
foreach ($review->get_tags_list() as $tag) {

View File

@ -31,16 +31,6 @@ Pluf::loadFunction('Pluf_Shortcuts_GetFormForModel');
*/
class IDF_Views_Source
{
/**
* Extension supported by the syntax highlighter.
*/
public static $supportedExtenstions = array(
'ascx', 'ashx', 'asmx', 'aspx', 'browser', 'bsh', 'c', 'cc',
'config', 'cpp', 'cs', 'csh', 'csproj', 'css', 'cv', 'cyc',
'html', 'html', 'java', 'js', 'master', 'perl', 'php', 'pl',
'pm', 'py', 'rb', 'sh', 'sitemap', 'skin', 'sln', 'svc', 'vala',
'vb', 'vbproj', 'wsdl', 'xhtml', 'xml', 'xsd', 'xsl', 'xslt');
/**
* Display help on how to checkout etc.
*/
@ -59,30 +49,56 @@ class IDF_Views_Source
$params, $request);
}
public $changeLog_precond = array('IDF_Precondition::accessSource');
/**
* Is displayed in case an invalid revision is requested
*/
public $invalidRevision_precond = array('IDF_Precondition::accessSource');
public function invalidRevision($request, $match)
{
$title = sprintf(__('%s Invalid Revision'), (string) $request->project);
$commit = $match[2];
$params = array(
'page_title' => $title,
'title' => $title,
'commit' => $commit,
);
return Pluf_Shortcuts_RenderToResponse('idf/source/invalid_revision.html',
$params, $request);
}
/**
* Is displayed in case a revision identifier cannot be uniquely resolved
* to one single revision
*/
public $disambiguateRevision_precond = array('IDF_Precondition::accessSource',
'IDF_Views_Source_Precondition::scmAvailable');
public function disambiguateRevision($request, $match)
{
$title = sprintf(__('%s Ambiguous Revision'), (string) $request->project);
$commit = $match[2];
$redirect = $match[3];
$scm = IDF_Scm::get($request->project);
$revisions = $scm->disambiguateRevision($commit);
$params = array(
'page_title' => $title,
'title' => $title,
'commit' => $commit,
'revisions' => $revisions,
'redirect' => $redirect,
);
return Pluf_Shortcuts_RenderToResponse('idf/source/disambiguate_revision.html',
$params, $request);
}
public $changeLog_precond = array('IDF_Precondition::accessSource',
'IDF_Views_Source_Precondition::scmAvailable',
'IDF_Views_Source_Precondition::revisionValid');
public function changeLog($request, $match)
{
$scm = IDF_Scm::get($request->project);
if (!$scm->isAvailable()) {
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help',
array($request->project->shortname));
return new Pluf_HTTP_Response_Redirect($url);
}
$branches = $scm->getBranches();
$commit = $match[2];
if (!$scm->isValidRevision($commit)) {
if (count($branches) == 0) {
// Redirect to the project source help
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help',
array($request->project->shortname));
return new Pluf_HTTP_Response_Redirect($url);
}
// Redirect to the first branch
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::changeLog',
array($request->project->shortname,
$scm->getMainBranch()));
return new Pluf_HTTP_Response_Redirect($url);
}
$title = sprintf(__('%1$s %2$s Change Log'), (string) $request->project,
$this->getScmType($request));
$changes = $scm->getChangeLog($commit, 25);
@ -111,22 +127,17 @@ class IDF_Views_Source
$request);
}
public $treeBase_precond = array('IDF_Precondition::accessSource');
public $treeBase_precond = array('IDF_Precondition::accessSource',
'IDF_Views_Source_Precondition::scmAvailable',
'IDF_Views_Source_Precondition::revisionValid');
public function treeBase($request, $match)
{
$scm = IDF_Scm::get($request->project);
if (!$scm->isAvailable()) {
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help',
array($request->project->shortname));
return new Pluf_HTTP_Response_Redirect($url);
}
$commit = $match[2];
$cobject = $scm->getCommit($commit);
if (!$cobject) {
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
array($request->project->shortname,
$scm->getMainBranch()));
return new Pluf_HTTP_Response_Redirect($url);
throw new Exception('could not retrieve commit object for '. $commit);
}
$title = sprintf(__('%1$s %2$s Source Tree'),
$request->project, $this->getScmType($request));
@ -143,6 +154,7 @@ class IDF_Views_Source
}
$scmConf = $request->conf->getVal('scm', 'git');
$props = $scm->getProperties($commit);
$res->uasort(array('IDF_Views_Source', 'treeSort'));
return Pluf_Shortcuts_RenderToResponse('idf/source/'.$scmConf.'/tree.html',
array(
'page_title' => $title,
@ -159,20 +171,14 @@ class IDF_Views_Source
$request);
}
public $tree_precond = array('IDF_Precondition::accessSource');
public $tree_precond = array('IDF_Precondition::accessSource',
'IDF_Views_Source_Precondition::scmAvailable',
'IDF_Views_Source_Precondition::revisionValid');
public function tree($request, $match)
{
$scm = IDF_Scm::get($request->project);
$commit = $match[2];
if (!$scm->isAvailable()) {
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help',
array($request->project->shortname));
return new Pluf_HTTP_Response_Redirect($url);
}
$fburl = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
array($request->project->shortname,
$scm->getMainBranch()));
$request_file = $match[3];
if (substr($request_file, -1) == '/') {
$request_file = substr($request_file, 0, -1);
@ -181,13 +187,13 @@ class IDF_Views_Source
$request_file));
return new Pluf_HTTP_Response_Redirect($url, 301);
}
if (!$scm->isValidRevision($commit, $request_file)) {
// Redirect to the first branch
return new Pluf_HTTP_Response_Redirect($fburl);
}
$request_file_info = $scm->getPathInfo($request_file, $commit);
if (!$request_file_info) {
// Redirect to the first branch
// Redirect to the main branch
$fburl = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
array($request->project->shortname,
$scm->getMainBranch()));
return new Pluf_HTTP_Response_Redirect($fburl);
}
$branches = $scm->getBranches();
@ -195,7 +201,7 @@ class IDF_Views_Source
if ($request_file_info->type != 'tree') {
$info = self::getRequestedFileMimeType($request_file_info,
$commit, $scm);
if (!self::isText($info)) {
if (!IDF_FileUtil::isText($info)) {
$rep = new Pluf_HTTP_Response($scm->getFile($request_file_info),
$info[0]);
$rep->headers['Content-Disposition'] = 'attachment; filename="'.$info[1].'"';
@ -241,6 +247,7 @@ class IDF_Views_Source
$previous = substr($request_file, 0, -strlen($l.' '));
$scmConf = $request->conf->getVal('scm', 'git');
$props = $scm->getProperties($commit, $request_file);
$res->uasort(array('IDF_Views_Source', 'treeSort'));
return Pluf_Shortcuts_RenderToResponse('idf/source/'.$scmConf.'/tree.html',
array(
'page_title' => $page_title,
@ -277,44 +284,42 @@ class IDF_Views_Source
return '<span class="breadcrumb">'.implode('<span class="sep">'.$sep.'</span>', $out).'</span>';
}
public $commit_precond = array('IDF_Precondition::accessSource');
public $commit_precond = array('IDF_Precondition::accessSource',
'IDF_Views_Source_Precondition::scmAvailable',
'IDF_Views_Source_Precondition::revisionValid');
public function commit($request, $match)
{
$scm = IDF_Scm::get($request->project);
$commit = $match[2];
if (!$scm->isValidRevision($commit)) {
// Redirect to the first branch
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
array($request->project->shortname,
$scm->getMainBranch()));
return new Pluf_HTTP_Response_Redirect($url);
}
$large = $scm->isCommitLarge($commit);
$cobject = $scm->getCommit($commit, !$large);
if (!$cobject) {
// Redirect to the first branch
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
array($request->project->shortname,
$scm->getMainBranch()));
return new Pluf_HTTP_Response_Redirect($url);
throw new Exception('could not retrieve commit object for '. $commit);
}
$title = sprintf(__('%s Commit Details'), (string) $request->project);
$page_title = sprintf(__('%s Commit Details - %s'), (string) $request->project, $commit);
$rcommit = IDF_Commit::getOrAdd($cobject, $request->project);
$diff = new IDF_Diff($cobject->changes);
$diff = new IDF_Diff($cobject->diff);
$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();
$in_tags = $scm->inTags($cobject->commit, '');
return Pluf_Shortcuts_RenderToResponse('idf/source/commit.html',
return Pluf_Shortcuts_RenderToResponse('idf/source/'.$scmConf.'/commit.html',
array(
'page_title' => $page_title,
'title' => $title,
'diff' => $diff,
'cobject' => $cobject,
'commit' => $commit,
'changes' => $changes,
'branches' => $branches,
'tree_in' => $in_branches,
'tags' => $tags,
@ -326,20 +331,18 @@ class IDF_Views_Source
$request);
}
public $downloadDiff_precond = array('IDF_Precondition::accessSource');
public $downloadDiff_precond = array('IDF_Precondition::accessSource',
'IDF_Views_Source_Precondition::scmAvailable',
'IDF_Views_Source_Precondition::revisionValid');
public function downloadDiff($request, $match)
{
$scm = IDF_Scm::get($request->project);
$commit = $match[2];
if (!$scm->isValidRevision($commit)) {
// Redirect to the first branch
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
array($request->project->shortname,
$scm->getMainBranch()));
return new Pluf_HTTP_Response_Redirect($url);
}
$cobject = $scm->getCommit($commit, true);
$rep = new Pluf_HTTP_Response($cobject->changes, 'text/plain');
if (!$cobject) {
throw new Exception('could not retrieve commit object for '. $commit);
}
$rep = new Pluf_HTTP_Response($cobject->diff, 'text/plain');
$rep->headers['Content-Disposition'] = 'attachment; filename="'.$commit.'.diff"';
return $rep;
}
@ -368,7 +371,7 @@ class IDF_Views_Source
$previous = substr($request_file, 0, -strlen($l.' '));
$scmConf = $request->conf->getVal('scm', 'git');
$props = $scm->getProperties($commit, $request_file);
$content = self::highLight($extra['mime'], $scm->getFile($request_file_info));
$content = IDF_FileUtil::highLight($extra['mime'], $scm->getFile($request_file_info));
return Pluf_Shortcuts_RenderToResponse('idf/source/'.$scmConf.'/file.html',
array(
'page_title' => $page_title,
@ -394,19 +397,14 @@ class IDF_Views_Source
* Get a given file at a given commit.
*
*/
public $getFile_precond = array('IDF_Precondition::accessSource');
public $getFile_precond = array('IDF_Precondition::accessSource',
'IDF_Views_Source_Precondition::scmAvailable',
'IDF_Views_Source_Precondition::revisionValid');
public function getFile($request, $match)
{
$scm = IDF_Scm::get($request->project);
$commit = $match[2];
$request_file = $match[3];
if (!$scm->isValidRevision($commit)) {
// Redirect to the first branch
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
array($request->project->shortname,
$scm->getMainBranch()));
return new Pluf_HTTP_Response_Redirect($url);
}
$request_file_info = $scm->getPathInfo($request_file, $commit);
if (!$request_file_info or $request_file_info->type == 'tree') {
// Redirect to the first branch
@ -415,7 +413,7 @@ class IDF_Views_Source
$scm->getMainBranch()));
return new Pluf_HTTP_Response_Redirect($url);
}
$info = self::getRequestedFileMimeType($request_file_info,
$info = self::getRequestedFileMimeType($request_file_info,
$commit, $scm);
$rep = new Pluf_HTTP_Response($scm->getFile($request_file_info),
$info[0]);
@ -427,27 +425,20 @@ class IDF_Views_Source
* Get a zip archive of the current commit.
*
*/
public $download_precond = array('IDF_Precondition::accessSource');
public $download_precond = array('IDF_Precondition::accessSource',
'IDF_Views_Source_Precondition::scmAvailable',
'IDF_Views_Source_Precondition::revisionValid');
public function download($request, $match)
{
$commit = trim($match[2]);
$scm = IDF_Scm::get($request->project);
if (!$scm->isValidRevision($commit)) {
// Redirect to the first branch
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::treeBase',
array($request->project->shortname,
$scm->getMainBranch()));
return new Pluf_HTTP_Response_Redirect($url);
}
$base = $request->project->shortname.'-'.$commit;
$cmd = $scm->getArchiveCommand($commit, $base.'/');
$rep = new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
$rep = $scm->getArchiveStream($commit, $base.'/');
$rep->headers['Content-Transfer-Encoding'] = 'binary';
$rep->headers['Content-Disposition'] = 'attachment; filename="'.$base.'.zip"';
return $rep;
}
/**
* Find the mime type of a requested file.
*
@ -458,120 +449,34 @@ class IDF_Views_Source
*/
public static function getRequestedFileMimeType($file_info, $commit, $scm)
{
$mime = self::getMimeType($file_info->file);
$mime = IDF_FileUtil::getMimeType($file_info->file);
if ('application/octet-stream' != $mime[0]) {
return $mime;
}
return self::getMimeTypeFromContent($file_info->file,
$scm->getFile($file_info));
}
/**
* Find the mime type of a file using the fileinfo class.
*
* @param string Filename/Filepath
* @param string File content
* @return array Mime type found or 'application/octet-stream', basename, extension
*/
public static function getMimeTypeFromContent($file, $filedata)
{
$info = pathinfo($file);
$res = array('application/octet-stream',
$info['basename'],
isset($info['extension']) ? $info['extension'] : 'bin');
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME);
$mime = finfo_buffer($finfo, $filedata);
finfo_close($finfo);
if ($mime) {
$res[0] = $mime;
}
if (!isset($info['extension']) && $mime) {
$res[2] = (0 === strpos($mime, 'text/')) ? 'txt' : 'bin';
} elseif (!isset($info['extension'])) {
$res[2] = 'bin';
}
}
return $res;
}
/**
* Find the mime type of a file.
*
* Use /etc/mime.types to find the type.
*
* @param string Filename/Filepath
* @param array Mime type found or 'application/octet-stream', basename, extension
*/
public static function getMimeType($file)
{
$src= Pluf::f('idf_mimetypes_db', '/etc/mime.types');
$mimes = preg_split("/\015\012|\015|\012/", file_get_contents($src));
$info = pathinfo($file);
if (isset($info['extension'])) {
foreach ($mimes as $mime) {
if ('#' != substr($mime, 0, 1)) {
$elts = preg_split('/ |\t/', $mime, -1, PREG_SPLIT_NO_EMPTY);
if (in_array($info['extension'], $elts)) {
return array($elts[0], $info['basename'], $info['extension']);
}
}
}
} else {
// we consider that if no extension and base name is all
// uppercase, then we have a text file.
if ($info['basename'] == strtoupper($info['basename'])) {
return array('text/plain', $info['basename'], 'txt');
}
$info['extension'] = 'bin';
}
return array('application/octet-stream', $info['basename'], $info['extension']);
return IDF_FileUtil::getMimeTypeFromContent($file_info->file,
$scm->getFile($file_info));
}
/**
* Find if a given mime type is a text file.
* This uses the output of the self::getMimeType function.
*
* @param array (Mime type, file name, extension)
* @return bool Is text
* Callback function to sort tree entries
*/
public static function isText($fileinfo)
public static function treeSort($a, $b)
{
if (0 === strpos($fileinfo[0], 'text/')) {
return true;
// compare two nodes of the same type
if ($a->type === $b->type) {
if (mb_convert_case($a->file, MB_CASE_LOWER) <
mb_convert_case ($b->file, MB_CASE_LOWER)) {
return -1;
}
return 1;
}
$ext = 'mdtext php-dist h gitignore diff patch'
.Pluf::f('idf_extra_text_ext', '');
$ext = array_merge(self::$supportedExtenstions, explode(' ' , $ext));
return (in_array($fileinfo[2], $ext));
}
public static function highLight($fileinfo, $content)
{
$pretty = '';
if (self::isSupportedExtension($fileinfo[2])) {
$pretty = ' prettyprint';
// compare two nodes of different types, directories ("tree")
// should come before files ("blob")
if ($a->type > $b->type) {
return -1;
}
$table = array();
$i = 1;
foreach (preg_split("/\015\012|\015|\012/", $content) as $line) {
$table[] = '<tr class="c-line"><td class="code-lc" id="L'.$i.'"><a href="#L'.$i.'">'.$i.'</a></td>'
.'<td class="code mono'.$pretty.'">'.IDF_Diff::padLine(Pluf_esc($line)).'</td></tr>';
$i++;
}
return Pluf_Template::markSafe(implode("\n", $table));
}
/**
* Test if an extension is supported by the syntax highlighter.
*
* @param string The extension to test
* @return bool
*/
public static function isSupportedExtension($extension)
{
return in_array($extension, self::$supportedExtenstions);
return 1;
}
/**
@ -597,3 +502,15 @@ function IDF_Views_Source_PrettySizeSimple($size)
return Pluf_Utils::prettySize($size);
}
function IDF_Views_Source_ShortenString($string, $length)
{
$ellipse = "...";
$length = max(strlen($ellipse) + 2, $length);
$preflen = ceil($length / 10);
if (mb_strlen($string) < $length)
return $string;
return substr($string, 0, $preflen).$ellipse.
substr($string, -($length - $preflen - mb_strlen($ellipse)));
}

View File

@ -0,0 +1,74 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
class IDF_Views_Source_Precondition
{
/**
* Ensures that the configured SCM for the project is available
*
* @param $request
* @return true | Pluf_HTTP_Response_Redirect
*/
static public function scmAvailable($request)
{
$scm = IDF_Scm::get($request->project);
if (!$scm->isAvailable()) {
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::help',
array($request->project->shortname));
return new Pluf_HTTP_Response_Redirect($url);
}
return true;
}
/**
* Validates the revision given in the URL path and acts accordingly
*
* @param $request
* @return true | Pluf_HTTP_Response_Redirect
* @throws Exception
*/
static public function revisionValid($request)
{
list($url_info, $url_matches) = $request->view;
list(, $project, $commit) = $url_matches;
$scm = IDF_Scm::get($request->project);
$res = $scm->validateRevision($commit);
switch ($res) {
case IDF_Scm::REVISION_VALID:
return true;
case IDF_Scm::REVISION_INVALID:
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::invalidRevision',
array($request->project->shortname, $commit));
return new Pluf_HTTP_Response_Redirect($url);
case IDF_Scm::REVISION_AMBIGUOUS:
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::disambiguateRevision',
array($request->project->shortname,
$commit,
$url_info['model'].'::'.$url_info['method']));
return new Pluf_HTTP_Response_Redirect($url);
default:
throw new Exception('unknown validation result: '. $res);
}
}
}

View File

@ -33,13 +33,13 @@ Pluf::loadFunction('Pluf_Shortcuts_RenderToResponse');
class IDF_Views_User
{
/**
* Dashboard of a user.
* Dashboard of a user.
*
* Shows all the open issues assigned to the user.
*
* TODO: This views is a SQL horror. What needs to be done to cut
* by many the number of SQL queries:
* - Add a table to cache the open/closed status ids for all the
* - Add a table to cache the open/closed status ids for all the
* projects.
* - Left join the issues with the project to get the shortname.
*
@ -110,7 +110,10 @@ class IDF_Views_User
$ext_pass = substr(sha1($request->user->password.Pluf::f('secret_key')), 0, 8);
$params = array('user' => $request->user);
if ($request->method == 'POST') {
$form = new IDF_Form_UserAccount($request->POST, $params);
$form = new IDF_Form_UserAccount(array_merge(
(array)$request->POST,
(array)$request->FILES
), $params);
if ($form->isValid()) {
$user = $form->save();
$url = Pluf_HTTP_URL_urlForView('IDF_Views_User::myAccount');
@ -121,10 +124,11 @@ class IDF_Views_User
} else {
$data = $request->user->getData();
unset($data['password']);
$form = new IDF_Form_UserAccount($data, $params);
$form = new IDF_Form_UserAccount(null, $params);
}
$keys = $request->user->get_idf_key_list();
return Pluf_Shortcuts_RenderToResponse('idf/user/myaccount.html',
return Pluf_Shortcuts_RenderToResponse('idf/user/myaccount.html',
array('page_title' => __('Your Account'),
'api_key' => $api_key,
'ext_pass' => $ext_pass,
@ -134,7 +138,7 @@ class IDF_Views_User
}
/**
* Delete a SSH key.
* Delete a public key.
*
* This is redirecting to the preferences
*/
@ -148,7 +152,7 @@ class IDF_Views_User
return new Pluf_HTTP_Response_Forbidden($request);
}
$key->delete();
$request->user->setMessage(__('The SSH key has been deleted.'));
$request->user->setMessage(__('The public key has been deleted.'));
}
return new Pluf_HTTP_Response_Redirect($url);
}
@ -170,11 +174,11 @@ class IDF_Views_User
} else {
$form = new IDF_Form_UserChangeEmail();
}
return Pluf_Shortcuts_RenderToResponse('idf/user/changeemail.html',
return Pluf_Shortcuts_RenderToResponse('idf/user/changeemail.html',
array('page_title' => __('Confirm The Email Change'),
'form' => $form),
$request);
}
/**
@ -208,13 +212,17 @@ class IDF_Views_User
public function view($request, $match)
{
$sql = new Pluf_SQL('login=%s', array($match[1]));
$users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen()));
$users = Pluf::factory('Pluf_User')->getList(array('filter'=>$sql->gen()));
if (count($users) != 1 or !$users[0]->active) {
throw new Pluf_HTTP_Error404();
}
return Pluf_Shortcuts_RenderToResponse('idf/user/public.html',
array('page_title' => (string) $users[0],
'member' => $users[0],
$user = $users[0];
$user_data = IDF_UserData::factory($user);
return Pluf_Shortcuts_RenderToResponse('idf/user/public.html',
array('page_title' => (string) $user,
'member' => $user,
'user_data' => $user_data,
),
$request);
}
@ -230,11 +238,11 @@ class IDF_Views_User
function IDF_Views_IssueSummaryAndLabels($field, $issue, $extra='')
{
$project = $issue->get_project();
$edit = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
$edit = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
array($project->shortname, $issue->id));
$tags = array();
foreach ($issue->get_tags_list() as $tag) {
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::listLabel',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::listLabel',
array($project->shortname, $tag->id, 'open'));
$tags[] = sprintf('<a class="label" href="%s">%s</a>', $url, Pluf_esc((string) $tag));
}

104
src/IDF/Webhook.php Normal file
View File

@ -0,0 +1,104 @@
<?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, 2009, 2010 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Management of the webhooks.
*
* The class provides the tools to perform the POST request with
* authentication for the webhooks.
*
*/
class IDF_Webhook
{
/**
* Perform the POST request given the webhook payload.
*
* @param array Payload
* @return bool Success or error
*/
public static function postNotification($payload)
{
$data = json_encode($payload['to_send']);
$sign = hash_hmac('md5', $data, $payload['authkey']);
$params = array('http' => array(
'method' => 'POST',
'content' => $data,
'user_agent' => 'Indefero Hook Sender (http://www.indefero.net)',
'max_redirects' => 0,
'timeout' => 15,
'header'=> 'Post-Commit-Hook-Hmac: '.$sign."\r\n"
.'Content-Type: application/json'."\r\n",
)
);
$url = $payload['url'];
$ctx = stream_context_create($params);
$fp = @fopen($url, 'rb', false, $ctx);
if (!$fp) {
return false;
}
$meta = stream_get_meta_data($fp);
@fclose($fp);
if (!isset($meta['wrapper_data'][0]) or $meta['timed_out']) {
return false;
}
if (0 === strpos($meta['wrapper_data'][0], 'HTTP/1.1 2') or
0 === strpos($meta['wrapper_data'][0], 'HTTP/1.1 3')) {
return true;
}
return false;
}
/**
* Process the webhook.
*
*/
public static function process($sender, &$params)
{
$item = $params['item'];
if ($item->type != 'new_commit') {
// We do nothing.
return;
}
if (isset($params['res']['IDF_Webhook::process']) and
$params['res']['IDF_Webhook::process'] == true) {
// Already processed.
return;
}
if ($item->payload['url'] == '') {
// We do nothing.
return;
}
// We have either to retry or to push for the first time.
$res = self::postNotification($item->payload);
if ($res) {
$params['res']['IDF_Webhook::process'] = true;
} elseif ($item->trials >= 9) {
// We are at trial 10, give up
$params['res']['IDF_Webhook::process'] = true;
} else {
// Need to try again
$params['res']['IDF_Webhook::process'] = false;
}
}
}

View File

@ -38,9 +38,9 @@ class IDF_WikiRevision extends Pluf_Model
'id' =>
array(
'type' => 'Pluf_DB_Field_Sequence',
'blank' => true,
'blank' => true,
),
'wikipage' =>
'wikipage' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_WikiPage',
@ -55,7 +55,7 @@ class IDF_WikiRevision extends Pluf_Model
'default' => false,
'help_text' => 'If this revision is the latest, we mark it as being the head revision.',
'index' => true,
),
'summary' =>
array(
@ -71,7 +71,7 @@ class IDF_WikiRevision extends Pluf_Model
'blank' => false,
'verbose' => __('content'),
),
'submitter' =>
'submitter' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
@ -92,7 +92,7 @@ class IDF_WikiRevision extends Pluf_Model
'verbose' => __('creation date'),
),
);
$this->_a['idx'] = array(
$this->_a['idx'] = array(
'creation_dtime_idx' =>
array(
'col' => 'creation_dtime',
@ -138,7 +138,7 @@ class IDF_WikiRevision extends Pluf_Model
$sql = new Pluf_SQL('wikipage=%s', array($this->wikipage));
$rev = Pluf::factory('IDF_WikiRevision')->getList(array('filter'=>$sql->gen()));
if ($rev->count() > 1) {
IDF_Timeline::insert($this, $this->get_wikipage()->get_project(),
IDF_Timeline::insert($this, $this->get_wikipage()->get_project(),
$this->get_submitter());
foreach ($rev as $r) {
if ($r->id != $this->id and $r->is_head) {
@ -156,7 +156,7 @@ class IDF_WikiRevision extends Pluf_Model
public function timelineFragment($request)
{
$page = $this->get_wikipage();
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view',
array($request->project->shortname,
$page->title));
$out = "\n".'<tr class="log"><td><a href="'.$url.'">'.
@ -186,7 +186,7 @@ class IDF_WikiRevision extends Pluf_Model
}
$out .= '</td></tr>';
$out .= "\n".'<tr class="extra"><td colspan="2">
<div class="helptext right">'.sprintf(__('Change of <a href="%s">%s</a>, by %s'), $url, Pluf_esc($page->title), $user).'</div></td></tr>';
<div class="helptext right">'.sprintf(__('Change of <a href="%s">%s</a>, by %s'), $url, Pluf_esc($page->title), $user).'</div></td></tr>';
return Pluf_Template::markSafe($out);
}
@ -195,13 +195,13 @@ class IDF_WikiRevision extends Pluf_Model
$page = $this->get_wikipage();
if (!$this->is_head) {
$url = Pluf::f('url_base')
.Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view',
.Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view',
array($request->project->shortname,
$page->title),
array('rev' => $this->id));
} else {
$url = Pluf::f('url_base')
.Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view',
.Pluf_HTTP_URL_urlForView('IDF_Views_Wiki::view',
array($request->project->shortname,
$page->title));
}
@ -248,7 +248,7 @@ class IDF_WikiRevision extends Pluf_Model
}
$current_locale = Pluf_Translation::getLocale();
$langs = Pluf::f('languages', array('en'));
Pluf_Translation::loadSetLocale($langs[0]);
Pluf_Translation::loadSetLocale($langs[0]);
$context = new Pluf_Template_Context(
array(
'page' => $this->get_wikipage(),
@ -260,23 +260,28 @@ class IDF_WikiRevision extends Pluf_Model
if ($create) {
$template = 'idf/wiki/wiki-created-email.txt';
$title = sprintf(__('New Documentation Page %s - %s (%s)'),
$this->get_wikipage()->title,
$this->get_wikipage()->summary,
$this->get_wikipage()->title,
$this->get_wikipage()->summary,
$this->get_wikipage()->get_project()->shortname);
} else {
$template = 'idf/wiki/wiki-updated-email.txt';
$title = sprintf(__('Documentation Page Changed %s - %s (%s)'),
$this->get_wikipage()->title,
$this->get_wikipage()->summary,
$this->get_wikipage()->title,
$this->get_wikipage()->summary,
$this->get_wikipage()->get_project()->shortname);
}
$tmpl = new Pluf_Template($template);
$text_email = $tmpl->render($context);
$email = new Pluf_Mail(Pluf::f('from_email'),
$conf->getVal('wiki_notification_email'),
$title);
$email->addTextMessage($text_email);
$email->sendMail();
$addresses = explode(',', $conf->getVal('wiki_notification_email'));
foreach ($addresses as $address) {
$email = new Pluf_Mail(Pluf::f('from_email'),
$address,
$title);
$email->addTextMessage($text_email);
$email->sendMail();
}
Pluf_Translation::loadSetLocale($current_locale);
}
}

View File

@ -27,14 +27,14 @@ $cfg = array();
# You must set them to false once everything is running ok.
#
$cfg['debug'] = true;
# It will help you catch errors at beginning when configuring your
# It will help you catch errors at beginning when configuring your
# SCM backend. It must be turned off in production.
$cfg['debug_scm'] = false;
$cfg['debug_scm'] = false;
#
# Note: By default, InDefero will not manage the repositories for
# you, you can enable the repositories management with the
# built-in plugins. The documentation of the plugins is available
# Note: By default, InDefero will not manage the repositories for
# you, you can enable the repositories management with the
# built-in plugins. The documentation of the plugins is available
# in the `doc/` folder.
#
@ -44,9 +44,9 @@ $cfg['debug_scm'] = false;
# For example: '/path/to/my/project/.git'
#
# If you have multiple repositories, you need to put %s where you
# want the shortname of the project to be replaced.
# want the shortname of the project to be replaced.
# For example:
# - You have many projects on your local computer and want to use
# - You have many projects on your local computer and want to use
# InDefero to see them. Put: '/home/yourlogin/Projects/%s/.git'
# - You have many projects on a remote server with only "bare" git
# repositories. Put: '/home/git/repositories/%s.git'
@ -64,7 +64,7 @@ $cfg['git_remote_url'] = 'git://localhost/%s.git';
$cfg['git_write_remote_url'] = 'git@localhost:%s.git';
# Same as for git, you can have multiple repositories, one for each
# project or a single one for all the projects.
# project or a single one for all the projects.
#
# In the case of subversion, the admin of a project can also select a
# remote repository from the web interface. From the web interface
@ -73,8 +73,51 @@ $cfg['git_write_remote_url'] = 'git@localhost:%s.git';
$cfg['svn_repositories'] = 'file:///home/svn/repositories/%s';
$cfg['svn_remote_url'] = 'http://localhost/svn/%s';
#
# You can setup monotone for use with indefero in several ways.
# Please look into doc/syncmonotone.mdtext for more information.
#
# Path to the monotone binary
$cfg['mtn_path'] = 'mtn';
# Additional options for the started monotone process
$cfg['mtn_opts'] = array('--no-workspace', '--no-standard-rcfiles');
# The path to a specific database (local use) or a writable project
# directory (remote / usher use). %s is replaced with the project name
$cfg['mtn_repositories'] = '/home/mtn/repositories/%s.mtn';
# The URL which is displayed as sync URL to the user and which is also
# used to connect to a remote usher
$cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s';
# Whether the particular database(s) are accessed locally (via automate stdio)
# or remotely (via automate remote_stdio). 'remote' is the default for
# use with usher and the SyncMonotone plugin, while 'local' access should be
# choosed for manual setups and / or ssh access.
$cfg['mtn_db_access'] = 'local';
# Full path to the directory tree which contains default configuration files
# that are automatically created for new projects. This is only needed
# if $cfg['mtn_db_access'] is set to remote, i.e. in case the SyncMonotone
# plugin should be used. If unset, it defaults to the tree underknees
# src/IDF/Plugin/SyncMonotone/. Don't forget the trailing slash!
#$cfg['mtn_confdir'] = '/path/to/dir/tree/';
# Additional configuration files you want to create / copy for new setups.
# All these file paths have to be relative to $cfg['mtn_confdir'].
#$cfg['mtn_confdir_extra'] = array('hooks.d/something.lua')
# Needs to be configured for remote / usher usage.
# This allows basic control of a running usher process via the forge
# administration. The variable must point to the full (writable)
# path of the usher configuration file which gets updated when new projects
# are added
#$cfg['mtn_usher_conf'] = '/path/to/usher.conf';
# Mercurial repositories path
#$cfg['mercurial_repositories'] = '/home/mercurial/repositories/%s';
$cfg['mercurial_repositories'] = '/home/mercurial/repositories/%s';
#$cfg['mercurial_remote_url'] = 'http://projects.ceondo.com/hg/%s';
# admins will get an email in case of errors in the system in non
@ -90,15 +133,15 @@ $cfg['mail_host'] = 'localhost';
$cfg['mail_port'] = 25;
# Paths/Url configuration.
#
#
# Examples:
# You have:
# You have:
# http://www.mydomain.com/myfolder/index.php
# Put:
# $cfg['idf_base'] = '/myfolder/index.php';
# $cfg['url_base'] = 'http://www.mydomain.com';
#
# You have mod_rewrite:
# You have mod_rewrite:
# http://www.mydomain.com/
# Put:
# $cfg['idf_base'] = '';
@ -109,7 +152,7 @@ $cfg['mail_port'] = 25;
$cfg['idf_base'] = '/index.php';
$cfg['url_base'] = 'http://localhost';
# Url to access the media folder which is in the www folder
# Url to access the media folder which is in the www folder
# of the archive
$cfg['url_media'] = 'http://localhost/media';
@ -120,9 +163,9 @@ $cfg['url_upload'] = 'http://localhost/media/upload';
$cfg['upload_path'] = '/home/www/indefero/www/media/upload';
#
# The following path *MUST NOT* be accessible through a web browser
# as user will be able to upload .html, .php files and this can
# create *TERRIBLE* security issues. In this folder, the attachments
# The following path *MUST NOT* be accessible through a web browser
# as user will be able to upload .html, .php files and this can
# create *TERRIBLE* security issues. In this folder, the attachments
# to the issues will be uploaded and we do not restrict the content type.
#
$cfg['upload_issue_path'] = '/home/www/indefero/attachments';
@ -130,10 +173,10 @@ $cfg['upload_issue_path'] = '/home/www/indefero/attachments';
#
# write here a long random string unique for this installation. This
# is critical to put a long string, with at least 40 characters.
$cfg['secret_key'] = '';
$cfg['secret_key'] = '';
# the sender of all the emails.
$cfg['from_email'] = 'sender@example.com';
$cfg['from_email'] = 'sender@example.com';
# Email address for the bounced messages.
$cfg['bounce_email'] = 'no-reply@example.com';
@ -150,22 +193,22 @@ $cfg['db_password'] = '';
$cfg['db_server'] = '';
$cfg['db_version'] = '5.1'; # Only needed for MySQL
# If you want to have different installations with the same DB
$cfg['db_table_prefix'] = 'indefero_';
# ** DO NOT USE SQLITE IN PRODUCTION **
$cfg['db_table_prefix'] = 'indefero_';
# ** DO NOT USE SQLITE IN PRODUCTION **
# This is not because of problems with the quality of the SQLite
# driver or with SQLite itself, this is due to the lack of migration
# support in Pluf for SQLite, this means we cannot modify the DB
# easily once it is loaded with data.
$cfg['db_engine'] = 'PostgreSQL'; # SQLite is also well tested or MySQL
$cfg['db_database'] = 'website'; # put absolute path to the db if you
# are using SQLite.
# are using SQLite.
#
# The extension of the downloads are limited. You can add extra
# extensions here. The list must start with a space.
# $cfg['idf_extra_upload_ext'] = ' ext1 ext2';
# extensions here.
# $cfg['idf_extra_upload_ext'] = 'ext1 ext2';
#
# By default, the size of the downloads is limited to 2MB.
# The php.ini upload_max_filesize configuration setting will
# The php.ini upload_max_filesize configuration setting will
# always have precedence.
# $cfg['max_upload_size'] = 2097152; // Size in bytes
@ -203,14 +246,28 @@ $cfg['template_context_processors'] = array('IDF_Middleware_ContextPreProcessor'
$cfg['idf_views'] = dirname(__FILE__).'/urls.php';
# available languages
$cfg['languages'] = array('en', 'fr');
$cfg['languages'] = array('en', 'fr');
# SCM base configuration
$cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git',
'svn' => 'IDF_Scm_Svn',
'mercurial' => 'IDF_Scm_Mercurial',
'mtn' => 'IDF_Scm_Monotone',
);
# Specific git config
# The core.quotepath is configured on new repository
# True -> All characters upper than 0x80 will be escape (default)
# False -> Characters is print directly, enable accented character in a UTF-8 shell
# $cfg['git_core_quotepath'] = false;
# Set to true when uploaded public keys should not only be validated
# syntactically, but also by the specific backend. For SSH public
# keys, ssh-keygen(3) must be available and usable in PATH, for
# monotone public keys, the monotone binary (as configured above)
# is used.
# $cfg['idf_strong_key_check'] = false;
# If you want to use another memtypes database
# $cfg['idf_mimetypes_db'] = '/etc/mime.types';
@ -218,8 +275,8 @@ $cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git',
# $cfg['idf_extra_text_ext'] = 'ext1 ext2 ext3';
# If you can execute the shell commands executed to get info
# from the scm with the user of your PHP process but it is
# not working from within PHP, this can be due to the environment
# from the scm with the user of your PHP process but it is
# not working from within PHP, this can be due to the environment
# variables not being set correctly. Note the trailing space.
# $cfg['idf_exec_cmd_prefix'] = '/usr/bin/env -i ';
@ -229,10 +286,15 @@ $cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git',
# To know which path you need to provide, just run:
# $ which git
# from the command line. This will give you the path to git.
# $cfg['svn_path'] = 'svn';
# $cfg['svnlook_path'] = 'svnlook';
# $cfg['svn_path'] = 'svn';
# $cfg['svnlook_path'] = 'svnlook';
# $cfg['svnadmin_path'] = 'svnadmin';
# $cfg['hg_path'] = 'hg';
# $cfg['git_path'] = 'git';
# $cfg['git_path'] = 'git';
# If you do not want to have calculations of the repositories, attachments
# and downloads size, set it to true. You can set to false some
# times to times to check the size.
# $cfg['idf_no_size_check'] = false;
return $cfg;

View File

@ -66,25 +66,26 @@ $ctl[] = array('regex' => '#^/logout/$#',
$ctl[] = array('regex' => '#^/help/$#',
'base' => $base,
'model' => 'IDF_Views',
'method' => 'faq');
'method' => 'faq',
'name' => 'idf_faq');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/$#',
'base' => $base,
'model' => 'IDF_Views_Project',
'method' => 'home');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/timeline/$#',
$ctl[] = array('regex' => '#^/p/([\-\w]+)/timeline/(\w+)/$#',
'base' => $base,
'model' => 'IDF_Views_Project',
'method' => 'timeline');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/$#',
$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/(\w+)/$#',
'base' => $base,
'model' => 'IDF_Views_Project',
'method' => 'timelineFeed',
'name' => 'idf_project_timeline_feed');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/token/(.*)/$#',
$ctl[] = array('regex' => '#^/p/([\-\w]+)/feed/timeline/(\w+)/token/(.*)/$#',
'base' => $base,
'model' => 'IDF_Views_Project',
'method' => 'timelineFeed',
@ -140,6 +141,16 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/view/attachment/(\d+)/(.*)$#',
'model' => 'IDF_Views_Issue',
'method' => 'viewAttachment');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/watchlist/(\w+)$#',
'base' => $base,
'model' => 'IDF_Views_Issue',
'method' => 'watchList');
$ctl[] = array('regex' => '#^/watchlist/(\w+)$#',
'base' => $base,
'model' => 'IDF_Views_Issue',
'method' => 'forgeWatchList');
// ---------- SCM ----------------------------------------
$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/help/$#',
@ -147,6 +158,16 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/source/help/$#',
'model' => 'IDF_Views_Source',
'method' => 'help');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/invalid/([^/]+)/$#',
'base' => $base,
'model' => 'IDF_Views_Source',
'method' => 'invalidRevision');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/disambiguate/([^/]+)/from/([^/]+)/$#',
'base' => $base,
'model' => 'IDF_Views_Source',
'method' => 'disambiguateRevision');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/source/tree/([^/]+)/$#',
'base' => $base,
'model' => 'IDF_Views_Source',
@ -385,6 +406,29 @@ $ctl[] = array('regex' => '#^/admin/users/(\d+)/$#',
'model' => 'IDF_Views_Admin',
'method' => 'userUpdate');
if (Pluf::f("mtn_usher_conf", null) !== null)
{
$ctl[] = array('regex' => '#^/admin/usher/$#',
'base' => $base,
'model' => 'IDF_Views_Admin',
'method' => 'usher');
$ctl[] = array('regex' => '#^/admin/usher/control/(.*)$#',
'base' => $base,
'model' => 'IDF_Views_Admin',
'method' => 'usherControl');
$ctl[] = array('regex' => '#^/admin/usher/server/(.+)/control/(.+)$#',
'base' => $base,
'model' => 'IDF_Views_Admin',
'method' => 'usherServerControl');
$ctl[] = array('regex' => '#^/admin/usher/server/(.+)/connections/$#',
'base' => $base,
'model' => 'IDF_Views_Admin',
'method' => 'usherServerConnections');
}
// ---------- UTILITY VIEWS -------------------------------
$ctl[] = array('regex' => '#^/register/$#',

4498
src/IDF/locale/cs/idf.po Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More