├── .gitignore ├── icon.png ├── icons ├── clone.png ├── file.png ├── fork.png ├── gists.png ├── issue.png ├── pulse.png ├── repo.png ├── stars.png ├── user.png ├── wiki.png ├── branch.png ├── commits.png ├── graphs.png ├── logout.png ├── mirror.png ├── project.png ├── search.png ├── update.png ├── dashboard.png ├── milestone.png ├── releases.png ├── settings.png ├── organization.png ├── private-fork.png ├── private-repo.png ├── pull-request.png ├── notifications.png └── private-mirror.png ├── screenshot.png ├── .php_cs.dist ├── LICENSE ├── bin ├── build └── create_icons.php ├── server.php ├── CHANGELOG.md ├── README.md ├── action.php ├── curl.php ├── item.php ├── workflow.php ├── info.plist └── search.php /.gitignore: -------------------------------------------------------------------------------- 1 | .php_cs.cache 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icon.png -------------------------------------------------------------------------------- /icons/clone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/clone.png -------------------------------------------------------------------------------- /icons/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/file.png -------------------------------------------------------------------------------- /icons/fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/fork.png -------------------------------------------------------------------------------- /icons/gists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/gists.png -------------------------------------------------------------------------------- /icons/issue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/issue.png -------------------------------------------------------------------------------- /icons/pulse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/pulse.png -------------------------------------------------------------------------------- /icons/repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/repo.png -------------------------------------------------------------------------------- /icons/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/stars.png -------------------------------------------------------------------------------- /icons/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/user.png -------------------------------------------------------------------------------- /icons/wiki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/wiki.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/screenshot.png -------------------------------------------------------------------------------- /icons/branch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/branch.png -------------------------------------------------------------------------------- /icons/commits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/commits.png -------------------------------------------------------------------------------- /icons/graphs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/graphs.png -------------------------------------------------------------------------------- /icons/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/logout.png -------------------------------------------------------------------------------- /icons/mirror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/mirror.png -------------------------------------------------------------------------------- /icons/project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/project.png -------------------------------------------------------------------------------- /icons/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/search.png -------------------------------------------------------------------------------- /icons/update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/update.png -------------------------------------------------------------------------------- /icons/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/dashboard.png -------------------------------------------------------------------------------- /icons/milestone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/milestone.png -------------------------------------------------------------------------------- /icons/releases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/releases.png -------------------------------------------------------------------------------- /icons/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/settings.png -------------------------------------------------------------------------------- /icons/organization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/organization.png -------------------------------------------------------------------------------- /icons/private-fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/private-fork.png -------------------------------------------------------------------------------- /icons/private-repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/private-repo.png -------------------------------------------------------------------------------- /icons/pull-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/pull-request.png -------------------------------------------------------------------------------- /icons/notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/notifications.png -------------------------------------------------------------------------------- /icons/private-mirror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bs/alfred-github-workflow/master/icons/private-mirror.png -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 14 | ->setRules([ 15 | '@Symfony' => true, 16 | '@Symfony:risky' => true, 17 | 'array_syntax' => ['syntax' => 'long'], 18 | 'blank_line_before_return' => false, 19 | 'combine_consecutive_unsets' => true, 20 | 'header_comment' => ['header' => $header], 21 | 'method_argument_space' => false, 22 | 'no_useless_else' => true, 23 | ]) 24 | ->setFinder(PhpCsFixer\Finder::create()->in(__DIR__)) 25 | ; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Gregor Harlan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | addFile($dir.'/'.$file, $file); 39 | } 40 | foreach (glob($dir.'/icons/*.png') as $path) { 41 | $zip->addFile($path, 'icons/'.basename($path)); 42 | } 43 | 44 | $zip->compressFiles(Phar::GZ); 45 | 46 | $workflow = $dir.'/github.alfredworkflow'; 47 | if (file_exists($workflow)) { 48 | unlink($workflow); 49 | } 50 | rename($zipFile, $workflow); 51 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 24 | alfred-github-workflow 25 | 56 | 57 | 58 |
59 | 60 |

