add github plugin

This commit is contained in:
2020-11-27 09:30:30 +11:00
parent 74b5054b69
commit dd69a40453
40 changed files with 2387 additions and 1 deletions
@@ -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)
==============================
[![Gitter](https://badges.gitter.im/gharlan/alfred-github-workflow.svg)](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)**
![Workflow Screenshot](screenshot.png)
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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

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 &gt; 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 &gt; 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 &gt; 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();
}
}