1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-09-19 16:58:48 +02:00

Adds base notification application

Summary: First diff in a series of diffs to add notifications to Phabricator. This is the notification application ONLY. This commit does not include the changes to other applications that makes them add notifications. As such, no notifications will be generated beyond the initial database import.

Test Plan: This is part of the notifications architecture which has been running on http://theoryphabricator.com for the past several months.

Reviewers: epriestley, btrahan, ddfisher

Reviewed By: epriestley

CC: allenjohnashton, keebuhm, aran, Korvin, jungejason, nh

Maniphest Tasks: T974

Differential Revision: https://secure.phabricator.com/D2571
This commit is contained in:
John-Ashton Allen 2012-06-08 06:31:30 -07:00 committed by epriestley
parent 692296a4d4
commit 3a6ee79190
16 changed files with 540 additions and 49 deletions

View file

@ -179,6 +179,8 @@ return array(
// extend AphrontMySQLDatabaseConnectionBase.
'mysql.implementation' => 'AphrontMySQLDatabaseConnection',
// -- Notifications ----//
'notification.enabled' => false,
// -- Email ----------------------------------------------------------------- //

View file

@ -0,0 +1,8 @@
CREATE TABLE if not exists phabricator_feed.feed_storynotification (
userPHID varchar(64) not null collate utf8_bin,
primaryObjectPHID varchar(64) not null collate utf8_bin,
chronologicalKey BIGINT UNSIGNED NOT NULL,
hasViewed boolean not null,
UNIQUE KEY (userPHID, chronologicalKey),
KEY (userPHID, hasViewed, primaryObjectPHID)
);

View file

@ -525,6 +525,9 @@ phutil_register_library_map(array(
'ManiphestView' => 'applications/maniphest/view/ManiphestView.php',
'MetaMTAConstants' => 'applications/metamta/constants/MetaMTAConstants.php',
'MetaMTANotificationType' => 'applications/metamta/constants/MetaMTANotificationType.php',
'NotificationMessage' => 'applications/notification/constants/message/NotificationMessage.php',
'NotificationPathname' => 'applications/notification/constants/pathname/NotificationPathname.php',
'NotificationType' => 'applications/notification/constants/type/NotificationType.php',
'OwnersPackageReplyHandler' => 'applications/owners/OwnersPackageReplyHandler.php',
'PackageCreateMail' => 'applications/owners/mail/PackageCreateMail.php',
'PackageDeleteMail' => 'applications/owners/mail/PackageDeleteMail.php',
@ -631,6 +634,7 @@ phutil_register_library_map(array(
'PhabricatorFeedStoryData' => 'applications/feed/storage/PhabricatorFeedStoryData.php',
'PhabricatorFeedStoryDifferential' => 'applications/feed/story/PhabricatorFeedStoryDifferential.php',
'PhabricatorFeedStoryManiphest' => 'applications/feed/story/PhabricatorFeedStoryManiphest.php',
'PhabricatorFeedStoryNotification' => 'applications/notification/storage/PhabricatorFeedStoryNotification.php',
'PhabricatorFeedStoryPhriction' => 'applications/feed/story/PhabricatorFeedStoryPhriction.php',
'PhabricatorFeedStoryProject' => 'applications/feed/story/PhabricatorFeedStoryProject.php',
'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php',
@ -733,6 +737,16 @@ phutil_register_library_map(array(
'PhabricatorMetaMTAWorker' => 'applications/metamta/PhabricatorMetaMTAWorker.php',
'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php',
'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php',
'PhabricatorNotificationBuilder' => 'applications/notification/builder/notification/PhabricatorNotificationBuilder.php',
'PhabricatorNotificationConstants' => 'applications/notification/constants/base/PhabricatorNotificationConstants.php',
'PhabricatorNotificationController' => 'applications/notification/controller/base/PhabricatorNotificationController.php',
'PhabricatorNotificationPanelController' => 'applications/notification/controller/panel/PhabricatorNotificationPanelController.php',
'PhabricatorNotificationQuery' => 'applications/notification/PhabricatorNotificationQuery.php',
'PhabricatorNotificationStory' => 'applications/notification/base/PhabricatorNotificationStory.php',
'PhabricatorNotificationStoryTypeConstants' => 'applications/notification/constants/story/PhabricatorNotificationStoryTypeConstants.php',
'PhabricatorNotificationStoryView' => 'applications/notification/view/story/PhabricatorNotificationStoryView.php',
'PhabricatorNotificationTestController' => 'applications/notification/controller/test/PhabricatorNotificationTestController.php',
'PhabricatorNotificationView' => 'applications/notification/view/base/PhabricatorNotificationView.php',
'PhabricatorNotificationsController' => 'applications/notifications/controller/PhabricatorNotificationsController.php',
'PhabricatorOAuthClientAuthorization' => 'applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php',
'PhabricatorOAuthClientAuthorizationBaseController' => 'applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationBaseController.php',
@ -1511,6 +1525,9 @@ phutil_register_library_map(array(
'ManiphestTransactionType' => 'ManiphestConstants',
'ManiphestView' => 'AphrontView',
'MetaMTANotificationType' => 'MetaMTAConstants',
'NotificationMessage' => 'PhabricatorNotificationConstants',
'NotificationPathname' => 'PhabricatorNotificationConstants',
'NotificationType' => 'PhabricatorNotificationConstants',
'OwnersPackageReplyHandler' => 'PhabricatorMailReplyHandler',
'PackageCreateMail' => 'PackageMail',
'PackageDeleteMail' => 'PackageMail',
@ -1604,6 +1621,7 @@ phutil_register_library_map(array(
'PhabricatorFeedStoryData' => 'PhabricatorFeedDAO',
'PhabricatorFeedStoryDifferential' => 'PhabricatorFeedStory',
'PhabricatorFeedStoryManiphest' => 'PhabricatorFeedStory',
'PhabricatorFeedStoryNotification' => 'PhabricatorFeedDAO',
'PhabricatorFeedStoryPhriction' => 'PhabricatorFeedStory',
'PhabricatorFeedStoryProject' => 'PhabricatorFeedStory',
'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO',
@ -1688,6 +1706,12 @@ phutil_register_library_map(array(
'PhabricatorMetaMTAWorker' => 'PhabricatorWorker',
'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController',
'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorNotificationController' => 'PhabricatorController',
'PhabricatorNotificationPanelController' => 'PhabricatorNotificationController',
'PhabricatorNotificationStoryTypeConstants' => 'PhabricatorNotificationConstants',
'PhabricatorNotificationStoryView' => 'PhabricatorNotificationView',
'PhabricatorNotificationTestController' => 'PhabricatorNotificationController',
'PhabricatorNotificationView' => 'AphrontView',
'PhabricatorNotificationsController' => 'PhabricatorController',
'PhabricatorOAuthClientAuthorization' => 'PhabricatorOAuthServerDAO',
'PhabricatorOAuthClientAuthorizationBaseController' => 'PhabricatorOAuthServerController',

View file

@ -421,8 +421,7 @@ class AphrontDefaultApplicationConfiguration
'PhabricatorChatLogChannelLogController',
),
'/aphlict/' => 'PhabricatorAphlictTestPageController',
'/notification/test/' => 'PhabricatorNotificationTestController',
'/flag/' => array(
'' => 'PhabricatorFlagListController',
'view/(?P<view>[^/]+)/' => 'PhabricatorFlagListController',

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,12 +23,23 @@ final class PhabricatorFeedStoryPublisher {
private $storyData;
private $storyTime;
private $storyAuthorPHID;
private $primaryObjectPHID;
private $subscribedPHIDs;
public function setRelatedPHIDs(array $phids) {
$this->relatedPHIDs = $phids;
return $this;
}
public function setSubscribedPHIDs(array $phids) {
$this->subscribedPHIDs = $phids;
return $this;
}
public function setPrimaryObjectPHID($phid) {
$this->primaryObjectPHID = $phid;
return $this;
}
public function setStoryType($story_type) {
$this->storyType = $story_type;
return $this;
@ -85,9 +96,43 @@ final class PhabricatorFeedStoryPublisher {
$ref->getTableName(),
implode(', ', $sql));
if (PhabricatorEnv::getEnvConfig('notification.enabled')) {
$this->insertNotifications($chrono_key);
}
return $story;
}
private function insertNotifications($chrono_key) {
if (!$this->primaryObjectPHID) {
throw
new Exception("Call setPrimaryObjectPHID() before Publishing!");
}
if ($this->subscribedPHIDs) {
$notif = new PhabricatorFeedStoryNotification();
$sql = array();
$conn = $notif->establishConnection('w');
foreach (array_unique($this->subscribedPHIDs) as $user_phid) {
$sql[] = qsprintf(
$conn,
'(%s, %s, %s, %d)',
$this->primaryObjectPHID,
$user_phid,
$chrono_key,
0);
}
queryfx(
$conn,
'INSERT INTO %T
(primaryObjectPHID, userPHID, chronologicalKey, hasViewed)
VALUES %Q',
$notif->getTableName(),
implode(', ', $sql));
}
}
/**
* We generate a unique chronological key for each story type because we want
* to be able to page through the stream with a cursor (i.e., select stories

View file

@ -19,7 +19,7 @@
abstract class PhabricatorFeedStory {
private $data;
private $hasViewed;
private $handles;
private $framed;
@ -33,6 +33,15 @@ abstract class PhabricatorFeedStory {
return array();
}
public function setHasViewed($has_viewed) {
$this->hasViewed = $has_viewed;
return $this;
}
public function getHasViewed() {
return $this->hasViewed;
}
public function getRequiredObjectPHIDs() {
return array();
}

View file

@ -16,72 +16,48 @@
* limitations under the License.
*/
final class PhabricatorFeedStoryManiphest extends PhabricatorFeedStory {
final class PhabricatorFeedStoryManiphest
extends PhabricatorFeedStory {
public function getRequiredHandlePHIDs() {
$data = $this->getStoryData();
return array_filter(
array(
array(
$this->getStoryData()->getAuthorPHID(),
$data->getValue('taskPHID'),
$data->getValue('ownerPHID'),
));
));
}
public function getRequiredObjectPHIDs() {
return array(
$this->getStoryData()->getAuthorPHID(),
);
);
}
public function renderView() {
$data = $this->getStoryData();
$author_phid = $data->getAuthorPHID();
$owner_phid = $data->getValue('ownerPHID');
$task_phid = $data->getValue('taskPHID');
$action = $data->getValue('action');
$view = new PhabricatorFeedStoryView();
$verb = ManiphestAction::getActionPastTenseVerb($action);
$extra = null;
switch ($action) {
case ManiphestAction::ACTION_ASSIGN:
if ($owner_phid) {
$extra =
' to '.
$this->linkTo($owner_phid);
} else {
$verb = 'placed';
$extra = ' up for grabs';
}
break;
}
$title =
$this->linkTo($author_phid).
" {$verb} task ".
$this->linkTo($task_phid);
$title .= $extra;
$title .= '.';
$view->setTitle($title);
switch ($action) {
case ManiphestAction::ACTION_CREATE:
$full_size = true;
break;
default:
$full_size = false;
break;
}
$view->setEpoch($data->getEpoch());
$action = $this->getLineForData($data);
$view->setTitle($action);
$view->setEpoch($data->getEpoch());
switch ($action) {
case ManiphestAction::ACTION_CREATE:
$full_size = true;
break;
default:
$full_size = false;
break;
}
if ($full_size) {
$view->setImage($this->getHandle($author_phid)->getImageURI());
$view->setImage($this->getHandle($this->getAuthorPHID())->getImageURI());
$content = $this->renderSummary($data->getValue('description'));
$view->appendChild($content);
} else {
@ -91,4 +67,46 @@ final class PhabricatorFeedStoryManiphest extends PhabricatorFeedStory {
return $view;
}
public function renderNotificationView() {
$data = $this->getStoryData();
$view = new PhabricatorNotificationStoryView();
$view->setEpoch($data->getEpoch());
$view->setTitle($this->getLineForData($data));
$view->setEpoch($data->getEpoch());
$view->setViewed($this->getHasViewed());
return $view;
}
private function getLineForData($data) {
$actor_phid = $data->getAuthorPHID();
$owner_phid = $data->getValue('ownerPHID');
$task_phid = $data->getValue('taskPHID');
$action = $data->getValue('action');
$description = $data->getValue('description');
$comments = phutil_escape_html(
phutil_utf8_shorten(
$data->getValue('comments'),
140));
$actor_link = $this->linkTo($actor_phid);
$task_link = $this->linkTo($task_phid);
$owner_link = $this->linkTo($owner_phid);
$verb = ManiphestAction::getActionPastTenseVerb($action);
$one_line = "{$actor_link} {$verb} {$task_link}";
switch ($action) {
case ManiphestAction::ACTION_ASSIGN:
$one_line .= " to {$owner_link}";
break;
default:
break;
}
return $one_line;
}
}

View file

@ -510,6 +510,9 @@ final class ManiphestTaskDetailController extends ManiphestController {
$transaction_view->setAuxiliaryFields($aux_fields);
$transaction_view->setMarkupEngine($engine);
PhabricatorFeedStoryNotification::updateObjectNotificationViews(
$user, $task->getPHID());
return $this->buildStandardPageResponse(
array(
$context_bar,

View file

@ -314,7 +314,8 @@ final class ManiphestTransactionEditor {
if ($transaction->hasComments()) {
$comments = $transaction->getComments();
}
switch ($transaction->getTransactionType()) {
$type = $transaction->getTransactionType();
switch ($type) {
case ManiphestTransactionType::TYPE_OWNER:
$actions[] = ManiphestAction::ACTION_ASSIGN;
break;
@ -323,6 +324,8 @@ final class ManiphestTransactionEditor {
$actions[] = ManiphestAction::ACTION_CLOSE;
} else if ($this->isCreate($transactions)) {
$actions[] = ManiphestAction::ACTION_CREATE;
} else {
$actions[] = ManiphestAction::ACTION_REOPEN;
}
break;
default:
@ -335,6 +338,7 @@ final class ManiphestTransactionEditor {
$actor_phid = head($transactions)->getAuthorPHID();
$author_phid = $task->getAuthorPHID();
id(new PhabricatorFeedStoryPublisher())
->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_MANIPHEST)
->setStoryData(array(
@ -357,6 +361,17 @@ final class ManiphestTransactionEditor {
$owner_phid,
)),
$task->getProjectPHIDs()))
->setPrimaryObjectPHID($task->getPHID())
->setSubscribedPHIDs(
array_merge(
array_filter(
array(
$author_phid,
$owner_phid,
$actor_phid
)
),
$task->getCCPHIDs()))
->publish();
}

View file

@ -0,0 +1,81 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorNotificationQuery {
private $limit = 100;
private $userPHID;
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function setUserPHID($user_phid) {
$this->userPHID = $user_phid;
return $this;
}
public function execute() {
if (!$this->userPHID) {
throw new Exception("Call setUser() before executing the query");
}
//TODO throw an exception if no user
$story_table = new PhabricatorFeedStoryData();
$notification_table = new PhabricatorFeedStoryNotification();
$conn = $story_table->establishConnection('r');
$data = queryfx_all(
$conn,
"SELECT story.*, notif.hasViewed FROM %T notif
JOIN %T story ON notif.chronologicalKey = story.chronologicalKey
WHERE notif.userPHID = %s
ORDER BY notif.chronologicalKey desc
LIMIT %d",
$notification_table->getTableName(),
$story_table->getTableName(),
$this->userPHID,
$this->limit);
$viewed_map = ipull($data, 'hasViewed', 'chronologicalKey');
$data = $story_table->loadAllFromArray($data);
$stories = array();
foreach ($data as $story_data) {
$class = $story_data->getStoryType();
try {
if (!class_exists($class) ||
!is_subclass_of($class, 'PhabricatorFeedStory')) {
$class = 'PhabricatorFeedStoryUnknown';
}
} catch (PhutilMissingSymbolException $ex) {
$class = 'PhabricatorFeedStoryUnknown';
}
$story = newv($class, array($story_data));
$story->setHasViewed($viewed_map[$story->getChronologicalKey()]);
$stories[] = $story;
}
return $stories;
}
}

View file

@ -0,0 +1,54 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorNotificationBuilder {
private $stories;
public function __construct(array $stories) {
$this->stories = $stories;
}
public function buildView() {
$stories = $this->stories;
$handles = array();
$objects = array();
if ($stories) {
$handle_phids = array_mergev(mpull($stories, 'getRequiredHandlePHIDs'));
$handles = id(new PhabricatorObjectHandleData($handle_phids))
->loadHandles();
}
$null_view = new AphrontNullView();
foreach ($stories as $story) {
$story->setHandles($handles);
$view = $story->renderNotificationView();
$null_view->appendChild($view);
}
return id(new AphrontNullView())->appendChild(
'<div class="phabricator-notification-frame">'.
$null_view->render().
'</div>');
}
}

View file

@ -0,0 +1,37 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class PhabricatorNotificationController
extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Notification');
$page->setBaseURI('/notification/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph('!');
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}

View file

@ -0,0 +1,53 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorNotificationTestController
extends PhabricatorNotificationController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$query = new PhabricatorNotificationQuery();
$query->setUserPHID($user->getPHID());
$stories = $query->execute();
$builder = new PhabricatorNotificationBuilder($stories);
$notifications_view = $builder->buildView();
$num_unconsumed = 0;
foreach ($stories as $story) {
if (!$story->getHasViewed()) {
$num_unconsumed++;
}
}
$json = array(
$notifications_view->render()
);
return $this->buildStandardPageResponse(
$json,
array('title' => 'Notification Test Page'));
}
}

View file

@ -0,0 +1,57 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorFeedStoryNotification extends PhabricatorFeedDAO {
protected $userPHID;
protected $primaryObjectPHID;
protected $chronologicalKey;
protected $hasViewed;
public function getConfiguration() {
return array(
self::CONFIG_IDS => self::IDS_MANUAL,
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
static public function updateObjectNotificationViews(PhabricatorUser $user,
$object_phid) {
if (PhabricatorEnv::getEnvConfig('notification.enabled')) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$notification_table = new PhabricatorFeedStoryNotification();
$conn = $notification_table->establishConnection('w');
queryfx(
$conn,
"UPDATE %T
SET hasViewed = 1
WHERE userPHID = %s
AND primaryObjectPHID = %s
AND hasViewed = 0",
$notification_table->getTableName(),
$user->getPHID(),
$object_phid);
unset($unguarded);
}
}
}

View file

@ -0,0 +1,21 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class PhabricatorNotificationView extends AphrontView {
}

View file

@ -0,0 +1,65 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorNotificationStoryView
extends PhabricatorNotificationView {
private $title;
private $phid;
private $epoch;
private $viewed;
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setEpoch($epoch) {
$this->epoch = $epoch;
return $this;
}
public function setViewed($viewed) {
$this->viewed = $viewed;
}
public function render() {
$title = $this->title;
if (!$this->viewed) {
$title = '<b>'.$title.'</b>';
}
$head = phutil_render_tag(
'div',
array(
'class' => 'phabricator-notification-story-head',
),
nonempty($title, 'Untitled Story'));
return phutil_render_tag(
'div',
array(
'class' =>
'phabricator-notification '.
'phabricator-notification-story-one-line'),
$head);
}
}