diff --git a/Dockerfile b/Dockerfile index cf64544..4299992 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM node:8.2.1-alpine -ENV GTT_VERSION 1.7.2 +ENV GTT_VERSION 1.7.4 RUN yarn global add --prefix /usr/local "gitlab-time-tracker@$GTT_VERSION" diff --git a/package.json b/package.json index ec92e80..27d9ad1 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitlab-time-tracker", - "version": "1.7.2", + "version": "1.7.4", "description": "A command line interface for GitLabs time tracking feature.", "bugs": { "url": "https://github.com/kriskbx/gitlab-time-tracker/issues" diff --git a/preview/icon.png b/preview/icon.png new file mode 100644 index 0000000..1b65712 Binary files /dev/null and b/preview/icon.png differ diff --git a/readme.md b/readme.md index be553d1..1b082da 100755 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -# gtt +# ![gtt](https://raw.githubusercontent.com/kriskbx/gitlab-time-tracker/master/preview/icon.png) gtt [![npm](https://img.shields.io/npm/dt/gitlab-time-tracker.svg?style=flat-square)](https://www.npmjs.com/package/gitlab-time-tracker) [![npm](https://img.shields.io/npm/v/gitlab-time-tracker.svg?style=flat-square)](https://www.npmjs.com/package/gitlab-time-tracker) @@ -30,6 +30,7 @@ stored on GitLab. * [config file](#config-file) * [time format](#time-format) * [how to use gtt as a library](#how-to-use-gtt-as-a-library) +* [dumps](#dumps) * [faqs](#faqs) * [contributing](#contributing) * [buy me a beer 🍺](#buy-me-a-beer) @@ -72,7 +73,7 @@ token: 01234567891011 ## updating -**Updating from version <= 1.5? Please [click here](https://github.com/kriskbx/gitlab-time-tracker/blob/master/upgrade.md)!** +**Updating from version <= 1.5? Please [click here](https://github.com/kriskbx/gitlab-time-tracker/blob/master/UPGRADE.md)!** Update gtt via yarn: @@ -796,6 +797,10 @@ report.mergeRequests.forEach(mergeRequest => { }); ``` +## dumps + +Starting with 1.7.4 gtt can dump the results of all API requests within a report and use it on another machine without access to the GitLab instance itself. This is very useful for debugging purposes. If you stumble upon a bug which could be unique to your set of data, please rerun the report with these options to save a dump to the given file: `--output=dump --file=/path/dump.json` Check your dump for sensitive information and provide it when asked. + ## faqs #### What is the difference between 'total spent' and 'spent'? diff --git a/src/gtt-report.js b/src/gtt-report.js index bd7285d..94edef9 100755 --- a/src/gtt-report.js +++ b/src/gtt-report.js @@ -13,7 +13,8 @@ const Output = { table: require('./output/table'), csv: require('./output/csv'), pdf: require('./output/pdf'), - markdown: require('./output/markdown') + markdown: require('./output/markdown'), + dump: require('./output/dump') }; // this collects options @@ -56,12 +57,24 @@ program .option('--check_token', 'check the access token') .option('--show_without_times', 'show issues/merge requests without time records') .option('-p --proxy ', 'use a proxy server with the given url') + .option('--from_dump ', 'instead of querying gitlab, use data from the given dump file') .parse(process.argv); // init helpers let config = new Config(process.cwd()); let cli = new Cli(program.args); +// if using a dump, set the config accordingly +if (program.from_dump && fs.existsSync(program.from_dump)) { + let data = JSON.parse(fs.readFileSync(program.from_dump)); + + if (data.data) _.each(data.data, (v, i) => { + config.set(i, v); + }); + + if (data._dump) config.dump = data._dump; +} + // overwrite config with args and opts config .set('project', cli.project()) @@ -94,12 +107,12 @@ config .set('type', program.type) .set('subgroups', program.subgroups) .set('_verbose', program.verbose) - .set('_checkToken', program.check_token); + .set('_checkToken', program.check_token) + .set('_createDump', program.output === 'dump'); Cli.quiet = config.get('quiet'); Cli.verbose = config.get('_verbose'); - // create stuff let reports = new ReportCollection(config), master = new Report(config), diff --git a/src/gtt.js b/src/gtt.js index 52f7289..db6dd7a 100755 --- a/src/gtt.js +++ b/src/gtt.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -const version = '1.7.2'; +const version = '1.7.4'; const program = require('commander'); program diff --git a/src/models/base.js b/src/models/base.js index 3b0fcc0..0897a5b 100755 --- a/src/models/base.js +++ b/src/models/base.js @@ -1,6 +1,7 @@ const request = require('request-promise-native'); const url = require('url'); const async = require('async'); +const crypto = require('crypto'); /** * base model @@ -29,17 +30,25 @@ class base { * @returns {*} */ post(path, data) { + let key = base.createDumpKey(path, data); + if (this.config.dump) return this.getDump(key); + data.private_token = this.token; - return request.post(`${this.url}${path}`, { - json: true, - body: data, - insecure: this._insecure, - proxy: this._proxy, - resolveWithFullResponse: true, - headers: { - 'PRIVATE-TOKEN': this.token - } + return new Promise((resolve, reject) => { + request.post(`${this.url}${path}`, { + json: true, + body: data, + insecure: this._insecure, + proxy: this._proxy, + resolveWithFullResponse: true, + headers: { + 'PRIVATE-TOKEN': this.token + } + }).then(response => { + if (this.config.get('_createDump')) this.setDump(response, key); + resolve(response); + }).catch(e => reject(e)); }); } @@ -51,17 +60,25 @@ class base { * @returns {Promise} */ get(path, page = 1, perPage = this._perPage) { + let key = base.createDumpKey(path, page, perPage); + if (this.config.dump) return this.getDump(key); + path += (path.includes('?') ? '&' : '?') + `private_token=${this.token}`; path += `&page=${page}&per_page=${perPage}`; - return request(`${this.url}${path}`, { - json: true, - insecure: this._insecure, - proxy: this._proxy, - resolveWithFullResponse: true, - headers: { - 'PRIVATE-TOKEN': this.token - } + return new Promise((resolve, reject) => { + request(`${this.url}${path}`, { + json: true, + insecure: this._insecure, + proxy: this._proxy, + resolveWithFullResponse: true, + headers: { + 'PRIVATE-TOKEN': this.token + } + }).then(response => { + if (this.config.get('_createDump')) this.setDump(response, key); + resolve(response); + }).catch(e => reject(e)); }); } @@ -124,6 +141,29 @@ class base { }, runners); } + /** + * save the given response to dump + * @param response + * @param key + */ + setDump(response, key) { + if (!this.config._dump) this.config._dump = {}; + + this.config._dump[key] = { + headers: response.headers, + body: response.body + }; + } + + /** + * get from dump + * @param key + * @returns {Promise} + */ + getDump(key) { + return new Promise(r => r(this.config.dump[key])); + } + /** * create a task list to get all pages from * the given path @@ -142,6 +182,14 @@ class base { return tasks; } + + /** + * create a key representing a request + * @param args + */ + static createDumpKey(...args) { + return crypto.createHash('md5').update(JSON.stringify(args)).digest("hex"); + } } module.exports = base; \ No newline at end of file diff --git a/src/output/base.js b/src/output/base.js index f2fcb73..297b682 100755 --- a/src/output/base.js +++ b/src/output/base.js @@ -120,9 +120,22 @@ class base { totalEstimate += parseInt(issue.stats.time_estimate); totalSpent += parseInt(issue.stats.total_time_spent); }); + + this.report[type].sort((a, b) => { + if (a.iid === b.iid) return 0; + + return (a.iid - b.iid) < 0 ? 1 : -1; + }); }); + this.times = times; + this.times.sort((a, b) => { + if (a.date.isSame(b.date)) return 0; + + return a.date.isBefore(b.date) ? 1 : -1; + }); + this.users = _.mapObject(users, user => this.config.toHumanReadable(user, 'stats')); this.projects = _.mapObject(projects, project => this.config.toHumanReadable(project, 'stats')); this.stats = { diff --git a/src/output/dump.js b/src/output/dump.js new file mode 100644 index 0000000..ba6c9ca --- /dev/null +++ b/src/output/dump.js @@ -0,0 +1,30 @@ +const Base = require('./base'); +const Report = require('../models/report'); + +class dump extends Base { + constructor(config, report) { + super(config, report); + + config.set('url', null, true); + config.set('token', null, true); + config.set('_createDump', false); + config.workDir = null; + config.cache = null; + + this.write(JSON.stringify(config)); + } + + makeStats() { + } + + makeIssues() { + } + + makeMergeRequests() { + } + + makeRecords() { + } +} + +module.exports = dump; \ No newline at end of file