diff --git a/conf/default.conf.php b/conf/default.conf.php index 2b9eaf0fd..4c44ce01c 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -1,709 +1,714 @@ <?php /* * Copyright 2011 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. */ return array( // The root URI which Phabricator is installed on. // Example: "http://phabricator.example.com/" 'phabricator.base-uri' => null, // If you have multiple environments, provide the production environment URI // here so that emails, etc., generated in development/sandbox environments // contain the right links. 'phabricator.production-uri' => null, // Setting this to 'true' will invoke a special setup mode which helps guide // you through setting up Phabricator. 'phabricator.setup' => false, // The default PHID for users who haven't uploaded a profile image. It should // be 50x50px. 'user.default-profile-image-phid' => 'PHID-FILE-4d61229816cfe6f2b2a3', // -- IMPORTANT! Security! -------------------------------------------------- // // IMPORTANT: By default, Phabricator serves files from the same domain the // application lives on. This is convenient but not secure: it creates // a vulnerability where an external attacker can: // // - Convince a privileged user to upload a file which appears to be an // image or some other inoccuous type of file (the file is actually both // a JAR and an image); and // - convince the user to give them the URI for the image; and // - convince the user to click a link to a site which embeds the "image" // using an <applet /> tag. This steals the user's credentials. // // If the attacker is internal, they can execute the first two steps // themselves and need only convince another user to click a link in order to // steal their credentials. // // To avoid this, you should configure a second domain in the same way you // have the primary domain configured (e.g., point it at the same machine and // set up the same vhost rules) and provide it here. For instance, if your // primary install is on "http://www.phabricator-example.com/", you could // configure "http://www.phabricator-files.com/" and specify the entire // domain (with protocol) here. This will enforce that viewable files are // served only from the alternate domain. Ideally, you should use a completely // separate domain name rather than just a different subdomain. // // It is STRONGLY RECOMMENDED that you configure this. Phabricator makes this // attack difficult, but it is viable unless you isolate the file domain. 'security.alternate-file-domain' => null, // -- DarkConsole ----------------------------------------------------------- // // DarkConsole is a administrative debugging/profiling tool built into // Phabricator. You can leave it disabled unless you're developing against // Phabricator. // Determines whether or not DarkConsole is available. DarkConsole exposes // some data like queries and stack traces, so you should be careful about // turning it on in production (although users can not normally see it, even // if the deployment configuration enables it). 'darkconsole.enabled' => false, // Always enable DarkConsole, even for logged out users. This potentially // exposes sensitive information to users, so make sure untrusted users can // not access an install running in this mode. You should definitely leave // this off in production. It is only really useful for using DarkConsole // utilties to debug or profile logged-out pages. You must set // 'darkconsole.enabled' to use this option. 'darkconsole.always-on' => false, // Allows you to mask certain configuration values from appearing in the // "Config" tab of DarkConsole. 'darkconsole.config-mask' => array( 'mysql.pass', 'amazon-ses.secret-key', 'recaptcha.private-key', 'phabricator.csrf-key', 'facebook.application-secret', 'github.application-secret', ), // -- MySQL --------------------------------------------------------------- // // The username to use when connecting to MySQL. 'mysql.user' => 'root', // The password to use when connecting to MySQL. 'mysql.pass' => '', // The MySQL server to connect to. If you want to connect to a different // port than the default (which is 3306), specify it in the hostname // (e.g., db.example.com:1234). 'mysql.host' => 'localhost', // -- Email ----------------------------------------------------------------- // // Some Phabricator tools send email notifications, e.g. when Differential // revisions are updated or Maniphest tasks are changed. These options allow // you to configure how email is delivered. // You can test your mail setup by going to "MetaMTA" in the web interface, // clicking "Send New Message", and then composing a message. // Default address to send mail "From". 'metamta.default-address' => 'noreply@example.com', // Domain used to generate Message-IDs. 'metamta.domain' => 'example.com', // When a user takes an action which generates an email notification (like // commenting on a Differential revision), Phabricator can either send that // mail "From" the user's email address (like "alincoln@logcabin.com") or // "From" the 'metamta.default-address' address. The user experience is // generally better if Phabricator uses the user's real address as the "From" // since the messages are easier to organize when they appear in mail clients, // but this will only work if the server is authorized to send email on behalf // of the "From" domain. Practically, this means: // - If you are doing an install for Example Corp and all the users will // have corporate @corp.example.com addresses and any hosts Phabricator // is running on are authorized to send email from corp.example.com, // you can enable this to make the user experience a little better. // - If you are doing an install for an open source project and your // users will be registering via Facebook and using personal email // addresses, you MUST NOT enable this or virtually all of your outgoing // email will vanish into SFP blackholes. // - If your install is anything else, you're much safer leaving this // off since the risk in turning it on is that your outgoing mail will // mostly never arrive. 'metamta.can-send-as-user' => false, // Adapter class to use to transmit mail to the MTA. The default uses // PHPMailerLite, which will invoke "sendmail". This is appropriate // if sendmail actually works on your host, but if you haven't configured mail // it may not be so great. You can also use Amazon SES, by changing this to // 'PhabricatorMailImplementationAmazonSESAdapter', signing up for SES, and // filling in your 'amazon-ses.access-key' and 'amazon-ses.secret-key' below. 'metamta.mail-adapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', // When email is sent, try to hand it off to the MTA immediately. This may // be worth disabling if your MTA infrastructure is slow or unreliable. If you // disable this option, you must run the 'metamta_mta.php' daemon or mail // won't be handed off to the MTA. If you're using Amazon SES it can be a // little slugish sometimes so it may be worth disabling this and moving to // the daemon after you've got your install up and running. If you have a // properly configured local MTA it should not be necessary to disable this. 'metamta.send-immediately' => true, // If you're using Amazon SES to send email, provide your AWS access key // and AWS secret key here. To set up Amazon SES with Phabricator, you need // to: // - Make sure 'metamta.mail-adapter' is set to: // "PhabricatorMailImplementationAmazonSESAdapter" // - Make sure 'metamta.can-send-as-user' is false. // - Make sure 'metamta.default-address' is configured to something sensible. // - Make sure 'metamta.default-address' is a validated SES "From" address. 'amazon-ses.access-key' => null, 'amazon-ses.secret-key' => null, // If you're using Sendgrid to send email, provide your access credentials // here. This will use the REST API. You can also use Sendgrid as a normal // SMTP service. 'sendgrid.api-user' => null, 'sendgrid.api-key' => null, // You can configure a reply handler domain so that email sent from Maniphest // will have a special "Reply To" address like "T123+82+af19f@example.com" // that allows recipients to reply by email and interact with tasks. For // instructions on configurating reply handlers, see the article // "Configuring Inbound Email" in the Phabricator documentation. By default, // this is set to 'null' and Phabricator will use a generic 'noreply@' address // or the address of the acting user instead of a special reply handler // address (see 'metamta.default-address'). If you set a domain here, // Phabricator will begin generating private reply handler addresses. See // also 'metamta.maniphest.reply-handler' to further configure behavior. // This key should be set to the domain part after the @, like "example.com". 'metamta.maniphest.reply-handler-domain' => null, // You can follow the instructions in "Configuring Inbound Email" in the // Phabricator documentation and set 'metamta.maniphest.reply-handler-domain' // to support updating Maniphest tasks by email. If you want more advanced // customization than this provides, you can override the reply handler // class with an implementation of your own. This will allow you to do things // like have a single public reply handler or change how private reply // handlers are generated and validated. // This key should be set to a loadable subclass of // PhabricatorMailReplyHandler (and possibly of ManiphestReplyHandler). 'metamta.maniphest.reply-handler' => 'ManiphestReplyHandler', // If you don't want phabricator to take up an entire domain // (or subdomain for that matter), you can use this and set a common // prefix for mail sent by phabricator. It will make use of the fact that // a mail-address such as phabricator+D123+1hjk213h@example.com will be // delivered to the phabricator users mailbox. // Set this to the left part of the email address and it well get // prepended to all outgoing mail. If you want to use e.g. // 'phabricator@example.com' this should be set to 'phabricator'. 'metamta.single-reply-handler-prefix' => null, // Prefix prepended to mail sent by Maniphest. You can change this to // distinguish between testing and development installs, for example. 'metamta.maniphest.subject-prefix' => '[Maniphest]', // See 'metamta.maniphest.reply-handler-domain'. This does the same thing, // but allows email replies via Differential. 'metamta.differential.reply-handler-domain' => null, // See 'metamta.maniphest.reply-handler'. This does the same thing, but // affects Differential. 'metamta.differential.reply-handler' => 'DifferentialReplyHandler', // Prefix prepended to mail sent by Differential. 'metamta.differential.subject-prefix' => '[Differential]', + // Set this to true if you want patches to be attached to mail from + // Differential. This won't work if you are using SendGrid as your mail + // adapter. + 'metamta.differential.attach-patches' => false, + // By default, Phabricator generates unique reply-to addresses and sends a // separate email to each recipient when you enable reply handling. This is // more secure than using "From" to establish user identity, but can mean // users may receive multiple emails when they are on mailing lists. Instead, // you can use a single, non-unique reply to address and authenticate users // based on the "From" address by setting this to 'true'. This trades away // a little bit of security for convenience, but it's reasonable in many // installs. Object interactions are still protected using hashes in the // single public email address, so objects can not be replied to blindly. 'metamta.public-replies' => false, // You can configure an email address like "bugs@phabricator.example.com" // which will automatically create Maniphest tasks when users send email // to it. This relies on the "From" address to authenticate users, so it is // is not completely secure. To set this up, enter a complete email // address like "bugs@phabricator.example.com" and then configure mail to // that address so it routed to Phabricator (if you've already configured // reply handlers, you're probably already done). See "Configuring Inbound // Email" in the documentation for more information. 'metamta.maniphest.public-create-email' => null, // If you enable 'metamta.public-replies', Phabricator uses "From" to // authenticate users. You can additionally enable this setting to try to // authenticate with 'Reply-To'. Note that this is completely spoofable and // insecure (any user can set any 'Reply-To' address) but depending on the // nature of your install or other deliverability conditions this might be // okay. Generally, you can't do much more by spoofing Reply-To than be // annoying (you can write but not read content). But, you know, this is // still **COMPLETELY INSECURE**. 'metamta.insecure-auth-with-reply-to' => false, // -- Auth ------------------------------------------------------------------ // // Can users login with a username/password, or by following the link from // a password reset email? You can disable this and configure one or more // OAuth providers instead. 'auth.password-auth-enabled' => true, // Maximum number of simultaneous web sessions each user is permitted to have. // Setting this to "1" will prevent a user from logging in on more than one // browser at the same time. 'auth.sessions.web' => 5, // Maximum number of simultaneous Conduit sessions each user is permitted // to have. 'auth.sessions.conduit' => 3, // Set this true to enable the Settings -> SSH Public Keys panel, which will // allow users to associated SSH public keys with their accounts. This is only // really useful if you're setting up services over SSH and want to use // Phabricator for authentication; in most situations you can leave this // disabled. 'auth.sshkeys.enabled' => false, // -- Accounts -------------------------------------------------------------- // // Is basic account information (email, real name, profile picture) editable? // If you set up Phabricator to automatically synchronize account information // from some other authoritative system, you can disable this to ensure // information remains consistent across both systems. 'account.editable' => true, // -- Facebook ------------------------------------------------------------ // // Can users use Facebook credentials to login to Phabricator? 'facebook.auth-enabled' => false, // Can users use Facebook credentials to create new Phabricator accounts? 'facebook.registration-enabled' => true, // Are Facebook accounts permanently linked to Phabricator accounts, or can // the user unlink them? 'facebook.auth-permanent' => false, // The Facebook "Application ID" to use for Facebook API access. 'facebook.application-id' => null, // The Facebook "Application Secret" to use for Facebook API access. 'facebook.application-secret' => null, // -- Github ---------------------------------------------------------------- // // Can users use Github credentials to login to Phabricator? 'github.auth-enabled' => false, // Can users use Github credentials to create new Phabricator accounts? 'github.registration-enabled' => true, // Are Github accounts permanently linked to Phabricator accounts, or can // the user unlink them? 'github.auth-permanent' => false, // The Github "Client ID" to use for Github API access. 'github.application-id' => null, // The Github "Secret" to use for Github API access. 'github.application-secret' => null, // -- Google ---------------------------------------------------------------- // // Can users use Google credentials to login to Phabricator? 'google.auth-enabled' => false, // Can users use Google credentials to create new Phabricator accounts? 'google.registration-enabled' => true, // Are Google accounts permanently linked to Phabricator accounts, or can // the user unlink them? 'google.auth-permanent' => false, // The Google "Client ID" to use for Google API access. 'google.application-id' => null, // The Google "Client Secret" to use for Google API access. 'google.application-secret' => null, // -- Recaptcha ------------------------------------------------------------- // // Is Recaptcha enabled? If disabled, captchas will not appear. 'recaptcha.enabled' => false, // Your Recaptcha public key, obtained from Recaptcha. 'recaptcha.public-key' => null, // Your Recaptcha private key, obtained from Recaptcha. 'recaptcha.private-key' => null, // -- Misc ------------------------------------------------------------------ // // This is hashed with other inputs to generate CSRF tokens. If you want, you // can change it to some other string which is unique to your install. This // will make your install more secure in a vague, mostly theoretical way. But // it will take you like 3 seconds of mashing on your keyboard to set it up so // you might as well. 'phabricator.csrf-key' => '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3', // This is hashed with other inputs to generate mail tokens. If you want, you // can change it to some other string which is unique to your install. In // particular, you will want to do this if you accidentally send a bunch of // mail somewhere you shouldn't have, to invalidate all old reply-to // addresses. 'phabricator.mail-key' => '5ce3e7e8787f6e40dfae861da315a5cdf1018f12', // This is hashed with other inputs to generate file secret keys. Changing // it will invalidate all file URIs if you have an alternate file domain // configured (see 'security.alternate-file-domain'). 'phabricator.file-key' => 'ade8dadc8b4382067069a4d4798112191af8a190', // Version string displayed in the footer. You probably should leave this // alone. 'phabricator.version' => 'UNSTABLE', // PHP requires that you set a timezone in your php.ini before using date // functions, or it will emit a warning. If this isn't possible (for instance, // because you are using HPHP) you can set some valid constant for // date_default_timezone_set() here and Phabricator will set it on your // behalf, silencing the warning. 'phabricator.timezone' => null, // When unhandled exceptions occur, stack traces are hidden by default. // You can enable traces for development to make it easier to debug problems. 'phabricator.show-stack-traces' => false, // When users write comments which have URIs, they'll be automaticaly linked // if the protocol appears in this set. This whitelist is primarily to prevent // security issues like javascript:// URIs. 'uri.allowed-protocols' => array( 'http' => true, 'https' => true, ), // Tokenizers are UI controls which let the user select other users, email // addresses, project names, etc., by typing the first few letters and having // the control autocomplete from a list. They can load their data in two ways: // either in a big chunk up front, or as the user types. By default, the data // is loaded in a big chunk. This is simpler and performs better for small // datasets. However, if you have a very large number of users or projects, // (in the ballpark of more than a thousand), loading all that data may become // slow enough that it's worthwhile to query on demand instead. This makes // the typeahead slightly less responsive but overall performance will be much // better if you have a ton of stuff. You can figure out which setting is // best for your install by changing this setting and then playing with a // user tokenizer (like the user selectors in Maniphest or Differential) and // seeing which setting loads faster and feels better. 'tokenizer.ondemand' => false, // -- Files ----------------------------------------------------------------- // // Lists which uploaded file types may be viewed in the browser. If a file // has a mime type which does not appear in this list, it will always be // downloaded instead of displayed. This is a security consideration: if a // user uploads a file of type "text/html" and it is displayed as // "text/html", they can easily execute XSS attacks. This is also a usability // consideration, since browsers tend to freak out when viewing enormous // binary files. // // The keys in this array are viewable mime types; the values are the mime // types they will be delivered as when they are viewed in the browser. // // IMPORTANT: Making any file types viewable is a security vulnerability if // you do not configure 'security.alternate-file-domain' above. 'files.viewable-mime-types' => array( 'image/jpeg' => 'image/jpeg', 'image/jpg' => 'image/jpg', 'image/png' => 'image/png', 'image/gif' => 'image/gif', 'text/plain' => 'text/plain; charset=utf-8', ), // Phabricator can proxy images from other servers so you can paste the URI // to a funny picture of a cat into the comment box and have it show up as an // image. However, this means the webserver Phabricator is running on will // make HTTP requests to arbitrary URIs. If the server has access to internal // resources, this could be a security risk. You should only enable it if you // are installed entirely a VPN and VPN access is required to access // Phabricator, or if the webserver has no special access to anything. If // unsure, it is safer to leave this disabled. 'files.enable-proxy' => false, // -- Storage --------------------------------------------------------------- // // Phabricator allows users to upload files, and can keep them in various // storage engines. This section allows you to configure which engines // Phabricator will use, and how it will use them. // The largest filesize Phabricator will store in the MySQL BLOB storage // engine, which just uses a database table to store files. While this isn't a // best practice, it's really easy to set up. This is hard-limited by the // value of 'max_allowed_packet' in MySQL (since this often defaults to 1MB, // the default here is slightly smaller than 1MB). Set this to 0 to disable // use of the MySQL blob engine. 'storage.mysql-engine.max-size' => 1000000, // Phabricator provides a local disk storage engine, which just writes files // to some directory on local disk. The webserver must have read/write // permissions on this directory. This is straightforward and suitable for // most installs, but will not scale past one web frontend unless the path // is actually an NFS mount, since you'll end up with some of the files // written to each web frontend and no way for them to share. To use the // local disk storage engine, specify the path to a directory here. To // disable it, specify null. 'storage.local-disk.path' => null, // If you want to store files in Amazon S3, specify an AWS access and secret // key here and a bucket name below. 'amazon-s3.access-key' => null, 'amazon-s3.secret-key' => null, // Set this to a valid Amazon S3 bucket to store files there. You must also // configure S3 access keys above. 'storage.s3.bucket' => null, // Phabricator uses a storage engine selector to choose which storage engine // to use when writing file data. If you add new storage engines or want to // provide very custom rules (e.g., write images to one storage engine and // other files to a different one), you can provide an alternate // implementation here. The default engine will use choose MySQL, Local Disk, // and S3, in that order, if they have valid configurations above and a file // fits within configured limits. 'storage.engine-selector' => 'PhabricatorDefaultFileStorageEngineSelector', // -- Search ---------------------------------------------------------------- // // Phabricator uses a search engine selector to choose which search engine // to use when indexing and reconstructing documents, and when executing // queries. You can override the engine selector to provide a new selector // class which can select some custom engine you implement, if you want to // store your documents in some search engine which does not have default // support. 'search.engine-selector' => 'PhabricatorDefaultSearchEngineSelector', // -- Differential ---------------------------------------------------------- // 'differential.revision-custom-detail-renderer' => null, // Array for custom remarkup rules. The array should have a list of // class names of classes that extend PhutilRemarkupRule 'differential.custom-remarkup-rules' => null, // Array for custom remarkup block rules. The array should have a list of // class names of classes that extend PhutilRemarkupEngineBlockRule 'differential.custom-remarkup-block-rules' => null, // Set display word-wrap widths for Differential. Specify a dictionary of // regular expressions mapping to column widths. The filename will be matched // against each regexp in order until one matches. The default configuration // uses a width of 100 for Java and 80 for other languages. Note that 80 is // the greatest column width of all time. Changes here will not be immediately // reflected in old revisions unless you purge the render cache. 'differential.wordwrap' => array( '/\.java$/' => 100, '/.*/' => 80, ), // List of file regexps were whitespace is meaningful and should not // use 'ignore-all' by default 'differential.whitespace-matters' => array( '/\.py$/', ), 'differential.field-selector' => 'DifferentialDefaultFieldSelector', // If you set this to true, users can "!accept" revisions via email (normally, // they can take other actions but can not "!accept"). This action is disabled // by default because email authentication can be configured to be very weak, // and, socially, email "!accept" is kind of sketchy and implies revisions may // not actually be receiving thorough review. 'differential.enable-email-accept' => false, // -- Maniphest ------------------------------------------------------------- // 'maniphest.enabled' => true, // Array of custom fields for Maniphest tasks. For details on adding custom // fields to Maniphest, see "Maniphest User Guide: Adding Custom Fields". 'maniphest.custom-fields' => array(), // Class which drives custom field construction. See "Maniphest User Guide: // Adding Custom Fields" in the documentation for more information. 'maniphest.custom-task-extensions-class' => 'ManiphestDefaultTaskExtensions', // -- Remarkup -------------------------------------------------------------- // // If you enable this, linked YouTube videos will be embeded inline. This has // mild security implications (you'll leak referrers to YouTube) and is pretty // silly (but sort of awesome). 'remarkup.enable-embedded-youtube' => false, // -- Garbage Collection ---------------------------------------------------- // // Phabricator generates various logs and caches in the database which can // be garbage collected after a while to make the total data size more // manageable. To run garbage collection, launch a // PhabricatorGarbageCollector daemon. // Since the GC daemon can issue large writes and table scans, you may want to // run it only during off hours or make sure it is scheduled so it doesn't // overlap with backups. This determines when the daemon can start running // each day. 'gcdaemon.run-at' => '12 AM', // How many seconds after 'gcdaemon.run-at' the daemon may collect garbage // for. By default it runs continuously, but you can set it to run for a // limited period of time. For instance, if you do backups at 3 AM, you might // run garbage collection for an hour beforehand. This is not a high-precision // limit so you may want to leave some room for the GC to actually stop, and // if you set it to something like 3 seconds you're on your own. 'gcdaemon.run-for' => 24 * 60 * 60, // These 'ttl' keys configure how much old data the GC daemon keeps around. // Objects older than the ttl will be collected. Set any value to 0 to store // data indefinitely. 'gcdaemon.ttl.herald-transcripts' => 30 * (24 * 60 * 60), 'gcdaemon.ttl.daemon-logs' => 7 * (24 * 60 * 60), 'gcdaemon.ttl.differential-parse-cache' => 14 * (24 * 60 * 60), // -- Feed ------------------------------------------------------------------ // // If you set this to true, you can embed Phabricator activity feeds in other // pages using iframes. These feeds are completely public, and a login is not // required to view them! This is intended for things like open source // projects that want to expose an activity feed on the project homepage. 'feed.public' => false, // -- Customization --------------------------------------------------------- // // Paths to additional phutil libraries to load. 'load-libraries' => array(), 'aphront.default-application-configuration-class' => 'AphrontDefaultApplicationConfiguration', 'controller.oauth-registration' => 'PhabricatorOAuthDefaultRegistrationController', // Directory that phd (the Phabricator daemon control script) should use to // track running daemons. 'phd.pid-directory' => '/var/tmp/phd', // This value is an input to the hash function when building resource hashes. // It has no security value, but if you accidentally poison user caches (by // pushing a bad patch or having something go wrong with a CDN, e.g.) you can // change this to something else and rebuild the Celerity map to break user // caches. Unless you are doing Celerity development, it is exceptionally // unlikely that you need to modify this. 'celerity.resource-hash' => 'd9455ea150622ee044f7931dabfa52aa', // In a development environment, it is desirable to force static resources // (CSS and JS) to be read from disk on every request, so that edits to them // appear when you reload the page even if you haven't updated the resource // maps. This setting ensures requests will be verified against the state on // disk. Generally, you should leave this off in production (caching behavior // and performance improve with it off) but turn it on in development. (These // settings are the defaults.) 'celerity.force-disk-reads' => false, // You can respond to various application events by installing listeners, // which will receive callbacks when interesting things occur. Specify a list // of classes which extend PhabricatorEventListener here. 'events.listeners' => array(), // -- Pygments -------------------------------------------------------------- // // Phabricator can highlight PHP by default, but if you want syntax // highlighting for other languages you should install the python package // 'Pygments', make sure the 'pygmentize' script is available in the // $PATH of the webserver, and then enable this. 'pygments.enabled' => false, // In places that we display a dropdown to syntax-highlight code, // this is where that list is defined. // Syntax is 'lexer-name' => 'Display Name', 'pygments.dropdown-choices' => array( 'apacheconf' => 'Apache Configuration', 'bash' => 'Bash Scripting', 'brainfuck' => 'Brainf*ck', 'c' => 'C', 'cpp' => 'C++', 'css' => 'CSS', 'diff' => 'Diff', 'django' => 'Django Templating', 'erb' => 'Embedded Ruby/ERB', 'erlang' => 'Erlang', 'html' => 'HTML', 'infer' => 'Infer from title (extension)', 'java' => 'Java', 'js' => 'Javascript', 'mysql' => 'MySQL', 'perl' => 'Perl', 'php' => 'PHP', 'text' => 'Plain Text', 'python' => 'Python', 'rainbow' => 'Rainbow', 'remarkup' => 'Remarkup', 'ruby' => 'Ruby', 'xml' => 'XML', ), 'pygments.dropdown-default' => 'infer', // This is an override list of regular expressions which allows you to choose // what language files are highlighted as. If your projects have certain rules // about filenames or use unusual or ambiguous language extensions, you can // create a mapping here. This is an ordered dictionary of regular expressions // which will be tested against the filename. They should map to either an // explicit language as a string value, or a numeric index into the captured // groups as an integer. 'syntax.filemap' => array( // Example: Treat all '*.xyz' files as PHP. // '@\\.xyz$@' => 'php', // Example: Treat 'httpd.conf' as 'apacheconf'. // '@/httpd\\.conf$@' => 'apacheconf', // Example: Treat all '*.x.bak' file as '.x'. NOTE: we map to capturing // group 1 by specifying the mapping as "1". // '@\\.([^.]+)\\.bak$@' => 1, ), ); diff --git a/src/applications/conduit/method/differential/getdiff/ConduitAPI_differential_getdiff_Method.php b/src/applications/conduit/method/differential/getdiff/ConduitAPI_differential_getdiff_Method.php index 1d1c24c7b..296807901 100644 --- a/src/applications/conduit/method/differential/getdiff/ConduitAPI_differential_getdiff_Method.php +++ b/src/applications/conduit/method/differential/getdiff/ConduitAPI_differential_getdiff_Method.php @@ -1,131 +1,78 @@ <?php /* * Copyright 2011 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. */ /** * @group conduit */ class ConduitAPI_differential_getdiff_Method extends ConduitAPIMethod { public function getMethodDescription() { return "Load the content of a diff from Differential."; } public function defineParamTypes() { return array( 'revision_id' => 'optional id', 'diff_id' => 'optional id', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR_BAD_REVISION' => 'No such revision exists.', 'ERR_BAD_DIFF' => 'No such diff exists.', ); } protected function execute(ConduitAPIRequest $request) { $diff = null; $revision_id = $request->getValue('revision_id'); if ($revision_id) { $revision = id(new DifferentialRevision())->load($revision_id); if (!$revision) { throw new ConduitException('ERR_BAD_REVISION'); } $diff = id(new DifferentialDiff())->loadOneWhere( 'revisionID = %d ORDER BY id DESC LIMIT 1', $revision->getID()); } else { $diff_id = $request->getValue('diff_id'); if ($diff_id) { $diff = id(new DifferentialDiff())->load($diff_id); } } if (!$diff) { throw new ConduitException('ERR_BAD_DIFF'); } $diff->attachChangesets($diff->loadChangesets()); // TODO: We could batch this to improve performance. foreach ($diff->getChangesets() as $changeset) { $changeset->attachHunks($changeset->loadHunks()); } - return $this->createDiffDict($diff); - } - - public static function createDiffDict(DifferentialDiff $diff) { - $dict = array( - 'id' => $diff->getID(), - 'parent' => $diff->getParentRevisionID(), - 'revisionID' => $diff->getRevisionID(), - 'sourceControlBaseRevision' => $diff->getSourceControlBaseRevision(), - 'sourceControlPath' => $diff->getSourceControlPath(), - 'unitStatus' => $diff->getUnitStatus(), - 'lintStatus' => $diff->getLintStatus(), - 'changes' => array(), - 'properties' => array(), - ); - - foreach ($diff->getChangesets() as $changeset) { - $hunks = array(); - foreach ($changeset->getHunks() as $hunk) { - $hunks[] = array( - 'oldOffset' => $hunk->getOldOffset(), - 'newOffset' => $hunk->getNewOffset(), - 'oldLength' => $hunk->getOldLen(), - 'newLength' => $hunk->getNewLen(), - 'addLines' => null, - 'delLines' => null, - 'isMissingOldNewline' => null, - 'isMissingNewNewline' => null, - 'corpus' => $hunk->getChanges(), - ); - } - $change = array( - 'metadata' => $changeset->getMetadata(), - 'oldPath' => $changeset->getOldFile(), - 'currentPath' => $changeset->getFileName(), - 'awayPaths' => $changeset->getAwayPaths(), - 'oldProperties' => $changeset->getOldProperties(), - 'newProperties' => $changeset->getNewProperties(), - 'type' => $changeset->getChangeType(), - 'fileType' => $changeset->getFileType(), - 'commitHash' => null, - 'hunks' => $hunks, - ); - $dict['changes'][] = $change; - } - - $properties = id(new DifferentialDiffProperty())->loadAllWhere( - 'diffID = %d', - $diff->getID()); - foreach ($properties as $property) { - $dict['properties'][$property->getName()] = $property->getData(); - } - - return $dict; + return $diff->getDiffDict(); } } diff --git a/src/applications/conduit/method/differential/getdiff/__init__.php b/src/applications/conduit/method/differential/getdiff/__init__.php index 5ad4547ee..15399190d 100644 --- a/src/applications/conduit/method/differential/getdiff/__init__.php +++ b/src/applications/conduit/method/differential/getdiff/__init__.php @@ -1,18 +1,17 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'applications/conduit/method/base'); phutil_require_module('phabricator', 'applications/conduit/protocol/exception'); phutil_require_module('phabricator', 'applications/differential/storage/diff'); -phutil_require_module('phabricator', 'applications/differential/storage/diffproperty'); phutil_require_module('phabricator', 'applications/differential/storage/revision'); phutil_require_module('phutil', 'utils'); phutil_require_source('ConduitAPI_differential_getdiff_Method.php'); diff --git a/src/applications/conduit/method/differential/getrevision/ConduitAPI_differential_getrevision_Method.php b/src/applications/conduit/method/differential/getrevision/ConduitAPI_differential_getrevision_Method.php index 30ccd480a..04ef4014f 100644 --- a/src/applications/conduit/method/differential/getrevision/ConduitAPI_differential_getrevision_Method.php +++ b/src/applications/conduit/method/differential/getrevision/ConduitAPI_differential_getrevision_Method.php @@ -1,120 +1,119 @@ <?php /* * Copyright 2011 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. */ /** * @group conduit */ class ConduitAPI_differential_getrevision_Method extends ConduitAPIMethod { public function getMethodDescription() { return "Load the content of a revision from Differential."; } public function defineParamTypes() { return array( 'revision_id' => 'required id', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR_BAD_REVISION' => 'No such revision exists.', ); } protected function execute(ConduitAPIRequest $request) { $diff = null; $revision_id = $request->getValue('revision_id'); $revision = id(new DifferentialRevision())->load($revision_id); if (!$revision) { throw new ConduitException('ERR_BAD_REVISION'); } $revision->loadRelationships(); $reviewer_phids = array_values($revision->getReviewers()); $diffs = $revision->loadDiffs(); $diff_dicts = array(); foreach ($diffs as $diff) { $diff->attachChangesets($diff->loadChangesets()); // TODO: We could batch this to improve performance. foreach ($diff->getChangesets() as $changeset) { $changeset->attachHunks($changeset->loadHunks()); } - $diff_dicts[] = - ConduitAPI_differential_getdiff_Method::createDiffDict($diff); + $diff_dicts[] = $diff->getDiffDict(); } $commit_dicts = array(); $commit_phids = $revision->loadCommitPHIDs(); $handles = id(new PhabricatorObjectHandleData($commit_phids)) ->loadHandles(); foreach ($commit_phids as $commit_phid) { $commit_dicts[] = array( 'fullname' => $handles[$commit_phid]->getFullName(), 'dateCommitted' => $handles[$commit_phid]->getTimestamp(), ); } $auxiliary_fields = $this->loadAuxiliaryFields($revision); $dict = array( 'id' => $revision->getID(), 'phid' => $revision->getPHID(), 'authorPHID' => $revision->getAuthorPHID(), 'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()), 'title' => $revision->getTitle(), 'status' => $revision->getStatus(), 'statusName' => DifferentialRevisionStatus::getNameForRevisionStatus( $revision->getStatus()), 'summary' => $revision->getSummary(), 'testPlan' => $revision->getTestPlan(), 'lineCount' => $revision->getLineCount(), 'reviewerPHIDs' => $reviewer_phids, 'diffs' => $diff_dicts, 'commits' => $commit_dicts, 'auxiliary' => $auxiliary_fields, ); return $dict; } private function loadAuxiliaryFields(DifferentialRevision $revision) { $aux_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); foreach ($aux_fields as $key => $aux_field) { if (!$aux_field->shouldAppearOnConduitView()) { unset($aux_fields[$key]); } } $aux_fields = DifferentialAuxiliaryField::loadFromStorage( $revision, $aux_fields); return mpull($aux_fields, 'getValueForConduit', 'getKeyForConduit'); } } diff --git a/src/applications/conduit/method/differential/getrevision/__init__.php b/src/applications/conduit/method/differential/getrevision/__init__.php index 66949aaa7..7865804c4 100644 --- a/src/applications/conduit/method/differential/getrevision/__init__.php +++ b/src/applications/conduit/method/differential/getrevision/__init__.php @@ -1,22 +1,21 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'applications/conduit/method/base'); -phutil_require_module('phabricator', 'applications/conduit/method/differential/getdiff'); phutil_require_module('phabricator', 'applications/conduit/protocol/exception'); phutil_require_module('phabricator', 'applications/differential/constants/revisionstatus'); phutil_require_module('phabricator', 'applications/differential/field/selector/base'); phutil_require_module('phabricator', 'applications/differential/storage/auxiliaryfield'); phutil_require_module('phabricator', 'applications/differential/storage/revision'); phutil_require_module('phabricator', 'applications/phid/handle/data'); phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phutil', 'utils'); phutil_require_source('ConduitAPI_differential_getrevision_Method.php'); diff --git a/src/applications/differential/mail/base/DifferentialMail.php b/src/applications/differential/mail/base/DifferentialMail.php index 3b0b67c91..ea898bed4 100644 --- a/src/applications/differential/mail/base/DifferentialMail.php +++ b/src/applications/differential/mail/base/DifferentialMail.php @@ -1,330 +1,354 @@ <?php /* * Copyright 2011 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 DifferentialMail { protected $to = array(); protected $cc = array(); protected $actorHandle; protected $revision; protected $comment; protected $changesets; protected $inlineComments; protected $isFirstMailAboutRevision; protected $isFirstMailToRecipients; protected $heraldTranscriptURI; protected $heraldRulesHeader; protected $replyHandler; protected $parentMessageID; abstract protected function renderSubject(); abstract protected function renderBody(); public function setActorHandle($actor_handle) { $this->actorHandle = $actor_handle; return $this; } public function getActorHandle() { return $this->actorHandle; } protected function getActorName() { $handle = $this->getActorHandle(); if ($handle) { return $handle->getName(); } return '???'; } public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } public function setXHeraldRulesHeader($header) { $this->heraldRulesHeader = $header; return $this; } public function send() { $to_phids = $this->getToPHIDs(); if (!$to_phids) { throw new Exception('No "To:" users provided!'); } - $cc_phids = $this->getCCPHIDs(); - $subject = $this->buildSubject(); - $body = $this->buildBody(); + $cc_phids = $this->getCCPHIDs(); + $subject = $this->buildSubject(); + $body = $this->buildBody(); + $attachments = $this->buildAttachments(); $template = new PhabricatorMetaMTAMail(); $actor_handle = $this->getActorHandle(); $reply_handler = $this->getReplyHandler(); if ($actor_handle) { $template->setFrom($actor_handle->getPHID()); } $template ->setSubject($subject) ->setBody($body) ->setIsHTML($this->shouldMarkMailAsHTML()) ->setParentMessageID($this->parentMessageID) ->addHeader('Thread-Topic', $this->getRevision()->getTitle()); + foreach ($attachments as $attachment) { + $template->addAttachment( + $attachment['data'], + $attachment['filename'], + $attachment['mimetype'] + ); + } + $template->setThreadID( $this->getThreadID(), $this->isFirstMailAboutRevision()); if ($this->heraldRulesHeader) { $template->addHeader('X-Herald-Rules', $this->heraldRulesHeader); } $template->setRelatedPHID($this->getRevision()->getPHID()); $phids = array(); foreach ($to_phids as $phid) { $phids[$phid] = true; } foreach ($cc_phids as $phid) { $phids[$phid] = true; } $phids = array_keys($phids); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $mails = $reply_handler->multiplexMail( $template, array_select_keys($handles, $to_phids), array_select_keys($handles, $cc_phids)); foreach ($mails as $mail) { $mail->saveAndSend(); } } protected function getSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix'); } protected function buildSubject() { return trim($this->getSubjectPrefix().' '.$this->renderSubject()); } protected function shouldMarkMailAsHTML() { return false; } protected function buildBody() { $body = $this->renderBody(); $reply_handler = $this->getReplyHandler(); $reply_instructions = $reply_handler->getReplyHandlerInstructions(); if ($reply_instructions) { $body .= "\nREPLY HANDLER ACTIONS\n". " {$reply_instructions}\n"; } if ($this->getHeraldTranscriptURI() && $this->isFirstMailToRecipients()) { $manage_uri = PhabricatorEnv::getProductionURI( '/herald/view/differential/'); $xscript_uri = $this->getHeraldTranscriptURI(); $body .= <<<EOTEXT MANAGE HERALD DIFFERENTIAL RULES {$manage_uri} WHY DID I GET THIS EMAIL? {$xscript_uri} Tip: use the X-Herald-Rules header to filter Herald messages in your client. EOTEXT; } return $body; } + /** + * You can override this method in a subclass and return array of attachments + * to be sent with the email. Each attachment is a dictionary with 'data', + * 'filename' and 'mimetype' keys. For example: + * + * array( + * 'data' => 'some text', + * 'filename' => 'example.txt', + * 'mimetype' => 'text/plain' + * ); + */ + protected function buildAttachments() { + return array(); + } + public function getReplyHandler() { if ($this->replyHandler) { return $this->replyHandler; } $handler_class = PhabricatorEnv::getEnvConfig( 'metamta.differential.reply-handler'); $reply_handler = self::newReplyHandlerForRevision($this->getRevision()); $this->replyHandler = $reply_handler; return $this->replyHandler; } public static function newReplyHandlerForRevision( DifferentialRevision $revision) { $handler_class = PhabricatorEnv::getEnvConfig( 'metamta.differential.reply-handler'); $reply_handler = newv($handler_class, array()); $reply_handler->setMailReceiver($revision); return $reply_handler; } protected function formatText($text) { $text = explode("\n", $text); foreach ($text as &$line) { $line = rtrim(' '.$line); } unset($line); return implode("\n", $text); } public function setToPHIDs(array $to) { $this->to = $this->filterContactPHIDs($to); return $this; } public function setCCPHIDs(array $cc) { $this->cc = $this->filterContactPHIDs($cc); return $this; } protected function filterContactPHIDs(array $phids) { return $phids; // TODO: actually do this? // Differential revisions use Subscriptions for CCs, so any arbitrary // PHID can end up CC'd to them. Only try to actually send email PHIDs // which have ToolsHandle types that are marked emailable. If we don't // filter here, sending the email will fail. /* $handles = array(); prep(new ToolsHandleData($phids, $handles)); foreach ($handles as $phid => $handle) { if (!$handle->isEmailable()) { unset($handles[$phid]); } } return array_keys($handles); */ } protected function getToPHIDs() { return $this->to; } protected function getCCPHIDs() { return $this->cc; } public function setRevision($revision) { $this->revision = $revision; return $this; } public function getRevision() { return $this->revision; } protected function getThreadID() { $phid = $this->getRevision()->getPHID(); $domain = PhabricatorEnv::getEnvConfig('metamta.domain'); return "<differential-rev-{$phid}-req@{$domain}>"; } public function setComment($comment) { $this->comment = $comment; return $this; } public function getComment() { return $this->comment; } public function setChangesets($changesets) { $this->changesets = $changesets; return $this; } public function getChangesets() { return $this->changesets; } public function setInlineComments(array $inline_comments) { $this->inlineComments = $inline_comments; return $this; } public function getInlineComments() { return $this->inlineComments; } public function renderRevisionDetailLink() { $uri = $this->getRevisionURI(); return "REVISION DETAIL\n {$uri}"; } public function getRevisionURI() { return PhabricatorEnv::getProductionURI('/D'.$this->getRevision()->getID()); } public function setIsFirstMailToRecipients($first) { $this->isFirstMailToRecipients = $first; return $this; } public function isFirstMailToRecipients() { return $this->isFirstMailToRecipients; } public function setIsFirstMailAboutRevision($first) { $this->isFirstMailAboutRevision = $first; return $this; } public function isFirstMailAboutRevision() { return $this->isFirstMailAboutRevision; } public function setHeraldTranscriptURI($herald_transcript_uri) { $this->heraldTranscriptURI = $herald_transcript_uri; return $this; } public function getHeraldTranscriptURI() { return $this->heraldTranscriptURI; } protected function renderHandleList(array $handles, array $phids) { $names = array(); foreach ($phids as $phid) { $names[] = $handles[$phid]->getName(); } return implode(', ', $names); } } diff --git a/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php b/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php index 95ad604a5..f08d6f3a8 100644 --- a/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php +++ b/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php @@ -1,80 +1,115 @@ <?php /* * Copyright 2011 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 DifferentialReviewRequestMail extends DifferentialMail { protected $comments; public function setComments($comments) { $this->comments = $comments; return $this; } public function getComments() { return $this->comments; } public function __construct( DifferentialRevision $revision, PhabricatorObjectHandle $actor, array $changesets) { $this->setRevision($revision); $this->setActorHandle($actor); $this->setChangesets($changesets); } protected function renderReviewersLine() { $reviewers = $this->getRevision()->getReviewers(); $handles = id(new PhabricatorObjectHandleData($reviewers))->loadHandles(); return 'Reviewers: '.$this->renderHandleList($handles, $reviewers); } protected function renderReviewRequestBody() { $revision = $this->getRevision(); $body = array(); if ($this->isFirstMailToRecipients()) { $body[] = $this->formatText($revision->getSummary()); $body[] = null; $body[] = 'TEST PLAN'; $body[] = $this->formatText($revision->getTestPlan()); $body[] = null; } else { if (strlen($this->getComments())) { $body[] = $this->formatText($this->getComments()); $body[] = null; } } $body[] = $this->renderRevisionDetailLink(); $body[] = null; $changesets = $this->getChangesets(); if ($changesets) { $body[] = 'AFFECTED FILES'; foreach ($changesets as $changeset) { $body[] = ' '.$changeset->getFilename(); } $body[] = null; } return implode("\n", $body); } + + protected function buildAttachments() { + $attachments = array(); + if (PhabricatorEnv::getEnvConfig('metamta.differential.attach-patches')) { + $revision = $this->getRevision(); + $revision_id = $revision->getID(); + + $diffs = $revision->loadDiffs(); + $diff_number = count($diffs); + $diff = array_pop($diffs); + + $filename = "D{$revision_id}.{$diff_number}.diff"; + + $diff->attachChangesets($diff->loadChangesets()); + // TODO: We could batch this to improve performance. + foreach ($diff->getChangesets() as $changeset) { + $changeset->attachHunks($changeset->loadHunks()); + } + $diff_dict = $diff->getDiffDict(); + + $changes = array(); + foreach ($diff_dict['changes'] as $changedict) { + $changes[] = ArcanistDiffChange::newFromDictionary($changedict); + } + $bundle = ArcanistBundle::newFromChanges($changes); + $unified_diff = $bundle->toUnifiedDiff(); + + $attachments[] = array( + 'data' => $unified_diff, + 'filename' => $filename, + 'mimetype' => 'text/x-diff; charset=utf-8' + ); + } + return $attachments; + } } diff --git a/src/applications/differential/mail/reviewrequest/__init__.php b/src/applications/differential/mail/reviewrequest/__init__.php index 2189b1648..e47543ab0 100644 --- a/src/applications/differential/mail/reviewrequest/__init__.php +++ b/src/applications/differential/mail/reviewrequest/__init__.php @@ -1,15 +1,19 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ +phutil_require_module('arcanist', 'parser/bundle'); +phutil_require_module('arcanist', 'parser/diff/change'); + phutil_require_module('phabricator', 'applications/differential/mail/base'); phutil_require_module('phabricator', 'applications/phid/handle/data'); +phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phutil', 'utils'); phutil_require_source('DifferentialReviewRequestMail.php'); diff --git a/src/applications/differential/storage/diff/DifferentialDiff.php b/src/applications/differential/storage/diff/DifferentialDiff.php index 18c0e2e43..310ca810e 100644 --- a/src/applications/differential/storage/diff/DifferentialDiff.php +++ b/src/applications/differential/storage/diff/DifferentialDiff.php @@ -1,149 +1,201 @@ <?php /* * Copyright 2011 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. */ class DifferentialDiff extends DifferentialDAO { protected $revisionID; protected $authorPHID; protected $sourceMachine; protected $sourcePath; protected $sourceControlSystem; protected $sourceControlBaseRevision; protected $sourceControlPath; protected $lintStatus; protected $unitStatus; protected $lineCount; protected $branch; protected $parentRevisionID; protected $arcanistProjectPHID; protected $creationMethod; protected $repositoryUUID; protected $description; private $unsavedChangesets = array(); private $changesets; public function addUnsavedChangeset(DifferentialChangeset $changeset) { if ($this->changesets === null) { $this->changesets = array(); } $this->unsavedChangesets[] = $changeset; $this->changesets[] = $changeset; return $this; } public function attachChangesets(array $changesets) { $this->changesets = $changesets; return $this; } public function getChangesets() { if ($this->changesets === null) { throw new Exception("Must load and attach changesets first!"); } return $this->changesets; } public function loadChangesets() { if (!$this->getID()) { return array(); } return id(new DifferentialChangeset())->loadAllWhere( 'diffID = %d', $this->getID()); } public function loadArcanistProject() { if (!$this->getArcanistProjectPHID()) { return null; } return id(new PhabricatorRepositoryArcanistProject())->loadOneWhere( 'phid = %s', $this->getArcanistProjectPHID()); } public function save() { // TODO: sort out transactions // $this->openTransaction(); $ret = parent::save(); foreach ($this->unsavedChangesets as $changeset) { $changeset->setDiffID($this->getID()); $changeset->save(); } // $this->saveTransaction(); return $ret; } public function delete() { // $this->openTransaction(); foreach ($this->loadChangesets() as $changeset) { $changeset->delete(); } $ret = parent::delete(); // $this->saveTransaction(); return $ret; } public static function newFromRawChanges(array $changes) { $diff = new DifferentialDiff(); $lines = 0; foreach ($changes as $change) { $changeset = new DifferentialChangeset(); $add_lines = 0; $del_lines = 0; foreach ($change->getHunks() as $hunk) { $dhunk = new DifferentialHunk(); $dhunk->setOldOffset($hunk->getOldOffset()); $dhunk->setOldLen($hunk->getOldLength()); $dhunk->setNewOffset($hunk->getNewOffset()); $dhunk->setNewLen($hunk->getNewLength()); $dhunk->setChanges($hunk->getCorpus()); $changeset->addUnsavedHunk($dhunk); $add_lines += $hunk->getAddLines(); $del_lines += $hunk->getDelLines(); $lines += $add_lines + $del_lines; } $changeset->setOldFile($change->getOldPath()); $changeset->setFilename($change->getCurrentPath()); $changeset->setChangeType($change->getType()); $changeset->setFileType($change->getFileType()); $changeset->setMetadata($change->getAllMetadata()); $changeset->setOldProperties($change->getOldProperties()); $changeset->setNewProperties($change->getNewProperties()); $changeset->setAwayPaths($change->getAwayPaths()); $changeset->setAddLines($add_lines); $changeset->setDelLines($del_lines); $diff->addUnsavedChangeset($changeset); } $diff->setLineCount($lines); return $diff; } + public function getDiffDict() { + $dict = array( + 'id' => $this->getID(), + 'parent' => $this->getParentRevisionID(), + 'revisionID' => $this->getRevisionID(), + 'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(), + 'sourceControlPath' => $this->getSourceControlPath(), + 'unitStatus' => $this->getUnitStatus(), + 'lintStatus' => $this->getLintStatus(), + 'changes' => array(), + 'properties' => array(), + ); + + foreach ($this->getChangesets() as $changeset) { + $hunks = array(); + foreach ($changeset->getHunks() as $hunk) { + $hunks[] = array( + 'oldOffset' => $hunk->getOldOffset(), + 'newOffset' => $hunk->getNewOffset(), + 'oldLength' => $hunk->getOldLen(), + 'newLength' => $hunk->getNewLen(), + 'addLines' => null, + 'delLines' => null, + 'isMissingOldNewline' => null, + 'isMissingNewNewline' => null, + 'corpus' => $hunk->getChanges(), + ); + } + $change = array( + 'metadata' => $changeset->getMetadata(), + 'oldPath' => $changeset->getOldFile(), + 'currentPath' => $changeset->getFileName(), + 'awayPaths' => $changeset->getAwayPaths(), + 'oldProperties' => $changeset->getOldProperties(), + 'newProperties' => $changeset->getNewProperties(), + 'type' => $changeset->getChangeType(), + 'fileType' => $changeset->getFileType(), + 'commitHash' => null, + 'hunks' => $hunks, + ); + $dict['changes'][] = $change; + } + + $properties = id(new DifferentialDiffProperty())->loadAllWhere( + 'diffID = %d', + $this->getID()); + foreach ($properties as $property) { + $dict['properties'][$property->getName()] = $property->getData(); + } + + return $dict; + } } diff --git a/src/applications/differential/storage/diff/__init__.php b/src/applications/differential/storage/diff/__init__.php index 719264253..cdd9d21fe 100644 --- a/src/applications/differential/storage/diff/__init__.php +++ b/src/applications/differential/storage/diff/__init__.php @@ -1,17 +1,18 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'applications/differential/storage/base'); phutil_require_module('phabricator', 'applications/differential/storage/changeset'); +phutil_require_module('phabricator', 'applications/differential/storage/diffproperty'); phutil_require_module('phabricator', 'applications/differential/storage/hunk'); phutil_require_module('phabricator', 'applications/repository/storage/arcanistproject'); phutil_require_module('phutil', 'utils'); phutil_require_source('DifferentialDiff.php');