add github plugin
@@ -1 +0,0 @@
|
||||
/Users/junwei.zhao/.nvm/versions/node/v10.20.1/lib/node_modules/alfred-mvns
|
||||
@@ -0,0 +1,98 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Version 1.6.2 – 2018-02-13
|
||||
--------------------------
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Api pagination didn't work correctly any more (missing results from page > 2)
|
||||
|
||||
|
||||
Version 1.6.1 – 2017-09-23
|
||||
--------------------------
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Support for macOS 10.13 High Sierra
|
||||
* Commit search results had wrong urls on GitHub Enterprise (@beparker)
|
||||
|
||||
|
||||
Version 1.6 – 2017-05-07
|
||||
------------------------
|
||||
|
||||
### Features
|
||||
|
||||
* new command `gh user/repo projects` (@dagio)
|
||||
* new command `gh my pulls review requested` (@AeroEchelon)
|
||||
* better sorting for issues (most recently updated on top) and commits (most recent on top) (@danielma)
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* On macOS 10.12.5 Beta URLs didn't opened in browser anymore
|
||||
|
||||
|
||||
Version 1.5 – 2016-12-13
|
||||
------------------------
|
||||
|
||||
### Features
|
||||
|
||||
* new commands for searching repos and users globally in GitHub (`gh s repo` and `gh s @user`)
|
||||
* new command `gh my repos` (@jacobkossman)
|
||||
* new command `gh > delete database`
|
||||
* source repos with higher priority than forks
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* in some situations private repos were missing (@lxynox)
|
||||
* after saving GitHub Enterprise url the workflow didn't reopen correctly
|
||||
* updated user sub commands ("Activity" tab does not exist any more on GitHub)
|
||||
|
||||
|
||||
Version 1.4.1 – 2016-22-07
|
||||
--------------------------
|
||||
|
||||
* fixed reading environment variables (important for hotkey support)
|
||||
|
||||
|
||||
Version 1.4 – 2016-22-07
|
||||
------------------------
|
||||
|
||||
* Hotkey support
|
||||
* use native update mechanism of Alfred (to keep your hotkeys)
|
||||
* new command `gh user/repo releases` (@altern8tif)
|
||||
* cache warmup after login
|
||||
* lower cpu usage in multi curl
|
||||
* fixed autocomplete values in GitHub Enterprise
|
||||
|
||||
|
||||
Version 1.3 – 2016-17-07
|
||||
------------------------
|
||||
|
||||
**Important:** This is the last version for Alfred 2.
|
||||
|
||||
* Disabled updates in Alfred 2
|
||||
* Updates in Alfred 3 are loaded from new location (GitHub releases)
|
||||
|
||||
|
||||
Version 1.2 – 2016-04-17
|
||||
------------------------
|
||||
|
||||
### Features
|
||||
|
||||
* New sub commands for `gh my issues/pull`: `created`, `assigned` and `mentioned`
|
||||
* New help command: `gh > help`
|
||||
* Longer cache lifetime
|
||||
|
||||
|
||||
Version 1.1 – 2015-01-10
|
||||
------------------------
|
||||
|
||||
### Features
|
||||
|
||||
* GitHub Enterprise support (use `ghe`)
|
||||
* Commit search (`gh user/repo *hash`)
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* A space after `gh` is required to avoid confusion when using commands of other workflows like `ghost`
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Gregor Harlan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,112 @@
|
||||
GitHub Workflow for [Alfred 3](http://www.alfredapp.com)
|
||||
==============================
|
||||
|
||||
[](https://gitter.im/gharlan/alfred-github-workflow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
You can search through GitHub (`gh`) and your GitHub Enterprise instance (`ghe`).
|
||||
|
||||
You have to login (`gh > login`) before you can use the workflow. The login uses OAuth, so you do not have to enter your credentials.
|
||||
|
||||
**[DOWNLOAD](https://github.com/gharlan/alfred-github-workflow/releases)**
|
||||
|
||||

|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
### For github.com
|
||||
|
||||
In Alfred type (`gh > login`) to authenticate against your account. The login uses OAuth, so you do not have to enter your credentials.
|
||||
|
||||
### For github enterprise
|
||||
|
||||
1. In Alfred type (`ghe > url https://github.mycompany.com`)
|
||||
2. Create a new Personal Access Token (`ghe > generate token` or `https://github.mycompany.com/settings/applications`). It only needs access to your repos. Copy this token to your clipboard.
|
||||
3. In Alfred type (`ghe > login <paste token here>`)
|
||||
4. You can now `ghe your_enterprise_repo_name`
|
||||
|
||||
Key Combinations
|
||||
----------------
|
||||
|
||||
Key Combination | Action
|
||||
---------------------- | ------
|
||||
`enter` | Open entry in default browser
|
||||
`cmd` + `c` | Copy URL of the entry
|
||||
`cmd` + `enter` | Paste URL to front most app
|
||||
`shift` or `cmd` + `y` | Open URL in QuickLook
|
||||
|
||||
Commands
|
||||
--------
|
||||
|
||||
To search through your GitHub Enterprise instance replace `gh` by `ghe`.
|
||||
|
||||
### Repo commands
|
||||
|
||||
* `gh user/repo`
|
||||
* `gh user/repo #123`
|
||||
* `gh user/repo @branch`
|
||||
* `gh user/repo *commit`
|
||||
* `gh user/repo /path/to/file`
|
||||
* `gh user/repo admin`
|
||||
* `gh user/repo clone`
|
||||
* `gh user/repo graphs`
|
||||
* `gh user/repo issues`
|
||||
* `gh user/repo milestones`
|
||||
* `gh user/repo network`
|
||||
* `gh user/repo new issue`
|
||||
* `gh user/repo new pull`
|
||||
* `gh user/repo projects`
|
||||
* `gh user/repo pulls`
|
||||
* `gh user/repo pulse`
|
||||
* `gh user/repo releases`
|
||||
* `gh user/repo wiki`
|
||||
* `gh user/repo projects`
|
||||
|
||||
### User commands
|
||||
|
||||
* `gh @user`
|
||||
* `gh @user overview`
|
||||
* `gh @user repositories`
|
||||
* `gh @user stars`
|
||||
* `gh @user gists`
|
||||
|
||||
### Search commands
|
||||
|
||||
* `gh s repo`
|
||||
* `gh s @user`
|
||||
|
||||
### "My" commands
|
||||
|
||||
* `gh my dashboard`
|
||||
* `gh my notifications`
|
||||
* `gh my profile`
|
||||
* `gh my issues`
|
||||
* `gh my issues created`
|
||||
* `gh my issues assigned`
|
||||
* `gh my issues mentioned`
|
||||
* `gh my pulls`
|
||||
* `gh my pulls created`
|
||||
* `gh my pulls assigned`
|
||||
* `gh my pulls mentioned`
|
||||
* `gh my pulls review requested`
|
||||
* `gh my repos`
|
||||
* `gh my settings`
|
||||
* `gh my stars`
|
||||
* `gh my gists`
|
||||
|
||||
### Workflow commands
|
||||
|
||||
* `gh > login`
|
||||
* `gh > logout`
|
||||
* `gh > delete cache`
|
||||
* `gh > delete database`
|
||||
* `gh > update`
|
||||
* `gh > activate autoupdate`
|
||||
* `gh > deactivate autoupdate`
|
||||
* `gh > help`
|
||||
* `gh > changelog`
|
||||
* `ghe > url` (GitHub Enterprise only)
|
||||
* `ghe > generate token` (GitHub Enterprise only)
|
||||
* `ghe > enterprise reset` (GitHub Enterprise only)
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the alfred-github-workflow package.
|
||||
*
|
||||
* (c) Gregor Harlan
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
require 'workflow.php';
|
||||
|
||||
$query = trim($argv[1]);
|
||||
|
||||
if ('>' !== $query[0] && 0 !== strpos($query, 'e >')) {
|
||||
if ('.git' == substr($query, -4)) {
|
||||
$query = 'github-mac://openRepo/'.substr($query, 0, -4);
|
||||
}
|
||||
exec('open '.$query);
|
||||
return;
|
||||
}
|
||||
|
||||
$enterprise = 0 === strpos($query, 'e ');
|
||||
if ($enterprise) {
|
||||
$query = substr($query, 2);
|
||||
}
|
||||
$parts = explode(' ', $query);
|
||||
|
||||
Workflow::init($enterprise);
|
||||
|
||||
switch ($parts[1]) {
|
||||
case 'enterprise-url':
|
||||
Workflow::setConfig('enterprise_url', rtrim($parts[2], '/'));
|
||||
exec('osascript -e "tell application \"Alfred 3\" to search \"ghe \""');
|
||||
break;
|
||||
|
||||
case 'enterprise-reset':
|
||||
Workflow::removeConfig('enterprise_url');
|
||||
Workflow::removeConfig('enterprise_access_token');
|
||||
Workflow::deleteCache();
|
||||
break;
|
||||
|
||||
case 'login':
|
||||
if (isset($parts[2]) && $parts[2]) {
|
||||
Workflow::setAccessToken($parts[2]);
|
||||
echo 'Successfully logged in';
|
||||
} elseif (!$enterprise) {
|
||||
Workflow::startServer();
|
||||
$state = version_compare(PHP_VERSION, '5.4', '<') ? 'm' : '';
|
||||
$url = Workflow::getBaseUrl().'/login/oauth/authorize?client_id=2d4f43826cb68e11c17c&scope=repo&state='.$state;
|
||||
exec('open '.escapeshellarg($url));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'logout':
|
||||
Workflow::removeAccessToken();
|
||||
Workflow::deleteCache();
|
||||
echo 'Successfully logged out';
|
||||
break;
|
||||
|
||||
case 'delete-cache':
|
||||
Workflow::deleteCache();
|
||||
echo 'Successfully deleted cache';
|
||||
break;
|
||||
|
||||
case 'delete-database':
|
||||
Workflow::deleteDatabase();
|
||||
echo 'Successfully deleted database';
|
||||
break;
|
||||
|
||||
case 'refresh-cache':
|
||||
$curl = new Curl();
|
||||
foreach (explode(',', $parts[2]) as $url) {
|
||||
Workflow::requestCache($url, $curl, null, false, 0, false);
|
||||
}
|
||||
$curl->execute();
|
||||
Workflow::cleanCache();
|
||||
break;
|
||||
|
||||
case 'activate-autoupdate':
|
||||
Workflow::setConfig('autoupdate', 1);
|
||||
echo 'Activated auto updating';
|
||||
break;
|
||||
|
||||
case 'deactivate-autoupdate':
|
||||
Workflow::setConfig('autoupdate', 0);
|
||||
echo 'Deactivated auto updating';
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
$release = json_decode(Workflow::request('https://api.github.com/repos/gharlan/alfred-github-workflow/releases/latest'));
|
||||
if (!isset($release->assets[0]->browser_download_url)) {
|
||||
echo 'Update failed';
|
||||
exit;
|
||||
}
|
||||
$response = Workflow::request($release->assets[0]->browser_download_url, null, null, false);
|
||||
if (!$response) {
|
||||
echo 'Update failed';
|
||||
exit;
|
||||
}
|
||||
$path = getenv('alfred_workflow_data').'/github.alfredworkflow';
|
||||
file_put_contents($path, $response);
|
||||
exec('open '.escapeshellarg($path));
|
||||
break;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the alfred-github-workflow package.
|
||||
*
|
||||
* (c) Gregor Harlan
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
class Curl
|
||||
{
|
||||
/** @var CurlRequest[] */
|
||||
private $requests = array();
|
||||
private $running = false;
|
||||
private $debug = false;
|
||||
|
||||
private static $multiHandle;
|
||||
|
||||
public function add(CurlRequest $request)
|
||||
{
|
||||
$this->requests[$request->url] = $request;
|
||||
if ($this->running) {
|
||||
$this->addHandle($request);
|
||||
}
|
||||
}
|
||||
|
||||
public function execute()
|
||||
{
|
||||
$this->running = true;
|
||||
if (!is_resource(self::$multiHandle)) {
|
||||
self::$multiHandle = curl_multi_init();
|
||||
}
|
||||
|
||||
foreach ($this->requests as $request) {
|
||||
$this->addHandle($request);
|
||||
}
|
||||
|
||||
$finish = false;
|
||||
$running = true;
|
||||
do {
|
||||
$finish = !$running;
|
||||
while (CURLM_CALL_MULTI_PERFORM == $execrun = curl_multi_exec(self::$multiHandle, $running));
|
||||
if ($execrun != CURLM_OK) {
|
||||
break;
|
||||
}
|
||||
while ($done = curl_multi_info_read(self::$multiHandle)) {
|
||||
$ch = $done['handle'];
|
||||
$info = curl_getinfo($ch);
|
||||
$url = self::getHeader($info['request_header'], 'X-Url');
|
||||
$request = $this->requests[$url];
|
||||
$rawResponse = curl_multi_getcontent($ch);
|
||||
if (preg_match("@^HTTP/\\d\\.\\d 200 Connection established\r\n\r\n@i", $rawResponse)) {
|
||||
list(, $header, $body) = explode("\r\n\r\n", $rawResponse, 3);
|
||||
} else {
|
||||
list($header, $body) = explode("\r\n\r\n", $rawResponse, 2);
|
||||
}
|
||||
$response = new CurlResponse();
|
||||
$response->request = $request;
|
||||
$response->status = $info['http_code'];
|
||||
$headerNames = array(
|
||||
'etag' => 'ETag',
|
||||
'contentType' => 'Content-Type',
|
||||
'link' => 'Link',
|
||||
);
|
||||
foreach ($headerNames as $key => $name) {
|
||||
$response->$key = self::getHeader($header, $name);
|
||||
}
|
||||
if (200 == $response->status) {
|
||||
$response->content = $body;
|
||||
}
|
||||
$callback = $request->callback;
|
||||
$callback($response);
|
||||
curl_multi_remove_handle(self::$multiHandle, $ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
if ($running || !$finish) {
|
||||
if (curl_multi_select(self::$multiHandle, 1) === -1) {
|
||||
usleep(250);
|
||||
}
|
||||
}
|
||||
} while ($running || !$finish);
|
||||
|
||||
$this->running = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private function addHandle(CurlRequest $request)
|
||||
{
|
||||
$defaultOptions = array(
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_USERAGENT => 'alfred-github-workflow',
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLINFO_HEADER_OUT => true,
|
||||
);
|
||||
if ($this->debug) {
|
||||
$defaultOptions[CURLOPT_PROXY] = 'localhost';
|
||||
$defaultOptions[CURLOPT_PROXYPORT] = 8888;
|
||||
$defaultOptions[CURLOPT_SSL_VERIFYPEER] = 0;
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
$options = $defaultOptions;
|
||||
$options[CURLOPT_URL] = $request->url;
|
||||
$header = array();
|
||||
$header[] = 'X-Url: '.$request->url;
|
||||
if ($request->token) {
|
||||
$header[] = 'Authorization: token '.$request->token;
|
||||
}
|
||||
if ($request->etag) {
|
||||
$header[] = 'If-None-Match: '.$request->etag;
|
||||
}
|
||||
$options[CURLOPT_HTTPHEADER] = $header;
|
||||
curl_setopt_array($ch, $options);
|
||||
curl_multi_add_handle(self::$multiHandle, $ch);
|
||||
}
|
||||
|
||||
public static function getHeader($header, $key)
|
||||
{
|
||||
if (preg_match('/^'.preg_quote($key, '/').': (\V*)/mi', $header, $match)) {
|
||||
return $match[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class CurlRequest
|
||||
{
|
||||
public $url;
|
||||
public $etag;
|
||||
public $token;
|
||||
public $callback;
|
||||
|
||||
public function __construct($url, $etag, $token, $callback)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->etag = $etag;
|
||||
$this->token = $token;
|
||||
$this->callback = $callback;
|
||||
}
|
||||
}
|
||||
|
||||
class CurlResponse
|
||||
{
|
||||
/** @var CurlRequest */
|
||||
public $request;
|
||||
public $status;
|
||||
public $contentType;
|
||||
public $etag;
|
||||
public $link;
|
||||
public $content;
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,602 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>bundleid</key>
|
||||
<string>de.gh01.alfred.github</string>
|
||||
<key>category</key>
|
||||
<string>Tools</string>
|
||||
<key>connections</key>
|
||||
<dict>
|
||||
<key>14CA19E2-6328-4201-905E-A751E2BA47B5</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>destinationuid</key>
|
||||
<string>ABE8DD6E-CD29-4E70-87CF-1382A0446009</string>
|
||||
<key>modifiers</key>
|
||||
<integer>0</integer>
|
||||
<key>modifiersubtext</key>
|
||||
<string></string>
|
||||
<key>vitoclose</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</array>
|
||||
<key>29045171-6618-4FA4-BAB0-39C10422CF31</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>destinationuid</key>
|
||||
<string>67ADBB8D-C705-4981-BB9B-7C9238BEFF2E</string>
|
||||
<key>modifiers</key>
|
||||
<integer>0</integer>
|
||||
<key>modifiersubtext</key>
|
||||
<string></string>
|
||||
<key>vitoclose</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</array>
|
||||
<key>94B48881-907F-4752-AAA0-234EA30CFC18</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>destinationuid</key>
|
||||
<string>FC76BC27-8BCB-4BEE-AD42-EAC2D9B01F0F</string>
|
||||
<key>modifiers</key>
|
||||
<integer>0</integer>
|
||||
<key>modifiersubtext</key>
|
||||
<string></string>
|
||||
<key>vitoclose</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</array>
|
||||
<key>95CD1A63-A0C6-4458-9817-9C6B1A90C827</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>destinationuid</key>
|
||||
<string>29045171-6618-4FA4-BAB0-39C10422CF31</string>
|
||||
<key>modifiers</key>
|
||||
<integer>0</integer>
|
||||
<key>modifiersubtext</key>
|
||||
<string></string>
|
||||
<key>vitoclose</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>destinationuid</key>
|
||||
<string>9715D8FA-0199-450F-99B8-64796B5AC6CB</string>
|
||||
<key>modifiers</key>
|
||||
<integer>1048576</integer>
|
||||
<key>modifiersubtext</key>
|
||||
<string>Copy and paste URL</string>
|
||||
<key>vitoclose</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</array>
|
||||
<key>9715D8FA-0199-450F-99B8-64796B5AC6CB</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>destinationuid</key>
|
||||
<string>87F10E6D-697C-47CE-BD78-E7174F5DF815</string>
|
||||
<key>modifiers</key>
|
||||
<integer>0</integer>
|
||||
<key>modifiersubtext</key>
|
||||
<string></string>
|
||||
<key>vitoclose</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</array>
|
||||
<key>ABE8DD6E-CD29-4E70-87CF-1382A0446009</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>destinationuid</key>
|
||||
<string>95CD1A63-A0C6-4458-9817-9C6B1A90C827</string>
|
||||
<key>modifiers</key>
|
||||
<integer>0</integer>
|
||||
<key>modifiersubtext</key>
|
||||
<string></string>
|
||||
<key>vitoclose</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</array>
|
||||
<key>DAA505B9-F86C-4AF8-818B-8F614F01485E</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>destinationuid</key>
|
||||
<string>94B48881-907F-4752-AAA0-234EA30CFC18</string>
|
||||
<key>modifiers</key>
|
||||
<integer>0</integer>
|
||||
<key>modifiersubtext</key>
|
||||
<string></string>
|
||||
<key>vitoclose</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</array>
|
||||
<key>FC76BC27-8BCB-4BEE-AD42-EAC2D9B01F0F</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>destinationuid</key>
|
||||
<string>29045171-6618-4FA4-BAB0-39C10422CF31</string>
|
||||
<key>modifiers</key>
|
||||
<integer>0</integer>
|
||||
<key>modifiersubtext</key>
|
||||
<string></string>
|
||||
<key>vitoclose</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>destinationuid</key>
|
||||
<string>9715D8FA-0199-450F-99B8-64796B5AC6CB</string>
|
||||
<key>modifiers</key>
|
||||
<integer>1048576</integer>
|
||||
<key>modifiersubtext</key>
|
||||
<string>Copy and paste URL</string>
|
||||
<key>vitoclose</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>createdby</key>
|
||||
<string>Gregor Harlan</string>
|
||||
<key>description</key>
|
||||
<string>GitHub for Alfred</string>
|
||||
<key>disabled</key>
|
||||
<false/>
|
||||
<key>name</key>
|
||||
<string>GitHub</string>
|
||||
<key>objects</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>action</key>
|
||||
<integer>0</integer>
|
||||
<key>argument</key>
|
||||
<integer>0</integer>
|
||||
<key>focusedappvariable</key>
|
||||
<false/>
|
||||
<key>focusedappvariablename</key>
|
||||
<string></string>
|
||||
<key>leftcursor</key>
|
||||
<false/>
|
||||
<key>modsmode</key>
|
||||
<integer>2</integer>
|
||||
<key>relatedAppsMode</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.trigger.hotkey</string>
|
||||
<key>uid</key>
|
||||
<string>DAA505B9-F86C-4AF8-818B-8F614F01485E</string>
|
||||
<key>version</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>alfredfiltersresults</key>
|
||||
<false/>
|
||||
<key>argumenttype</key>
|
||||
<integer>1</integer>
|
||||
<key>escaping</key>
|
||||
<integer>36</integer>
|
||||
<key>keyword</key>
|
||||
<string>gh</string>
|
||||
<key>queuedelaycustom</key>
|
||||
<integer>3</integer>
|
||||
<key>queuedelayimmediatelyinitially</key>
|
||||
<false/>
|
||||
<key>queuedelaymode</key>
|
||||
<integer>0</integer>
|
||||
<key>queuemode</key>
|
||||
<integer>2</integer>
|
||||
<key>runningsubtext</key>
|
||||
<string>Loading results…</string>
|
||||
<key>script</key>
|
||||
<string>php -f search.php -- github "{query}"</string>
|
||||
<key>scriptargtype</key>
|
||||
<integer>0</integer>
|
||||
<key>scriptfile</key>
|
||||
<string></string>
|
||||
<key>subtext</key>
|
||||
<string>Search or type a command</string>
|
||||
<key>title</key>
|
||||
<string>gh...</string>
|
||||
<key>type</key>
|
||||
<integer>0</integer>
|
||||
<key>withspace</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.input.scriptfilter</string>
|
||||
<key>uid</key>
|
||||
<string>FC76BC27-8BCB-4BEE-AD42-EAC2D9B01F0F</string>
|
||||
<key>version</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>concurrently</key>
|
||||
<false/>
|
||||
<key>escaping</key>
|
||||
<integer>36</integer>
|
||||
<key>script</key>
|
||||
<string>php -f action.php -- "{query}"</string>
|
||||
<key>scriptargtype</key>
|
||||
<integer>0</integer>
|
||||
<key>scriptfile</key>
|
||||
<string></string>
|
||||
<key>type</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.action.script</string>
|
||||
<key>uid</key>
|
||||
<string>29045171-6618-4FA4-BAB0-39C10422CF31</string>
|
||||
<key>version</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>lastpathcomponent</key>
|
||||
<false/>
|
||||
<key>onlyshowifquerypopulated</key>
|
||||
<true/>
|
||||
<key>removeextension</key>
|
||||
<false/>
|
||||
<key>text</key>
|
||||
<string>{query}</string>
|
||||
<key>title</key>
|
||||
<string>GitHub</string>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.output.notification</string>
|
||||
<key>uid</key>
|
||||
<string>67ADBB8D-C705-4981-BB9B-7C9238BEFF2E</string>
|
||||
<key>version</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>argument</key>
|
||||
<string>{query}</string>
|
||||
<key>variables</key>
|
||||
<dict>
|
||||
<key>hotkey</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.utility.argument</string>
|
||||
<key>uid</key>
|
||||
<string>94B48881-907F-4752-AAA0-234EA30CFC18</string>
|
||||
<key>version</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>action</key>
|
||||
<integer>0</integer>
|
||||
<key>argument</key>
|
||||
<integer>0</integer>
|
||||
<key>focusedappvariable</key>
|
||||
<false/>
|
||||
<key>focusedappvariablename</key>
|
||||
<string></string>
|
||||
<key>leftcursor</key>
|
||||
<false/>
|
||||
<key>modsmode</key>
|
||||
<integer>2</integer>
|
||||
<key>relatedAppsMode</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.trigger.hotkey</string>
|
||||
<key>uid</key>
|
||||
<string>14CA19E2-6328-4201-905E-A751E2BA47B5</string>
|
||||
<key>version</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>alfredfiltersresults</key>
|
||||
<false/>
|
||||
<key>argumenttype</key>
|
||||
<integer>1</integer>
|
||||
<key>escaping</key>
|
||||
<integer>36</integer>
|
||||
<key>keyword</key>
|
||||
<string>ghe</string>
|
||||
<key>queuedelaycustom</key>
|
||||
<integer>3</integer>
|
||||
<key>queuedelayimmediatelyinitially</key>
|
||||
<true/>
|
||||
<key>queuedelaymode</key>
|
||||
<integer>0</integer>
|
||||
<key>queuemode</key>
|
||||
<integer>2</integer>
|
||||
<key>runningsubtext</key>
|
||||
<string>Loading results…</string>
|
||||
<key>script</key>
|
||||
<string>php -f search.php -- enterprise "{query}"</string>
|
||||
<key>scriptargtype</key>
|
||||
<integer>0</integer>
|
||||
<key>scriptfile</key>
|
||||
<string></string>
|
||||
<key>subtext</key>
|
||||
<string>Search or type a command (GitHub Enterprise)</string>
|
||||
<key>title</key>
|
||||
<string>ghe...</string>
|
||||
<key>type</key>
|
||||
<integer>0</integer>
|
||||
<key>withspace</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.input.scriptfilter</string>
|
||||
<key>uid</key>
|
||||
<string>95CD1A63-A0C6-4458-9817-9C6B1A90C827</string>
|
||||
<key>version</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>autopaste</key>
|
||||
<true/>
|
||||
<key>clipboardtext</key>
|
||||
<string>{query}</string>
|
||||
<key>transient</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.output.clipboard</string>
|
||||
<key>uid</key>
|
||||
<string>87F10E6D-697C-47CE-BD78-E7174F5DF815</string>
|
||||
<key>version</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>argument</key>
|
||||
<string>{query}</string>
|
||||
<key>variables</key>
|
||||
<dict>
|
||||
<key>hotkey</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.utility.argument</string>
|
||||
<key>uid</key>
|
||||
<string>ABE8DD6E-CD29-4E70-87CF-1382A0446009</string>
|
||||
<key>version</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>config</key>
|
||||
<dict>
|
||||
<key>inputstring</key>
|
||||
<string>{query}</string>
|
||||
<key>matchcasesensitive</key>
|
||||
<false/>
|
||||
<key>matchmode</key>
|
||||
<integer>2</integer>
|
||||
<key>matchstring</key>
|
||||
<string>^\w+://</string>
|
||||
</dict>
|
||||
<key>type</key>
|
||||
<string>alfred.workflow.utility.filter</string>
|
||||
<key>uid</key>
|
||||
<string>9715D8FA-0199-450F-99B8-64796B5AC6CB</string>
|
||||
<key>version</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>readme</key>
|
||||
<string>Changelog
|
||||
=========
|
||||
|
||||
Version 1.6.2 – 2018-02-13
|
||||
--------------------------
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Api pagination didnt work correctly any more (missing results from page > 2)
|
||||
|
||||
|
||||
Version 1.6.1 – 2017-09-23
|
||||
--------------------------
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Support for macOS 10.13 High Sierra
|
||||
* Commit search results had wrong urls on GitHub Enterprise (@beparker)
|
||||
|
||||
|
||||
Version 1.6 – 2017-05-07
|
||||
------------------------
|
||||
|
||||
### Features
|
||||
|
||||
* new command `gh user/repo projects` (@dagio)
|
||||
* new command `gh my pulls review requested` (@AeroEchelon)
|
||||
* better sorting for issues (most recently updated on top) and commits (most recent on top) (@danielma)
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* On macOS 10.12.5 Beta URLs didnt opened in browser anymore
|
||||
|
||||
|
||||
Version 1.5 – 2016-12-13
|
||||
------------------------
|
||||
|
||||
### Features
|
||||
|
||||
* new commands for searching repos and users globally in GitHub (`gh s repo` and `gh s @user`)
|
||||
* new command `gh my repos` (@jacobkossman)
|
||||
* new command `gh > delete database`
|
||||
* source repos with higher priority than forks
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* in some situations private repos were missing (@lxynox)
|
||||
* after saving GitHub Enterprise url the workflow didn't reopen correctly
|
||||
* updated user sub commands (Activity tab does not exist any more on GitHub)
|
||||
|
||||
|
||||
Version 1.4.1 – 2016-22-07
|
||||
--------------------------
|
||||
|
||||
* fixed reading environment variables (important for hotkey support)
|
||||
|
||||
|
||||
Version 1.4 – 2016-22-07
|
||||
------------------------
|
||||
|
||||
* Hotkey support
|
||||
* use native update mechanism of Alfred (to keep your hotkeys)
|
||||
* new command `gh user/repo releases` (@altern8tif)
|
||||
* cache warmup after login
|
||||
* lower cpu usage in multi curl
|
||||
* fixed autocomplete values in GitHub Enterprise
|
||||
|
||||
|
||||
Version 1.3 – 2016-17-07
|
||||
------------------------
|
||||
|
||||
**Important:** This is the last version for Alfred 2.
|
||||
|
||||
* Disabled updates in Alfred 2
|
||||
* Updates in Alfred 3 are loaded from new location (GitHub releases)
|
||||
|
||||
|
||||
Version 1.2 – 2016-04-17
|
||||
------------------------
|
||||
|
||||
### Features
|
||||
|
||||
* New sub commands for `gh my issues/pull`: `created`, `assigned` and `mentioned`
|
||||
* New help command: `gh > help`
|
||||
* Longer cache lifetime
|
||||
|
||||
|
||||
Version 1.1 – 2015-01-10
|
||||
------------------------
|
||||
|
||||
### Features
|
||||
|
||||
* GitHub Enterprise support (use `ghe`)
|
||||
* Commit search (`gh user/repo *hash`)
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* A space after `gh` is required to avoid confusion when using commands of other workflows like `ghost`
|
||||
</string>
|
||||
<key>uidata</key>
|
||||
<dict>
|
||||
<key>14CA19E2-6328-4201-905E-A751E2BA47B5</key>
|
||||
<dict>
|
||||
<key>colorindex</key>
|
||||
<integer>9</integer>
|
||||
<key>note</key>
|
||||
<string>GitHub Enterprise</string>
|
||||
<key>xpos</key>
|
||||
<integer>30</integer>
|
||||
<key>ypos</key>
|
||||
<integer>230</integer>
|
||||
</dict>
|
||||
<key>29045171-6618-4FA4-BAB0-39C10422CF31</key>
|
||||
<dict>
|
||||
<key>xpos</key>
|
||||
<integer>600</integer>
|
||||
<key>ypos</key>
|
||||
<integer>40</integer>
|
||||
</dict>
|
||||
<key>67ADBB8D-C705-4981-BB9B-7C9238BEFF2E</key>
|
||||
<dict>
|
||||
<key>xpos</key>
|
||||
<integer>790</integer>
|
||||
<key>ypos</key>
|
||||
<integer>40</integer>
|
||||
</dict>
|
||||
<key>87F10E6D-697C-47CE-BD78-E7174F5DF815</key>
|
||||
<dict>
|
||||
<key>xpos</key>
|
||||
<integer>790</integer>
|
||||
<key>ypos</key>
|
||||
<integer>230</integer>
|
||||
</dict>
|
||||
<key>94B48881-907F-4752-AAA0-234EA30CFC18</key>
|
||||
<dict>
|
||||
<key>colorindex</key>
|
||||
<integer>12</integer>
|
||||
<key>xpos</key>
|
||||
<integer>220</integer>
|
||||
<key>ypos</key>
|
||||
<integer>70</integer>
|
||||
</dict>
|
||||
<key>95CD1A63-A0C6-4458-9817-9C6B1A90C827</key>
|
||||
<dict>
|
||||
<key>colorindex</key>
|
||||
<integer>9</integer>
|
||||
<key>note</key>
|
||||
<string>GitHub Enterprise</string>
|
||||
<key>xpos</key>
|
||||
<integer>330</integer>
|
||||
<key>ypos</key>
|
||||
<integer>230</integer>
|
||||
</dict>
|
||||
<key>9715D8FA-0199-450F-99B8-64796B5AC6CB</key>
|
||||
<dict>
|
||||
<key>xpos</key>
|
||||
<integer>680</integer>
|
||||
<key>ypos</key>
|
||||
<integer>260</integer>
|
||||
</dict>
|
||||
<key>ABE8DD6E-CD29-4E70-87CF-1382A0446009</key>
|
||||
<dict>
|
||||
<key>colorindex</key>
|
||||
<integer>9</integer>
|
||||
<key>xpos</key>
|
||||
<integer>220</integer>
|
||||
<key>ypos</key>
|
||||
<integer>260</integer>
|
||||
</dict>
|
||||
<key>DAA505B9-F86C-4AF8-818B-8F614F01485E</key>
|
||||
<dict>
|
||||
<key>colorindex</key>
|
||||
<integer>12</integer>
|
||||
<key>note</key>
|
||||
<string>github.com</string>
|
||||
<key>xpos</key>
|
||||
<integer>30</integer>
|
||||
<key>ypos</key>
|
||||
<integer>40</integer>
|
||||
</dict>
|
||||
<key>FC76BC27-8BCB-4BEE-AD42-EAC2D9B01F0F</key>
|
||||
<dict>
|
||||
<key>colorindex</key>
|
||||
<integer>12</integer>
|
||||
<key>note</key>
|
||||
<string>github.com</string>
|
||||
<key>xpos</key>
|
||||
<integer>330</integer>
|
||||
<key>ypos</key>
|
||||
<integer>40</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>variables</key>
|
||||
<dict>
|
||||
<key>hotkey</key>
|
||||
<string>0</string>
|
||||
</dict>
|
||||
<key>version</key>
|
||||
<string>1.6.2</string>
|
||||
<key>webaddress</key>
|
||||
<string>https://github.com/gharlan/alfred-github-workflow</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the alfred-github-workflow package.
|
||||
*
|
||||
* (c) Gregor Harlan
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
class Item
|
||||
{
|
||||
private $randomUid = false;
|
||||
private $prefix = '';
|
||||
private $prefixOnlyTitle = true;
|
||||
private $title;
|
||||
private $comparator;
|
||||
private $subtitle;
|
||||
private $icon;
|
||||
private $arg;
|
||||
private $valid = true;
|
||||
private $add = '…';
|
||||
private $autocomplete = true;
|
||||
private $prio = 0;
|
||||
private $missingChars = 0;
|
||||
private $sameChars = 0;
|
||||
|
||||
public static function create()
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function randomUid()
|
||||
{
|
||||
$this->randomUid = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function prefix($prefix, $onlyTitle = true)
|
||||
{
|
||||
$this->prefix = $prefix;
|
||||
$this->prefixOnlyTitle = $onlyTitle;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function title($title)
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function comparator($comparator)
|
||||
{
|
||||
$this->comparator = $comparator;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function subtitle($subtitle)
|
||||
{
|
||||
$this->subtitle = $subtitle;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function icon($icon)
|
||||
{
|
||||
$this->icon = $icon;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function arg($arg)
|
||||
{
|
||||
$this->arg = $arg;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function valid($valid, $add = '…')
|
||||
{
|
||||
$this->valid = (bool) $valid;
|
||||
$this->add = $add;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function autocomplete($autocomplete = true)
|
||||
{
|
||||
$this->autocomplete = $autocomplete;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function prio($prio)
|
||||
{
|
||||
$this->prio = $prio;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function match($query)
|
||||
{
|
||||
$comparator = strtolower($this->comparator ?: $this->title);
|
||||
$query = strtolower($query);
|
||||
if (!$this->prefixOnlyTitle && stripos($query, $this->prefix) === 0) {
|
||||
$query = substr($query, strlen($this->prefix));
|
||||
}
|
||||
$this->sameChars = 0;
|
||||
$queryLength = strlen($query);
|
||||
for ($i = 0, $k = 0; $i < $queryLength; ++$i, $k++) {
|
||||
for (; isset($comparator[$k]) && $comparator[$k] !== $query[$i]; ++$k);
|
||||
if (!isset($comparator[$k])) {
|
||||
return false;
|
||||
}
|
||||
if ($i === $k) {
|
||||
++$this->sameChars;
|
||||
}
|
||||
}
|
||||
$this->missingChars = strlen($comparator) - $queryLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function compare(self $another)
|
||||
{
|
||||
if ($this->sameChars != $another->sameChars) {
|
||||
return $this->sameChars < $another->sameChars ? 1 : -1;
|
||||
}
|
||||
if ($this->prio != $another->prio) {
|
||||
return $this->prio < $another->prio ? 1 : -1;
|
||||
}
|
||||
return $this->missingChars > $another->missingChars ? 1 : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param self[] $items
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function toXml(array $items, $enterprise, $hotkey, $baseUrl)
|
||||
{
|
||||
$xml = new SimpleXMLElement('<items></items>');
|
||||
$prefix = $hotkey ? '' : ' ';
|
||||
foreach ($items as $item) {
|
||||
$c = $xml->addChild('item');
|
||||
$title = $item->prefix.$item->title;
|
||||
$c->addAttribute('uid', $item->randomUid ? md5(time().$title) : md5($title));
|
||||
if ($item->icon && file_exists(__DIR__.'/icons/'.$item->icon.'.png')) {
|
||||
$c->addChild('icon', 'icons/'.$item->icon.'.png');
|
||||
} else {
|
||||
$c->addChild('icon', 'icon.png');
|
||||
}
|
||||
if ($item->arg) {
|
||||
$arg = $item->arg;
|
||||
if ('/' === $arg[0]) {
|
||||
$arg = $baseUrl.$arg;
|
||||
} elseif (false === strpos($arg, '://')) {
|
||||
$arg = ($enterprise ? 'e ' : '').$arg;
|
||||
}
|
||||
$c->addAttribute('arg', $arg);
|
||||
}
|
||||
if ($item->autocomplete) {
|
||||
if (is_string($item->autocomplete)) {
|
||||
$autocomplete = $item->autocomplete;
|
||||
} elseif (null !== $item->comparator) {
|
||||
$autocomplete = $item->comparator;
|
||||
} else {
|
||||
$autocomplete = $item->title;
|
||||
}
|
||||
$c->addAttribute('autocomplete', $prefix.($item->prefixOnlyTitle ? $autocomplete : $item->prefix.$autocomplete));
|
||||
}
|
||||
if (!$item->valid) {
|
||||
$c->addAttribute('valid', 'no');
|
||||
$title .= $item->add;
|
||||
}
|
||||
$c->addChild('title', htmlspecialchars($title));
|
||||
if ($item->subtitle) {
|
||||
$c->addChild('subtitle', htmlspecialchars($item->subtitle));
|
||||
}
|
||||
}
|
||||
return $xml->asXML();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,610 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the alfred-github-workflow package.
|
||||
*
|
||||
* (c) Gregor Harlan
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
require 'workflow.php';
|
||||
|
||||
class Search
|
||||
{
|
||||
private static $enterprise;
|
||||
private static $query;
|
||||
private static $parts;
|
||||
private static $user;
|
||||
|
||||
public static function run($scope, $query, $hotkey)
|
||||
{
|
||||
self::$enterprise = 'enterprise' === $scope;
|
||||
|
||||
Workflow::init(self::$enterprise, $query, $hotkey);
|
||||
|
||||
if (!$hotkey) {
|
||||
if ('' === $query) {
|
||||
self::addEmptyQueryCommand();
|
||||
return Workflow::getItemsAsXml();
|
||||
}
|
||||
if (' ' !== $query[0]) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
$query = ltrim($query);
|
||||
self::$query = $query;
|
||||
self::$parts = $parts = explode(' ', $query);
|
||||
|
||||
if (Workflow::checkUpdate()) {
|
||||
self::addUpdateCommands();
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::$enterprise && !Workflow::getBaseUrl()) {
|
||||
self::addEnterpriseUrlCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Workflow::getAccessToken() || !(self::$user = Workflow::requestApi('/user'))) {
|
||||
self::addLoginCommands();
|
||||
return;
|
||||
}
|
||||
|
||||
Workflow::stopServer();
|
||||
|
||||
if (isset($query[0]) && $query[0] == '>') {
|
||||
self::addSystemCommands();
|
||||
Workflow::sortItems();
|
||||
return;
|
||||
}
|
||||
|
||||
$isSearch = 's' === $parts[0] && isset($parts[1]);
|
||||
$isUser = isset($query[0]) && $query[0] == '@';
|
||||
$isRepo = false;
|
||||
$queryUser = null;
|
||||
if ($isUser) {
|
||||
$queryUser = ltrim($parts[0], '@');
|
||||
} elseif (!$isSearch && false !== $pos = strpos($parts[0], '/')) {
|
||||
$queryUser = substr($parts[0], 0, $pos);
|
||||
$isRepo = true;
|
||||
}
|
||||
|
||||
if ('my' == $parts[0] && isset($parts[1])) {
|
||||
self::addMyCommands();
|
||||
} elseif ($isSearch && strlen($query) > 5 && '@' !== substr($parts[1], 0, 1)) {
|
||||
self::addRepoSearchCommands();
|
||||
} elseif ($isSearch && strlen($query) > 6 && '@' === substr($parts[1], 0, 1)) {
|
||||
self::addUserSearchCommands();
|
||||
} elseif ($isUser && isset($parts[1])) {
|
||||
self::addUserSubCommands($queryUser);
|
||||
} elseif (!$isUser && $isRepo && isset($parts[1])) {
|
||||
self::addRepoSubCommands();
|
||||
} else {
|
||||
self::addDefaultCommands($isSearch, $isUser, $isRepo, $queryUser);
|
||||
}
|
||||
|
||||
Workflow::sortItems();
|
||||
|
||||
if (!$query) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$isSearch && !$isUser && !isset($parts[1])) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title('s '.$query)
|
||||
->subtitle('Search repo (in alfred workflow)')
|
||||
->comparator($query)
|
||||
->autocomplete('s '.$query)
|
||||
->icon('repo')
|
||||
->valid(false)
|
||||
, false);
|
||||
}
|
||||
|
||||
if (!$isSearch && !$isRepo && !isset($parts[1])) {
|
||||
$title = 's @'.ltrim($query, '@');
|
||||
Workflow::addItem(Item::create()
|
||||
->title($title)
|
||||
->subtitle('Search user (in alfred workflow)')
|
||||
->comparator($query)
|
||||
->autocomplete($title)
|
||||
->icon('user')
|
||||
->valid(false)
|
||||
, false);
|
||||
}
|
||||
|
||||
if (!$isUser && $isRepo && isset($parts[1])) {
|
||||
$repoQuery = substr($query, strlen($parts[0]) + 1);
|
||||
Workflow::addItem(Item::create()
|
||||
->title("Search '$parts[0]' for '$repoQuery'")
|
||||
->icon('search')
|
||||
->arg('/'.$parts[0].'/search?q='.urlencode($repoQuery))
|
||||
->autocomplete(false)
|
||||
, false);
|
||||
}
|
||||
|
||||
$path = $isUser ? $queryUser : 'search?q='.urlencode($query);
|
||||
$name = self::$enterprise ? 'GitHub Enterprise' : 'GitHub';
|
||||
Workflow::addItem(Item::create()
|
||||
->title("Search $name for '$query'")
|
||||
->icon('search')
|
||||
->arg('/'.$path)
|
||||
->autocomplete(false)
|
||||
, false);
|
||||
}
|
||||
|
||||
private static function addEmptyQueryCommand()
|
||||
{
|
||||
Workflow::addItem(Item::create()
|
||||
->title(self::$enterprise ? 'ghe' : 'gh')
|
||||
->subtitle('Search or type a command'.(self::$enterprise ? ' (GitHub Enterprise)' : ''))
|
||||
->comparator('')
|
||||
->valid(false)
|
||||
, false);
|
||||
}
|
||||
|
||||
private static function addUpdateCommands()
|
||||
{
|
||||
$cmds = array(
|
||||
'update' => 'There is an update for this Alfred workflow',
|
||||
'deactivate autoupdate' => 'Deactivate auto updating this Alfred Workflow',
|
||||
);
|
||||
foreach ($cmds as $cmd => $desc) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title('> '.$cmd)
|
||||
->subtitle($desc)
|
||||
->icon($cmd)
|
||||
->arg('> '.str_replace(' ', '-', $cmd))
|
||||
->randomUid()
|
||||
, false);
|
||||
}
|
||||
|
||||
Workflow::addItem(Item::create()
|
||||
->title('> changelog')
|
||||
->subtitle('View the changelog')
|
||||
->icon('file')
|
||||
->arg('https://github.com/gharlan/alfred-github-workflow/blob/master/CHANGELOG.md')
|
||||
->randomUid()
|
||||
, false);
|
||||
}
|
||||
|
||||
private static function addEnterpriseUrlCommand()
|
||||
{
|
||||
$url = null;
|
||||
if (count(self::$parts) > 1 && self::$parts[0] == '>' && self::$parts[1] == 'url' && isset(self::$parts[2])) {
|
||||
$url = self::$parts[2];
|
||||
}
|
||||
Workflow::addItem(Item::create()
|
||||
->title('> url '.$url)
|
||||
->subtitle('Set the GitHub Enterprise URL')
|
||||
->arg('> enterprise-url '.$url)
|
||||
->valid((bool) $url, '<URL>')
|
||||
->randomUid()
|
||||
, false);
|
||||
}
|
||||
|
||||
private static function addLoginCommands()
|
||||
{
|
||||
Workflow::removeAccessToken();
|
||||
$token = null;
|
||||
if (count(self::$parts) > 1 && self::$parts[0] == '>' && self::$parts[1] == 'login' && isset(self::$parts[2])) {
|
||||
$token = self::$parts[2];
|
||||
}
|
||||
if (!$token && !self::$enterprise) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title('> login')
|
||||
->subtitle('Generate OAuth access token')
|
||||
->arg('> login')
|
||||
->randomUid()
|
||||
, false);
|
||||
}
|
||||
Workflow::addItem(Item::create()
|
||||
->title('> login '.$token)
|
||||
->subtitle('Save access token')
|
||||
->arg('> login '.$token)
|
||||
->valid((bool) $token, '<access_token>')
|
||||
->randomUid()
|
||||
, false);
|
||||
if (!$token && self::$enterprise) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title('> generate token')
|
||||
->subtitle('Generate a new access token')
|
||||
->arg('/settings/applications')
|
||||
->randomUid()
|
||||
, false);
|
||||
Workflow::addItem(Item::create()
|
||||
->title('> enterprise reset')
|
||||
->subtitle('Reset the GitHub Enterprise URL')
|
||||
->arg('> enterprise-reset')
|
||||
->randomUid()
|
||||
, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static function addDefaultCommands($isSearch, $isUser, $isRepo, $queryUser)
|
||||
{
|
||||
$users = array();
|
||||
$repos = array();
|
||||
|
||||
$curl = new Curl();
|
||||
|
||||
if (!$isSearch && !$isUser) {
|
||||
$getRepos = function ($url, $prio) use ($curl, &$repos) {
|
||||
Workflow::requestApi($url, $curl, function ($urlRepos) use (&$repos, $prio) {
|
||||
foreach ($urlRepos as $repo) {
|
||||
$repo->score = 300 + $prio + ($repo->fork ? 0 : 10);
|
||||
$repos[$repo->id] = $repo;
|
||||
}
|
||||
});
|
||||
};
|
||||
if ($isRepo) {
|
||||
if ($queryUser != self::$user->login) {
|
||||
$urls = array('/users/'.$queryUser.'/repos', '/orgs/'.$queryUser.'/repos');
|
||||
} else {
|
||||
$urls = array('/user/repos');
|
||||
}
|
||||
} else {
|
||||
Workflow::requestApi('/user/orgs', $curl, function ($orgs) use ($getRepos) {
|
||||
foreach ($orgs as $org) {
|
||||
$getRepos('/orgs/'.$org->login.'/repos', 0);
|
||||
}
|
||||
});
|
||||
$urls = array('/user/starred', '/user/subscriptions', '/user/repos');
|
||||
}
|
||||
foreach ($urls as $prio => $url) {
|
||||
$getRepos($url, $prio + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isSearch && !$isRepo) {
|
||||
Workflow::requestApi('/user/following', $curl, function ($urlUsers) use (&$users) {
|
||||
$users = $urlUsers;
|
||||
});
|
||||
}
|
||||
|
||||
$curl->execute();
|
||||
|
||||
self::addRepos($repos);
|
||||
|
||||
foreach ($users as $user) {
|
||||
Workflow::addItem(Item::create()
|
||||
->prefix('@', false)
|
||||
->title($user->login.' ')
|
||||
->subtitle($user->type)
|
||||
->arg($user->html_url)
|
||||
->icon(lcfirst($user->type))
|
||||
->prio(200)
|
||||
);
|
||||
}
|
||||
|
||||
Workflow::addItem(Item::create()
|
||||
->title('s '.substr(self::$query, 2, 4))
|
||||
->subtitle('Search repo or @user (min 4 chars)')
|
||||
->prio(110)
|
||||
->valid(false)
|
||||
);
|
||||
|
||||
Workflow::addItem(Item::create()
|
||||
->title('my ')
|
||||
->subtitle('Dashboard, settings, and more')
|
||||
->prio(100)
|
||||
->valid(false)
|
||||
);
|
||||
}
|
||||
|
||||
private static function addRepoSearchCommands()
|
||||
{
|
||||
$q = substr(self::$query, 2);
|
||||
$repos = Workflow::requestApi('/search/repositories?q='.urlencode($q), null, null, true);
|
||||
|
||||
self::addRepos($repos, 's ');
|
||||
}
|
||||
|
||||
private static function addUserSearchCommands()
|
||||
{
|
||||
$q = substr(self::$query, 3);
|
||||
$users = Workflow::requestApi('/search/users?q='.urlencode($q), null, null, true);
|
||||
|
||||
self::addUsers($users, 's @');
|
||||
}
|
||||
|
||||
private static function addRepos($repos, $comparatorPrefix = '')
|
||||
{
|
||||
foreach ($repos as $repo) {
|
||||
$icon = 'repo';
|
||||
if ($repo->fork) {
|
||||
$icon = 'fork';
|
||||
} elseif ($repo->mirror_url) {
|
||||
$icon = 'mirror';
|
||||
}
|
||||
if ($repo->private) {
|
||||
$icon = 'private-'.$icon;
|
||||
}
|
||||
Workflow::addItem(Item::create()
|
||||
->title($repo->full_name.' ')
|
||||
->comparator($comparatorPrefix.$repo->full_name)
|
||||
->autocomplete($repo->full_name.' ')
|
||||
->subtitle($repo->description)
|
||||
->icon($icon)
|
||||
->arg('/'.$repo->full_name)
|
||||
->prio($repo->score)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static function addUsers($users, $comparatorPrefix = '')
|
||||
{
|
||||
foreach ($users as $user) {
|
||||
Workflow::addItem(Item::create()
|
||||
->prefix('@', false)
|
||||
->title($user->login.' ')
|
||||
->comparator($comparatorPrefix.$user->login)
|
||||
->autocomplete($user->login.' ')
|
||||
->subtitle($user->type)
|
||||
->arg($user->html_url)
|
||||
->icon(lcfirst($user->type))
|
||||
->prio(isset($user->score) ? $user->score : 200)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static function addRepoSubCommands()
|
||||
{
|
||||
$parts = self::$parts;
|
||||
if (isset($parts[1][0]) && in_array($parts[1][0], array('#', '@', '*', '/'))) {
|
||||
switch ($parts[1][0]) {
|
||||
case '*':
|
||||
$commits = Workflow::requestApi('/repos/'.$parts[0].'/commits');
|
||||
foreach ($commits as $commit) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title($commit->commit->message)
|
||||
->comparator($parts[0].' *'.$commit->sha)
|
||||
->subtitle($commit->commit->author->date.' ('.$commit->sha.')')
|
||||
->icon('commits')
|
||||
->arg('/'.$parts[0].'/commit/'.$commit->sha)
|
||||
->prio(strtotime($commit->commit->author->date))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case '@':
|
||||
$branches = Workflow::requestApi('/repos/'.$parts[0].'/branches');
|
||||
foreach ($branches as $branch) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title('@'.$branch->name)
|
||||
->comparator($parts[0].' @'.$branch->name)
|
||||
->subtitle($branch->commit->sha)
|
||||
->icon('branch')
|
||||
->arg('/'.$parts[0].'/tree/'.$branch->name)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case '/':
|
||||
$repo = Workflow::requestApi('/repos/'.$parts[0]);
|
||||
$files = Workflow::requestApi('/repos/'.$parts[0].'/git/trees/'.$repo->default_branch.'?recursive=1');
|
||||
foreach ($files->tree as $file) {
|
||||
if ('blob' === $file->type) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title(basename($file->path))
|
||||
->subtitle('/'.$file->path)
|
||||
->comparator($parts[0].' /'.$file->path)
|
||||
->icon('file')
|
||||
->arg('/'.$parts[0].'/blob/'.$repo->default_branch.'/'.$file->path)
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '#':
|
||||
$issues = Workflow::requestApi('/repos/'.$parts[0].'/issues?sort=updated&state=all');
|
||||
foreach ($issues as $issue) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title('#'.$issue->number)
|
||||
->comparator($parts[0].' #'.$issue->number)
|
||||
->subtitle($issue->title)
|
||||
->icon($issue->pull_request ? 'pull-request' : 'issue')
|
||||
->arg($issue->html_url)
|
||||
->prio(strtotime($issue->updated_at))
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$subs = array(
|
||||
'admin' => array('Manage this repo', 'settings'),
|
||||
'graphs' => array('All the graphs'),
|
||||
'issues ' => array('List, show and create issues', 'issue'),
|
||||
'milestones' => array('View milestones', 'milestone'),
|
||||
'network' => array('See the network', 'graphs'),
|
||||
'projects' => array('View projects', 'project'),
|
||||
'pulls' => array('Show open pull requests', 'pull-request'),
|
||||
'pulse' => array('See recent activity'),
|
||||
'wiki' => array('Pull up the wiki'),
|
||||
'commits' => array('View commit history'),
|
||||
'releases' => array('See latest releases'),
|
||||
);
|
||||
foreach ($subs as $key => $sub) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title($parts[0].' '.$key)
|
||||
->subtitle($sub[0])
|
||||
->icon(isset($sub[1]) ? $sub[1] : $key)
|
||||
->arg('/'.$parts[0].'/'.$key)
|
||||
);
|
||||
}
|
||||
Workflow::addItem(Item::create()
|
||||
->title($parts[0].' new issue')
|
||||
->subtitle('Create new issue')
|
||||
->icon('issue')
|
||||
->arg('/'.$parts[0].'/issues/new?source=c')
|
||||
);
|
||||
Workflow::addItem(Item::create()
|
||||
->title($parts[0].' new pull')
|
||||
->subtitle('Create new pull request')
|
||||
->icon('pull-request')
|
||||
->arg('/'.$parts[0].'/pull/new?source=c')
|
||||
);
|
||||
if (empty($parts[1])) {
|
||||
$subs = array(
|
||||
'#' => array('Show a specific issue by number', 'issue'),
|
||||
'@' => array('Show a specific branch', 'branch'),
|
||||
'*' => array('Show a specific commit', 'commits'),
|
||||
'/' => array('Show a blob', 'file'),
|
||||
);
|
||||
foreach ($subs as $key => $sub) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title($parts[0].' '.$key)
|
||||
->subtitle($sub[0])
|
||||
->icon($sub[1])
|
||||
->arg($key.' '.$parts[0])
|
||||
->valid(false)
|
||||
);
|
||||
}
|
||||
}
|
||||
Workflow::addItem(Item::create()
|
||||
->title($parts[0].' clone')
|
||||
->subtitle('Clone this repo')
|
||||
->icon('clone')
|
||||
->arg('/'.$parts[0].'.git')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static function addUserSubCommands($queryUser)
|
||||
{
|
||||
$subs = array(
|
||||
'overview' => array($queryUser, "View $queryUser's overview", 'user'),
|
||||
'repositories' => array($queryUser.'?tab=repositories', "View $queryUser's repositories", 'repo'),
|
||||
'stars' => array($queryUser.'?tab=stars', "View $queryUser's stars"),
|
||||
);
|
||||
$prio = count($subs) + 2;
|
||||
foreach ($subs as $key => $sub) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title('@'.$queryUser.' '.$key)
|
||||
->subtitle($sub[1])
|
||||
->icon(isset($sub[2]) ? $sub[2] : $key)
|
||||
->arg('/'.$sub[0])
|
||||
->prio($prio--)
|
||||
);
|
||||
}
|
||||
Workflow::addItem(Item::create()
|
||||
->title('@'.$queryUser.' gists')
|
||||
->subtitle("View $queryUser's' gists")
|
||||
->icon('gists')
|
||||
->arg(Workflow::getGistUrl().'/'.$queryUser)
|
||||
->prio(2)
|
||||
);
|
||||
|
||||
Workflow::addItem(Item::create()
|
||||
->title($queryUser.'/')
|
||||
->comparator('@'.$queryUser.' ')
|
||||
->autocomplete($queryUser.'/')
|
||||
->subtitle("View $queryUser's' repositories (in alfred workflow)")
|
||||
->icon('repo')
|
||||
->prio(1)
|
||||
->valid(false)
|
||||
);
|
||||
}
|
||||
|
||||
private static function addMyCommands()
|
||||
{
|
||||
$parts = self::$parts;
|
||||
if (isset($parts[2]) && in_array($parts[1], array('pulls', 'issues'))) {
|
||||
$icon = $parts[1] === 'pulls' ? 'pull-request' : 'issue';
|
||||
$items = $icon.'s';
|
||||
$subs = array(
|
||||
'created' => array($parts[1], 'View your '.$items),
|
||||
'assigned' => array($parts[1].'/assigned', 'View your assigned '.$items),
|
||||
'mentioned' => array($parts[1].'/mentioned', 'View '.$items.' that mentioned you'),
|
||||
);
|
||||
if ('pulls' === $parts[1]) {
|
||||
$subs['review requested'] = array($parts[1].'/review-requested', 'View '.$items.' that require review');
|
||||
}
|
||||
foreach ($subs as $key => $sub) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title('my '.$parts[1].' '.$key)
|
||||
->subtitle($sub[1])
|
||||
->icon($icon)
|
||||
->arg('/'.$sub[0])
|
||||
->prio(1)
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$myPages = array(
|
||||
'dashboard' => array('', 'View your dashboard'),
|
||||
'pulls ' => array('pulls', 'View your pull requests', 'pull-request'),
|
||||
'issues ' => array('issues', 'View your issues', 'issue'),
|
||||
'stars' => array('stars', 'View your starred repositories'),
|
||||
'profile' => array(self::$user->login, 'View your public user profile', 'user'),
|
||||
'settings' => array('settings', 'View or edit your account settings'),
|
||||
'notifications' => array('notifications', 'View all your notifications'),
|
||||
);
|
||||
foreach ($myPages as $key => $my) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title('my '.$key)
|
||||
->subtitle($my[1])
|
||||
->icon(isset($my[2]) ? $my[2] : rtrim($key))
|
||||
->arg('/'.$my[0])
|
||||
->prio(1)
|
||||
);
|
||||
}
|
||||
Workflow::addItem(Item::create()
|
||||
->title('my gists')
|
||||
->subtitle('View your gists')
|
||||
->icon('gists')
|
||||
->arg(Workflow::getGistUrl().'/'.self::$user->login)
|
||||
->prio(1)
|
||||
);
|
||||
|
||||
Workflow::addItem(Item::create()
|
||||
->title('my repos')
|
||||
->subtitle('View your repos')
|
||||
->icon('repo')
|
||||
->arg('/'.self::$user->login.'?tab=repositories')
|
||||
->prio(1)
|
||||
);
|
||||
}
|
||||
|
||||
private static function addSystemCommands()
|
||||
{
|
||||
$cmds = array(
|
||||
'delete cache' => 'Delete GitHub Cache',
|
||||
'logout' => 'Log out this workflow',
|
||||
'delete database' => 'Delete database (contains login, config and cache)',
|
||||
'update' => 'Update this Alfred workflow',
|
||||
);
|
||||
if (Workflow::getConfig('autoupdate', true)) {
|
||||
$cmds['deactivate autoupdate'] = 'Deactivate auto updating this Alfred Workflow';
|
||||
} else {
|
||||
$cmds['activate autoupdate'] = 'Activate auto updating this Alfred Workflow';
|
||||
}
|
||||
if (self::$enterprise) {
|
||||
$cmds['enterprise reset'] = 'Reset the GitHub Enterprise URL';
|
||||
}
|
||||
foreach ($cmds as $cmd => $desc) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title('> '.$cmd)
|
||||
->subtitle($desc)
|
||||
->icon($cmd)
|
||||
->arg('> '.str_replace(' ', '-', $cmd))
|
||||
);
|
||||
}
|
||||
|
||||
$cmds = array(
|
||||
'help' => 'readme',
|
||||
'changelog' => 'changelog',
|
||||
);
|
||||
foreach ($cmds as $cmd => $file) {
|
||||
Workflow::addItem(Item::create()
|
||||
->title('> '.$cmd)
|
||||
->subtitle('View the '.$file)
|
||||
->icon('file')
|
||||
->arg('https://github.com/gharlan/alfred-github-workflow/blob/master/'.strtoupper($file).'.md')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Search::run($argv[1], $argv[2], getenv('hotkey'));
|
||||
echo Workflow::getItemsAsXml();
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
if (preg_match('/\.(?:png|jpg|jpeg|gif)$/', $_SERVER['REQUEST_URI'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
require 'workflow.php';
|
||||
|
||||
Workflow::init();
|
||||
|
||||
if (!isset($_GET['access_token'])) {
|
||||
echo 'FAILURE (missing access_token parameter)!';
|
||||
exit;
|
||||
}
|
||||
|
||||
Workflow::setAccessToken($_GET['access_token']);
|
||||
Workflow::cacheWarmup();
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>alfred-github-workflow</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
background: #999;
|
||||
font-family: 'Helvetica Neue', sans-serif;
|
||||
font-size: 14pt;
|
||||
}
|
||||
div {
|
||||
padding-top: 200px;
|
||||
width: 450px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
}
|
||||
p {
|
||||
padding-top: 20px;
|
||||
}
|
||||
span {
|
||||
background: #444;
|
||||
color: #fff;
|
||||
padding: 5px;
|
||||
margin-bottom: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<img src="icon.png" width="125" height="125"/>
|
||||
<p>
|
||||
<span>alfred-github-workflow is ready.</span><br>
|
||||
<span>Have fun.</span>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,440 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the alfred-github-workflow package.
|
||||
*
|
||||
* (c) Gregor Harlan
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
require 'item.php';
|
||||
require 'curl.php';
|
||||
|
||||
class Workflow
|
||||
{
|
||||
const VERSION = '1.6.2';
|
||||
const BUNDLE = 'de.gh01.alfred.github';
|
||||
const DEFAULT_CACHE_MAX_AGE = 10;
|
||||
|
||||
private static $filePids;
|
||||
|
||||
private static $fileDb;
|
||||
/** @var PDO */
|
||||
private static $db;
|
||||
/** @var PDOStatement[] */
|
||||
private static $statements = array();
|
||||
|
||||
private static $enterprise;
|
||||
private static $baseUrl = 'https://github.com';
|
||||
private static $apiUrl = 'https://api.github.com';
|
||||
private static $gistUrl = 'https://gist.github.com';
|
||||
|
||||
private static $query;
|
||||
private static $hotkey;
|
||||
private static $items = array();
|
||||
|
||||
private static $refreshUrls = array();
|
||||
|
||||
private static $debug = false;
|
||||
|
||||
public static function init($enterprise = false, $query = null, $hotkey = false)
|
||||
{
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
self::$enterprise = $enterprise;
|
||||
self::$query = ltrim($query);
|
||||
self::$hotkey = $hotkey;
|
||||
|
||||
$dataDir = getenv('alfred_workflow_data');
|
||||
if (!$dataDir) {
|
||||
$dataDir = getenv('HOME').'/Library/Application Support/Alfred 3/Workflow Data/'.self::BUNDLE;
|
||||
putenv('alfred_workflow_data="'.$dataDir.'"');
|
||||
}
|
||||
if (!is_dir($dataDir)) {
|
||||
mkdir($dataDir);
|
||||
}
|
||||
|
||||
self::$filePids = $dataDir.'/pid';
|
||||
|
||||
self::$fileDb = $dataDir.'/db.sqlite';
|
||||
$exists = file_exists(self::$fileDb);
|
||||
self::$db = new PDO('sqlite:'.self::$fileDb, null, null);
|
||||
self::$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
if (!$exists) {
|
||||
self::createTables();
|
||||
}
|
||||
|
||||
if (self::$enterprise) {
|
||||
self::$baseUrl = self::getConfig('enterprise_url');
|
||||
self::$apiUrl = self::$baseUrl ? self::$baseUrl.'/api/v3' : null;
|
||||
self::$gistUrl = self::$baseUrl ? self::$baseUrl.'/gist' : null;
|
||||
}
|
||||
|
||||
self::$debug = getenv('alfred_debug') && defined('STDERR');
|
||||
|
||||
register_shutdown_function(array(__CLASS__, 'shutdown'));
|
||||
}
|
||||
|
||||
public static function shutdown()
|
||||
{
|
||||
if (self::$refreshUrls) {
|
||||
$urls = implode(',', array_keys(self::$refreshUrls));
|
||||
exec('php action.php "> refresh-cache '.$urls.'" > /dev/null 2>&1 &');
|
||||
self::log('refreshing cache in background for %s', $urls);
|
||||
}
|
||||
}
|
||||
|
||||
public static function setConfig($key, $value)
|
||||
{
|
||||
self::getStatement('REPLACE INTO config VALUES(?, ?)')->execute(array($key, $value));
|
||||
}
|
||||
|
||||
public static function getConfig($key, $default = null)
|
||||
{
|
||||
$stmt = self::getStatement('SELECT value FROM config WHERE key = ?');
|
||||
$stmt->execute(array($key));
|
||||
$value = $stmt->fetchColumn();
|
||||
return false !== $value ? $value : $default;
|
||||
}
|
||||
|
||||
public static function removeConfig($key)
|
||||
{
|
||||
self::getStatement('DELETE FROM config WHERE key = ?')->execute(array($key));
|
||||
}
|
||||
|
||||
public static function getBaseUrl()
|
||||
{
|
||||
return self::$baseUrl;
|
||||
}
|
||||
|
||||
public static function getApiUrl($path = null)
|
||||
{
|
||||
$url = self::$apiUrl;
|
||||
|
||||
if ($path) {
|
||||
$paramStart = false === strpos($path, '?') ? '?' : '&';
|
||||
$url .= $path.$paramStart.'per_page=100';
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public static function getGistUrl()
|
||||
{
|
||||
return self::$gistUrl;
|
||||
}
|
||||
|
||||
public static function setAccessToken($token)
|
||||
{
|
||||
self::setConfig(self::$enterprise ? 'enterprise_access_token' : 'access_token', $token);
|
||||
}
|
||||
|
||||
public static function getAccessToken()
|
||||
{
|
||||
return self::getConfig(self::$enterprise ? 'enterprise_access_token' : 'access_token');
|
||||
}
|
||||
|
||||
public static function removeAccessToken()
|
||||
{
|
||||
self::removeConfig(self::$enterprise ? 'enterprise_access_token' : 'access_token');
|
||||
}
|
||||
|
||||
public static function request($url, Curl $curl = null, $callback = null, $withAuthorization = true)
|
||||
{
|
||||
self::log('loading content for %s', $url);
|
||||
|
||||
$return = false;
|
||||
$returnValue = null;
|
||||
if (!$curl) {
|
||||
$curl = new Curl();
|
||||
$return = true;
|
||||
$callback = function ($content) use (&$returnValue) {
|
||||
$returnValue = $content;
|
||||
};
|
||||
}
|
||||
|
||||
$token = $withAuthorization ? self::getAccessToken() : null;
|
||||
$curl->add(new CurlRequest($url, null, $token, function (CurlResponse $response) use ($callback) {
|
||||
if (is_callable($callback) && isset($response->content)) {
|
||||
$callback($response->content);
|
||||
}
|
||||
}));
|
||||
|
||||
if ($return) {
|
||||
$curl->execute();
|
||||
}
|
||||
return $returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param Curl $curl
|
||||
* @param callable $callback
|
||||
* @param bool $firstPageOnly
|
||||
* @param int $maxAge
|
||||
* @param bool $refreshInBackground
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function requestCache($url, Curl $curl = null, $callback = null, $firstPageOnly = false, $maxAge = self::DEFAULT_CACHE_MAX_AGE, $refreshInBackground = true)
|
||||
{
|
||||
$return = false;
|
||||
$returnValue = null;
|
||||
if (!$curl) {
|
||||
$curl = new Curl();
|
||||
$return = true;
|
||||
$callback = function ($content) use (&$returnValue) {
|
||||
$returnValue = $content;
|
||||
};
|
||||
}
|
||||
|
||||
$stmt = self::getStatement('SELECT * FROM request_cache WHERE url = ?');
|
||||
$stmt->execute(array($url));
|
||||
$stmt->bindColumn('timestamp', $timestamp);
|
||||
$stmt->bindColumn('etag', $etag);
|
||||
$stmt->bindColumn('content', $content);
|
||||
$stmt->bindColumn('refresh', $refresh);
|
||||
$stmt->fetch(PDO::FETCH_BOUND);
|
||||
|
||||
$shouldRefresh = $timestamp < time() - 60 * $maxAge;
|
||||
$refreshInBackground = $refreshInBackground && !is_null($content);
|
||||
|
||||
if ($shouldRefresh && $refreshInBackground && $refresh < time() - 3 * 60) {
|
||||
self::getStatement('UPDATE request_cache SET refresh = ? WHERE url = ?')->execute(array(time(), $url));
|
||||
self::$refreshUrls[$url] = true;
|
||||
}
|
||||
|
||||
if (!$shouldRefresh || $refreshInBackground) {
|
||||
self::log('using cached content for %s', $url);
|
||||
$content = json_decode($content);
|
||||
|
||||
if (!$firstPageOnly) {
|
||||
$stmt = self::getStatement('SELECT url, content FROM request_cache WHERE parent = ? ORDER BY `timestamp` DESC');
|
||||
while ($stmt->execute(array($url)) && $data = $stmt->fetchObject()) {
|
||||
$content = array_merge($content, json_decode($data->content));
|
||||
$url = $data->url;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_callable($callback)) {
|
||||
$callback($content);
|
||||
}
|
||||
|
||||
return $returnValue;
|
||||
}
|
||||
|
||||
$responses = array();
|
||||
|
||||
$handleResponse = function (CurlResponse $response, $content, $parent = null) use (&$handleResponse, $curl, &$responses, $stmt, $callback, $firstPageOnly) {
|
||||
$url = $response->request->url;
|
||||
if ($response && in_array($response->status, array(200, 304))) {
|
||||
$checkNext = false;
|
||||
if (304 == $response->status) {
|
||||
$response->content = $content;
|
||||
$checkNext = true;
|
||||
} elseif (false === stripos($response->contentType, 'json')) {
|
||||
$response->content = json_encode($response->content);
|
||||
}
|
||||
$response->content = json_decode($response->content);
|
||||
if (isset($response->content->items)) {
|
||||
$response->content = $response->content->items;
|
||||
}
|
||||
$responses[] = $response->content;
|
||||
Workflow::getStatement('REPLACE INTO request_cache VALUES(?, ?, ?, ?, 0, ?)')
|
||||
->execute(array($url, time(), $response->etag, json_encode($response->content), $parent));
|
||||
|
||||
if ($firstPageOnly) {
|
||||
// do nothing
|
||||
} elseif ($checkNext || $response->link && preg_match('/<([^<>]+)>; rel="next"/U', $response->link, $match)) {
|
||||
$stmt = Workflow::getStatement('SELECT * FROM request_cache WHERE parent = ?');
|
||||
$stmt->execute(array($url));
|
||||
if ($checkNext) {
|
||||
$stmt->bindColumn('url', $nextUrl);
|
||||
} else {
|
||||
$nextUrl = $match[1];
|
||||
}
|
||||
$stmt->bindColumn('etag', $etag);
|
||||
$stmt->bindColumn('content', $content);
|
||||
$stmt->fetch(PDO::FETCH_BOUND);
|
||||
if ($nextUrl) {
|
||||
$curl->add(new CurlRequest($nextUrl, $etag, Workflow::getAccessToken(), function (CurlResponse $response) use ($handleResponse, $url, $content) {
|
||||
$handleResponse($response, $content, $url);
|
||||
}));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Workflow::getStatement('DELETE FROM request_cache WHERE parent = ?')->execute(array($url));
|
||||
}
|
||||
} else {
|
||||
Workflow::getStatement('DELETE FROM request_cache WHERE url = ?')->execute(array($url));
|
||||
$url = null;
|
||||
}
|
||||
|
||||
if (is_callable($callback)) {
|
||||
if (empty($responses)) {
|
||||
$callback(array());
|
||||
return;
|
||||
}
|
||||
if (1 === count($responses)) {
|
||||
$callback($responses[0]);
|
||||
return;
|
||||
}
|
||||
$callback(array_reduce($responses, function ($content, $response) {
|
||||
return array_merge($content, $response);
|
||||
}, array()));
|
||||
}
|
||||
};
|
||||
|
||||
self::log('loading content for %s', $url);
|
||||
$curl->add(new CurlRequest($url, $etag, self::getAccessToken(), function (CurlResponse $response) use (&$responses, $handleResponse, $callback, $content) {
|
||||
$handleResponse($response, $content);
|
||||
}));
|
||||
|
||||
if ($return) {
|
||||
$curl->execute();
|
||||
}
|
||||
return $returnValue;
|
||||
}
|
||||
|
||||
public static function requestApi($url, Curl $curl = null, $callback = null, $firstPageOnly = false, $maxAge = self::DEFAULT_CACHE_MAX_AGE)
|
||||
{
|
||||
$url = self::getApiUrl($url);
|
||||
return self::requestCache($url, $curl, $callback, $firstPageOnly, $maxAge);
|
||||
}
|
||||
|
||||
public static function cleanCache()
|
||||
{
|
||||
self::$db->exec('DELETE FROM request_cache WHERE timestamp < '.(time() - 100 * 24 * 60 * 60));
|
||||
}
|
||||
|
||||
public static function deleteCache()
|
||||
{
|
||||
self::$db->exec('DELETE FROM request_cache');
|
||||
}
|
||||
|
||||
public static function cacheWarmup()
|
||||
{
|
||||
$paths = array('/user', '/user/orgs', '/user/starred', '/user/subscriptions', '/user/repos', '/user/following');
|
||||
foreach ($paths as $path) {
|
||||
self::$refreshUrls[self::getApiUrl($path)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static function startServer()
|
||||
{
|
||||
if (version_compare(PHP_VERSION, '5.4', '>=')) {
|
||||
self::stopServer();
|
||||
shell_exec(sprintf(
|
||||
'alfred_workflow_data=%s php -d variables_order=EGPCS -S localhost:2233 server.php > /dev/null 2>&1 & echo $! >> %s',
|
||||
escapeshellarg(getenv('alfred_workflow_data')),
|
||||
escapeshellarg(self::$filePids)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public static function stopServer()
|
||||
{
|
||||
if (file_exists(self::$filePids)) {
|
||||
$pids = file(self::$filePids);
|
||||
foreach ($pids as $pid) {
|
||||
shell_exec('kill -9 '.$pid);
|
||||
}
|
||||
unlink(self::$filePids);
|
||||
}
|
||||
}
|
||||
|
||||
public static function checkUpdate()
|
||||
{
|
||||
if (self::getConfig('version') !== self::VERSION) {
|
||||
self::setConfig('version', self::VERSION);
|
||||
}
|
||||
if (!self::getConfig('autoupdate', 1)) {
|
||||
return false;
|
||||
}
|
||||
$release = self::requestCache('https://api.github.com/repos/gharlan/alfred-github-workflow/releases/latest', null, null, true, 1440);
|
||||
if (!$release) {
|
||||
return false;
|
||||
}
|
||||
$version = ltrim($release->tag_name, 'v');
|
||||
return version_compare($version, self::VERSION) > 0;
|
||||
}
|
||||
|
||||
private static function createTables()
|
||||
{
|
||||
self::$db->exec('
|
||||
CREATE TABLE config (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT
|
||||
) WITHOUT ROWID
|
||||
');
|
||||
|
||||
self::$db->exec('
|
||||
CREATE TABLE request_cache (
|
||||
url TEXT PRIMARY KEY NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
etag TEXT,
|
||||
content TEXT,
|
||||
refresh INTEGER,
|
||||
parent TEXT
|
||||
) WITHOUT ROWID
|
||||
');
|
||||
self::$db->exec('CREATE INDEX parent_url ON request_cache(parent) WHERE parent IS NOT NULL');
|
||||
}
|
||||
|
||||
public static function deleteDatabase()
|
||||
{
|
||||
self::closeCursors();
|
||||
self::$db = null;
|
||||
unlink(self::$fileDb);
|
||||
}
|
||||
|
||||
public static function addItem(Item $item, $check = true)
|
||||
{
|
||||
if (!$check || $item->match(self::$query)) {
|
||||
self::$items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
public static function sortItems()
|
||||
{
|
||||
usort(self::$items, function (Item $a, Item $b) {
|
||||
return $a->compare($b);
|
||||
});
|
||||
}
|
||||
|
||||
public static function getItemsAsXml()
|
||||
{
|
||||
return Item::toXml(self::$items, self::$enterprise, self::$hotkey, self::getBaseUrl());
|
||||
}
|
||||
|
||||
public static function log($msg)
|
||||
{
|
||||
if (self::$debug) {
|
||||
fwrite(STDERR, "\n".call_user_func_array('sprintf', func_get_args()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
*
|
||||
* @return PDOStatement
|
||||
*/
|
||||
public static function getStatement($query)
|
||||
{
|
||||
if (!isset(self::$statements[$query])) {
|
||||
self::$statements[$query] = self::$db->prepare($query);
|
||||
}
|
||||
return self::$statements[$query];
|
||||
}
|
||||
|
||||
protected static function closeCursors()
|
||||
{
|
||||
foreach (self::$statements as $statement) {
|
||||
$statement->closeCursor();
|
||||
}
|
||||
self::$statements = array();
|
||||
}
|
||||
}
|
||||