├── .gitignore ├── README.md ├── cache └── .gitignore ├── composer.json ├── confs ├── .gitignore └── samples │ ├── gitlab.ini │ └── static-repos.json ├── examples └── packages.json └── htdocs ├── .htaccess └── packages.php /.gitignore: -------------------------------------------------------------------------------- 1 | /confs/static-repos.json 2 | /vendor 3 | /composer.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gitlab Composer repository 2 | 3 | Small script that loops through all branches and tags of all projects in a Gitlab installation 4 | and if it contains a `composer.json`, adds it to an index. 5 | 6 | This is very similar to the behaviour of Packagist.org 7 | 8 | See [example](examples/packages.json). 9 | 10 | ## Installation 11 | 12 | 1. Run `composer.phar install` 13 | 2. Copy `confs/samples/gitlab.ini` into `confs/gitlab.ini`, following instructions in comments 14 | 3. Ensure cache is writable 15 | 4. Change the TTL as desired (default is 60 seconds) 16 | 5. Ensure an alias exists for /packages.json => /packages.php (.htaccess is provided) 17 | 18 | ## Usage 19 | 20 | Simply include a composer.json in your project, all branches and tags respecting 21 | the [formats for versions](http://getcomposer.org/doc/04-schema.md#version) will be detected. 22 | 23 | By default, the package `name` must be equal to the path of the project. i.e.: `my-group/my-project`. 24 | This is not a design requirement, it is mostly to prevent common errors when you copy a `composer.json` 25 | from another project without changing its name. To enable support for differences between package names and project 26 | paths, set `allow_package_name_mismatch` to `true` in `confs/gitlab.ini`. 27 | 28 | Then, to use your repository, add this in the `composer.json` of your project: 29 | ```json 30 | { 31 | "repositories": [ 32 | { 33 | "type": "composer", 34 | "url": "http://gitlab-composer.stage.wemakecustom.com/" 35 | } 36 | ] 37 | } 38 | ``` 39 | 40 | ## Caveats 41 | 42 | While your projects will be protected through SSH, they will be publicly listed. 43 | If you require protection of the package list, [I suggest this reading](https://github.com/composer/composer/blob/master/doc/articles/handling-private-packages-with-satis.md). 44 | 45 | ## Author 46 | * [Sébastien Lavoie](http://blog.lavoie.sl/2013/08/composer-repository-for-gitlab-projects.html) 47 | * [WeMakeCustom](http://www.wemakecustom.com) 48 | 49 | -------------------------------------------------------------------------------- /cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "m4tthumphrey/php-gitlab-api": "^9.0", 4 | "php-http/guzzle6-adapter": "^1.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /confs/.gitignore: -------------------------------------------------------------------------------- 1 | /*.ini 2 | -------------------------------------------------------------------------------- /confs/samples/gitlab.ini: -------------------------------------------------------------------------------- 1 | ; 2 | ; Copy this file in parent directory 3 | ; api_key can be found at http://gitlab.example.com/profile/account 4 | ; method It is the method for the URL of the project (ssh/http) 5 | endpoint="http://gitlab.example.com/api/v4/" 6 | api_key="ASDFGHJKL12345678" 7 | method="ssh" 8 | 9 | ; You can restrict to some gitlab groups: 10 | ;groups[]="one_group" 11 | ;groups[]="other_group" 12 | 13 | ; Allow package names to differ from their group/project names in GitLab 14 | ;allow_package_name_mismatch=true -------------------------------------------------------------------------------- /confs/samples/static-repos.json: -------------------------------------------------------------------------------- 1 | { 2 | "bige/sitech": { 3 | "1.2": { 4 | "name": "bige/sitech", 5 | "version": "1.2", 6 | "source": { 7 | "url": "https://github.com/BigE/SiTech", 8 | "type": "git", 9 | "reference": "889eb1677b78ad83b12a0235cbb6d9bcec63ad9d" 10 | }, 11 | "autoload": { 12 | "classmap": ["lib/"], 13 | "psr-0": {"SiTech_": "lib/"} 14 | }, 15 | "autoload-dev": { 16 | "classmap": [ 17 | "Tests/", "Tools/" 18 | ] 19 | } 20 | ,"include-path": [ 21 | "lib/" 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | "my-group/my-project": { 4 | "dev-master": { 5 | "name": "my-group/my-project", 6 | "source": { 7 | "reference": "882816c7c05b5b5704e84bdb0f7ad69230df3c0c", 8 | "type": "git", 9 | "url": "git@gitlab.example.com:my-group/my-project.git" 10 | }, 11 | "type": "project", 12 | "version": "dev-master" 13 | }, 14 | "v1.5": { 15 | "name": "my-group/my-project", 16 | "source": { 17 | "reference": "882816c7c05b5b5704e84bdb0f7ad69230df3c0c", 18 | "type": "git", 19 | "url": "git@gitlab.example.com:my-group/my-project.git" 20 | }, 21 | "type": "project", 22 | "version": "v1.5" 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /htdocs/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteBase / 3 | RewriteRule ^$ packages.json [R=302,L] 4 | RewriteRule packages.json packages.php 5 | -------------------------------------------------------------------------------- /htdocs/packages.php: -------------------------------------------------------------------------------- 1 | = $mtime) { 22 | header('HTTP/1.0 304 Not Modified'); 23 | } else { 24 | readfile($file); 25 | } 26 | die(); 27 | }; 28 | 29 | // See ../confs/samples/gitlab.ini 30 | $config_file = __DIR__ . '/../confs/gitlab.ini'; 31 | if (!file_exists($config_file)) { 32 | header('HTTP/1.0 500 Internal Server Error'); 33 | die('confs/gitlab.ini missing'); 34 | } 35 | $confs = parse_ini_file($config_file); 36 | 37 | $client = Client::create($confs['endpoint']); 38 | $client->authenticate($confs['api_key'], Client::AUTH_URL_TOKEN); 39 | 40 | $groups = $client->api('groups'); 41 | $projects = $client->api('projects'); 42 | $repos = $client->api('repositories'); 43 | 44 | $validMethods = array('ssh', 'http'); 45 | if (isset($confs['method']) && in_array($confs['method'], $validMethods)) { 46 | define('method', $confs['method']); 47 | } else { 48 | define('method', 'ssh'); 49 | } 50 | 51 | $allow_package_name_mismatches = !empty($confs['allow_package_name_mismatch']); 52 | 53 | /** 54 | * Retrieves some information about a project's composer.json 55 | * 56 | * @param array $project 57 | * @param string $ref commit id 58 | * @return array|false 59 | */ 60 | $fetch_composer = function($project, $ref) use ($repos, $allow_package_name_mismatches) { 61 | try { 62 | $c = $repos->getFile($project['id'], 'composer.json', $ref); 63 | 64 | if(!isset($c['content'])) { 65 | return false; 66 | } 67 | 68 | $composer = json_decode(base64_decode($c['content']), true); 69 | 70 | if (empty($composer['name']) || (!$allow_package_name_mismatches && strcasecmp($composer['name'], $project['path_with_namespace']) !== 0)) { 71 | return false; // packages must have a name and must match 72 | } 73 | 74 | return $composer; 75 | } catch (RuntimeException $e) { 76 | return false; 77 | } 78 | }; 79 | 80 | /** 81 | * Retrieves some information about a project for a specific ref 82 | * 83 | * @param array $project 84 | * @param string $ref commit id 85 | * @return array [$version => ['name' => $name, 'version' => $version, 'source' => [...]]] 86 | */ 87 | $fetch_ref = function($project, $ref) use ($fetch_composer) { 88 | 89 | static $ref_cache = []; 90 | 91 | $ref_key = md5(serialize($project) . serialize($ref)); 92 | 93 | if (!isset($ref_cache[$ref_key])) { 94 | if (preg_match('/^v?\d+\.\d+(\.\d+)*(\-(dev|patch|alpha|beta|RC)\d*)?$/', $ref['name'])) { 95 | $version = $ref['name']; 96 | } else { 97 | $version = 'dev-' . $ref['name']; 98 | } 99 | 100 | if (($data = $fetch_composer($project, $ref['commit']['id'])) !== false) { 101 | $data['version'] = $version; 102 | $data['source'] = [ 103 | 'url' => $project[method . '_url_to_repo'], 104 | 'type' => 'git', 105 | 'reference' => $ref['commit']['id'], 106 | ]; 107 | 108 | $ref_cache[$ref_key] = [$version => $data]; 109 | } else { 110 | $ref_cache[$ref_key] = []; 111 | } 112 | } 113 | 114 | return $ref_cache[$ref_key]; 115 | }; 116 | 117 | /** 118 | * Retrieves some information about a project for all refs 119 | * @param array $project 120 | * @return array Same as $fetch_ref, but for all refs 121 | */ 122 | $fetch_refs = function($project) use ($fetch_ref, $repos) { 123 | $datas = array(); 124 | try { 125 | foreach (array_merge($repos->branches($project['id']), $repos->tags($project['id'])) as $ref) { 126 | foreach ($fetch_ref($project, $ref) as $version => $data) { 127 | $datas[$version] = $data; 128 | } 129 | } 130 | } catch (RuntimeException $e) { 131 | // The repo has no commits — skipping it. 132 | } 133 | 134 | return $datas; 135 | }; 136 | 137 | /** 138 | * Caching layer on top of $fetch_refs 139 | * Uses last_activity_at from the $project array, so no invalidation is needed 140 | * 141 | * @param array $project 142 | * @return array Same as $fetch_refs 143 | */ 144 | $load_data = function($project) use ($fetch_refs) { 145 | $file = __DIR__ . "/../cache/{$project['path_with_namespace']}.json"; 146 | $mtime = strtotime($project['last_activity_at']); 147 | 148 | if (!is_dir(dirname($file))) { 149 | mkdir(dirname($file), 0777, true); 150 | } 151 | 152 | if (file_exists($file) && filemtime($file) >= $mtime) { 153 | if (filesize($file) > 0) { 154 | return json_decode(file_get_contents($file)); 155 | } else { 156 | return false; 157 | } 158 | } elseif ($data = $fetch_refs($project)) { 159 | file_put_contents($file, json_encode($data)); 160 | touch($file, $mtime); 161 | 162 | return $data; 163 | } else { 164 | $f = fopen($file, 'w'); 165 | fclose($f); 166 | touch($file, $mtime); 167 | 168 | return false; 169 | } 170 | }; 171 | 172 | /** 173 | * Determine the name to use for the package. 174 | * 175 | * @param array $project 176 | * @return string The name of the project 177 | */ 178 | $get_package_name = function($project) use ($allow_package_name_mismatches, $fetch_ref, $repos) { 179 | if ($allow_package_name_mismatches) { 180 | $ref = $fetch_ref($project, $repos->branch($project['id'], $project['default_branch'])); 181 | return reset($ref)['name']; 182 | } 183 | 184 | return $project['path_with_namespace']; 185 | }; 186 | 187 | // Load projects 188 | $all_projects = array(); 189 | $mtime = 0; 190 | if (!empty($confs['groups'])) { 191 | // We have to get projects from specifics groups 192 | foreach ($groups->all(array('page' => 1, 'per_page' => 100)) as $group) { 193 | if (!in_array($group['name'], $confs['groups'], true)) { 194 | continue; 195 | } 196 | for ($page = 1; count($p = $groups->projects($group['id'], array('page' => $page, 'per_page' => 100))); $page++) { 197 | foreach ($p as $project) { 198 | $all_projects[] = $project; 199 | $mtime = max($mtime, strtotime($project['last_activity_at'])); 200 | } 201 | } 202 | } 203 | } else { 204 | // We have to get all accessible projects 205 | $me = $client->api('users')->me(); 206 | for ($page = 1; count($p = $projects->all(array('page' => $page, 'per_page' => 100))); $page++) { 207 | foreach ($p as $project) { 208 | $all_projects[] = $project; 209 | $mtime = max($mtime, strtotime($project['last_activity_at'])); 210 | } 211 | } 212 | } 213 | 214 | // Regenerate packages_file is needed 215 | if (!file_exists($packages_file) || filemtime($packages_file) < $mtime) { 216 | $packages = array(); 217 | foreach ($all_projects as $project) { 218 | if (($package = $load_data($project)) && ($package_name = $get_package_name($project))) { 219 | $packages[$package_name] = $package; 220 | } 221 | } 222 | if ( file_exists( $static_file ) ) { 223 | $static_packages = json_decode( file_get_contents( $static_file ) ); 224 | foreach ( $static_packages as $name => $package ) { 225 | foreach ( $package as $version => $root ) { 226 | if ( isset( $root->extra ) ) { 227 | $source = '_source'; 228 | while ( isset( $root->extra->{$source} ) ) { 229 | $source = '_' . $source; 230 | } 231 | $root->extra->{$source} = 'static'; 232 | } 233 | else { 234 | $root->extra = array( 235 | '_source' => 'static', 236 | ); 237 | } 238 | } 239 | $packages[$name] = $package; 240 | } 241 | } 242 | $data = json_encode(array( 243 | 'packages' => array_filter($packages), 244 | )); 245 | 246 | file_put_contents($packages_file, $data); 247 | } 248 | 249 | $outputFile($packages_file); 250 | --------------------------------------------------------------------------------