├── .editorconfig ├── .eslintrc.js ├── .github ├── dependabot.yml ├── release-drafter.yml └── workflows │ └── release-drafter.yml ├── .gitignore ├── .prettierrc ├── LICENSE.txt ├── README.md ├── docker ├── Dockerfile └── readme.md ├── index.js ├── options.js ├── package.json └── renovate.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true 4 | }, 5 | "extends": "eslint:recommended", 6 | "globals": { 7 | "process": true, 8 | "require": true, 9 | "module": true, 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ], 28 | "no-console": [ 29 | "off" 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "23:30" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What’s Changed 3 | 4 | $CHANGES 5 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v5.11.0 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional eslint cache 38 | .eslintcache 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Package manager lock files 44 | package-lock.json 45 | yarn.lock 46 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Vishwanath Arondekar 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gitlab-cli 2 | ================== 3 | 4 | gitlab-cli is a command line utility created in JavaScript. Inspired from [hub](https://github.com/github/hub). It tries to provide commands which makes working with gitlab from the command line easier. 5 | 6 | Creating a merge request with gitlab-cli is as simple as 7 | 8 | ```sh 9 | $ lab merge-request 10 | ``` 11 | 12 | ## Installation 13 | 14 | Install it using npm 15 | 16 | ```sh 17 | $ npm install git-lab-cli -g 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```sh 23 | $ lab command [options] 24 | ``` 25 | 26 | To get a list of available commands 27 | 28 | ```sh 29 | $ lab --help 30 | ``` 31 | 32 | ## Commands available 33 | 34 | browse [options] Open current branch or a specific page in gitlab 35 | compare [options] Open compare page between two branches 36 | merge-request [options] Create merge request on gitlab 37 | merge-requests [options] Opens merge request page for the repo. 38 | 39 | Check help of each command like following 40 | 41 | ```sh 42 | $ lab merge-request --help 43 | ``` 44 | 45 | ### Running example 46 | 47 | ```sh 48 | $ lab merge-request -b feature/feature-name -t develop 49 | ``` 50 | 51 | Above will create merge request for merging feature/feature-name in develop. 52 | 53 | ### Options for create-merge-request 54 | 55 | -b, --base [optional] Base branch name 56 | -t, --target [optional] Target branch name 57 | -m, --message [optional] Title of the merge request 58 | -a, --assignee [optional] User to assign merge request to 59 | -l, --labels [optional] Comma separated list of labels to assign while creating merge request 60 | -r, --remove_source_branch [optional] Flag indicating if a merge request should remove the source branch when merging 61 | -s, --squash [optional] Squash commits into a single commit when merging 62 | -e, --edit [optional] If supplied opens edit page of merge request. Prints the merge request URL otherwise 63 | -o, --open [optional] If supplied open the page of the merge request. Prints the merge request URL otherwise 64 | -p, --print [deprecated] Doesn't do anything. Kept here for backward compatibility. Default is print. 65 | -v, --verbose [optional] Detailed logging emitted on console for debug purpose 66 | -h, --help output usage information 67 | 68 | ## Configurations 69 | 70 | gitlab-cli **captures configurations needed for itself on the first run**. Just run the command you want to run and it will capture the information needed. 71 | 72 | You can also set the configurations yourself as git config (project specific) or environment variables (global). 73 | 74 | ### git config 75 | 76 | Setting git config allows you to provide separate configurations for each gitlab repository. 77 | 78 | ```sh 79 | $ git config --add gitlab.url "https://gitlab.yourcompany.com" 80 | $ git config --add gitlab.token "abcdefghijskl-1230" 81 | ``` 82 | 83 | Find your gitlab token at [https://gitlab.yourcompany.com/profile/account](http://gitlab.yourcompany.com/profile/account) 84 | 85 | ### Environment variables 86 | 87 | Setting environment variables allows you to provide global configurations which will be used for all your gitlab repositories when using gitlab-cli. 88 | 89 | GITLAB_URL=https://gitlab.yourcompany.com 90 | GITLAB_TOKEN=abcdefghijskl-1230 91 | 92 | Find your gitlab token at [https://gitlab.yourcompany.com/profile/account](http://gitlab.yourcompany.com/profile/account) 93 | 94 | ### Features supported 95 | 96 | 1. Base branch is optional. If base branch is not provided. Current branch is used as base branch. 97 | 2. Target branch is optional. If target branch is not provided, default branch of the repo in gitlab will be used. 98 | 3. Created merge request page will be opened automatically after successful creation. 99 | 4. If title is not supported with -m option value. It will be taken from in place editor opened. First line is taken as title. 100 | 5. In place editor opened contains latest commit message. 101 | 6. In the editor opened third line onwards takes as description. 102 | 7. Comma separated list of labels can be provided with its option. 103 | 8. Supports forks. If base branch and target branch are on different remotes. Merge request will be created between forks. 104 | 9. Supports setting assignee for merge request. 105 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | MAINTAINER Pierre Duchemin 4 | 5 | ENV LANG=C.UTF-8 6 | ENV GITLAB_URL= 7 | ENV GITLAB_TOKEN= 8 | 9 | RUN apk update \ 10 | && apk add --no-cache git 11 | RUN npm install git-lab-cli -g 12 | 13 | WORKDIR /home 14 | 15 | ENTRYPOINT ["lab"] 16 | -------------------------------------------------------------------------------- /docker/readme.md: -------------------------------------------------------------------------------- 1 | # How to run this 2 | 3 | Change url and token in Dockerfile. 4 | 5 | ```bash 6 | docker build . -t gitlab-cli 7 | docker run -v $PWD:/home -ti --rm gitlab-cli merge-request -b BRANCH_NAME -t SOME_TAG -v -a USERNAME -m MR_TITLE 8 | ``` 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('dotenv').config() 3 | var program = require('commander') 4 | var childProcess = require('child_process') 5 | var colors = require('colors') 6 | var { Gitlab } = require('@gitbeaker/node') 7 | var editor = require('editor') 8 | var exec = childProcess.exec 9 | var fs = require('fs') 10 | var legacies = {} 11 | var open = require('open') 12 | var projectDir = process.cwd() 13 | var Promise = require('promise') 14 | var URL = require('url') 15 | var options = require('./options') 16 | var packageJson = require('./package.json') 17 | 18 | var regexParseProjectName = /(.+:\/\/.+?\/|.+:)(.+\/[^\.]+)+(\.git)?/ 19 | 20 | var gitlab = new Gitlab(options) 21 | gitlab.options = options 22 | 23 | var store = (function() { 24 | return { 25 | set: function(obj) { 26 | Object.assign(this, obj) 27 | }, 28 | get: function(key) { 29 | return this[key] 30 | }, 31 | } 32 | })() 33 | var log = { 34 | getInstance: function(verbose) { 35 | return { 36 | log: function() { 37 | if (verbose) { 38 | console.log.apply(console, arguments) 39 | } 40 | }, 41 | } 42 | }, 43 | } 44 | 45 | var allRemotes = null 46 | //Will be assigned value after parsing of options 47 | var logger = null 48 | 49 | function getMergeRequestTitle(title) { 50 | logger.log('\nGetting merge request title. Argument provided : ' + title) 51 | var promise = new Promise(function(resolve /*, reject*/) { 52 | if (title) { 53 | logger.log('Title obtained with -m option: ' + title.green) 54 | resolve(title) 55 | } else { 56 | exec('git rev-parse --show-toplevel', function( 57 | error, 58 | repoDir /*, stderr*/ 59 | ) { 60 | var filePath = repoDir.trim() + '/.git/PULL_REQUEST_TITLE' 61 | exec( 62 | 'git log -1 --pretty=%B > ' + filePath, 63 | function(/*error, remote, stderr*/) { 64 | exec('git config core.editor', function( 65 | error, 66 | gitEditor /*, stderr*/ 67 | ) { 68 | editor( 69 | filePath, 70 | { editor: gitEditor.trim() || null }, 71 | function(/*code, sig*/) { 72 | fs.readFile(filePath, 'utf8', function(err, data) { 73 | title = data 74 | logger.log('Input obtained using editor: ' + title.green) 75 | resolve(title) 76 | }) 77 | } 78 | ) 79 | }) 80 | } 81 | ) 82 | }) 83 | } 84 | }) 85 | return promise 86 | } 87 | 88 | function getAllRemotes() { 89 | var promise = new Promise(function(resolve /*, reject*/) { 90 | if (allRemotes) { 91 | resolve(allRemotes) 92 | return 93 | } 94 | 95 | exec('git remote', { cwd: projectDir }, function( 96 | error, 97 | allRemotes /*, stderr*/ 98 | ) { 99 | if (error) { 100 | logger.log(colors.red('Error occurred :\n'), colors.red(error)) 101 | process.exit(1) 102 | } 103 | 104 | allRemotes = allRemotes.split('\n') 105 | resolve(allRemotes) 106 | }) 107 | }) 108 | return promise 109 | } 110 | 111 | function parseBranchRemoteInfo(branchName) { 112 | var promise = new Promise(function(resolve /*, reject*/) { 113 | if (branchName.indexOf('/') != -1) { 114 | getAllRemotes().then(function(allRemotes) { 115 | var splits = branchName.split('/') 116 | var maybeRemote = splits[0].trim() 117 | var remoteName = null 118 | if (allRemotes.indexOf(maybeRemote) != -1) { 119 | branchName = splits.slice(1).join('/') 120 | remoteName = maybeRemote 121 | } 122 | logger.log('Branch name obtained :', branchName.green) 123 | resolve({ 124 | branch: branchName, 125 | remote: remoteName, 126 | }) 127 | }) 128 | } else { 129 | resolve({ 130 | branch: branchName, 131 | }) 132 | } 133 | }) 134 | return promise 135 | } 136 | 137 | function getBaseBranchName(baseBranchName) { 138 | logger.log('\nGetting base branch name : ') 139 | var promise = new Promise(function(resolve /*, reject*/) { 140 | if (baseBranchName) { 141 | logger.log('Argument provided : ' + baseBranchName) 142 | 143 | parseBranchRemoteInfo(baseBranchName).then(function(branchRemoteInfo) { 144 | logger.log('Base branch name obtained :', branchRemoteInfo.branch.green) 145 | resolve(branchRemoteInfo.branch) 146 | }) 147 | } else { 148 | logger.log('Executing git rev-parse --abbrev-ref HEAD') 149 | 150 | exec('git rev-parse --abbrev-ref HEAD', { cwd: projectDir }, function( 151 | error, 152 | stdout /*, stderr*/ 153 | ) { 154 | if (error) { 155 | logger.log(colors.red('Error occured :\n'), colors.red(error)) 156 | process.exit(1) 157 | } 158 | var curBranchName = stdout.replace('\n', '') 159 | logger.log('Base branch name obtained :', curBranchName.green) 160 | resolve(curBranchName) 161 | }) 162 | } 163 | }) 164 | return promise 165 | } 166 | 167 | function getTargetBranchName(branchName) { 168 | var promise = new Promise(function(resolve /*, reject*/) { 169 | parseBranchRemoteInfo(branchName).then(function(branchRemoteInfo) { 170 | logger.log('Remote branch name obtained :', branchRemoteInfo.branch.green) 171 | resolve(branchRemoteInfo.branch) 172 | }) 173 | }) 174 | return promise 175 | } 176 | 177 | function getRemoteForBranch(branchName) { 178 | logger.log('\nGetting remote of branch :', branchName) 179 | 180 | var promise = new Promise(function(resolve /*, reject*/) { 181 | parseBranchRemoteInfo(branchName).then(function(branchRemoteInfo) { 182 | if (branchRemoteInfo.remote) { 183 | //Remote info supplied in the branch name 184 | logger.log('Remote obtained : ' + branchRemoteInfo.remote.green) 185 | resolve(branchRemoteInfo.remote) 186 | } else { 187 | //Remote info is not supplied. Get it from remote set 188 | logger.log( 189 | 'Executing git config branch.' + branchName.trim() + '.remote' 190 | ) 191 | exec( 192 | 'git config branch.' + branchName.trim() + '.remote', 193 | { cwd: projectDir }, 194 | function(error, remote /*, stderr*/) { 195 | if (error) { 196 | console.error( 197 | colors.red( 198 | 'Error occured while getting remote of the branch: ', 199 | branchName, 200 | '\n' 201 | ) 202 | ) 203 | console.log( 204 | '\n\nSet the remote tracking by `git branch --set-upstream-to=origin/' + 205 | branchName + 206 | '`. Assuming origin is your remote.' 207 | ) 208 | console.log( 209 | 'Look at https://git-scm.com/docs/git-branch#Documentation/git-branch.txt---set-upstream for more details.' 210 | ) 211 | console.log( 212 | '\n\nBonus tip : You can avoid doing this each time by adding -u option while pushing to your origin.' 213 | ) 214 | console.log('Eg: `git push origin ' + branchName + ' -u`') 215 | process.exit(1) 216 | } 217 | logger.log('Remote obtained : ' + remote.green) 218 | resolve(remote.trim()) 219 | } 220 | ) 221 | } 222 | }) 223 | }) 224 | return promise 225 | } 226 | 227 | function getURLOfRemote(remote) { 228 | logger.log('\nGetting URL of remote : ' + remote) 229 | var promise = new Promise(function(resolve /*, reject*/) { 230 | logger.log('Executing ', 'git config remote.' + remote.trim() + '.url') 231 | exec( 232 | 'git config remote.' + remote.trim() + '.url', 233 | { cwd: projectDir }, 234 | function(error, remoteURL /*, stderr*/) { 235 | if (error) { 236 | console.error(colors.red('Error occured :\n'), colors.red(error)) 237 | 238 | process.exit(1) 239 | } 240 | logger.log('URL of remote obtained : ' + remoteURL.green) 241 | resolve(remoteURL.trim()) 242 | } 243 | ) 244 | }) 245 | return promise 246 | } 247 | 248 | function browse(options) { 249 | logger = log.getInstance(options.verbose) 250 | getBaseBranchName().then(function(curBranchName) { 251 | getRemoteForBranch(curBranchName).then(function(remote) { 252 | if (!remote) { 253 | console.error( 254 | colors.red( 255 | 'Branch ' + curBranchName + ' is not tracked by any remote branch.' 256 | ) 257 | ) 258 | console.log( 259 | 'Set the remote tracking by `git branch --set-upstream /`' 260 | ) 261 | console.log('Eg: `git branch --set-upstream foo upstream/foo`') 262 | } 263 | 264 | getURLOfRemote(remote).then(function(remoteURL) { 265 | var projectName = remoteURL.match(regexParseProjectName)[2] 266 | var page = options.page || '' 267 | if (page === '') { 268 | open( 269 | gitlab.options.host + '/' + projectName + '/tree/' + curBranchName 270 | ) 271 | } else { 272 | open(gitlab.options.host + '/' + projectName + '/' + page) 273 | } 274 | }) 275 | }) 276 | }) 277 | } 278 | 279 | function compare(options) { 280 | logger = log.getInstance(options.verbose) 281 | 282 | getBaseBranchName(options.base).then(function(baseBranch) { 283 | getRemoteForBranch(baseBranch).then(function(remote) { 284 | if (!remote) { 285 | console.error( 286 | colors.red( 287 | 'Branch ' + baseBranch + ' is not tracked by any remote branch.' 288 | ) 289 | ) 290 | console.log( 291 | 'Set the remote tracking by `git branch --set-upstream /`' 292 | ) 293 | console.log('Eg: `git branch --set-upstream foo upstream/foo`') 294 | process.exit(1) 295 | } 296 | 297 | getURLOfRemote(remote).then(function(remoteURL) { 298 | var projectName = remoteURL.match(regexParseProjectName)[2] 299 | gitlab.Projects.search(projectName, { 300 | search_namespaces: true, 301 | membership: true, 302 | }) 303 | .then(function(project) { 304 | project = project[0] 305 | var defaultBranch = project.default_branch 306 | var targetBranch = options.target || defaultBranch 307 | var sourceBranch = baseBranch 308 | open( 309 | gitlab.options.host + 310 | '/' + 311 | projectName + 312 | '/compare/' + 313 | targetBranch + 314 | '...' + 315 | sourceBranch 316 | ) 317 | }) 318 | .catch(function(err) { 319 | console.log('Project info fetch failed : ' + err) 320 | }) 321 | }) 322 | }) 323 | }) 324 | } 325 | 326 | function getRemote(options) { 327 | logger.log('\nGetting remote for options provided : ' + options.remote) 328 | var promise = new Promise(function(resolve /*, reject*/) { 329 | if (options.remote) { 330 | resolve(options.remote) 331 | return 332 | } 333 | 334 | getBaseBranchName(options.base).then(function(baseBranch) { 335 | getRemoteForBranch(baseBranch).then(function(remote) { 336 | resolve(remote, baseBranch) 337 | }) 338 | }) 339 | }) 340 | return promise 341 | } 342 | 343 | function getUser(query) { 344 | var promise = new Promise(function(resolve /*, reject*/) { 345 | if (typeof query !== 'string') { 346 | resolve(null) 347 | return 348 | } 349 | 350 | logger.log('\nGetting user matching : ' + query) 351 | 352 | gitlab.Users.search(query) 353 | .then(function(userInfo) { 354 | if (userInfo instanceof Array && userInfo.length > 0) { 355 | var user = userInfo[0] 356 | resolve(user) 357 | } else { 358 | console.error( 359 | colors.yellow( 360 | 'User matching "' + 361 | query + 362 | '" was not found. Please check input and try again.' 363 | ) 364 | ) 365 | process.exit(1) 366 | } 367 | }) 368 | .catch(function(err) { 369 | console.log('User search fetch failed : ' + err) 370 | }) 371 | }) 372 | 373 | return promise 374 | } 375 | 376 | function openMergeRequests(options) { 377 | logger = log.getInstance(options.verbose) 378 | 379 | getRemote(options).then(function(remote, baseBranch) { 380 | if (!remote) { 381 | console.error( 382 | colors.red( 383 | 'Branch ' + baseBranch + ' is not tracked by any remote branch.' 384 | ) 385 | ) 386 | console.log( 387 | 'Set the remote tracking by `git branch --set-upstream /`' 388 | ) 389 | console.log('Eg: `git branch --set-upstream foo upstream/foo`') 390 | process.exit(1) 391 | } 392 | 393 | getURLOfRemote(remote).then(function(remoteURL) { 394 | getUser(options.assignee).then(function(assignee) { 395 | var projectName = remoteURL.match(regexParseProjectName)[2] 396 | 397 | var query = '?' 398 | 399 | if (options.state) { 400 | query += 'state=' + options.state + '&' 401 | } 402 | 403 | if (assignee) { 404 | query += 'assignee_id=' + assignee.id + '&' 405 | } 406 | 407 | open( 408 | gitlab.options.host + 409 | '/' + 410 | projectName + 411 | '/merge_requests' + 412 | query.slice(0, -1) 413 | ) 414 | }) 415 | }) 416 | }) 417 | } 418 | 419 | function createMergeRequest(options) { 420 | logger = log.getInstance(options.verbose) 421 | 422 | if (options.verbose) { 423 | logger.log( 424 | 'Verbose option used. Detailed logging information will be emitted.'.green 425 | ) 426 | } 427 | 428 | logger.log('\n\n\nGetting base branch information'.blue) 429 | getBaseBranchName(options.base) 430 | .then(function(sourceBranch) { 431 | store.set({ sourceBranch: sourceBranch }) 432 | return getRemoteForBranch(options.base || store.get('sourceBranch')) 433 | }) 434 | .then(function(remote) { 435 | store.set({ sourceRemote: remote }) 436 | return getURLOfRemote(remote) 437 | }) 438 | .then(function(remoteURL) { 439 | var gitlabHost = URL.parse(gitlab.options.host).host 440 | 441 | logger.log('\ngitlab host obtained : ' + gitlabHost.green) 442 | 443 | var match = remoteURL.match(regexParseProjectName) 444 | if (match) { 445 | var projectName = match[2] 446 | } else { 447 | console.error( 448 | colors.red( 449 | 'The remote at which ' + 450 | store.get('sourceBranch') + 451 | " is tracked doesn't match the expected format." 452 | ) 453 | ) 454 | console.log( 455 | 'Please contact developer if this is a valid gitlab repository.' 456 | ) 457 | process.exit(1) 458 | } 459 | logger.log('\nProject name derived from host :', projectName) 460 | logger.log('\nGetting gitlab project info for :', projectName) 461 | store.set({ sourceRemoteURL: remoteURL }) 462 | store.set({ sourceProjectName: projectName }) 463 | return gitlab.Projects.search(projectName, { 464 | search_namespaces: true, 465 | membership: true, 466 | }) 467 | }) 468 | .then(function(project) { 469 | var sourceProjectName = store.get('sourceProjectName') 470 | project = project.find((innerProject)=>{ return innerProject.path_with_namespace === sourceProjectName}) 471 | logger.log('Base project info obtained :', JSON.stringify(project).green) 472 | 473 | var defaultBranch = project.default_branch 474 | var targetBranch = options.target || defaultBranch 475 | 476 | logger.log('\n\n\nGetting target branch information'.blue) 477 | 478 | store.set({ sourceProject: project }) 479 | return getTargetBranchName(options.target || targetBranch) 480 | }) 481 | .then(function(targetBranch) { 482 | store.set({ targetBranch: targetBranch }) 483 | return getRemoteForBranch(options.target || targetBranch) 484 | }) 485 | .then(function(targetRemote) { 486 | store.set({ targetRemote: targetRemote }) 487 | return getURLOfRemote(targetRemote) 488 | }) 489 | .then(function(targetRemoteUrl) { 490 | var targetMatch = targetRemoteUrl.match(regexParseProjectName) 491 | if (targetMatch) { 492 | var targetProjectName = targetMatch[2] 493 | } else { 494 | console.error( 495 | colors.red( 496 | 'The remote at which ' + 497 | targetBranch + 498 | " is tracked doesn't match the expected format." 499 | ) 500 | ) 501 | console.log( 502 | 'Please contact developer if this is a valid gitlab repository.' 503 | ) 504 | process.exit(1) 505 | } 506 | 507 | logger.log('Getting target project information') 508 | store.set({ targetRemoteUrl: targetRemoteUrl }) 509 | store.set({ targetProjectName: targetProjectName }) 510 | return gitlab.Projects.search(targetProjectName, { 511 | search_namespaces: true, 512 | membership: true, 513 | }) 514 | }) 515 | .then(function(targetProject) { 516 | var targetProjectName = store.get('targetProjectName') 517 | targetProject = targetProject.find((project)=>{ return project.path_with_namespace === targetProjectName}) 518 | logger.log( 519 | 'Target project info obtained :', 520 | JSON.stringify(targetProject).green 521 | ) 522 | 523 | var targetProjectId = targetProject.id 524 | var sourceBranch = store.get('sourceBranch') 525 | var targetBranch = store.get('targetBranch') 526 | var projectId = store.get('sourceProject').id 527 | 528 | if (sourceBranch == targetBranch && projectId == targetProjectId) { 529 | console.error(colors.red('\nCan not create this merge request')) 530 | console.log( 531 | colors.red( 532 | 'You can not use same project/branch for source and target' 533 | ) 534 | ) 535 | process.exit(1) 536 | } 537 | store.set({ targetProject: targetProject }) 538 | return getUser(options.assignee) 539 | }) 540 | .then(function(assignee) { 541 | store.set({ assignee: assignee }) 542 | return getMergeRequestTitle(options.message) 543 | }) 544 | .then(function(userMessage) { 545 | var title = userMessage.split('\n')[0] 546 | var description = 547 | options.description || 548 | userMessage 549 | .split('\n') 550 | .slice(2) 551 | .join(' \n') 552 | 553 | logger.log('Merge request title : ' + title.green) 554 | if (description) 555 | logger.log('Merge request description : ' + description.green) 556 | logger.log('\n\nCreating merge request'.blue) 557 | 558 | var sourceBranch = store.get('sourceBranch') 559 | var targetBranch = store.get('targetBranch') 560 | var sourceProject = store.get('sourceProject') 561 | var sourceProjectId = sourceProject.id 562 | var targetProject = store.get('targetProject') 563 | var targetProjectId = targetProject.id 564 | var labels = options.labels || '' 565 | var remove_source_branch = options.remove_source_branch || false 566 | var squash = options.squash || false 567 | var assignee = store.get('assignee') 568 | 569 | return gitlab.MergeRequests.create( 570 | sourceProjectId, 571 | sourceBranch, 572 | targetBranch, 573 | title, 574 | { 575 | description: description, 576 | labels: labels, 577 | assignee_id: assignee && assignee.id, 578 | target_project_id: targetProjectId, 579 | remove_source_branch: remove_source_branch, 580 | squash: squash, 581 | } 582 | ) 583 | store.set({ userMessage: userMessage }) 584 | }) 585 | .then(function(mergeRequestResponse) { 586 | logger.log('Merge request response: \n\n', mergeRequestResponse) 587 | 588 | if (mergeRequestResponse.iid) { 589 | var url = mergeRequestResponse.web_url 590 | 591 | if (!url) { 592 | url = 593 | gitlab.options.host + 594 | '/' + 595 | targetProjectName + 596 | '/merge_requests/' + 597 | mergeRequestResponse.iid 598 | } 599 | 600 | if (options.edit) { 601 | url += '/edit' 602 | } 603 | 604 | if (options.open) { 605 | open(url) 606 | } else { 607 | console.log(url) 608 | } 609 | } 610 | }) 611 | .catch(function(err) { 612 | if (err.message) { 613 | console.error(colors.red("Couldn't create merge request")) 614 | console.log(colors.red(err.message)) 615 | } else if (err instanceof Array) { 616 | console.error(colors.red("Couldn't create merge request")) 617 | console.log(colors.red(err.join())) 618 | } 619 | }) 620 | } 621 | 622 | program.description('gitlab command line utility').version(packageJson.version) 623 | 624 | program.Command.prototype.legacy = function(alias) { 625 | legacies[alias] = this._name 626 | return this 627 | } 628 | 629 | program 630 | .command('browse') 631 | .option( 632 | '-v, --verbose [optional]', 633 | 'Detailed logging emitted on console for debug purpose' 634 | ) 635 | .option( 636 | '-p, --page [optional]', 637 | 'Page name. e.g: issues, boards, merge_requests, pipelines, etc.' 638 | ) 639 | .description('Open current branch page in gitlab') 640 | .action(function(options) { 641 | browse(options) 642 | }) 643 | 644 | program 645 | .command('compare') 646 | .option('-b, --base [optional]', 'Base branch name') 647 | .option('-t, --target [optional]', 'Target branch name') 648 | .option( 649 | '-v, --verbose [optional]', 650 | 'Detailed logging emitted on console for debug purpose' 651 | ) 652 | .description('Open compare page between two branches') 653 | .action(function(options) { 654 | compare(options) 655 | }) 656 | 657 | program 658 | .command('merge-request') 659 | .alias('mr') 660 | .legacy('create-merge-request') 661 | .option('-b, --base [optional]', 'Base branch name') 662 | .option('-t, --target [optional]', 'Target branch name') 663 | .option('-m, --message [optional]', 'Title of the merge request') 664 | .option('-d, --description [optional]', 'Description for the merge request') 665 | .option('-a, --assignee [optional]', 'User to assign merge request to') 666 | .option( 667 | '-l, --labels [optional]', 668 | 'Comma separated list of labels to assign while creating merge request' 669 | ) 670 | .option( 671 | '-r, --remove_source_branch [optional]', 672 | 'Flag indicating if a merge request should remove the source branch when merging' 673 | ) 674 | .option( 675 | '-s, --squash [optional]', 676 | 'Squash commits into a single commit when merging' 677 | ) 678 | .option( 679 | '-e, --edit [optional]', 680 | 'If supplied opens edit page of merge request. Prints the merge request URL otherwise' 681 | ) 682 | .option( 683 | '-o, --open [optional]', 684 | 'If supplied open the page of the merge request. Prints the merge request URL otherwise' 685 | ) 686 | .option( 687 | '-p, --print [deprecated]', 688 | 'Doesn`t do anything. Kept here for backward compatibility. Default is print.' 689 | ) 690 | .option( 691 | '-v, --verbose [optional]', 692 | 'Detailed logging emitted on console for debug purpose' 693 | ) 694 | .description('Create merge request on gitlab') 695 | .action(function(options) { 696 | createMergeRequest(options) 697 | }) 698 | 699 | program 700 | .command('merge-requests') 701 | .alias('mrs') 702 | .legacy('open-merge-requests') 703 | .option( 704 | '-v, --verbose [optional]', 705 | 'Detailed logging emitted on console for debug purpose' 706 | ) 707 | .option('-r, --remote [optional]', 'If provided this will be used as remote') 708 | .option( 709 | '-a, --assignee [optional]', 710 | 'If provided, merge requests assigned to only this user will be shown' 711 | ) 712 | .option( 713 | '-s, --state [optional]', 714 | 'If provide merge requests with state provided will be shown' 715 | ) 716 | .description('Opens merge requests page for the repo') 717 | .action(function(options) { 718 | openMergeRequests(options) 719 | }) 720 | 721 | program.on('command:*', function() { 722 | console.error(('Invalid command ' + program.args[0]).red) 723 | program.outputHelp() 724 | }) 725 | 726 | if (legacies[process.argv[2]]) process.argv[2] = legacies[process.argv[2]] 727 | 728 | program.parse(process.argv) 729 | 730 | if (program.args.length < 1) { 731 | program.outputHelp() 732 | } 733 | 734 | module.exports = function() {} -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | var childProcess = require('child_process'); 2 | var gitUrlParse = require('git-url-parse'); 3 | var readlineSync = require('readline-sync'); 4 | var exec = childProcess.exec; 5 | var execSync = childProcess.execSync; 6 | var projectDir = process.cwd(); 7 | 8 | var git = { 9 | config: { 10 | get: function (key) { 11 | return execSync('git config --get ' + key + ' || true', { cwd: projectDir }).toString().trim(); 12 | }, 13 | set: function (key, value) { 14 | execSync('git config --add ' + key + ' ' + value, { cwd: projectDir }); 15 | }, 16 | } 17 | }; 18 | 19 | var options = (function () { 20 | var options = { 21 | host: git.config.get('gitlab.url') || process.env.GITLAB_URL, 22 | token: git.config.get('gitlab.token') || process.env.GITLAB_TOKEN, 23 | }; 24 | 25 | if (!options.host) { 26 | var defaultInput = (function () { 27 | var url = git.config.get('remote.origin.url'); 28 | if (!url || url.indexOf('bitbucket') !== -1 || url.indexOf('github') !== -1) { 29 | url = 'https://gitlab.com'; 30 | } 31 | return 'https://' + gitUrlParse(url).resource; 32 | })(); 33 | var urlQuestion = ('Enter GitLab URL (' + defaultInput + '): ').yellow; 34 | while (!options.host) { 35 | options.host = readlineSync.question(urlQuestion, { defaultInput: defaultInput }); 36 | urlQuestion = 'Invalid URL (try again): '.red; 37 | } 38 | git.config.set('gitlab.url', options.host); 39 | } 40 | 41 | if (!options.token) { 42 | var url = options.host + '/profile/personal_access_tokens'; 43 | console.log('A personal access token is needed to use the GitLab API\n' + url.grey); 44 | var tokenQuestion = 'Enter personal access token: '.yellow; 45 | while (!options.token) { 46 | options.token = readlineSync.question(tokenQuestion); 47 | tokenQuestion = 'Invalid token (try again): '.red; 48 | } 49 | git.config.set('gitlab.token', options.token); 50 | } 51 | 52 | options.rejectUnauthorized = false; 53 | 54 | return options; 55 | })(); 56 | 57 | module.exports = options 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-lab-cli", 3 | "version": "2.0.12", 4 | "repository": "https://github.com/vishwanatharondekar/gitlab-cli", 5 | "description": "Create a merge request from command line in gitlab", 6 | "keywords": [ 7 | "cli", 8 | "gitlab", 9 | "gitlab-cli", 10 | "git", 11 | "command-line", 12 | "cli-utilities", 13 | "cli-utility", 14 | "github", 15 | "javascript", 16 | "command-line-tool", 17 | "commandline", 18 | "utility", 19 | "utilities", 20 | "nodejs", 21 | "merge-request", 22 | "merge-req" 23 | ], 24 | "main": "index.js", 25 | "scripts": { 26 | "start": "node index.js" 27 | }, 28 | "author": "Vishwanath Arondekar https://github.com/vishwanatharondekar", 29 | "license": "MIT", 30 | "bin": { 31 | "gitlab-cli": "index.js", 32 | "git-lab": "index.js", 33 | "lab": "index.js" 34 | }, 35 | "dependencies": { 36 | "@gitbeaker/node": "^28.4.1", 37 | "colors": "^1.1.2", 38 | "commander": "^2.12.1", 39 | "dotenv": "^4.0.0", 40 | "editor": "^1.0.0", 41 | "git-url-parse": "^11.1.3", 42 | "open": "7.2.0", 43 | "promise": "^7.1.1", 44 | "readline-sync": "^1.4.7", 45 | "shell-escape": "^0.2.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | --------------------------------------------------------------------------------