61 | alfred-github-workflow is ready.
62 | Have fun. 63 |

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /bin/create_icons.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | array( 6 | 'mark-github' => 'github', 7 | 8 | 'repo' => 'repo', 9 | 'repo-forked' => 'fork', 10 | 'mirror' => 'mirror', 11 | 12 | 'person' => 'user', 13 | 'organization' => 'organization', 14 | 'star' => 'stars', 15 | 'gist' => 'gists', 16 | 17 | 'issue-opened' => 'issue', 18 | 'git-pull-request' => 'pull-request', 19 | 'milestone' => 'milestone', 20 | 'file' => 'file', 21 | 'graph' => 'graphs', 22 | 'pulse' => 'pulse', 23 | 'project' => 'project', 24 | 'book' => 'wiki', 25 | 'git-commit' => 'commits', 26 | 'git-branch' => 'branch', 27 | 'repo-clone' => 'clone', 28 | 'tag' => 'releases', 29 | 30 | 'dashboard' => 'dashboard', 31 | 'gear' => 'settings', 32 | 'bell' => 'notifications', 33 | 34 | 'search' => 'search', 35 | 36 | 'cloud-download' => 'update', 37 | 'sign-out' => 'logout', 38 | ), 39 | '#e9dba5' => array( 40 | 'repo' => 'private-repo', 41 | 'repo-forked' => 'private-fork', 42 | 'mirror' => 'private-mirror', 43 | ), 44 | ); 45 | 46 | $dir = __DIR__.'/../icons/'; 47 | 48 | $baseImg = new Imagick(); 49 | $baseImg->newImage(256, 256, new ImagickPixel('transparent')); 50 | $baseImg->setImageFormat('png'); 51 | 52 | $draw = new ImagickDraw(); 53 | $draw->setFillColor('#444444'); 54 | $draw->roundRectangle(0, 0, 256, 256, 50, 50); 55 | $baseImg->drawImage($draw); 56 | 57 | foreach ($icons as $color => $set) { 58 | foreach ($set as $svgName => $name) { 59 | $img = clone $baseImg; 60 | 61 | $svg = new Imagick(); 62 | $svg->setBackgroundColor(new ImagickPixel('transparent')); 63 | $svg->setResolution(1020, 1020); 64 | 65 | $file = file_get_contents(__DIR__.'/../node_modules/octicons/build/svg/'.$svgName.'.svg'); 66 | $file = str_replace('readImageBlob($file); 70 | 71 | $x = (256 - $svg->getImageWidth()) / 2; 72 | $y = (256 - $svg->getImageHeight()) / 2; 73 | 74 | $img->compositeImage($svg, Imagick::COMPOSITE_DEFAULT, $x, $y); 75 | $img->writeImage($dir.$name.'.png'); 76 | } 77 | } 78 | 79 | rename($dir.'github.png', __DIR__.'/../icon.png'); 80 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Version 1.6.2 – 2018-02-13 5 | -------------------------- 6 | 7 | ### Bugfixes 8 | 9 | * Api pagination didn't work correctly any more (missing results from page > 2) 10 | 11 | 12 | Version 1.6.1 – 2017-09-23 13 | -------------------------- 14 | 15 | ### Bugfixes 16 | 17 | * Support for macOS 10.13 High Sierra 18 | * Commit search results had wrong urls on GitHub Enterprise (@beparker) 19 | 20 | 21 | Version 1.6 – 2017-05-07 22 | ------------------------ 23 | 24 | ### Features 25 | 26 | * new command `gh user/repo projects` (@dagio) 27 | * new command `gh my pulls review requested` (@AeroEchelon) 28 | * better sorting for issues (most recently updated on top) and commits (most recent on top) (@danielma) 29 | 30 | ### Bugfixes 31 | 32 | * On macOS 10.12.5 Beta URLs didn't opened in browser anymore 33 | 34 | 35 | Version 1.5 – 2016-12-13 36 | ------------------------ 37 | 38 | ### Features 39 | 40 | * new commands for searching repos and users globally in GitHub (`gh s repo` and `gh s @user`) 41 | * new command `gh my repos` (@jacobkossman) 42 | * new command `gh > delete database` 43 | * source repos with higher priority than forks 44 | 45 | ### Bugfixes 46 | 47 | * in some situations private repos were missing (@lxynox) 48 | * after saving GitHub Enterprise url the workflow didn't reopen correctly 49 | * updated user sub commands ("Activity" tab does not exist any more on GitHub) 50 | 51 | 52 | Version 1.4.1 – 2016-22-07 53 | -------------------------- 54 | 55 | * fixed reading environment variables (important for hotkey support) 56 | 57 | 58 | Version 1.4 – 2016-22-07 59 | ------------------------ 60 | 61 | * Hotkey support 62 | * use native update mechanism of Alfred (to keep your hotkeys) 63 | * new command `gh user/repo releases` (@altern8tif) 64 | * cache warmup after login 65 | * lower cpu usage in multi curl 66 | * fixed autocomplete values in GitHub Enterprise 67 | 68 | 69 | Version 1.3 – 2016-17-07 70 | ------------------------ 71 | 72 | **Important:** This is the last version for Alfred 2. 73 | 74 | * Disabled updates in Alfred 2 75 | * Updates in Alfred 3 are loaded from new location (GitHub releases) 76 | 77 | 78 | Version 1.2 – 2016-04-17 79 | ------------------------ 80 | 81 | ### Features 82 | 83 | * New sub commands for `gh my issues/pull`: `created`, `assigned` and `mentioned` 84 | * New help command: `gh > help` 85 | * Longer cache lifetime 86 | 87 | 88 | Version 1.1 – 2015-01-10 89 | ------------------------ 90 | 91 | ### Features 92 | 93 | * GitHub Enterprise support (use `ghe`) 94 | * Commit search (`gh user/repo *hash`) 95 | 96 | ### Bugfixes 97 | 98 | * A space after `gh` is required to avoid confusion when using commands of other workflows like `ghost` 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GitHub Workflow for [Alfred 3](http://www.alfredapp.com) 2 | ============================== 3 | 4 | [![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) 5 | 6 | You can search through GitHub (`gh`) and your GitHub Enterprise instance (`ghe`). 7 | 8 | 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. 9 | 10 | **[DOWNLOAD](https://github.com/gharlan/alfred-github-workflow/releases)** 11 | 12 | ![Workflow Screenshot](screenshot.png) 13 | 14 | Setup 15 | ----- 16 | 17 | ### For github.com 18 | 19 | In Alfred type (`gh > login`) to authenticate against your account. The login uses OAuth, so you do not have to enter your credentials. 20 | 21 | ### For github enterprise 22 | 23 | 1. In Alfred type (`ghe > url https://github.mycompany.com`) 24 | 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. 25 | 3. In Alfred type (`ghe > login `) 26 | 4. You can now `ghe your_enterprise_repo_name` 27 | 28 | Key Combinations 29 | ---------------- 30 | 31 | Key Combination | Action 32 | ---------------------- | ------ 33 | `enter` | Open entry in default browser 34 | `cmd` + `c` | Copy URL of the entry 35 | `cmd` + `enter` | Paste URL to front most app 36 | `shift` or `cmd` + `y` | Open URL in QuickLook 37 | 38 | Commands 39 | -------- 40 | 41 | To search through your GitHub Enterprise instance replace `gh` by `ghe`. 42 | 43 | ### Repo commands 44 | 45 | * `gh user/repo` 46 | * `gh user/repo #123` 47 | * `gh user/repo @branch` 48 | * `gh user/repo *commit` 49 | * `gh user/repo /path/to/file` 50 | * `gh user/repo admin` 51 | * `gh user/repo clone` 52 | * `gh user/repo graphs` 53 | * `gh user/repo issues` 54 | * `gh user/repo milestones` 55 | * `gh user/repo network` 56 | * `gh user/repo new issue` 57 | * `gh user/repo new pull` 58 | * `gh user/repo projects` 59 | * `gh user/repo pulls` 60 | * `gh user/repo pulse` 61 | * `gh user/repo releases` 62 | * `gh user/repo wiki` 63 | * `gh user/repo projects` 64 | 65 | ### User commands 66 | 67 | * `gh @user` 68 | * `gh @user overview` 69 | * `gh @user repositories` 70 | * `gh @user stars` 71 | * `gh @user gists` 72 | 73 | ### Search commands 74 | 75 | * `gh s repo` 76 | * `gh s @user` 77 | 78 | ### "My" commands 79 | 80 | * `gh my dashboard` 81 | * `gh my notifications` 82 | * `gh my profile` 83 | * `gh my issues` 84 | * `gh my issues created` 85 | * `gh my issues assigned` 86 | * `gh my issues mentioned` 87 | * `gh my pulls` 88 | * `gh my pulls created` 89 | * `gh my pulls assigned` 90 | * `gh my pulls mentioned` 91 | * `gh my pulls review requested` 92 | * `gh my repos` 93 | * `gh my settings` 94 | * `gh my stars` 95 | * `gh my gists` 96 | 97 | ### Workflow commands 98 | 99 | * `gh > login` 100 | * `gh > logout` 101 | * `gh > delete cache` 102 | * `gh > delete database` 103 | * `gh > update` 104 | * `gh > activate autoupdate` 105 | * `gh > deactivate autoupdate` 106 | * `gh > help` 107 | * `gh > changelog` 108 | * `ghe > url` (GitHub Enterprise only) 109 | * `ghe > generate token` (GitHub Enterprise only) 110 | * `ghe > enterprise reset` (GitHub Enterprise only) 111 | 112 | 113 | -------------------------------------------------------------------------------- /action.php: -------------------------------------------------------------------------------- 1 | ' !== $query[0] && 0 !== strpos($query, 'e >')) { 17 | if ('.git' == substr($query, -4)) { 18 | $query = 'github-mac://openRepo/'.substr($query, 0, -4); 19 | } 20 | exec('open '.$query); 21 | return; 22 | } 23 | 24 | $enterprise = 0 === strpos($query, 'e '); 25 | if ($enterprise) { 26 | $query = substr($query, 2); 27 | } 28 | $parts = explode(' ', $query); 29 | 30 | Workflow::init($enterprise); 31 | 32 | switch ($parts[1]) { 33 | case 'enterprise-url': 34 | Workflow::setConfig('enterprise_url', rtrim($parts[2], '/')); 35 | exec('osascript -e "tell application \"Alfred 3\" to search \"ghe \""'); 36 | break; 37 | 38 | case 'enterprise-reset': 39 | Workflow::removeConfig('enterprise_url'); 40 | Workflow::removeConfig('enterprise_access_token'); 41 | Workflow::deleteCache(); 42 | break; 43 | 44 | case 'login': 45 | if (isset($parts[2]) && $parts[2]) { 46 | Workflow::setAccessToken($parts[2]); 47 | echo 'Successfully logged in'; 48 | } elseif (!$enterprise) { 49 | Workflow::startServer(); 50 | $state = version_compare(PHP_VERSION, '5.4', '<') ? 'm' : ''; 51 | $url = Workflow::getBaseUrl().'/login/oauth/authorize?client_id=2d4f43826cb68e11c17c&scope=repo&state='.$state; 52 | exec('open '.escapeshellarg($url)); 53 | } 54 | break; 55 | 56 | case 'logout': 57 | Workflow::removeAccessToken(); 58 | Workflow::deleteCache(); 59 | echo 'Successfully logged out'; 60 | break; 61 | 62 | case 'delete-cache': 63 | Workflow::deleteCache(); 64 | echo 'Successfully deleted cache'; 65 | break; 66 | 67 | case 'delete-database': 68 | Workflow::deleteDatabase(); 69 | echo 'Successfully deleted database'; 70 | break; 71 | 72 | case 'refresh-cache': 73 | $curl = new Curl(); 74 | foreach (explode(',', $parts[2]) as $url) { 75 | Workflow::requestCache($url, $curl, null, false, 0, false); 76 | } 77 | $curl->execute(); 78 | Workflow::cleanCache(); 79 | break; 80 | 81 | case 'activate-autoupdate': 82 | Workflow::setConfig('autoupdate', 1); 83 | echo 'Activated auto updating'; 84 | break; 85 | 86 | case 'deactivate-autoupdate': 87 | Workflow::setConfig('autoupdate', 0); 88 | echo 'Deactivated auto updating'; 89 | break; 90 | 91 | case 'update': 92 | $release = json_decode(Workflow::request('https://api.github.com/repos/gharlan/alfred-github-workflow/releases/latest')); 93 | if (!isset($release->assets[0]->browser_download_url)) { 94 | echo 'Update failed'; 95 | exit; 96 | } 97 | $response = Workflow::request($release->assets[0]->browser_download_url, null, null, false); 98 | if (!$response) { 99 | echo 'Update failed'; 100 | exit; 101 | } 102 | $path = getenv('alfred_workflow_data').'/github.alfredworkflow'; 103 | file_put_contents($path, $response); 104 | exec('open '.escapeshellarg($path)); 105 | break; 106 | } 107 | -------------------------------------------------------------------------------- /curl.php: -------------------------------------------------------------------------------- 1 | requests[$request->url] = $request; 24 | if ($this->running) { 25 | $this->addHandle($request); 26 | } 27 | } 28 | 29 | public function execute() 30 | { 31 | $this->running = true; 32 | if (!is_resource(self::$multiHandle)) { 33 | self::$multiHandle = curl_multi_init(); 34 | } 35 | 36 | foreach ($this->requests as $request) { 37 | $this->addHandle($request); 38 | } 39 | 40 | $finish = false; 41 | $running = true; 42 | do { 43 | $finish = !$running; 44 | while (CURLM_CALL_MULTI_PERFORM == $execrun = curl_multi_exec(self::$multiHandle, $running)); 45 | if ($execrun != CURLM_OK) { 46 | break; 47 | } 48 | while ($done = curl_multi_info_read(self::$multiHandle)) { 49 | $ch = $done['handle']; 50 | $info = curl_getinfo($ch); 51 | $url = self::getHeader($info['request_header'], 'X-Url'); 52 | $request = $this->requests[$url]; 53 | $rawResponse = curl_multi_getcontent($ch); 54 | if (preg_match("@^HTTP/\\d\\.\\d 200 Connection established\r\n\r\n@i", $rawResponse)) { 55 | list(, $header, $body) = explode("\r\n\r\n", $rawResponse, 3); 56 | } else { 57 | list($header, $body) = explode("\r\n\r\n", $rawResponse, 2); 58 | } 59 | $response = new CurlResponse(); 60 | $response->request = $request; 61 | $response->status = $info['http_code']; 62 | $headerNames = array( 63 | 'etag' => 'ETag', 64 | 'contentType' => 'Content-Type', 65 | 'link' => 'Link', 66 | ); 67 | foreach ($headerNames as $key => $name) { 68 | $response->$key = self::getHeader($header, $name); 69 | } 70 | if (200 == $response->status) { 71 | $response->content = $body; 72 | } 73 | $callback = $request->callback; 74 | $callback($response); 75 | curl_multi_remove_handle(self::$multiHandle, $ch); 76 | curl_close($ch); 77 | } 78 | if ($running || !$finish) { 79 | if (curl_multi_select(self::$multiHandle, 1) === -1) { 80 | usleep(250); 81 | } 82 | } 83 | } while ($running || !$finish); 84 | 85 | $this->running = false; 86 | return true; 87 | } 88 | 89 | private function addHandle(CurlRequest $request) 90 | { 91 | $defaultOptions = array( 92 | CURLOPT_HEADER => true, 93 | CURLOPT_RETURNTRANSFER => true, 94 | CURLOPT_USERAGENT => 'alfred-github-workflow', 95 | CURLOPT_FOLLOWLOCATION => true, 96 | CURLOPT_MAXREDIRS => 5, 97 | CURLINFO_HEADER_OUT => true, 98 | ); 99 | if ($this->debug) { 100 | $defaultOptions[CURLOPT_PROXY] = 'localhost'; 101 | $defaultOptions[CURLOPT_PROXYPORT] = 8888; 102 | $defaultOptions[CURLOPT_SSL_VERIFYPEER] = 0; 103 | } 104 | 105 | $ch = curl_init(); 106 | $options = $defaultOptions; 107 | $options[CURLOPT_URL] = $request->url; 108 | $header = array(); 109 | $header[] = 'X-Url: '.$request->url; 110 | if ($request->token) { 111 | $header[] = 'Authorization: token '.$request->token; 112 | } 113 | if ($request->etag) { 114 | $header[] = 'If-None-Match: '.$request->etag; 115 | } 116 | $options[CURLOPT_HTTPHEADER] = $header; 117 | curl_setopt_array($ch, $options); 118 | curl_multi_add_handle(self::$multiHandle, $ch); 119 | } 120 | 121 | public static function getHeader($header, $key) 122 | { 123 | if (preg_match('/^'.preg_quote($key, '/').': (\V*)/mi', $header, $match)) { 124 | return $match[1]; 125 | } 126 | return null; 127 | } 128 | } 129 | 130 | class CurlRequest 131 | { 132 | public $url; 133 | public $etag; 134 | public $token; 135 | public $callback; 136 | 137 | public function __construct($url, $etag, $token, $callback) 138 | { 139 | $this->url = $url; 140 | $this->etag = $etag; 141 | $this->token = $token; 142 | $this->callback = $callback; 143 | } 144 | } 145 | 146 | class CurlResponse 147 | { 148 | /** @var CurlRequest */ 149 | public $request; 150 | public $status; 151 | public $contentType; 152 | public $etag; 153 | public $link; 154 | public $content; 155 | } 156 | -------------------------------------------------------------------------------- /item.php: -------------------------------------------------------------------------------- 1 | randomUid = true; 37 | return $this; 38 | } 39 | 40 | public function prefix($prefix, $onlyTitle = true) 41 | { 42 | $this->prefix = $prefix; 43 | $this->prefixOnlyTitle = $onlyTitle; 44 | return $this; 45 | } 46 | 47 | public function title($title) 48 | { 49 | $this->title = $title; 50 | return $this; 51 | } 52 | 53 | public function comparator($comparator) 54 | { 55 | $this->comparator = $comparator; 56 | return $this; 57 | } 58 | 59 | public function subtitle($subtitle) 60 | { 61 | $this->subtitle = $subtitle; 62 | return $this; 63 | } 64 | 65 | public function icon($icon) 66 | { 67 | $this->icon = $icon; 68 | return $this; 69 | } 70 | 71 | public function arg($arg) 72 | { 73 | $this->arg = $arg; 74 | return $this; 75 | } 76 | 77 | public function valid($valid, $add = '…') 78 | { 79 | $this->valid = (bool) $valid; 80 | $this->add = $add; 81 | return $this; 82 | } 83 | 84 | public function autocomplete($autocomplete = true) 85 | { 86 | $this->autocomplete = $autocomplete; 87 | return $this; 88 | } 89 | 90 | public function prio($prio) 91 | { 92 | $this->prio = $prio; 93 | return $this; 94 | } 95 | 96 | public function match($query) 97 | { 98 | $comparator = strtolower($this->comparator ?: $this->title); 99 | $query = strtolower($query); 100 | if (!$this->prefixOnlyTitle && stripos($query, $this->prefix) === 0) { 101 | $query = substr($query, strlen($this->prefix)); 102 | } 103 | $this->sameChars = 0; 104 | $queryLength = strlen($query); 105 | for ($i = 0, $k = 0; $i < $queryLength; ++$i, $k++) { 106 | for (; isset($comparator[$k]) && $comparator[$k] !== $query[$i]; ++$k); 107 | if (!isset($comparator[$k])) { 108 | return false; 109 | } 110 | if ($i === $k) { 111 | ++$this->sameChars; 112 | } 113 | } 114 | $this->missingChars = strlen($comparator) - $queryLength; 115 | return true; 116 | } 117 | 118 | public function compare(self $another) 119 | { 120 | if ($this->sameChars != $another->sameChars) { 121 | return $this->sameChars < $another->sameChars ? 1 : -1; 122 | } 123 | if ($this->prio != $another->prio) { 124 | return $this->prio < $another->prio ? 1 : -1; 125 | } 126 | return $this->missingChars > $another->missingChars ? 1 : -1; 127 | } 128 | 129 | /** 130 | * @param self[] $items 131 | * 132 | * @return string 133 | */ 134 | public static function toXml(array $items, $enterprise, $hotkey, $baseUrl) 135 | { 136 | $xml = new SimpleXMLElement(''); 137 | $prefix = $hotkey ? '' : ' '; 138 | foreach ($items as $item) { 139 | $c = $xml->addChild('item'); 140 | $title = $item->prefix.$item->title; 141 | $c->addAttribute('uid', $item->randomUid ? md5(time().$title) : md5($title)); 142 | if ($item->icon && file_exists(__DIR__.'/icons/'.$item->icon.'.png')) { 143 | $c->addChild('icon', 'icons/'.$item->icon.'.png'); 144 | } else { 145 | $c->addChild('icon', 'icon.png'); 146 | } 147 | if ($item->arg) { 148 | $arg = $item->arg; 149 | if ('/' === $arg[0]) { 150 | $arg = $baseUrl.$arg; 151 | } elseif (false === strpos($arg, '://')) { 152 | $arg = ($enterprise ? 'e ' : '').$arg; 153 | } 154 | $c->addAttribute('arg', $arg); 155 | } 156 | if ($item->autocomplete) { 157 | if (is_string($item->autocomplete)) { 158 | $autocomplete = $item->autocomplete; 159 | } elseif (null !== $item->comparator) { 160 | $autocomplete = $item->comparator; 161 | } else { 162 | $autocomplete = $item->title; 163 | } 164 | $c->addAttribute('autocomplete', $prefix.($item->prefixOnlyTitle ? $autocomplete : $item->prefix.$autocomplete)); 165 | } 166 | if (!$item->valid) { 167 | $c->addAttribute('valid', 'no'); 168 | $title .= $item->add; 169 | } 170 | $c->addChild('title', htmlspecialchars($title)); 171 | if ($item->subtitle) { 172 | $c->addChild('subtitle', htmlspecialchars($item->subtitle)); 173 | } 174 | } 175 | return $xml->asXML(); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /workflow.php: -------------------------------------------------------------------------------- 1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 65 | 66 | if (!$exists) { 67 | self::createTables(); 68 | } 69 | 70 | if (self::$enterprise) { 71 | self::$baseUrl = self::getConfig('enterprise_url'); 72 | self::$apiUrl = self::$baseUrl ? self::$baseUrl.'/api/v3' : null; 73 | self::$gistUrl = self::$baseUrl ? self::$baseUrl.'/gist' : null; 74 | } 75 | 76 | self::$debug = getenv('alfred_debug') && defined('STDERR'); 77 | 78 | register_shutdown_function(array(__CLASS__, 'shutdown')); 79 | } 80 | 81 | public static function shutdown() 82 | { 83 | if (self::$refreshUrls) { 84 | $urls = implode(',', array_keys(self::$refreshUrls)); 85 | exec('php action.php "> refresh-cache '.$urls.'" > /dev/null 2>&1 &'); 86 | self::log('refreshing cache in background for %s', $urls); 87 | } 88 | } 89 | 90 | public static function setConfig($key, $value) 91 | { 92 | self::getStatement('REPLACE INTO config VALUES(?, ?)')->execute(array($key, $value)); 93 | } 94 | 95 | public static function getConfig($key, $default = null) 96 | { 97 | $stmt = self::getStatement('SELECT value FROM config WHERE key = ?'); 98 | $stmt->execute(array($key)); 99 | $value = $stmt->fetchColumn(); 100 | return false !== $value ? $value : $default; 101 | } 102 | 103 | public static function removeConfig($key) 104 | { 105 | self::getStatement('DELETE FROM config WHERE key = ?')->execute(array($key)); 106 | } 107 | 108 | public static function getBaseUrl() 109 | { 110 | return self::$baseUrl; 111 | } 112 | 113 | public static function getApiUrl($path = null) 114 | { 115 | $url = self::$apiUrl; 116 | 117 | if ($path) { 118 | $paramStart = false === strpos($path, '?') ? '?' : '&'; 119 | $url .= $path.$paramStart.'per_page=100'; 120 | } 121 | 122 | return $url; 123 | } 124 | 125 | public static function getGistUrl() 126 | { 127 | return self::$gistUrl; 128 | } 129 | 130 | public static function setAccessToken($token) 131 | { 132 | self::setConfig(self::$enterprise ? 'enterprise_access_token' : 'access_token', $token); 133 | } 134 | 135 | public static function getAccessToken() 136 | { 137 | return self::getConfig(self::$enterprise ? 'enterprise_access_token' : 'access_token'); 138 | } 139 | 140 | public static function removeAccessToken() 141 | { 142 | self::removeConfig(self::$enterprise ? 'enterprise_access_token' : 'access_token'); 143 | } 144 | 145 | public static function request($url, Curl $curl = null, $callback = null, $withAuthorization = true) 146 | { 147 | self::log('loading content for %s', $url); 148 | 149 | $return = false; 150 | $returnValue = null; 151 | if (!$curl) { 152 | $curl = new Curl(); 153 | $return = true; 154 | $callback = function ($content) use (&$returnValue) { 155 | $returnValue = $content; 156 | }; 157 | } 158 | 159 | $token = $withAuthorization ? self::getAccessToken() : null; 160 | $curl->add(new CurlRequest($url, null, $token, function (CurlResponse $response) use ($callback) { 161 | if (is_callable($callback) && isset($response->content)) { 162 | $callback($response->content); 163 | } 164 | })); 165 | 166 | if ($return) { 167 | $curl->execute(); 168 | } 169 | return $returnValue; 170 | } 171 | 172 | /** 173 | * @param string $url 174 | * @param Curl $curl 175 | * @param callable $callback 176 | * @param bool $firstPageOnly 177 | * @param int $maxAge 178 | * @param bool $refreshInBackground 179 | * 180 | * @return mixed 181 | */ 182 | public static function requestCache($url, Curl $curl = null, $callback = null, $firstPageOnly = false, $maxAge = self::DEFAULT_CACHE_MAX_AGE, $refreshInBackground = true) 183 | { 184 | $return = false; 185 | $returnValue = null; 186 | if (!$curl) { 187 | $curl = new Curl(); 188 | $return = true; 189 | $callback = function ($content) use (&$returnValue) { 190 | $returnValue = $content; 191 | }; 192 | } 193 | 194 | $stmt = self::getStatement('SELECT * FROM request_cache WHERE url = ?'); 195 | $stmt->execute(array($url)); 196 | $stmt->bindColumn('timestamp', $timestamp); 197 | $stmt->bindColumn('etag', $etag); 198 | $stmt->bindColumn('content', $content); 199 | $stmt->bindColumn('refresh', $refresh); 200 | $stmt->fetch(PDO::FETCH_BOUND); 201 | 202 | $shouldRefresh = $timestamp < time() - 60 * $maxAge; 203 | $refreshInBackground = $refreshInBackground && !is_null($content); 204 | 205 | if ($shouldRefresh && $refreshInBackground && $refresh < time() - 3 * 60) { 206 | self::getStatement('UPDATE request_cache SET refresh = ? WHERE url = ?')->execute(array(time(), $url)); 207 | self::$refreshUrls[$url] = true; 208 | } 209 | 210 | if (!$shouldRefresh || $refreshInBackground) { 211 | self::log('using cached content for %s', $url); 212 | $content = json_decode($content); 213 | 214 | if (!$firstPageOnly) { 215 | $stmt = self::getStatement('SELECT url, content FROM request_cache WHERE parent = ? ORDER BY `timestamp` DESC'); 216 | while ($stmt->execute(array($url)) && $data = $stmt->fetchObject()) { 217 | $content = array_merge($content, json_decode($data->content)); 218 | $url = $data->url; 219 | } 220 | } 221 | 222 | if (is_callable($callback)) { 223 | $callback($content); 224 | } 225 | 226 | return $returnValue; 227 | } 228 | 229 | $responses = array(); 230 | 231 | $handleResponse = function (CurlResponse $response, $content, $parent = null) use (&$handleResponse, $curl, &$responses, $stmt, $callback, $firstPageOnly) { 232 | $url = $response->request->url; 233 | if ($response && in_array($response->status, array(200, 304))) { 234 | $checkNext = false; 235 | if (304 == $response->status) { 236 | $response->content = $content; 237 | $checkNext = true; 238 | } elseif (false === stripos($response->contentType, 'json')) { 239 | $response->content = json_encode($response->content); 240 | } 241 | $response->content = json_decode($response->content); 242 | if (isset($response->content->items)) { 243 | $response->content = $response->content->items; 244 | } 245 | $responses[] = $response->content; 246 | Workflow::getStatement('REPLACE INTO request_cache VALUES(?, ?, ?, ?, 0, ?)') 247 | ->execute(array($url, time(), $response->etag, json_encode($response->content), $parent)); 248 | 249 | if ($firstPageOnly) { 250 | // do nothing 251 | } elseif ($checkNext || $response->link && preg_match('/<([^<>]+)>; rel="next"/U', $response->link, $match)) { 252 | $stmt = Workflow::getStatement('SELECT * FROM request_cache WHERE parent = ?'); 253 | $stmt->execute(array($url)); 254 | if ($checkNext) { 255 | $stmt->bindColumn('url', $nextUrl); 256 | } else { 257 | $nextUrl = $match[1]; 258 | } 259 | $stmt->bindColumn('etag', $etag); 260 | $stmt->bindColumn('content', $content); 261 | $stmt->fetch(PDO::FETCH_BOUND); 262 | if ($nextUrl) { 263 | $curl->add(new CurlRequest($nextUrl, $etag, Workflow::getAccessToken(), function (CurlResponse $response) use ($handleResponse, $url, $content) { 264 | $handleResponse($response, $content, $url); 265 | })); 266 | return; 267 | } 268 | } else { 269 | Workflow::getStatement('DELETE FROM request_cache WHERE parent = ?')->execute(array($url)); 270 | } 271 | } else { 272 | Workflow::getStatement('DELETE FROM request_cache WHERE url = ?')->execute(array($url)); 273 | $url = null; 274 | } 275 | 276 | if (is_callable($callback)) { 277 | if (empty($responses)) { 278 | $callback(array()); 279 | return; 280 | } 281 | if (1 === count($responses)) { 282 | $callback($responses[0]); 283 | return; 284 | } 285 | $callback(array_reduce($responses, function ($content, $response) { 286 | return array_merge($content, $response); 287 | }, array())); 288 | } 289 | }; 290 | 291 | self::log('loading content for %s', $url); 292 | $curl->add(new CurlRequest($url, $etag, self::getAccessToken(), function (CurlResponse $response) use (&$responses, $handleResponse, $callback, $content) { 293 | $handleResponse($response, $content); 294 | })); 295 | 296 | if ($return) { 297 | $curl->execute(); 298 | } 299 | return $returnValue; 300 | } 301 | 302 | public static function requestApi($url, Curl $curl = null, $callback = null, $firstPageOnly = false, $maxAge = self::DEFAULT_CACHE_MAX_AGE) 303 | { 304 | $url = self::getApiUrl($url); 305 | return self::requestCache($url, $curl, $callback, $firstPageOnly, $maxAge); 306 | } 307 | 308 | public static function cleanCache() 309 | { 310 | self::$db->exec('DELETE FROM request_cache WHERE timestamp < '.(time() - 100 * 24 * 60 * 60)); 311 | } 312 | 313 | public static function deleteCache() 314 | { 315 | self::$db->exec('DELETE FROM request_cache'); 316 | } 317 | 318 | public static function cacheWarmup() 319 | { 320 | $paths = array('/user', '/user/orgs', '/user/starred', '/user/subscriptions', '/user/repos', '/user/following'); 321 | foreach ($paths as $path) { 322 | self::$refreshUrls[self::getApiUrl($path)] = true; 323 | } 324 | } 325 | 326 | public static function startServer() 327 | { 328 | if (version_compare(PHP_VERSION, '5.4', '>=')) { 329 | self::stopServer(); 330 | shell_exec(sprintf( 331 | 'alfred_workflow_data=%s php -d variables_order=EGPCS -S localhost:2233 server.php > /dev/null 2>&1 & echo $! >> %s', 332 | escapeshellarg(getenv('alfred_workflow_data')), 333 | escapeshellarg(self::$filePids) 334 | )); 335 | } 336 | } 337 | 338 | public static function stopServer() 339 | { 340 | if (file_exists(self::$filePids)) { 341 | $pids = file(self::$filePids); 342 | foreach ($pids as $pid) { 343 | shell_exec('kill -9 '.$pid); 344 | } 345 | unlink(self::$filePids); 346 | } 347 | } 348 | 349 | public static function checkUpdate() 350 | { 351 | if (self::getConfig('version') !== self::VERSION) { 352 | self::setConfig('version', self::VERSION); 353 | } 354 | if (!self::getConfig('autoupdate', 1)) { 355 | return false; 356 | } 357 | $release = self::requestCache('https://api.github.com/repos/gharlan/alfred-github-workflow/releases/latest', null, null, true, 1440); 358 | if (!$release) { 359 | return false; 360 | } 361 | $version = ltrim($release->tag_name, 'v'); 362 | return version_compare($version, self::VERSION) > 0; 363 | } 364 | 365 | private static function createTables() 366 | { 367 | self::$db->exec(' 368 | CREATE TABLE config ( 369 | key TEXT PRIMARY KEY NOT NULL, 370 | value TEXT 371 | ) WITHOUT ROWID 372 | '); 373 | 374 | self::$db->exec(' 375 | CREATE TABLE request_cache ( 376 | url TEXT PRIMARY KEY NOT NULL, 377 | timestamp INTEGER NOT NULL, 378 | etag TEXT, 379 | content TEXT, 380 | refresh INTEGER, 381 | parent TEXT 382 | ) WITHOUT ROWID 383 | '); 384 | self::$db->exec('CREATE INDEX parent_url ON request_cache(parent) WHERE parent IS NOT NULL'); 385 | } 386 | 387 | public static function deleteDatabase() 388 | { 389 | self::closeCursors(); 390 | self::$db = null; 391 | unlink(self::$fileDb); 392 | } 393 | 394 | public static function addItem(Item $item, $check = true) 395 | { 396 | if (!$check || $item->match(self::$query)) { 397 | self::$items[] = $item; 398 | } 399 | } 400 | 401 | public static function sortItems() 402 | { 403 | usort(self::$items, function (Item $a, Item $b) { 404 | return $a->compare($b); 405 | }); 406 | } 407 | 408 | public static function getItemsAsXml() 409 | { 410 | return Item::toXml(self::$items, self::$enterprise, self::$hotkey, self::getBaseUrl()); 411 | } 412 | 413 | public static function log($msg) 414 | { 415 | if (self::$debug) { 416 | fwrite(STDERR, "\n".call_user_func_array('sprintf', func_get_args())); 417 | } 418 | } 419 | 420 | /** 421 | * @param string $query 422 | * 423 | * @return PDOStatement 424 | */ 425 | public static function getStatement($query) 426 | { 427 | if (!isset(self::$statements[$query])) { 428 | self::$statements[$query] = self::$db->prepare($query); 429 | } 430 | return self::$statements[$query]; 431 | } 432 | 433 | protected static function closeCursors() 434 | { 435 | foreach (self::$statements as $statement) { 436 | $statement->closeCursor(); 437 | } 438 | self::$statements = array(); 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | de.gh01.alfred.github 7 | category 8 | Internet 9 | connections 10 | 11 | 14CA19E2-6328-4201-905E-A751E2BA47B5 12 | 13 | 14 | destinationuid 15 | ABE8DD6E-CD29-4E70-87CF-1382A0446009 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | vitoclose 21 | 22 | 23 | 24 | 29045171-6618-4FA4-BAB0-39C10422CF31 25 | 26 | 27 | destinationuid 28 | 67ADBB8D-C705-4981-BB9B-7C9238BEFF2E 29 | modifiers 30 | 0 31 | modifiersubtext 32 | 33 | vitoclose 34 | 35 | 36 | 37 | 94B48881-907F-4752-AAA0-234EA30CFC18 38 | 39 | 40 | destinationuid 41 | FC76BC27-8BCB-4BEE-AD42-EAC2D9B01F0F 42 | modifiers 43 | 0 44 | modifiersubtext 45 | 46 | vitoclose 47 | 48 | 49 | 50 | 95CD1A63-A0C6-4458-9817-9C6B1A90C827 51 | 52 | 53 | destinationuid 54 | 29045171-6618-4FA4-BAB0-39C10422CF31 55 | modifiers 56 | 0 57 | modifiersubtext 58 | 59 | vitoclose 60 | 61 | 62 | 63 | destinationuid 64 | 9715D8FA-0199-450F-99B8-64796B5AC6CB 65 | modifiers 66 | 1048576 67 | modifiersubtext 68 | Copy and paste URL 69 | vitoclose 70 | 71 | 72 | 73 | 9715D8FA-0199-450F-99B8-64796B5AC6CB 74 | 75 | 76 | destinationuid 77 | 87F10E6D-697C-47CE-BD78-E7174F5DF815 78 | modifiers 79 | 0 80 | modifiersubtext 81 | 82 | vitoclose 83 | 84 | 85 | 86 | ABE8DD6E-CD29-4E70-87CF-1382A0446009 87 | 88 | 89 | destinationuid 90 | 95CD1A63-A0C6-4458-9817-9C6B1A90C827 91 | modifiers 92 | 0 93 | modifiersubtext 94 | 95 | vitoclose 96 | 97 | 98 | 99 | DAA505B9-F86C-4AF8-818B-8F614F01485E 100 | 101 | 102 | destinationuid 103 | 94B48881-907F-4752-AAA0-234EA30CFC18 104 | modifiers 105 | 0 106 | modifiersubtext 107 | 108 | vitoclose 109 | 110 | 111 | 112 | FC76BC27-8BCB-4BEE-AD42-EAC2D9B01F0F 113 | 114 | 115 | destinationuid 116 | 29045171-6618-4FA4-BAB0-39C10422CF31 117 | modifiers 118 | 0 119 | modifiersubtext 120 | 121 | vitoclose 122 | 123 | 124 | 125 | destinationuid 126 | 9715D8FA-0199-450F-99B8-64796B5AC6CB 127 | modifiers 128 | 1048576 129 | modifiersubtext 130 | Copy and paste URL 131 | vitoclose 132 | 133 | 134 | 135 | 136 | createdby 137 | Gregor Harlan 138 | description 139 | GitHub for Alfred 140 | disabled 141 | 142 | name 143 | GitHub 144 | objects 145 | 146 | 147 | config 148 | 149 | action 150 | 0 151 | argument 152 | 0 153 | hotkey 154 | 0 155 | hotmod 156 | 0 157 | hotstring 158 | 159 | leftcursor 160 | 161 | modsmode 162 | 2 163 | relatedAppsMode 164 | 0 165 | 166 | type 167 | alfred.workflow.trigger.hotkey 168 | uid 169 | DAA505B9-F86C-4AF8-818B-8F614F01485E 170 | version 171 | 2 172 | 173 | 174 | config 175 | 176 | alfredfiltersresults 177 | 178 | argumenttype 179 | 1 180 | escaping 181 | 36 182 | keyword 183 | gh 184 | queuedelaycustom 185 | 3 186 | queuedelayimmediatelyinitially 187 | 188 | queuedelaymode 189 | 0 190 | queuemode 191 | 2 192 | runningsubtext 193 | Loading results… 194 | script 195 | php -f search.php -- github "{query}" 196 | scriptargtype 197 | 0 198 | scriptfile 199 | 200 | subtext 201 | Search or type a command 202 | title 203 | gh... 204 | type 205 | 0 206 | withspace 207 | 208 | 209 | type 210 | alfred.workflow.input.scriptfilter 211 | uid 212 | FC76BC27-8BCB-4BEE-AD42-EAC2D9B01F0F 213 | version 214 | 2 215 | 216 | 217 | config 218 | 219 | concurrently 220 | 221 | escaping 222 | 36 223 | script 224 | php -f action.php -- "{query}" 225 | scriptargtype 226 | 0 227 | scriptfile 228 | 229 | type 230 | 0 231 | 232 | type 233 | alfred.workflow.action.script 234 | uid 235 | 29045171-6618-4FA4-BAB0-39C10422CF31 236 | version 237 | 2 238 | 239 | 240 | config 241 | 242 | lastpathcomponent 243 | 244 | onlyshowifquerypopulated 245 | 246 | removeextension 247 | 248 | text 249 | {query} 250 | title 251 | GitHub 252 | 253 | type 254 | alfred.workflow.output.notification 255 | uid 256 | 67ADBB8D-C705-4981-BB9B-7C9238BEFF2E 257 | version 258 | 1 259 | 260 | 261 | config 262 | 263 | argument 264 | {query} 265 | variables 266 | 267 | hotkey 268 | 1 269 | 270 | 271 | type 272 | alfred.workflow.utility.argument 273 | uid 274 | 94B48881-907F-4752-AAA0-234EA30CFC18 275 | version 276 | 1 277 | 278 | 279 | config 280 | 281 | action 282 | 0 283 | argument 284 | 0 285 | hotkey 286 | 0 287 | hotmod 288 | 0 289 | hotstring 290 | 291 | leftcursor 292 | 293 | modsmode 294 | 2 295 | relatedAppsMode 296 | 0 297 | 298 | type 299 | alfred.workflow.trigger.hotkey 300 | uid 301 | 14CA19E2-6328-4201-905E-A751E2BA47B5 302 | version 303 | 2 304 | 305 | 306 | config 307 | 308 | alfredfiltersresults 309 | 310 | argumenttype 311 | 1 312 | escaping 313 | 36 314 | keyword 315 | ghe 316 | queuedelaycustom 317 | 3 318 | queuedelayimmediatelyinitially 319 | 320 | queuedelaymode 321 | 0 322 | queuemode 323 | 2 324 | runningsubtext 325 | Loading results… 326 | script 327 | php -f search.php -- enterprise "{query}" 328 | scriptargtype 329 | 0 330 | scriptfile 331 | 332 | subtext 333 | Search or type a command (GitHub Enterprise) 334 | title 335 | ghe... 336 | type 337 | 0 338 | withspace 339 | 340 | 341 | type 342 | alfred.workflow.input.scriptfilter 343 | uid 344 | 95CD1A63-A0C6-4458-9817-9C6B1A90C827 345 | version 346 | 2 347 | 348 | 349 | config 350 | 351 | autopaste 352 | 353 | clipboardtext 354 | {query} 355 | transient 356 | 357 | 358 | type 359 | alfred.workflow.output.clipboard 360 | uid 361 | 87F10E6D-697C-47CE-BD78-E7174F5DF815 362 | version 363 | 2 364 | 365 | 366 | config 367 | 368 | argument 369 | {query} 370 | variables 371 | 372 | hotkey 373 | 1 374 | 375 | 376 | type 377 | alfred.workflow.utility.argument 378 | uid 379 | ABE8DD6E-CD29-4E70-87CF-1382A0446009 380 | version 381 | 1 382 | 383 | 384 | config 385 | 386 | inputstring 387 | {query} 388 | matchcasesensitive 389 | 390 | matchmode 391 | 2 392 | matchstring 393 | ^\w+:// 394 | 395 | type 396 | alfred.workflow.utility.filter 397 | uid 398 | 9715D8FA-0199-450F-99B8-64796B5AC6CB 399 | version 400 | 1 401 | 402 | 403 | readme 404 | Changelog 405 | ========= 406 | 407 | Version 1.6.2 – 2018-02-13 408 | -------------------------- 409 | 410 | ### Bugfixes 411 | 412 | * Api pagination didnt work correctly any more (missing results from page > 2) 413 | 414 | 415 | Version 1.6.1 – 2017-09-23 416 | -------------------------- 417 | 418 | ### Bugfixes 419 | 420 | * Support for macOS 10.13 High Sierra 421 | * Commit search results had wrong urls on GitHub Enterprise (@beparker) 422 | 423 | 424 | Version 1.6 – 2017-05-07 425 | ------------------------ 426 | 427 | ### Features 428 | 429 | * new command `gh user/repo projects` (@dagio) 430 | * new command `gh my pulls review requested` (@AeroEchelon) 431 | * better sorting for issues (most recently updated on top) and commits (most recent on top) (@danielma) 432 | 433 | ### Bugfixes 434 | 435 | * On macOS 10.12.5 Beta URLs didnt opened in browser anymore 436 | 437 | 438 | Version 1.5 – 2016-12-13 439 | ------------------------ 440 | 441 | ### Features 442 | 443 | * new commands for searching repos and users globally in GitHub (`gh s repo` and `gh s @user`) 444 | * new command `gh my repos` (@jacobkossman) 445 | * new command `gh > delete database` 446 | * source repos with higher priority than forks 447 | 448 | ### Bugfixes 449 | 450 | * in some situations private repos were missing (@lxynox) 451 | * after saving GitHub Enterprise url the workflow didn't reopen correctly 452 | * updated user sub commands (Activity tab does not exist any more on GitHub) 453 | 454 | 455 | Version 1.4.1 – 2016-22-07 456 | -------------------------- 457 | 458 | * fixed reading environment variables (important for hotkey support) 459 | 460 | 461 | Version 1.4 – 2016-22-07 462 | ------------------------ 463 | 464 | * Hotkey support 465 | * use native update mechanism of Alfred (to keep your hotkeys) 466 | * new command `gh user/repo releases` (@altern8tif) 467 | * cache warmup after login 468 | * lower cpu usage in multi curl 469 | * fixed autocomplete values in GitHub Enterprise 470 | 471 | 472 | Version 1.3 – 2016-17-07 473 | ------------------------ 474 | 475 | **Important:** This is the last version for Alfred 2. 476 | 477 | * Disabled updates in Alfred 2 478 | * Updates in Alfred 3 are loaded from new location (GitHub releases) 479 | 480 | 481 | Version 1.2 – 2016-04-17 482 | ------------------------ 483 | 484 | ### Features 485 | 486 | * New sub commands for `gh my issues/pull`: `created`, `assigned` and `mentioned` 487 | * New help command: `gh > help` 488 | * Longer cache lifetime 489 | 490 | 491 | Version 1.1 – 2015-01-10 492 | ------------------------ 493 | 494 | ### Features 495 | 496 | * GitHub Enterprise support (use `ghe`) 497 | * Commit search (`gh user/repo *hash`) 498 | 499 | ### Bugfixes 500 | 501 | * A space after `gh` is required to avoid confusion when using commands of other workflows like `ghost` 502 | 503 | uidata 504 | 505 | 14CA19E2-6328-4201-905E-A751E2BA47B5 506 | 507 | colorindex 508 | 9 509 | note 510 | GitHub Enterprise 511 | xpos 512 | 30 513 | ypos 514 | 230 515 | 516 | 29045171-6618-4FA4-BAB0-39C10422CF31 517 | 518 | xpos 519 | 600 520 | ypos 521 | 40 522 | 523 | 67ADBB8D-C705-4981-BB9B-7C9238BEFF2E 524 | 525 | xpos 526 | 790 527 | ypos 528 | 40 529 | 530 | 87F10E6D-697C-47CE-BD78-E7174F5DF815 531 | 532 | xpos 533 | 790 534 | ypos 535 | 230 536 | 537 | 94B48881-907F-4752-AAA0-234EA30CFC18 538 | 539 | colorindex 540 | 12 541 | xpos 542 | 220 543 | ypos 544 | 70 545 | 546 | 95CD1A63-A0C6-4458-9817-9C6B1A90C827 547 | 548 | colorindex 549 | 9 550 | note 551 | GitHub Enterprise 552 | xpos 553 | 330 554 | ypos 555 | 230 556 | 557 | 9715D8FA-0199-450F-99B8-64796B5AC6CB 558 | 559 | xpos 560 | 680 561 | ypos 562 | 260 563 | 564 | ABE8DD6E-CD29-4E70-87CF-1382A0446009 565 | 566 | colorindex 567 | 9 568 | xpos 569 | 220 570 | ypos 571 | 260 572 | 573 | DAA505B9-F86C-4AF8-818B-8F614F01485E 574 | 575 | colorindex 576 | 12 577 | note 578 | github.com 579 | xpos 580 | 30 581 | ypos 582 | 40 583 | 584 | FC76BC27-8BCB-4BEE-AD42-EAC2D9B01F0F 585 | 586 | colorindex 587 | 12 588 | note 589 | github.com 590 | xpos 591 | 330 592 | ypos 593 | 40 594 | 595 | 596 | variables 597 | 598 | hotkey 599 | 0 600 | 601 | version 602 | 1.6.2 603 | webaddress 604 | https://github.com/gharlan/alfred-github-workflow 605 | 606 | 607 | -------------------------------------------------------------------------------- /search.php: -------------------------------------------------------------------------------- 1 | ') { 59 | self::addSystemCommands(); 60 | Workflow::sortItems(); 61 | return; 62 | } 63 | 64 | $isSearch = 's' === $parts[0] && isset($parts[1]); 65 | $isUser = isset($query[0]) && $query[0] == '@'; 66 | $isRepo = false; 67 | $queryUser = null; 68 | if ($isUser) { 69 | $queryUser = ltrim($parts[0], '@'); 70 | } elseif (!$isSearch && false !== $pos = strpos($parts[0], '/')) { 71 | $queryUser = substr($parts[0], 0, $pos); 72 | $isRepo = true; 73 | } 74 | 75 | if ('my' == $parts[0] && isset($parts[1])) { 76 | self::addMyCommands(); 77 | } elseif ($isSearch && strlen($query) > 5 && '@' !== substr($parts[1], 0, 1)) { 78 | self::addRepoSearchCommands(); 79 | } elseif ($isSearch && strlen($query) > 6 && '@' === substr($parts[1], 0, 1)) { 80 | self::addUserSearchCommands(); 81 | } elseif ($isUser && isset($parts[1])) { 82 | self::addUserSubCommands($queryUser); 83 | } elseif (!$isUser && $isRepo && isset($parts[1])) { 84 | self::addRepoSubCommands(); 85 | } else { 86 | self::addDefaultCommands($isSearch, $isUser, $isRepo, $queryUser); 87 | } 88 | 89 | Workflow::sortItems(); 90 | 91 | if (!$query) { 92 | return; 93 | } 94 | 95 | if (!$isSearch && !$isUser && !isset($parts[1])) { 96 | Workflow::addItem(Item::create() 97 | ->title('s '.$query) 98 | ->subtitle('Search repo (in alfred workflow)') 99 | ->comparator($query) 100 | ->autocomplete('s '.$query) 101 | ->icon('repo') 102 | ->valid(false) 103 | , false); 104 | } 105 | 106 | if (!$isSearch && !$isRepo && !isset($parts[1])) { 107 | $title = 's @'.ltrim($query, '@'); 108 | Workflow::addItem(Item::create() 109 | ->title($title) 110 | ->subtitle('Search user (in alfred workflow)') 111 | ->comparator($query) 112 | ->autocomplete($title) 113 | ->icon('user') 114 | ->valid(false) 115 | , false); 116 | } 117 | 118 | if (!$isUser && $isRepo && isset($parts[1])) { 119 | $repoQuery = substr($query, strlen($parts[0]) + 1); 120 | Workflow::addItem(Item::create() 121 | ->title("Search '$parts[0]' for '$repoQuery'") 122 | ->icon('search') 123 | ->arg('/'.$parts[0].'/search?q='.urlencode($repoQuery)) 124 | ->autocomplete(false) 125 | , false); 126 | } 127 | 128 | $path = $isUser ? $queryUser : 'search?q='.urlencode($query); 129 | $name = self::$enterprise ? 'GitHub Enterprise' : 'GitHub'; 130 | Workflow::addItem(Item::create() 131 | ->title("Search $name for '$query'") 132 | ->icon('search') 133 | ->arg('/'.$path) 134 | ->autocomplete(false) 135 | , false); 136 | } 137 | 138 | private static function addEmptyQueryCommand() 139 | { 140 | Workflow::addItem(Item::create() 141 | ->title(self::$enterprise ? 'ghe' : 'gh') 142 | ->subtitle('Search or type a command'.(self::$enterprise ? ' (GitHub Enterprise)' : '')) 143 | ->comparator('') 144 | ->valid(false) 145 | , false); 146 | } 147 | 148 | private static function addUpdateCommands() 149 | { 150 | $cmds = array( 151 | 'update' => 'There is an update for this Alfred workflow', 152 | 'deactivate autoupdate' => 'Deactivate auto updating this Alfred Workflow', 153 | ); 154 | foreach ($cmds as $cmd => $desc) { 155 | Workflow::addItem(Item::create() 156 | ->title('> '.$cmd) 157 | ->subtitle($desc) 158 | ->icon($cmd) 159 | ->arg('> '.str_replace(' ', '-', $cmd)) 160 | ->randomUid() 161 | , false); 162 | } 163 | 164 | Workflow::addItem(Item::create() 165 | ->title('> changelog') 166 | ->subtitle('View the changelog') 167 | ->icon('file') 168 | ->arg('https://github.com/gharlan/alfred-github-workflow/blob/master/CHANGELOG.md') 169 | ->randomUid() 170 | , false); 171 | } 172 | 173 | private static function addEnterpriseUrlCommand() 174 | { 175 | $url = null; 176 | if (count(self::$parts) > 1 && self::$parts[0] == '>' && self::$parts[1] == 'url' && isset(self::$parts[2])) { 177 | $url = self::$parts[2]; 178 | } 179 | Workflow::addItem(Item::create() 180 | ->title('> url '.$url) 181 | ->subtitle('Set the GitHub Enterprise URL') 182 | ->arg('> enterprise-url '.$url) 183 | ->valid((bool) $url, '') 184 | ->randomUid() 185 | , false); 186 | } 187 | 188 | private static function addLoginCommands() 189 | { 190 | Workflow::removeAccessToken(); 191 | $token = null; 192 | if (count(self::$parts) > 1 && self::$parts[0] == '>' && self::$parts[1] == 'login' && isset(self::$parts[2])) { 193 | $token = self::$parts[2]; 194 | } 195 | if (!$token && !self::$enterprise) { 196 | Workflow::addItem(Item::create() 197 | ->title('> login') 198 | ->subtitle('Generate OAuth access token') 199 | ->arg('> login') 200 | ->randomUid() 201 | , false); 202 | } 203 | Workflow::addItem(Item::create() 204 | ->title('> login '.$token) 205 | ->subtitle('Save access token') 206 | ->arg('> login '.$token) 207 | ->valid((bool) $token, '') 208 | ->randomUid() 209 | , false); 210 | if (!$token && self::$enterprise) { 211 | Workflow::addItem(Item::create() 212 | ->title('> generate token') 213 | ->subtitle('Generate a new access token') 214 | ->arg('/settings/applications') 215 | ->randomUid() 216 | , false); 217 | Workflow::addItem(Item::create() 218 | ->title('> enterprise reset') 219 | ->subtitle('Reset the GitHub Enterprise URL') 220 | ->arg('> enterprise-reset') 221 | ->randomUid() 222 | , false); 223 | } 224 | } 225 | 226 | private static function addDefaultCommands($isSearch, $isUser, $isRepo, $queryUser) 227 | { 228 | $users = array(); 229 | $repos = array(); 230 | 231 | $curl = new Curl(); 232 | 233 | if (!$isSearch && !$isUser) { 234 | $getRepos = function ($url, $prio) use ($curl, &$repos) { 235 | Workflow::requestApi($url, $curl, function ($urlRepos) use (&$repos, $prio) { 236 | foreach ($urlRepos as $repo) { 237 | $repo->score = 300 + $prio + ($repo->fork ? 0 : 10); 238 | $repos[$repo->id] = $repo; 239 | } 240 | }); 241 | }; 242 | if ($isRepo) { 243 | if ($queryUser != self::$user->login) { 244 | $urls = array('/users/'.$queryUser.'/repos', '/orgs/'.$queryUser.'/repos'); 245 | } else { 246 | $urls = array('/user/repos'); 247 | } 248 | } else { 249 | Workflow::requestApi('/user/orgs', $curl, function ($orgs) use ($getRepos) { 250 | foreach ($orgs as $org) { 251 | $getRepos('/orgs/'.$org->login.'/repos', 0); 252 | } 253 | }); 254 | $urls = array('/user/starred', '/user/subscriptions', '/user/repos'); 255 | } 256 | foreach ($urls as $prio => $url) { 257 | $getRepos($url, $prio + 1); 258 | } 259 | } 260 | 261 | if (!$isSearch && !$isRepo) { 262 | Workflow::requestApi('/user/following', $curl, function ($urlUsers) use (&$users) { 263 | $users = $urlUsers; 264 | }); 265 | } 266 | 267 | $curl->execute(); 268 | 269 | self::addRepos($repos); 270 | 271 | foreach ($users as $user) { 272 | Workflow::addItem(Item::create() 273 | ->prefix('@', false) 274 | ->title($user->login.' ') 275 | ->subtitle($user->type) 276 | ->arg($user->html_url) 277 | ->icon(lcfirst($user->type)) 278 | ->prio(200) 279 | ); 280 | } 281 | 282 | Workflow::addItem(Item::create() 283 | ->title('s '.substr(self::$query, 2, 4)) 284 | ->subtitle('Search repo or @user (min 4 chars)') 285 | ->prio(110) 286 | ->valid(false) 287 | ); 288 | 289 | Workflow::addItem(Item::create() 290 | ->title('my ') 291 | ->subtitle('Dashboard, settings, and more') 292 | ->prio(100) 293 | ->valid(false) 294 | ); 295 | } 296 | 297 | private static function addRepoSearchCommands() 298 | { 299 | $q = substr(self::$query, 2); 300 | $repos = Workflow::requestApi('/search/repositories?q='.urlencode($q), null, null, true); 301 | 302 | self::addRepos($repos, 's '); 303 | } 304 | 305 | private static function addUserSearchCommands() 306 | { 307 | $q = substr(self::$query, 3); 308 | $users = Workflow::requestApi('/search/users?q='.urlencode($q), null, null, true); 309 | 310 | self::addUsers($users, 's @'); 311 | } 312 | 313 | private static function addRepos($repos, $comparatorPrefix = '') 314 | { 315 | foreach ($repos as $repo) { 316 | $icon = 'repo'; 317 | if ($repo->fork) { 318 | $icon = 'fork'; 319 | } elseif ($repo->mirror_url) { 320 | $icon = 'mirror'; 321 | } 322 | if ($repo->private) { 323 | $icon = 'private-'.$icon; 324 | } 325 | Workflow::addItem(Item::create() 326 | ->title($repo->full_name.' ') 327 | ->comparator($comparatorPrefix.$repo->full_name) 328 | ->autocomplete($repo->full_name.' ') 329 | ->subtitle($repo->description) 330 | ->icon($icon) 331 | ->arg('/'.$repo->full_name) 332 | ->prio($repo->score) 333 | ); 334 | } 335 | } 336 | 337 | private static function addUsers($users, $comparatorPrefix = '') 338 | { 339 | foreach ($users as $user) { 340 | Workflow::addItem(Item::create() 341 | ->prefix('@', false) 342 | ->title($user->login.' ') 343 | ->comparator($comparatorPrefix.$user->login) 344 | ->autocomplete($user->login.' ') 345 | ->subtitle($user->type) 346 | ->arg($user->html_url) 347 | ->icon(lcfirst($user->type)) 348 | ->prio(isset($user->score) ? $user->score : 200) 349 | ); 350 | } 351 | } 352 | 353 | private static function addRepoSubCommands() 354 | { 355 | $parts = self::$parts; 356 | if (isset($parts[1][0]) && in_array($parts[1][0], array('#', '@', '*', '/'))) { 357 | switch ($parts[1][0]) { 358 | case '*': 359 | $commits = Workflow::requestApi('/repos/'.$parts[0].'/commits'); 360 | foreach ($commits as $commit) { 361 | Workflow::addItem(Item::create() 362 | ->title($commit->commit->message) 363 | ->comparator($parts[0].' *'.$commit->sha) 364 | ->subtitle($commit->commit->author->date.' ('.$commit->sha.')') 365 | ->icon('commits') 366 | ->arg('/'.$parts[0].'/commit/'.$commit->sha) 367 | ->prio(strtotime($commit->commit->author->date)) 368 | ); 369 | } 370 | break; 371 | case '@': 372 | $branches = Workflow::requestApi('/repos/'.$parts[0].'/branches'); 373 | foreach ($branches as $branch) { 374 | Workflow::addItem(Item::create() 375 | ->title('@'.$branch->name) 376 | ->comparator($parts[0].' @'.$branch->name) 377 | ->subtitle($branch->commit->sha) 378 | ->icon('branch') 379 | ->arg('/'.$parts[0].'/tree/'.$branch->name) 380 | ); 381 | } 382 | break; 383 | case '/': 384 | $repo = Workflow::requestApi('/repos/'.$parts[0]); 385 | $files = Workflow::requestApi('/repos/'.$parts[0].'/git/trees/'.$repo->default_branch.'?recursive=1'); 386 | foreach ($files->tree as $file) { 387 | if ('blob' === $file->type) { 388 | Workflow::addItem(Item::create() 389 | ->title(basename($file->path)) 390 | ->subtitle('/'.$file->path) 391 | ->comparator($parts[0].' /'.$file->path) 392 | ->icon('file') 393 | ->arg('/'.$parts[0].'/blob/'.$repo->default_branch.'/'.$file->path) 394 | ); 395 | } 396 | } 397 | break; 398 | case '#': 399 | $issues = Workflow::requestApi('/repos/'.$parts[0].'/issues?sort=updated&state=all'); 400 | foreach ($issues as $issue) { 401 | Workflow::addItem(Item::create() 402 | ->title('#'.$issue->number) 403 | ->comparator($parts[0].' #'.$issue->number) 404 | ->subtitle($issue->title) 405 | ->icon($issue->pull_request ? 'pull-request' : 'issue') 406 | ->arg($issue->html_url) 407 | ->prio(strtotime($issue->updated_at)) 408 | ); 409 | } 410 | break; 411 | } 412 | } else { 413 | $subs = array( 414 | 'admin' => array('Manage this repo', 'settings'), 415 | 'graphs' => array('All the graphs'), 416 | 'issues ' => array('List, show and create issues', 'issue'), 417 | 'milestones' => array('View milestones', 'milestone'), 418 | 'network' => array('See the network', 'graphs'), 419 | 'projects' => array('View projects', 'project'), 420 | 'pulls' => array('Show open pull requests', 'pull-request'), 421 | 'pulse' => array('See recent activity'), 422 | 'wiki' => array('Pull up the wiki'), 423 | 'commits' => array('View commit history'), 424 | 'releases' => array('See latest releases'), 425 | ); 426 | foreach ($subs as $key => $sub) { 427 | Workflow::addItem(Item::create() 428 | ->title($parts[0].' '.$key) 429 | ->subtitle($sub[0]) 430 | ->icon(isset($sub[1]) ? $sub[1] : $key) 431 | ->arg('/'.$parts[0].'/'.$key) 432 | ); 433 | } 434 | Workflow::addItem(Item::create() 435 | ->title($parts[0].' new issue') 436 | ->subtitle('Create new issue') 437 | ->icon('issue') 438 | ->arg('/'.$parts[0].'/issues/new?source=c') 439 | ); 440 | Workflow::addItem(Item::create() 441 | ->title($parts[0].' new pull') 442 | ->subtitle('Create new pull request') 443 | ->icon('pull-request') 444 | ->arg('/'.$parts[0].'/pull/new?source=c') 445 | ); 446 | if (empty($parts[1])) { 447 | $subs = array( 448 | '#' => array('Show a specific issue by number', 'issue'), 449 | '@' => array('Show a specific branch', 'branch'), 450 | '*' => array('Show a specific commit', 'commits'), 451 | '/' => array('Show a blob', 'file'), 452 | ); 453 | foreach ($subs as $key => $sub) { 454 | Workflow::addItem(Item::create() 455 | ->title($parts[0].' '.$key) 456 | ->subtitle($sub[0]) 457 | ->icon($sub[1]) 458 | ->arg($key.' '.$parts[0]) 459 | ->valid(false) 460 | ); 461 | } 462 | } 463 | Workflow::addItem(Item::create() 464 | ->title($parts[0].' clone') 465 | ->subtitle('Clone this repo') 466 | ->icon('clone') 467 | ->arg('/'.$parts[0].'.git') 468 | ); 469 | } 470 | } 471 | 472 | private static function addUserSubCommands($queryUser) 473 | { 474 | $subs = array( 475 | 'overview' => array($queryUser, "View $queryUser's overview", 'user'), 476 | 'repositories' => array($queryUser.'?tab=repositories', "View $queryUser's repositories", 'repo'), 477 | 'stars' => array($queryUser.'?tab=stars', "View $queryUser's stars"), 478 | ); 479 | $prio = count($subs) + 2; 480 | foreach ($subs as $key => $sub) { 481 | Workflow::addItem(Item::create() 482 | ->title('@'.$queryUser.' '.$key) 483 | ->subtitle($sub[1]) 484 | ->icon(isset($sub[2]) ? $sub[2] : $key) 485 | ->arg('/'.$sub[0]) 486 | ->prio($prio--) 487 | ); 488 | } 489 | Workflow::addItem(Item::create() 490 | ->title('@'.$queryUser.' gists') 491 | ->subtitle("View $queryUser's' gists") 492 | ->icon('gists') 493 | ->arg(Workflow::getGistUrl().'/'.$queryUser) 494 | ->prio(2) 495 | ); 496 | 497 | Workflow::addItem(Item::create() 498 | ->title($queryUser.'/') 499 | ->comparator('@'.$queryUser.' ') 500 | ->autocomplete($queryUser.'/') 501 | ->subtitle("View $queryUser's' repositories (in alfred workflow)") 502 | ->icon('repo') 503 | ->prio(1) 504 | ->valid(false) 505 | ); 506 | } 507 | 508 | private static function addMyCommands() 509 | { 510 | $parts = self::$parts; 511 | if (isset($parts[2]) && in_array($parts[1], array('pulls', 'issues'))) { 512 | $icon = $parts[1] === 'pulls' ? 'pull-request' : 'issue'; 513 | $items = $icon.'s'; 514 | $subs = array( 515 | 'created' => array($parts[1], 'View your '.$items), 516 | 'assigned' => array($parts[1].'/assigned', 'View your assigned '.$items), 517 | 'mentioned' => array($parts[1].'/mentioned', 'View '.$items.' that mentioned you'), 518 | ); 519 | if ('pulls' === $parts[1]) { 520 | $subs['review requested'] = array($parts[1].'/review-requested', 'View '.$items.' that require review'); 521 | } 522 | foreach ($subs as $key => $sub) { 523 | Workflow::addItem(Item::create() 524 | ->title('my '.$parts[1].' '.$key) 525 | ->subtitle($sub[1]) 526 | ->icon($icon) 527 | ->arg('/'.$sub[0]) 528 | ->prio(1) 529 | ); 530 | } 531 | return; 532 | } 533 | 534 | $myPages = array( 535 | 'dashboard' => array('', 'View your dashboard'), 536 | 'pulls ' => array('pulls', 'View your pull requests', 'pull-request'), 537 | 'issues ' => array('issues', 'View your issues', 'issue'), 538 | 'stars' => array('stars', 'View your starred repositories'), 539 | 'profile' => array(self::$user->login, 'View your public user profile', 'user'), 540 | 'settings' => array('settings', 'View or edit your account settings'), 541 | 'notifications' => array('notifications', 'View all your notifications'), 542 | ); 543 | foreach ($myPages as $key => $my) { 544 | Workflow::addItem(Item::create() 545 | ->title('my '.$key) 546 | ->subtitle($my[1]) 547 | ->icon(isset($my[2]) ? $my[2] : rtrim($key)) 548 | ->arg('/'.$my[0]) 549 | ->prio(1) 550 | ); 551 | } 552 | Workflow::addItem(Item::create() 553 | ->title('my gists') 554 | ->subtitle('View your gists') 555 | ->icon('gists') 556 | ->arg(Workflow::getGistUrl().'/'.self::$user->login) 557 | ->prio(1) 558 | ); 559 | 560 | Workflow::addItem(Item::create() 561 | ->title('my repos') 562 | ->subtitle('View your repos') 563 | ->icon('repo') 564 | ->arg('/'.self::$user->login.'?tab=repositories') 565 | ->prio(1) 566 | ); 567 | } 568 | 569 | private static function addSystemCommands() 570 | { 571 | $cmds = array( 572 | 'delete cache' => 'Delete GitHub Cache', 573 | 'logout' => 'Log out this workflow', 574 | 'delete database' => 'Delete database (contains login, config and cache)', 575 | 'update' => 'Update this Alfred workflow', 576 | ); 577 | if (Workflow::getConfig('autoupdate', true)) { 578 | $cmds['deactivate autoupdate'] = 'Deactivate auto updating this Alfred Workflow'; 579 | } else { 580 | $cmds['activate autoupdate'] = 'Activate auto updating this Alfred Workflow'; 581 | } 582 | if (self::$enterprise) { 583 | $cmds['enterprise reset'] = 'Reset the GitHub Enterprise URL'; 584 | } 585 | foreach ($cmds as $cmd => $desc) { 586 | Workflow::addItem(Item::create() 587 | ->title('> '.$cmd) 588 | ->subtitle($desc) 589 | ->icon($cmd) 590 | ->arg('> '.str_replace(' ', '-', $cmd)) 591 | ); 592 | } 593 | 594 | $cmds = array( 595 | 'help' => 'readme', 596 | 'changelog' => 'changelog', 597 | ); 598 | foreach ($cmds as $cmd => $file) { 599 | Workflow::addItem(Item::create() 600 | ->title('> '.$cmd) 601 | ->subtitle('View the '.$file) 602 | ->icon('file') 603 | ->arg('https://github.com/gharlan/alfred-github-workflow/blob/master/'.strtoupper($file).'.md') 604 | ); 605 | } 606 | } 607 | } 608 | 609 | Search::run($argv[1], $argv[2], getenv('hotkey')); 610 | echo Workflow::getItemsAsXml(); 611 | --------------------------------------------------------------------------------