Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15281240
D13900.id33564.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
45 KB
Referenced Files
None
Subscribers
None
D13900.id33564.diff
View Options
diff --git a/resources/sql/autopatches/20150814.harbormater.artifact.phid.sql b/resources/sql/autopatches/20150814.harbormater.artifact.phid.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20150814.harbormater.artifact.phid.sql
@@ -0,0 +1,5 @@
+ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildartifact
+ ADD phid VARBINARY(64) NOT NULL AFTER id;
+
+UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildartifact
+ SET phid = CONCAT('PHID-HMBA-', id) WHERE phid = '';
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -915,12 +915,14 @@
'FundSchemaSpec' => 'applications/fund/storage/FundSchemaSpec.php',
'HarbormasterArcLintBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php',
'HarbormasterArcUnitBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcUnitBuildStepImplementation.php',
+ 'HarbormasterArtifact' => 'applications/harbormaster/artifact/HarbormasterArtifact.php',
'HarbormasterAutotargetsTestCase' => 'applications/harbormaster/__tests__/HarbormasterAutotargetsTestCase.php',
'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php',
'HarbormasterBuildAbortedException' => 'applications/harbormaster/exception/HarbormasterBuildAbortedException.php',
'HarbormasterBuildActionController' => 'applications/harbormaster/controller/HarbormasterBuildActionController.php',
'HarbormasterBuildArcanistAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildArcanistAutoplan.php',
'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php',
+ 'HarbormasterBuildArtifactPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php',
'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php',
'HarbormasterBuildAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildAutoplan.php',
'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php',
@@ -980,9 +982,12 @@
'HarbormasterCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php',
'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php',
'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php',
+ 'HarbormasterCreateArtifactConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php',
'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php',
'HarbormasterExternalBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterExternalBuildStepGroup.php',
+ 'HarbormasterFileArtifact' => 'applications/harbormaster/artifact/HarbormasterFileArtifact.php',
'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php',
+ 'HarbormasterHostArtifact' => 'applications/harbormaster/artifact/HarbormasterHostArtifact.php',
'HarbormasterLeaseHostBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php',
'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php',
'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php',
@@ -1018,6 +1023,7 @@
'HarbormasterTestBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterTestBuildStepGroup.php',
'HarbormasterThrowExceptionBuildStep' => 'applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php',
'HarbormasterUIEventListener' => 'applications/harbormaster/event/HarbormasterUIEventListener.php',
+ 'HarbormasterURIArtifact' => 'applications/harbormaster/artifact/HarbormasterURIArtifact.php',
'HarbormasterUnitMessagesController' => 'applications/harbormaster/controller/HarbormasterUnitMessagesController.php',
'HarbormasterUnitPropertyView' => 'applications/harbormaster/view/HarbormasterUnitPropertyView.php',
'HarbormasterUploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php',
@@ -4603,6 +4609,7 @@
'FundSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'HarbormasterArcLintBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterArcUnitBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
+ 'HarbormasterArtifact' => 'Phobject',
'HarbormasterAutotargetsTestCase' => 'PhabricatorTestCase',
'HarbormasterBuild' => array(
'HarbormasterDAO',
@@ -4616,6 +4623,7 @@
'HarbormasterDAO',
'PhabricatorPolicyInterface',
),
+ 'HarbormasterBuildArtifactPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildAutoplan' => 'Phobject',
'HarbormasterBuildCommand' => 'HarbormasterDAO',
@@ -4700,9 +4708,12 @@
'HarbormasterCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod',
'HarbormasterController' => 'PhabricatorController',
+ 'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterDAO' => 'PhabricatorLiskDAO',
'HarbormasterExternalBuildStepGroup' => 'HarbormasterBuildStepGroup',
+ 'HarbormasterFileArtifact' => 'HarbormasterArtifact',
'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
+ 'HarbormasterHostArtifact' => 'HarbormasterArtifact',
'HarbormasterLeaseHostBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterLintMessagesController' => 'HarbormasterController',
'HarbormasterLintPropertyView' => 'AphrontView',
@@ -4738,6 +4749,7 @@
'HarbormasterTestBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterThrowExceptionBuildStep' => 'HarbormasterBuildStepImplementation',
'HarbormasterUIEventListener' => 'PhabricatorEventListener',
+ 'HarbormasterURIArtifact' => 'HarbormasterArtifact',
'HarbormasterUnitMessagesController' => 'HarbormasterController',
'HarbormasterUnitPropertyView' => 'AphrontView',
'HarbormasterUploadArtifactBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
diff --git a/src/applications/drydock/query/DrydockLeaseQuery.php b/src/applications/drydock/query/DrydockLeaseQuery.php
--- a/src/applications/drydock/query/DrydockLeaseQuery.php
+++ b/src/applications/drydock/query/DrydockLeaseQuery.php
@@ -27,19 +27,12 @@
return $this;
}
+ public function newResultObject() {
+ return new DrydockLease();
+ }
+
protected function loadPage() {
- $table = new DrydockLease();
- $conn_r = $table->establishConnection('r');
-
- $data = queryfx_all(
- $conn_r,
- 'SELECT lease.* FROM %T lease %Q %Q %Q',
- $table->getTableName(),
- $this->buildWhereClause($conn_r),
- $this->buildOrderClause($conn_r),
- $this->buildLimitClause($conn_r));
-
- return $table->loadAllFromArray($data);
+ return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $leases) {
@@ -69,40 +62,38 @@
return $leases;
}
- protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
- $where = array();
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
- if ($this->resourceIDs) {
+ if ($this->resourceIDs !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'resourceID IN (%Ld)',
$this->resourceIDs);
}
- if ($this->ids) {
+ if ($this->ids !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'id IN (%Ld)',
$this->ids);
}
- if ($this->phids) {
+ if ($this->phids !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'phid IN (%Ls)',
$this->phids);
}
- if ($this->statuses) {
+ if ($this->statuses !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'status IN (%Ld)',
$this->statuses);
}
- $where[] = $this->buildPagingClause($conn_r);
-
- return $this->formatWhereClause($where);
+ return $where;
}
}
diff --git a/src/applications/harbormaster/artifact/HarbormasterArtifact.php b/src/applications/harbormaster/artifact/HarbormasterArtifact.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/artifact/HarbormasterArtifact.php
@@ -0,0 +1,88 @@
+<?php
+
+
+abstract class HarbormasterArtifact extends Phobject {
+
+ private $buildArtifact;
+
+ abstract public function getArtifactTypeName();
+
+ public function getArtifactTypeSummary() {
+ return $this->getArtifactTypeDescription();
+ }
+
+ abstract public function getArtifactTypeDescription();
+ abstract public function getArtifactParameterSpecification();
+ abstract public function getArtifactParameterDescriptions();
+ abstract public function willCreateArtifact(PhabricatorUser $actor);
+
+ public function validateArtifactData(array $artifact_data) {
+ $artifact_spec = $this->getArtifactParameterSpecification();
+ PhutilTypeSpec::checkMap($artifact_data, $artifact_spec);
+ }
+
+ public function renderArtifactSummary(PhabricatorUser $viewer) {
+ return null;
+ }
+
+ public function releaseArtifact(PhabricatorUser $actor) {
+ return;
+ }
+
+ public function getArtifactDataExample() {
+ return null;
+ }
+
+ public function setBuildArtifact(HarbormasterBuildArtifact $build_artifact) {
+ $this->buildArtifact = $build_artifact;
+ return $this;
+ }
+
+ public function getBuildArtifact() {
+ return $this->buildArtifact;
+ }
+
+ final public function getArtifactConstant() {
+ $class = new ReflectionClass($this);
+
+ $const = $class->getConstant('ARTIFACTCONST');
+ if ($const === false) {
+ throw new Exception(
+ pht(
+ '"%s" class "%s" must define a "%s" property.',
+ __CLASS__,
+ get_class($this),
+ 'ARTIFACTCONST'));
+ }
+
+ $limit = self::getArtifactConstantByteLimit();
+ if (!is_string($const) || (strlen($const) > $limit)) {
+ throw new Exception(
+ pht(
+ '"%s" class "%s" has an invalid "%s" property. Action constants '.
+ 'must be strings and no more than %s bytes in length.',
+ __CLASS__,
+ get_class($this),
+ 'ARTIFACTCONST',
+ new PhutilNumber($limit)));
+ }
+
+ return $const;
+ }
+
+ final public static function getArtifactConstantByteLimit() {
+ return 32;
+ }
+
+ final public static function getAllArtifactTypes() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getArtifactConstant')
+ ->execute();
+ }
+
+ final public static function getArtifactType($type) {
+ return idx(self::getAllArtifactTypes(), $type);
+ }
+
+}
diff --git a/src/applications/harbormaster/artifact/HarbormasterFileArtifact.php b/src/applications/harbormaster/artifact/HarbormasterFileArtifact.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/artifact/HarbormasterFileArtifact.php
@@ -0,0 +1,66 @@
+<?php
+
+final class HarbormasterFileArtifact extends HarbormasterArtifact {
+
+ const ARTIFACTCONST = 'file';
+
+ public function getArtifactTypeName() {
+ return pht('File');
+ }
+
+ public function getArtifactTypeDescription() {
+ return pht(
+ 'Stores a reference to file data which has been uploaded to '.
+ 'Phabricator.');
+ }
+
+ public function getArtifactParameterSpecification() {
+ return array(
+ 'filePHID' => 'string',
+ );
+ }
+
+ public function getArtifactParameterDescriptions() {
+ return array(
+ 'filePHID' => pht('File to create an artifact from.'),
+ );
+ }
+
+ public function getArtifactDataExample() {
+ return array(
+ 'filePHID' => 'PHID-FILE-abcdefghijklmnopqrst',
+ );
+ }
+
+ public function renderArtifactSummary(PhabricatorUser $viewer) {
+ $artifact = $this->getBuildArtifact();
+ $file_phid = $artifact->getProperty('filePHID');
+ return $viewer->renderHandle($file_phid);
+ }
+
+ public function willCreateArtifact(PhabricatorUser $actor) {
+ // NOTE: This is primarily making sure the actor has permission to view the
+ // file. We don't want to let you run builds using files you don't have
+ // permission to see, since this could let you violate permissions.
+ $this->loadArtifactFile($actor);
+ }
+
+ public function loadArtifactFile(PhabricatorUser $viewer) {
+ $artifact = $this->getBuildArtifact();
+ $file_phid = $artifact->getProperty('filePHID');
+
+ $file = id(new PhabricatorFileQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($file_phid))
+ ->executeOne();
+ if (!$file) {
+ throw new Exception(
+ pht(
+ 'File PHID "%s" does not correspond to a valid file.',
+ $file_phid));
+ }
+
+ return $file;
+ }
+
+}
diff --git a/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php b/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php
@@ -0,0 +1,74 @@
+<?php
+
+final class HarbormasterHostArtifact extends HarbormasterArtifact {
+
+ const ARTIFACTCONST = 'host';
+
+ public function getArtifactTypeName() {
+ return pht('Drydock Host');
+ }
+
+ public function getArtifactTypeDescription() {
+ return pht('References a host lease from Drydock.');
+ }
+
+
+ public function getArtifactParameterSpecification() {
+ return array(
+ 'drydockLeasePHID' => 'string',
+ );
+ }
+
+ public function getArtifactParameterDescriptions() {
+ return array(
+ 'drydockLeasePHID' => pht(
+ 'Drydock host lease to create an artifact from.'),
+ );
+ }
+
+ public function getArtifactDataExample() {
+ return array(
+ 'drydockLeasePHID' => 'PHID-DRYL-abcdefghijklmnopqrst',
+ );
+ }
+
+ public function renderArtifactSummary(PhabricatorUser $viewer) {
+ $artifact = $this->getBuildArtifact();
+ $file_phid = $artifact->getProperty('drydockLeasePHID');
+ return $viewer->renderHandle($file_phid);
+ }
+
+ public function willCreateArtifact(PhabricatorUser $actor) {
+ $this->loadArtifactLease($actor);
+ }
+
+ public function loadArtifactLease(PhabricatorUser $viewer) {
+ $artifact = $this->getBuildArtifact();
+ $lease_phid = $artifact->getProperty('drydockLeasePHID');
+
+ $lease = id(new DrydockLeaseQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($lease_phid))
+ ->executeOne();
+ if (!$lease) {
+ throw new Exception(
+ pht(
+ 'Drydock lease PHID "%s" does not correspond to a valid lease.',
+ $lease_phid));
+ }
+
+ return $lease;
+ }
+
+ public function releaseArtifact(PhabricatorUser $actor) {
+ $lease = $this->loadArtifactLease($actor);
+ $resource = $lease->getResource();
+ $blueprint = $resource->getBlueprint();
+
+ if ($lease->isActive()) {
+ $blueprint->releaseLease($resource, $lease);
+ }
+ }
+
+
+}
diff --git a/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php b/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php
@@ -0,0 +1,100 @@
+<?php
+
+final class HarbormasterURIArtifact extends HarbormasterArtifact {
+
+ const ARTIFACTCONST = 'uri';
+
+ public function getArtifactTypeName() {
+ return pht('URI');
+ }
+
+ public function getArtifactTypeSummary() {
+ return pht('Stores a URI.');
+ }
+
+ public function getArtifactTypeDescription() {
+ return pht(
+ "Stores a URI.\n\n".
+ "With `ui.external`, you can use this artifact type to add links to ".
+ "build results in an external build system.");
+ }
+
+ public function getArtifactParameterSpecification() {
+ return array(
+ 'uri' => 'string',
+ 'name' => 'optional string',
+ 'ui.external' => 'optional bool',
+ );
+ }
+
+ public function getArtifactParameterDescriptions() {
+ return array(
+ 'uri' => pht('The URI to store.'),
+ 'name' => pht('Optional label for this URI.'),
+ 'ui.external' => pht(
+ 'If true, display this URI in the UI as an link to '.
+ 'additional build details in an external build system.'),
+ );
+ }
+
+ public function getArtifactDataExample() {
+ return array(
+ 'uri' => 'https://buildserver.mycompany.com/build/123/',
+ 'name' => pht('View External Build Results'),
+ 'ui.external' => true,
+ );
+ }
+
+ public function renderArtifactSummary(PhabricatorUser $viewer) {
+ $artifact = $this->getBuildArtifact();
+ $uri = $artifact->getProperty('uri');
+
+ try {
+ $this->validateURI($uri);
+ } catch (Exception $ex) {
+ return pht('<Invalid URI>');
+ }
+
+ $name = $artifact->getProperty('name', $uri);
+
+ return phutil_tag(
+ 'a',
+ array(
+ 'href' => $uri,
+ 'target' => '_blank',
+ ),
+ $name);
+ }
+
+ public function willCreateArtifact(PhabricatorUser $actor) {
+ $artifact = $this->getBuildArtifact();
+ $uri = $artifact->getProperty('uri');
+ $this->validateURI($uri);
+ }
+
+ private function validateURI($raw_uri) {
+ $uri = new PhutilURI($raw_uri);
+
+ $protocol = $uri->getProtocol();
+ if (!strlen($protocol)) {
+ throw new Exception(
+ pht(
+ 'Unable to identify the protocol for URI "%s". URIs must be '.
+ 'fully qualified and have an identifiable protocol.',
+ $raw_uri));
+ }
+
+ $protocol_key = 'uri.allowed-protocols';
+ $protocols = PhabricatorEnv::getEnvConfig($protocol_key);
+ if (empty($protocols[$protocol])) {
+ throw new Exception(
+ pht(
+ 'URI "%s" does not have an allowable protocol. Configure '.
+ 'protocols in `%s`. Allowed protocols are: %s.',
+ $raw_uri,
+ $protocol_key,
+ implode(', ', array_keys($protocols))));
+ }
+ }
+
+}
diff --git a/src/applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php
--- a/src/applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php
+++ b/src/applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php
@@ -15,4 +15,16 @@
return pht('All Harbormaster APIs are new and subject to change.');
}
+ protected function returnArtifactList(array $artifacts) {
+ $list = array();
+
+ foreach ($artifacts as $artifact) {
+ $list[] = array(
+ 'phid' => $artifact->getPHID(),
+ );
+ }
+
+ return $list;
+ }
+
}
diff --git a/src/applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php
@@ -0,0 +1,129 @@
+<?php
+
+final class HarbormasterCreateArtifactConduitAPIMethod
+ extends HarbormasterConduitAPIMethod {
+
+ public function getAPIMethodName() {
+ return 'harbormaster.createartifact';
+ }
+
+ public function getMethodSummary() {
+ return pht('Create a build artifact.');
+ }
+
+ public function getMethodDescription() {
+ $types = HarbormasterArtifact::getAllArtifactTypes();
+ $types = msort($types, 'getArtifactTypeName');
+
+ $head_key = pht('Key');
+ $head_type = pht('Type');
+ $head_desc = pht('Description');
+ $head_atype = pht('Artifact Type');
+ $head_name = pht('Name');
+ $head_summary = pht('Summary');
+
+ $out = array();
+ $out[] = pht(
+ 'Use this method to attach artifacts to build targets while running '.
+ 'builds. Artifacts can be used to carry data through a complex build '.
+ 'workflow, provide extra information to users, or store build results.');
+ $out[] = null;
+ $out[] = pht(
+ 'When creating an artifact, you will choose an `artifactType` from '.
+ 'this table. These types of artifacts are supported:');
+
+ $out[] = "| {$head_atype} | {$head_name} | {$head_summary} |";
+ $out[] = '|-------------|--------------|--------------|';
+ foreach ($types as $type) {
+ $type_name = $type->getArtifactTypeName();
+ $type_const = $type->getArtifactConstant();
+ $type_summary = $type->getArtifactTypeSummary();
+ $out[] = "| `{$type_const}` | **{$type_name}** | {$type_summary} |";
+ }
+
+ $out[] = null;
+ $out[] = pht(
+ 'Each artifact also needs an `artifactKey`, which names the artifact. '.
+ 'Finally, you will provide some `artifactData` to fill in the content '.
+ 'of the artifact. The data you provide depends on what type of artifact '.
+ 'you are creating.');
+
+ foreach ($types as $type) {
+ $type_name = $type->getArtifactTypeName();
+ $type_const = $type->getArtifactConstant();
+
+ $out[] = $type_name;
+ $out[] = '--------------------------';
+ $out[] = null;
+ $out[] = $type->getArtifactTypeDescription();
+ $out[] = null;
+ $out[] = pht(
+ 'Create an artifact of this type by passing `%s` as the '.
+ '`artifactType`. When creating an artifact of this type, provide '.
+ 'these parameters as a dictionary to `artifactData`:',
+ $type_const);
+
+ $spec = $type->getArtifactParameterSpecification();
+ $desc = $type->getArtifactParameterDescriptions();
+ $out[] = "| {$head_key} | {$head_type} | {$head_desc} |";
+ $out[] = '|-------------|--------------|--------------|';
+ foreach ($spec as $key => $key_type) {
+ $key_desc = idx($desc, $key);
+ $out[] = "| `{$key}` | //{$key_type}// | {$key_desc} |";
+ }
+
+ $example = $type->getArtifactDataExample();
+ if ($example !== null) {
+ $json = new PhutilJSON();
+ $rendered = $json->encodeFormatted($example);
+
+ $out[] = pht('For example:');
+ $out[] = '```lang=json';
+ $out[] = $rendered;
+ $out[] = '```';
+ }
+ }
+
+ return implode("\n", $out);
+ }
+
+ protected function defineParamTypes() {
+ return array(
+ 'buildTargetPHID' => 'phid',
+ 'artifactKey' => 'string',
+ 'artifactType' => 'string',
+ 'artifactData' => 'map<string, wild>',
+ );
+ }
+
+ protected function defineReturnType() {
+ return 'wild';
+ }
+
+ protected function execute(ConduitAPIRequest $request) {
+ $viewer = $request->getUser();
+
+ $build_target_phid = $request->getValue('buildTargetPHID');
+ $build_target = id(new HarbormasterBuildTargetQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($build_target_phid))
+ ->executeOne();
+ if (!$build_target) {
+ throw new Exception(
+ pht(
+ 'No such build target "%s"!',
+ $build_target_phid));
+ }
+
+ $artifact = $build_target->createArtifact(
+ $viewer,
+ $request->getValue('artifactKey'),
+ $request->getValue('artifactType'),
+ $request->getValue('artifactData'));
+
+ return array(
+ 'data' => $this->returnArtifactList(array($artifact)),
+ );
+ }
+
+}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
--- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
@@ -3,17 +3,11 @@
final class HarbormasterBuildViewController
extends HarbormasterController {
- private $id;
-
- public function willProcessRequest(array $data) {
- $this->id = $data['id'];
- }
-
- public function processRequest() {
+ public function handleRequest(AphrontRequest $request) {
$request = $this->getRequest();
$viewer = $request->getUser();
- $id = $this->id;
+ $id = $request->getURIData('id');
$generation = $request->getInt('g');
$build = id(new HarbormasterBuildQuery())
@@ -224,29 +218,51 @@
));
}
- private function buildArtifacts(
- HarbormasterBuildTarget $build_target) {
-
- $request = $this->getRequest();
- $viewer = $request->getUser();
+ private function buildArtifacts(HarbormasterBuildTarget $build_target) {
+ $viewer = $this->getViewer();
$artifacts = id(new HarbormasterBuildArtifactQuery())
->setViewer($viewer)
->withBuildTargetPHIDs(array($build_target->getPHID()))
->execute();
- $list = id(new PHUIObjectItemListView())
- ->setNoDataString(pht('This target has no associated artifacts.'))
- ->setFlush(true);
+ $artifacts = msort($artifacts, 'getArtifactKey');
+ $rows = array();
foreach ($artifacts as $artifact) {
- $item = $artifact->getObjectItemView($viewer);
- if ($item !== null) {
- $list->addItem($item);
+ $impl = $artifact->getArtifactImplementation();
+
+ if ($impl) {
+ $summary = $impl->renderArtifactSummary($viewer);
+ $type_name = $impl->getArtifactTypeName();
+ } else {
+ $summary = pht('<Unknown Artifact Type>');
+ $type_name = $artifact->getType();
}
+
+ $rows[] = array(
+ $artifact->getArtifactKey(),
+ $type_name,
+ $summary,
+ );
}
- return $list;
+ $table = id(new AphrontTableView($rows))
+ ->setNoDataString(pht('This target has no associated artifacts.'))
+ ->setHeaders(
+ array(
+ pht('Key'),
+ pht('Type'),
+ pht('Summary'),
+ ))
+ ->setColumnClasses(
+ array(
+ 'pri',
+ '',
+ 'wide',
+ ));
+
+ return $table;
}
private function buildLog(
diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
--- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
+++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
@@ -483,7 +483,7 @@
->execute();
foreach ($artifacts as $artifact) {
- $artifact->release();
+ $artifact->releaseArtifact();
}
}
diff --git a/src/applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php b/src/applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php
@@ -0,0 +1,35 @@
+<?php
+
+final class HarbormasterBuildArtifactPHIDType extends PhabricatorPHIDType {
+
+ const TYPECONST = 'HMBA';
+
+ public function getTypeName() {
+ return pht('Build Artifact');
+ }
+
+ public function newObject() {
+ return new HarbormasterBuildArtifact();
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new HarbormasterBuildArtifactQuery())
+ ->withPHIDs($phids);
+ }
+
+ public function loadHandles(
+ PhabricatorHandleQuery $query,
+ array $handles,
+ array $objects) {
+
+ foreach ($handles as $phid => $handle) {
+ $artifact = $objects[$phid];
+ $artifact_id = $artifact->getID();
+ $handle->setName(pht('Build Artifact %d', $artifact_id));
+ }
+ }
+
+}
diff --git a/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php b/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php
--- a/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php
+++ b/src/applications/harbormaster/query/HarbormasterBuildArtifactQuery.php
@@ -6,7 +6,7 @@
private $ids;
private $buildTargetPHIDs;
private $artifactTypes;
- private $artifactKeys;
+ private $artifactIndexes;
private $keyBuildPHID;
private $keyBuildGeneration;
@@ -25,29 +25,17 @@
return $this;
}
- public function withArtifactKeys(
- $build_phid,
- $build_gen,
- array $artifact_keys) {
- $this->keyBuildPHID = $build_phid;
- $this->keyBuildGeneration = $build_gen;
- $this->artifactKeys = $artifact_keys;
+ public function withArtifactIndexes(array $artifact_indexes) {
+ $this->artifactIndexes = $artifact_indexes;
return $this;
}
+ public function newResultObject() {
+ return new HarbormasterBuildArtifact();
+ }
+
protected function loadPage() {
- $table = new HarbormasterBuildArtifact();
- $conn_r = $table->establishConnection('r');
-
- $data = queryfx_all(
- $conn_r,
- 'SELECT * FROM %T %Q %Q %Q',
- $table->getTableName(),
- $this->buildWhereClause($conn_r),
- $this->buildOrderClause($conn_r),
- $this->buildLimitClause($conn_r));
-
- return $table->loadAllFromArray($data);
+ return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $page) {
@@ -75,46 +63,38 @@
return $page;
}
- protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
- $where = array();
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
- if ($this->ids) {
+ if ($this->ids !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'id IN (%Ld)',
$this->ids);
}
- if ($this->buildTargetPHIDs) {
+ if ($this->buildTargetPHIDs !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'buildTargetPHID IN (%Ls)',
$this->buildTargetPHIDs);
}
- if ($this->artifactTypes) {
+ if ($this->artifactTypes !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'artifactType in (%Ls)',
$this->artifactTypes);
}
- if ($this->artifactKeys) {
- $indexes = array();
- foreach ($this->artifactKeys as $key) {
- $indexes[] = PhabricatorHash::digestForIndex(
- $this->keyBuildPHID.$this->keyBuildGeneration.$key);
- }
-
+ if ($this->artifactIndexes !== null) {
$where[] = qsprintf(
- $conn_r,
+ $conn,
'artifactIndex IN (%Ls)',
- $indexes);
+ $this->artifactIndexes);
}
- $where[] = $this->buildPagingClause($conn_r);
-
- return $this->formatWhereClause($where);
+ return $where;
}
public function getQueryApplicationClass() {
diff --git a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php
@@ -43,9 +43,9 @@
$settings = $this->getSettings();
$variables = $build_target->getVariables();
- $artifact = $build->loadArtifact($settings['hostartifact']);
-
- $lease = $artifact->loadDrydockLease();
+ $artifact = $build_target->loadArtifact($settings['hostartifact']);
+ $impl = $artifact->getArtifactImplementation();
+ $lease = $impl->loadArtifactLease();
$this->platform = $lease->getAttribute('platform');
@@ -120,9 +120,9 @@
public function getArtifactInputs() {
return array(
array(
- 'name' => pht('Run on Host'),
- 'key' => $this->getSetting('hostartifact'),
- 'type' => HarbormasterBuildArtifact::TYPE_HOST,
+ 'name' => pht('Run on Host'),
+ 'key' => $this->getSetting('hostartifact'),
+ 'type' => HarbormasterHostArtifact::ARTIFACTCONST,
),
);
}
diff --git a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php
@@ -36,14 +36,13 @@
$lease->waitUntilActive();
// Create the associated artifact.
- $artifact = $build->createArtifact(
- $build_target,
+ $artifact = $build_target->createArtifact(
+ PhabricatorUser::getOmnipotentUser(),
$settings['name'],
- HarbormasterBuildArtifact::TYPE_HOST);
- $artifact->setArtifactData(array(
- 'drydock-lease' => $lease->getID(),
- ));
- $artifact->save();
+ HarbormasterHostArtifact::ARTIFACTCONST,
+ array(
+ 'drydockLeasePHID' => $lease->getPHID(),
+ ));
}
public function getArtifactOutputs() {
@@ -51,7 +50,7 @@
array(
'name' => pht('Leased Host'),
'key' => $this->getSetting('name'),
- 'type' => HarbormasterBuildArtifact::TYPE_HOST,
+ 'type' => HarbormasterHostArtifact::ARTIFACTCONST,
),
);
}
diff --git a/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php
@@ -29,33 +29,34 @@
$settings = $this->getSettings();
$variables = $build_target->getVariables();
+ $viewer = PhabricatorUser::getOmnipotentUser();
$path = $this->mergeVariables(
'vsprintf',
$settings['path'],
$variables);
- $artifact = $build->loadArtifact($settings['artifact']);
-
- $file = $artifact->loadPhabricatorFile();
+ $artifact = $build_target->loadArtifact($settings['artifact']);
+ $impl = $artifact->getArtifactImplementation();
+ $file = $impl->loadArtifactFile($viewer);
$fragment = id(new PhragmentFragmentQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->setViewer($viewer)
->withPaths(array($path))
->executeOne();
if ($fragment === null) {
PhragmentFragment::createFromFile(
- PhabricatorUser::getOmnipotentUser(),
+ $viewer,
$file,
$path,
PhabricatorPolicies::getMostOpenPolicy(),
PhabricatorPolicies::POLICY_USER);
} else {
if ($file->getMimeType() === 'application/zip') {
- $fragment->updateFromZIP(PhabricatorUser::getOmnipotentUser(), $file);
+ $fragment->updateFromZIP($viewer, $file);
} else {
- $fragment->updateFromFile(PhabricatorUser::getOmnipotentUser(), $file);
+ $fragment->updateFromFile($viewer, $file);
}
}
}
@@ -65,7 +66,7 @@
array(
'name' => pht('Publishes File'),
'key' => $this->getSetting('artifact'),
- 'type' => HarbormasterBuildArtifact::TYPE_FILE,
+ 'type' => HarbormasterFileArtifact::ARTIFACTCONST,
),
);
}
diff --git a/src/applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php
@@ -34,8 +34,7 @@
$settings['path'],
$variables);
- $artifact = $build->loadArtifact($settings['hostartifact']);
-
+ $artifact = $build_target->loadArtifact($settings['hostartifact']);
$lease = $artifact->loadDrydockLease();
$interface = $lease->getInterface('filesystem');
@@ -44,14 +43,13 @@
$file = $interface->saveFile($path, $settings['name']);
// Insert the artifact record.
- $artifact = $build->createArtifact(
- $build_target,
+ $artifact = $build_target->createArtifact(
+ PhabricatorUser::getOmnipotentUser(),
$settings['name'],
- HarbormasterBuildArtifact::TYPE_FILE);
- $artifact->setArtifactData(array(
- 'filePHID' => $file->getPHID(),
- ));
- $artifact->save();
+ HarbormasterFileArtifact::ARTIFACTCONST,
+ array(
+ 'filePHID' => $file->getPHID(),
+ ));
}
public function getArtifactInputs() {
@@ -59,7 +57,7 @@
array(
'name' => pht('Upload From Host'),
'key' => $this->getSetting('hostartifact'),
- 'type' => HarbormasterBuildArtifact::TYPE_HOST,
+ 'type' => HarbormasterHostArtifact::ARTIFACTCONST,
),
);
}
@@ -69,7 +67,7 @@
array(
'name' => pht('Uploaded File'),
'key' => $this->getSetting('name'),
- 'type' => HarbormasterBuildArtifact::TYPE_FILE,
+ 'type' => HarbormasterHostArtifact::ARTIFACTCONST,
),
);
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
--- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
@@ -235,36 +235,6 @@
return $log;
}
- public function createArtifact(
- HarbormasterBuildTarget $build_target,
- $artifact_key,
- $artifact_type) {
-
- $artifact =
- HarbormasterBuildArtifact::initializeNewBuildArtifact($build_target);
- $artifact->setArtifactKey(
- $this->getPHID(),
- $this->getBuildGeneration(),
- $artifact_key);
- $artifact->setArtifactType($artifact_type);
- $artifact->save();
- return $artifact;
- }
-
- public function loadArtifact($name) {
- $artifact = id(new HarbormasterBuildArtifactQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withArtifactKeys(
- $this->getPHID(),
- $this->getBuildGeneration(),
- array($name))
- ->executeOne();
- if ($artifact === null) {
- throw new Exception(pht('Artifact not found!'));
- }
- return $artifact;
- }
-
public function retrieveVariablesFromBuild() {
$results = array(
'buildable.diff' => null,
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php
@@ -10,19 +10,18 @@
protected $artifactData = array();
private $buildTarget = self::ATTACHABLE;
-
- const TYPE_FILE = 'file';
- const TYPE_HOST = 'host';
- const TYPE_URI = 'uri';
+ private $artifactImplementation;
public static function initializeNewBuildArtifact(
HarbormasterBuildTarget $build_target) {
return id(new HarbormasterBuildArtifact())
+ ->attachBuildTarget($build_target)
->setBuildTargetPHID($build_target->getPHID());
}
protected function getConfiguration() {
return array(
+ self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'artifactData' => self::SERIALIZATION_JSON,
),
@@ -43,6 +42,11 @@
) + parent::getConfiguration();
}
+ public function generatePHID() {
+ return PhabricatorPHID::generateNewPHID(
+ HarbormasterBuildArtifactPHIDType::TYPECONST);
+ }
+
public function attachBuildTarget(HarbormasterBuildTarget $build_target) {
$this->buildTarget = $build_target;
return $this;
@@ -52,113 +56,58 @@
return $this->assertAttached($this->buildTarget);
}
- public function setArtifactKey($build_phid, $build_gen, $key) {
- $this->artifactIndex =
- PhabricatorHash::digestForIndex($build_phid.$build_gen.$key);
+ public function setArtifactKey($key) {
+ $target = $this->getBuildTarget();
+ $this->artifactIndex = self::getArtifactIndex($target, $key);
$this->artifactKey = $key;
return $this;
}
- public function getObjectItemView(PhabricatorUser $viewer) {
- $data = $this->getArtifactData();
- switch ($this->getArtifactType()) {
- case self::TYPE_FILE:
- $handle = id(new PhabricatorHandleQuery())
- ->setViewer($viewer)
- ->withPHIDs($data)
- ->executeOne();
-
- return id(new PHUIObjectItemView())
- ->setObjectName(pht('File'))
- ->setHeader($handle->getFullName())
- ->setHref($handle->getURI());
- case self::TYPE_HOST:
- $leases = id(new DrydockLeaseQuery())
- ->setViewer($viewer)
- ->withIDs(array($data['drydock-lease']))
- ->execute();
- $lease = idx($leases, $data['drydock-lease']);
-
- return id(new PHUIObjectItemView())
- ->setObjectName(pht('Drydock Lease'))
- ->setHeader($lease->getID())
- ->setHref('/drydock/lease/'.$lease->getID());
- case self::TYPE_URI:
- return id(new PHUIObjectItemView())
- ->setObjectName($data['name'])
- ->setHeader($data['uri'])
- ->setHref($data['uri']);
- default:
- return null;
- }
- }
-
- public function loadDrydockLease() {
- if ($this->getArtifactType() !== self::TYPE_HOST) {
- throw new Exception(
- pht(
- '`%s` may only be called on host artifacts.',
- __FUNCTION__));
- }
+ public static function getArtifactIndex(
+ HarbormasterBuildTarget $target,
+ $artifact_key) {
- $data = $this->getArtifactData();
+ $build = $target->getBuild();
- // FIXME: Is there a better way of doing this?
- // TODO: Policy stuff, etc.
- $lease = id(new DrydockLease())->load(
- $data['drydock-lease']);
- if ($lease === null) {
- throw new Exception(pht('Associated Drydock lease not found!'));
- }
- $resource = id(new DrydockResource())->load(
- $lease->getResourceID());
- if ($resource === null) {
- throw new Exception(pht('Associated Drydock resource not found!'));
- }
- $lease->attachResource($resource);
+ $parts = array(
+ $build->getPHID(),
+ $target->getBuildGeneration(),
+ $artifact_key,
+ );
+ $parts = implode("\0", $parts);
- return $lease;
+ return PhabricatorHash::digestForIndex($parts);
}
- public function loadPhabricatorFile() {
- if ($this->getArtifactType() !== self::TYPE_FILE) {
- throw new Exception(
- pht(
- '`%s` may only be called on file artifacts.',
- __FUNCTION__));
+ public function releaseArtifact() {
+ $impl = $this->getArtifactImplementation();
+
+ if ($impl) {
+ $impl->releaseArtifact(PhabricatorUser::getOmnipotentUser());
}
- $data = $this->getArtifactData();
+ return null;
+ }
- // The data for TYPE_FILE is an array with a single PHID in it.
- $phid = $data['filePHID'];
+ public function getArtifactImplementation() {
+ if ($this->artifactImplementation === null) {
+ $type = $this->getArtifactType();
+ $impl = HarbormasterArtifact::getArtifactType($type);
+ if (!$impl) {
+ return null;
+ }
- $file = id(new PhabricatorFileQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withPHIDs(array($phid))
- ->executeOne();
- if ($file === null) {
- throw new Exception(pht('Associated file not found!'));
+ $impl = clone $impl;
+ $impl->setBuildArtifact($this);
+ $this->artifactImplementation = $impl;
}
- return $file;
- }
- public function release() {
- switch ($this->getArtifactType()) {
- case self::TYPE_HOST:
- $this->releaseDrydockLease();
- break;
- }
+ return $this->artifactImplementation;
}
- public function releaseDrydockLease() {
- $lease = $this->loadDrydockLease();
- $resource = $lease->getResource();
- $blueprint = $resource->getBlueprint();
- if ($lease->isActive()) {
- $blueprint->releaseLease($resource, $lease);
- }
+ public function getProperty($key, $default = null) {
+ return idx($this->artifactData, $key, $default);
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php
@@ -201,6 +201,54 @@
);
}
+ public function createArtifact(
+ PhabricatorUser $actor,
+ $artifact_key,
+ $artifact_type,
+ array $artifact_data) {
+
+ $impl = HarbormasterArtifact::getArtifactType($artifact_type);
+ if (!$impl) {
+ throw new Exception(
+ pht(
+ 'There is no implementation available for artifacts of type "%s".',
+ $artifact_type));
+ }
+
+ $impl->validateArtifactData($artifact_data);
+
+ $artifact = HarbormasterBuildArtifact::initializeNewBuildArtifact($this)
+ ->setArtifactKey($artifact_key)
+ ->setArtifactType($artifact_type)
+ ->setArtifactData($artifact_data);
+
+ $impl = $artifact->getArtifactImplementation();
+ $impl->willCreateArtifact($actor);
+
+ return $artifact->save();
+ }
+
+ public function loadArtifact($artifact_key) {
+ $indexes = array();
+
+ $indexes[] = HarbormasterBuildArtifact::getArtifactIndex(
+ $this,
+ $artifact_key);
+
+ $artifact = id(new HarbormasterBuildArtifactQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withArtifactIndexes($indexes)
+ ->executeOne();
+ if ($artifact === null) {
+ throw new Exception(
+ pht(
+ 'Artifact "%s" not found!',
+ $artifact_key));
+ }
+
+ return $artifact;
+ }
+
/* -( Status )------------------------------------------------------------- */
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Mar 4, 4:24 PM (18 h, 56 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7222182
Default Alt Text
D13900.id33564.diff (45 KB)
Attached To
Mode
D13900: Add `harbormaster.createartifact`
Attached
Detach File
Event Timeline
Log In to Comment